171 lines
15 KiB
Markdown
171 lines
15 KiB
Markdown
# Raven + RustDesk Remote Access — Dossiê Técnico
|
|
|
|
## 1. Visão Geral
|
|
|
|
| Componente | Caminho/Fonte | Papel |
|
|
|------------|---------------|-------|
|
|
| Raven (app desktop Tauri) | `apps/desktop/src/main.tsx` + `apps/desktop/src-tauri/src/*.rs` | Provisiona máquinas, coleta inventário e automatiza instalação/config do RustDesk. |
|
|
| Serviço RustDesk | Binário Windows (`C:\Program Files\RustDesk\rustdesk.exe`) comandado via Tauri | Obtém ID (`--get-id`), aplica senha (`--password`) e escreve TOML em `C:\ProgramData\RustDesk`. |
|
|
| Backend Convex | `convex/machines.ts` | Mantém tokens de máquina, dados de acesso remoto e funções de auto-heal (`upsertRemoteAccessViaToken`, heartbeat). |
|
|
| API Next.js | `src/app/api/machines/remote-access/route.ts` etc. | Camada HTTP que o Raven chama para sincronizar o RustDesk. |
|
|
| Admin UI | `src/components/admin/devices/admin-devices-overview.tsx` | Exibe os acessos remotos (`device.remoteAccess`). |
|
|
|
|
Fluxo ideal:
|
|
|
|
1. **Raven registra a máquina** (`/api/machines/register`), recebe `machineToken` e salva em `%LOCALAPPDATA%\br.com.esdrasrenan.sistemadechamados\machine-agent.json` via `@tauri-apps/plugin-store` (`STORE_FILENAME = "machine-agent.json"`).
|
|
2. **Provisionamento RustDesk** — módulo Rust (`apps/desktop/src-tauri/src/rustdesk.rs`) executa:
|
|
- `rustdesk.exe --silent-install` (se necessário)
|
|
- `--import-config` com `RustDesk2.toml`
|
|
- `--password <default>` (hoje `FMQ9MA>e73r.FI<b*34Vmx_8P`)
|
|
- `--get-id` + gravação nos TOML em `C:\ProgramData\RustDesk\config` e `%APPDATA%\RustDesk\config`.
|
|
- Logs ficam em `%LOCALAPPDATA%\br.com.esdrasrenan.sistemadechamados\logs\rustdesk.log`.
|
|
3. **Sincronização com o backend** — `syncRustdeskAccess()` em `apps/desktop/src/main.tsx` envia `POST /api/machines/remote-access` com `{machineToken, provider: "RustDesk", identifier: ID, url, password, notes}`. O Convex (`upsertRemoteAccessViaToken`) converte em `machine.remoteAccess[]`.
|
|
4. **Admin UI** pega `remoteAccess` via `api.devices.listByTenant`/`getById` e exibe botão “Conectar via RustDesk”.
|
|
|
|
## 2. Implementação Atual (Novembro/2025)
|
|
|
|
### 2.1 Raven (apps/desktop/src/main.tsx)
|
|
|
|
- **Persistência local**: `Store.load()` tenta `executableDir()/data` (ex.: `C:\Program Files\Raven\data\machine-agent.json`), fallback em `%LOCALAPPDATA%`. Estrutura salva:
|
|
```json
|
|
{
|
|
"token": "<machineToken>",
|
|
"config": {
|
|
"machineId": "...",
|
|
"companySlug": "contabil",
|
|
"collaboratorEmail": "renan.pac@...",
|
|
"provisioningCode": "2353e53c...",
|
|
...
|
|
},
|
|
"rustdesk": {
|
|
"id": "372726409",
|
|
"password": "FMQ9MA>...",
|
|
"installedVersion": "1.4.3",
|
|
"lastProvisionedAt": 1762895600000,
|
|
"lastSyncedAt": null,
|
|
"lastError": null
|
|
}
|
|
}
|
|
```
|
|
- **Provisionamento RustDesk**: `provision_rustdesk` invoca `rustdesk::provision(&machine_id)` (`apps/desktop/src-tauri/src/rustdesk.rs`). Esse módulo:
|
|
- Baixa release mais recente via GitHub API (`RELEASES_API`).
|
|
- Instala/atualiza binário, configura `RustDesk2.toml` com `relay-server`, `api-server`, etc.
|
|
- Define ID determinístico (hash do `machine_id`, mas depois compara com `--get-id` e usa o “reportado” caso o serviço tenha um ID próprio).
|
|
- Reinicia serviço `RustDesk` (`sc start RustDesk` — pode falhar com `status Some(5)` se não há privilégio admin). Mesmo com falha, a CLI continua e grava o ID nos arquivos.
|
|
- **Autoelevação única**: na primeira execução do botão “Preparar”, o Raven dispara um PowerShell elevado (`takeown + icacls`) para liberar ACL dos perfis `LocalService` e `LocalSystem`. O sucesso grava `rustdeskAclUnlockedAt` dentro de `%LOCALAPPDATA%\br.com.esdrasrenan.sistemadechamados\machine-agent.json` e cria o flag `rustdesk_acl_unlocked.flag`, evitando novos prompts de UAC nas execuções seguintes.
|
|
- **Kill/restart seguro**: antes de tocar nos TOML, o Raven roda `sc stop RustDesk` + `taskkill /F /T /IM rustdesk.exe`. Isso garante que nenhum cliente sobrescreva o `RustDesk_local.toml` enquanto aplicamos a senha.
|
|
- **Serviço em background reforçado**: `ensure_service_running` sempre instala o serviço, força `start= auto`, aplica `sc failure ... actions= restart/5000/...` e revalida se o serviço ficou em `RUNNING`. Se não subir, reaplica `--install-service` + `sc start` antes de seguir. Assim ele permanece no background/bandeja após boot, sem depender do usuário abrir a UI.
|
|
- **Sem pop-up de GUI**: após configurar o serviço, removemos entradas de auto-run/atalhos (`HKCU/HKLM\...\Run` e `Start Menu\Programs\Startup\RustDesk.lnk`) para impedir que a janela do RustDesk abra a cada execução do Raven. O serviço continua ativo em segundo plano.
|
|
- **Replicação completa de perfis**: após aplicar `--password`, limpamos quaisquer arquivos antigos (`RustDesk*.toml`, `password`, `passwd*`) em `%APPDATA%`, `ProgramData`, `LocalService` e `LocalSystem` e, em seguida, reescrevemos tudo. Agora os `RustDesk2.toml` herdados recebem `verification-method = "use-permanent-password"` e `approve-mode = "password"` no bloco `[options]` antes mesmo do serviço subir, evitando que o RustDesk volte para "Use both".
|
|
- **Validação por arquivo**: o Raven agora registra no `rustdesk.log` se o `password` gravado em cada `RustDesk_local.toml` coincide com o PIN configurado. Para inspecionar manualmente, rode no PowerShell (e valide que todas as linhas exibem o PIN esperado):
|
|
|
|
```powershell
|
|
$targets = @(
|
|
"$env:APPDATA\RustDesk\config\RustDesk.toml",
|
|
"$env:APPDATA\RustDesk\config\RustDesk_local.toml",
|
|
"$env:ProgramData\RustDesk\config\RustDesk.toml",
|
|
"$env:ProgramData\RustDesk\config\RustDesk_local.toml",
|
|
"C:\\Windows\\ServiceProfiles\\LocalService\\AppData\\Roaming\\RustDesk\\config\\RustDesk.toml",
|
|
"C:\\Windows\\ServiceProfiles\\LocalService\\AppData\\Roaming\\RustDesk\\config\\RustDesk_local.toml",
|
|
"C:\\Windows\\System32\\config\\systemprofile\\AppData\\Roaming\\RustDesk\\config\\RustDesk.toml",
|
|
"C:\\Windows\\System32\\config\\systemprofile\\AppData\\Roaming\\RustDesk\\config\\RustDesk_local.toml"
|
|
)
|
|
foreach ($path in $targets) {
|
|
if (Test-Path $path) {
|
|
"`n$path"
|
|
Select-String -Path $path -Pattern '^password'
|
|
} else {
|
|
"`n$path (inexistente)"
|
|
}
|
|
}
|
|
```
|
|
- **Checklist pós-preparo**: todo ciclo bem-sucedido sempre termina com este bloco no `rustdesk.log` (nessa ordem):
|
|
1. `Senha padrão definida com sucesso`
|
|
2. `Aplicando senha nos perfis do RustDesk`
|
|
3. Quatro linhas `Senha escrita via fallback ...` / `verification-method ...` / `approve-mode ...` cobrindo `%APPDATA%`, `ProgramData`, `LocalService`, `SystemProfile`
|
|
4. `Senha e flags de segurança gravadas ...` seguido por `Senha confirmada em ... (FM***8P)` para cada `RustDesk.toml`
|
|
5. Logs de propagação (`RustDesk.toml propagado para ...`, `RustDesk_local.toml propagado para ...`, `RustDesk2.toml propagado ...`)
|
|
6. `Serviço RustDesk reiniciado/run ativo` e `remote_id atualizado para ...`
|
|
|
|
> Observação: as mensagens `Aviso: chave 'password' não encontrada em ...RustDesk_local.toml` são esperadas — esse arquivo nunca armazena a senha, apenas `verification-method`/`approve-mode`.
|
|
- **Quando algo der errado**:
|
|
- Se o log parar em `Senha padrão definida...` e nada mais aparecer, algum erro interrompeu o módulo antes de gravar os arquivos. Rode o Raven com DevTools (`Ctrl+Shift`) e capture o stack trace.
|
|
- Se aparecer “senha divergente” para algum diretório, execute o script PowerShell acima para confirmar e, se necessário, rode “Preparar” novamente (ele sempre derruba `rustdesk.exe`, limpa os perfis e reaplica o PIN).
|
|
- Se `Serviço RustDesk reiniciado...` não surgir, pare/inicie manualmente (`Stop-Service RustDesk; Start-Service RustDesk`) após verificar que os arquivos já trazem a senha nova.
|
|
- **Reforço contínuo**: toda nova execução do botão “Preparar” repete o ciclo (kill → copiar → gravar flags → `sc start`). Se alguém voltar manualmente para “Use both”, o Raven mata o processo, reescreve os TOML e reinicia o serviço — o RustDesk volta travado na senha permanente com o PIN registrado nos logs (`rustdesk.log`).
|
|
- **Sincronização**: `syncRustdeskAccess(machineToken, info)` chama `/api/machines/remote-access`. Há retries automáticos:
|
|
```ts
|
|
if (response.status === 401/500 contendo "token revogado") {
|
|
await attemptSelfHeal("remote-access")
|
|
// re-register + replay
|
|
}
|
|
```
|
|
Além disso, o heartbeat (`/api/machines/heartbeat`) envia `metadata.remoteAccessSnapshot` para garantir que o Convex receba o RustDesk mesmo que o POST falhe.
|
|
- **Self-heal** (`attemptSelfHeal` + `reRegisterMachine`): usa o `provisioningCode` salvo para chamar `/api/machines/register` novamente quando o backend diz “token revogado”. Isso reaproveita o mesmo `machineId` e grava token novo sem intervenção.
|
|
- **Logs adicionais (commit cdf3fea)**: `logDesktop("remoteAccess:sync:success", {...})` escreve no console do Raven (DevTools). Assim conseguimos saber se o POST rodou e se houve erro (`remoteAccess:sync:failed`).
|
|
|
|
### 2.2 Backend Convex (convex/machines.ts)
|
|
|
|
- **Tokens**: tabela `machineTokens` agora possui `revokedAt`. Ao registrar (`register` mutation), todos os tokens antigos são marcados `revoked=true`, `revokedAt=now`.
|
|
- **Grace window**: `REMOTE_ACCESS_TOKEN_GRACE_MS` (default 15 min) permite que `upsertRemoteAccessViaToken` aceite tokens recém-revogados — ideal para reinstalações onde o Raven ainda não gravou o token novo.
|
|
- **Heartbeat Snapshot**: se `metadata.remoteAccessSnapshot` vier, `upsertRemoteAccessSnapshotFromHeartbeat` normaliza e grava diretamente em `machine.remoteAccess`.
|
|
|
|
### 2.3 API Next.js
|
|
|
|
- `src/app/api/machines/remote-access/route.ts`: recebe `{machineToken, provider, identifier, url, password, notes}` e chama `client.mutation(api.devices.upsertRemoteAccessViaToken, payload)`.
|
|
- `src/components/admin/devices/admin-devices-overview.tsx`: lê `device.remoteAccessEntries` e monta o cartão com “Copiar ID”, “Conectar via RustDesk”, etc.
|
|
|
|
## 3. Problemas Encontrados
|
|
|
|
### 3.1 Token fica inválido após reinstalação
|
|
- Motivo: `register` revoga todos os tokens anteriores. O Raven reinstalado continuava com o token antigo, logo qualquer POST recebia `ConvexError: Token de dispositivo revogado`.
|
|
- Mitigação já aplicada: grace window + self-heal automático (re-register). Além disso, `heartbeat` com snapshot garante que, mesmo sem POST, o RustDesk seja reaplicado.
|
|
|
|
### 3.2 `machine-agent.json` sem a chave `rustdesk`
|
|
- Observado no dump enviado: apenas `token` e `config`. Isso significa que `writeRustdeskInfo(store, info)` não executou (o provisioning terminou, mas algo impediu o salvamento). Sem essa chave, `syncRustdeskAccess` nunca roda porque ele depende de `rustdeskInfo` carregado no `useEffect`.
|
|
- Logs do `rustdesk.log` só mostraram o provisioning, nenhuma linha de “sincronização” ou erro. Ou seja: o app não chegou a chamar o POST.
|
|
- Melhorias: commit `cdf3fea` adicionou `logDesktop()` e passa a salvar `lastError`, permitindo ver no console se houve tentativa/erro.
|
|
|
|
### 3.3 Deploys derrubando WebSocket (Convex)
|
|
- Cada `docker stack deploy` reinicia o serviço `sistema_convex_backend`, derrubando o socket por ~40s. Durante esse período o navegador mostra `WebSocket closed code 1011/1006`, `Unexpected response 504`, mas reconecta depois do boot.
|
|
- Sem 2ª réplica, esse “buraco” é inevitável. Solução futura: escalar para 2 replicas com `update_config: order=start-first` ou evitar redeploys frequentes.
|
|
|
|
### 3.4 Falha na sincronização automática
|
|
- Cenário observado: reinstalação -> ID/senha gerados, mas `remoteAccess` só aparece quando rodamos `curl` manual. Diagnóstico atual: o Raven não salvou `rustdeskInfo` (ou não executou `syncRustdeskAccess`). Precisamos capturar o log do console (`remoteAccess:sync:*`) para confirmar.
|
|
|
|
## 4. Como estamos operando hoje
|
|
|
|
- **Ambiente Linux (dev)**: fazemos alterações no WSL/Linux, rodamos `bun run lint/build/test`, e `rsync` o diretório inteiro para `/srv/apps/sistema` via `scp/rsync -e "ssh -i ./codex_ed25519"`. Depois `docker stack deploy ...` e, se `convex/` mudou, `docker run ... bun x convex deploy` usando `.ci.env` (com `CONVEX_SELF_HOSTED_URL` + `CONVEX_SELF_HOSTED_ADMIN_KEY`).
|
|
- **Ambiente Windows (para Tauri)**: há uma cópia do repositório sincronizada com `git pull origin main`. Lá rodamos `bun install`, `bun run --cwd apps/desktop tauri build`, geramos o instalador `.exe`, reinstalamos o Raven e testamos na estação real. Os testes de sincronização dependem de olhar `%LOCALAPPDATA%\...\machine-agent.json` e `rustdesk.log`.
|
|
- **Sem app.log**: atualmente só temos `rustdesk.log`. Os novos `console.log` servem como “log” enquanto não adicionamos um arquivo dedicado. Próximo passo: escrever também em `%LOCALAPPDATA%\...\logs\app.log` (não feito ainda).
|
|
|
|
## 5. Próximos passos recomendados
|
|
|
|
1. **Distribuir o build com logs** (commit `cdf3fea`) e reproduzir a instalação. Abrir o Raven com `CTRL+SHIFT` para inspecionar o console. Precisamos ver uma dessas linhas após o provisioning:
|
|
- `remoteAccess:sync:success { id: "372726409" }`
|
|
- `remoteAccess:sync:failed { error: "..." }`
|
|
Isso dirá se o POST está falhando ou nem tenta.
|
|
2. **Persistir logs em arquivo** — criar `app.log` com `logDesktop` escrevendo em `%LOCALAPPDATA%`. Hoje o console só aparece se o usuário abre as DevTools; um arquivo facilita coleta remota.
|
|
3. **Verificar permissões do Store** — se `machine-agent.json` continua sendo gravado em `Program Files\Raven\data`, pode haver bloqueio de escrita sem admin. Talvez forçar o uso de `%LOCALAPPDATA%` resolva.
|
|
4. **Adicionar telemetria no backend** — logar sempre que `upsertRemoteAccessViaToken` é chamado (hoje só loga no erro). Assim sabemos se alguém tentou.
|
|
5. **Documentar processo de reinstall** para a equipe: “Resetar agente → reinstalar Raven → aguardar log `remoteAccess:sync:success`”.
|
|
|
|
## 6. Referências rápidas
|
|
|
|
- **Provisionamento manual (PowerShell)** — pode ser útil para comparação:
|
|
```pwsh
|
|
$rustdesk_pw = (-join ((65..90)+(97..122) | Get-Random -Count 12 | % {[char]$_}))
|
|
$rustdesk_cfg = "configstring" # fornecido pela portal RustDesk
|
|
Invoke-WebRequest https://github.com/rustdesk/rustdesk/releases/latest ...
|
|
Start-Process .\rustdesk.exe --silent-install
|
|
.\rustdesk.exe --password $rustdesk_pw
|
|
.\rustdesk.exe --get-id
|
|
```
|
|
- **curl para testar backend**:
|
|
```bash
|
|
curl -X POST https://tickets.esdrasrenan.com.br/api/machines/remote-access \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"machineToken":"<TOKEN>","provider":"RustDesk","identifier":"372726409","url":"rustdesk://372726409","password":"FMQ9MA>e73r.FI<b*34Vmx_8P"}'
|
|
```
|
|
|
|
Este dossiê compila tudo o que sabemos até agora sobre o provisionamento automático e onde ainda estamos com lacunas (principalmente o salvamento do `rustdeskInfo`). Com ele conseguimos alinhar próximos testes e, se necessário, dividir tarefas (ex.: criar app.log, investigar permissões no Store, etc.).
|