sistema-de-chamados/docs/RUSTDESK-PROVISIONING.md

15 KiB

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 backendsyncRustdeskAccess() 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:
    {
      "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 e aplica sc failure ... actions= restart/5000/... para reiniciar o RustDesk automaticamente em caso de falha. Assim ele permanece no background/bandeja após boot, sem depender do usuário abrir a UI.

    • 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):

      $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:
    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:
    $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:
    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.).