Files
rmancinasandClaude Opus 5 88be2bf867
Build and Deploy Remote Control Support / Test (push) Successful in 11s
Build and Deploy Remote Control Support / Build Image (push) Successful in 47s
Build and Deploy Remote Control Support / Deploy to Portainer (push) Successful in 7s
fix(http): 400 on malformed JSON body instead of 500
express.json() raising a parse error was falling through to the generic
500 handler, which reads as a server fault for what is a bad request.
Also maps the body-size limit to 413.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 23:55:48 -07:00

8.9 KiB

Remote Control Support Webapp — Plan

Self-hosted TeamViewer-style remote support tool built on VNC/RFB, browser-based (noVNC), with invite links, access control, garagedoor SSO, and a persisted client list.

Status legend: [ ] todo · [~] in progress · [x] done


1. Architecture

        browser (noVNC)                    hub (this app)                 client machine
   ┌───────────────────────┐        ┌────────────────────────┐        ┌───────────────────┐
   │  viewer.html          │  wss   │ express + ws           │        │  VNC server :5900 │
   │  RFB over WebSocket   │◄──────►│  /vnc?ticket=…         │        │                   │
   └───────────────────────┘        │                        │        │                   │
                                    │  bridge:               │  tcp   │                   │
                                    │   direct mode ─────────┼───────►│                   │
                                    │                        │        │                   │
                                    │   agent mode           │  wss   │  agent.js         │
                                    │   /agent (control) ◄───┼────────┤  outbound only    │
                                    │   /tunnel (data)   ◄───┼────────┤  pipes to :5900   │
                                    └────────────────────────┘        └───────────────────┘
                                              │
                                         SQLite (clients, grants,
                                         invites, sessions, audit)

Two connection modes — a client row is one or the other:

Mode How Use for
direct Hub opens TCP to host:port. LAN machines with a reachable VNC server.
agent Client runs agent/agent.js, dials out to the hub over WSS and holds it open. Hub asks it to open a data tunnel per session. NAT'd / remote machines. The TeamViewer-shaped path.

Why a proxy and not raw noVNC: the hub terminates the RFB handshake itself. That lets it (a) authenticate to the real VNC server with a server-side stored password the browser never sees, and (b) enforce view-only by dropping input messages on the client→server stream. Both are required for invite links to be safe.

2. Tech stack

  • Node 22, CommonJS, Express 4 — matches stash-ex-webapp.
  • Deployed by Gitea Actions to Portainer's Swarm endpoint; single replica, pinned placement, named volume (see README).
  • ws for all WebSocket endpoints; Node's built-in node:sqlite for persistence (no native modules, so the container has no build stage).
  • @novnc/novnc vendored and served as browser ES modules.
  • Vanilla JS frontend in public/ (no build step).
  • Auth delegated to garagedoor-node-ws (http://192.168.4.208:8000) — proxy pattern, never hold the JWT secret locally.

3. Data model (SQLite)

  • clients — id, name, mode, host/port, encrypted VNC password, agent key hash, require_consent, tags, os, agent_version, last_seen_at, status, created_by.
  • grants — per-user access to a client with a role and optional expiry.
  • invites — hashed token, kind (enroll | session), target client, role, max_uses, uses, expiry, revocation.
  • sessions — audit trail of every connection: who, what client, role, bytes, duration, source (web | invite), end reason.
  • audit — admin actions (client created/deleted, invite issued/revoked, grant changed).

Secrets at rest (VNC passwords, agent keys) are AES-256-GCM encrypted with a key derived from ENCRYPTION_KEY. Invite and agent tokens are stored hashed, never plaintext.

4. Access control model

Roles resolved per (user, client) at connect time:

Role Can
admin Everything: CRUD clients, issue/revoke invites, manage grants, view audit.
operator Connect with full keyboard/mouse control to granted clients.
viewer Connect view-only (input filtered at the proxy) to granted clients.
  • Admin = garagedoor username in ADMIN_USERS, or garagedoor levelADMIN_LEVEL.
  • Non-admins see only clients they hold a grant for.
  • require_consent on a client makes the agent prompt the local user before each session.
  • Every WS connect uses a one-time, 30-second ticket minted by POST /api/sessions, so long-lived JWTs never appear in URLs or proxy logs.
  1. Enrollment invite /enroll/<token> — hand to a machine you want to manage. Page shows the install one-liner with a single-use token baked in; running it registers the machine as a client and it appears in the list.
  2. Session invite /s/<token> — hand to a person. Time-limited, use-limited access to one client at a fixed role (viewer or operator), no login required. This is the "send the customer a link" flow.

Both are revocable, expiring, and logged.


6. Feature checklist

Phase 1 — Foundation

  • Project scaffold, package.json, env config, .gitignore
  • SQLite schema + migrations on boot
  • Secret encryption helper (AES-256-GCM)
  • garagedoor auth proxy + requireAuth / requireAdmin middleware
  • Login screen, token in localStorage, 401 → re-login

Phase 2 — Clients & persistence

  • GET/POST/PATCH/DELETE /api/clients
  • Client list UI with status, tags, last-seen
  • Add/edit client form (direct mode: host/port/password)
  • Online/offline status tracking for agent clients

Phase 3 — VNC bridge

  • WS↔TCP bridge for direct mode
  • Server-side RFB handshake + VNC Authentication (password never reaches browser)
  • View-only enforcement by filtering client→server RFB messages
  • noVNC viewer page: scaling, fullscreen, clipboard, Ctrl-Alt-Del
  • One-time session tickets

Phase 4 — Agent (NAT traversal)

  • Agent control channel /agent with heartbeat + reconnect
  • Data tunnel /tunnel paired to a waiting browser socket
  • agent/agent.js — zero npm dependencies, uses Node 22's global WebSocket
  • Enrollment via invite token → agent key issued once
  • Local consent prompt when require_consent is set

Phase 5 — Invites & access control

  • Issue/list/revoke invites (both kinds)
  • /enroll/<token> enrollment page
  • /s/<token> session invite page (no login)
  • Per-user grants CRUD
  • Role resolution + enforcement at connect

Phase 6 — Audit & operations

  • Session history table + live "who is connected now"
  • Audit log of admin actions
  • Admin can force-disconnect an active session
  • Dockerfile + docker-compose + Gitea Actions CI/CD to Portainer
  • README with deploy + client setup instructions
  • End-to-end test suite (pnpm test) — fake VNC server + fake auth service, 72 assertions covering auth, RBAC, VNC auth, view-only, invites, agent tunnel

Phase 7 — Front end polish

  • Modern visual design pass across all five pages
  • Particle background (network-of-machines motif), reduced-motion aware, never rendered behind a live VNC canvas

Deployed

Live at http://192.168.4.212:5910 (Swarm ingress). Published port is 5910, not 8091 — 8091 is reserved on the ingress by ai-training-lab_ai-lab, and that reservation holds even while nothing answers on it, so probing the port cannot tell you it is taken. Only the Swarm's own view is authoritative.

Remaining to be usable from outside the LAN console:

  • support.freakma.com DNS + Nginx Proxy Manager host, WebSocket support on
  • First machine enrolled

Phase 8 — Later / nice to have

  • File transfer between operator and client
  • Clipboard sync toggle per session
  • Multi-monitor selection
  • Session recording (RFB stream capture + replay)
  • Chat sidebar during a support session
  • Wake-on-LAN integration (sibling wol-fleet-webapp)
  • TOTP step-up before controlling a flagged client
  • Agent auto-update
  • RDP backend alongside VNC

7. Security notes

  • The hub is the only thing that knows VNC passwords; browsers get an RFB stream that has already cleared authentication.
  • View-only is enforced server-side, not by hiding UI.
  • Invite tokens: 32 bytes of crypto.randomBytes, stored as SHA-256, compared in constant time, single-use by default.
  • Deploy behind HTTPS (see the lan-https-host setup) — RFB over plain ws:// is cleartext framebuffer data.
  • Known upstream debt in garagedoor (SQL injection, hardcoded secret) is not inherited: this app never touches that DB or that secret.