CallMeTechie
DE Login
Home Products Blog About Contact

Remote Desktop

v1.0 · Updated 1 month ago

Overview

An RDP route describes a Windows machine that a user should reach from the GateControl desktop client via RDP. In contrast to HTTP or L4 routes (which are generic Caddy upstream traffic), an RDP route is a combination of target address, NLA settings, credentials in the credential vault, Wake-on-LAN configuration, maintenance windows, session tracking, and an optionally prepended Home Gateway or Microsoft RD Gateway.

RDP routes have their own table rdp_routes and their own API endpoints (/api/v1/rdp/*). They are NOT rendered in Caddy — RDP bypasses the reverse proxy. However, if the admin chooses access_mode='gateway', the server-side port bridge is automatically created as a linked L4 route (and cleaned up again on delete/toggle).

The entire feature is gated behind the license flag remote_desktop; the gateway variant additionally behind rdp_via_gateway.

Architecture

  Desktop-Client ──(LAN direkt)──────────────▶ Windows-Host   (access_mode=internal)
        │
        ├──(externes Port-Forward)───────────▶ external_host:external_port → NAT → Windows-Host
        │                                                                 (access_mode=external/both)
        │
        ├──(Server :listen_port → WG-Tunnel)──▶ Gateway ──LAN──▶ Windows-Host
        │                                         (access_mode=gateway, gelinkte L4-Route)
        │
        └──(optional) Microsoft RD-Gateway (TSGateway) davor vorgeschaltet
              via rdp_routes.gateway_host/gateway_port — nur als Config-Weitergabe

Components

Component Role
rdp_routes Central table, own migration history
rdp_sessions Session log (start/heartbeat/end)
rdpMonitor Health-check service, TCP probe on port 3389
Credential vault AES-GCM-encrypted in username_encrypted / password_encrypted
ECDH endpoint E2EE download of credentials to the desktop client
Linked L4 route Auto-created Caddy L4 route on access_mode='gateway'

Data model

CREATE TABLE rdp_routes (
  id INTEGER PRIMARY KEY AUTOINCREMENT,

  -- Connection
  name TEXT NOT NULL,
  host TEXT NOT NULL,                 -- LAN-Ziel bzw. interner Hostname
  port INTEGER NOT NULL DEFAULT 3389,
  external_hostname TEXT,             -- für access_mode=external
  external_port INTEGER,
  access_mode TEXT NOT NULL DEFAULT 'internal',
  gateway_host TEXT,                  -- Microsoft RD-Gateway (TSGateway), nicht Home-Gateway
  gateway_port INTEGER DEFAULT 443,
  enabled INTEGER NOT NULL DEFAULT 1,

  -- Credentials (AES-256-GCM)
  credential_mode TEXT NOT NULL DEFAULT 'none',   -- 'none' | 'user_only' | 'full'
  username_encrypted TEXT,
  password_encrypted TEXT,
  domain TEXT,

  -- Display, Redirects, Audio, Network-Profile … (viele Spalten)
  resolution_mode, resolution_width, resolution_height, multi_monitor, color_depth,
  redirect_clipboard, redirect_printers, redirect_drives, redirect_usb, redirect_smartcard,
  audio_mode, network_profile, nla_enabled, ...

  -- Wake-on-LAN
  wol_enabled INTEGER, wol_mac_address TEXT,

  -- Maintenance
  maintenance_enabled INTEGER,
  maintenance_schedule TEXT,          -- JSON-serialisiert, Zeilen wie "Mo-Fr 08:00-18:00"

  -- Credential-Rotation
  credential_rotation_enabled INTEGER,
  credential_rotation_days INTEGER DEFAULT 90,
  credential_rotation_last TEXT,

  -- Access-Control
  token_ids TEXT,                     -- JSON-Array, legacy
  user_ids TEXT,                      -- JSON-Array, neuer Pfad

  -- Home-Gateway-Link (Migration 38)
  gateway_peer_id INTEGER REFERENCES peers(id),
  gateway_listen_port INTEGER,
  gateway_l4_route_id INTEGER REFERENCES routes(id)
);

Access modes

Mode Target How the client connects
internal LAN host, reachable over the VPN tunnel <host>:<port> directly
external Publicly exposed port forward <external_hostname>:<external_port>
both Client may use either path Client-side preference
gateway Home Gateway companion forwards into the LAN <server-public-host>:<gateway_listen_port> → server Caddy L4 → gateway → <host>:<port>

In addition, any mode can have a Microsoft RD Gateway (TSGateway) prepended via the fields gateway_host + gateway_port (not to be confused with the Home Gateway mode). GateControl merely passes these values through as configuration to the desktop client; the TSGateway role itself runs outside.

Gateway mode: the linked L4 route

The point of access_mode='gateway': the Windows host has no WG peer of its own, lives purely in the LAN behind a Home Gateway. The server must still provide an external port on which the desktop client lands, and forward the TCP stream to the gateway.

Auto-create on save

_syncLinkedL4Route(rdpId, previousL4RouteId) in src/services/rdp.js is called on create and on relevant update cases. For access_mode='gateway':

routes.create({
  route_type: 'l4',
  target_kind: 'gateway',
  target_peer_id: rdp.gateway_peer_id,
  target_lan_host: rdp.host,
  target_lan_port: rdp.port || 3389,
  l4_protocol: 'tcp',
  l4_listen_port: String(rdp.gateway_listen_port || rdp.port || 3389),
  l4_tls_mode: 'none',
  enabled: rdp.enabled,
  description: `auto-created for RDP route "${rdp.name}"`
})

The returned route.id is stored in rdp_routes.gateway_l4_route_id. If the L4 create fails, the RDP route is deleted again — no half-configured state remains.

Update synchronization

When one of these fields changes in the RDP update: access_mode, gateway_peer_id, gateway_listen_port, host, port, enabled_syncLinkedL4Route is called again:

  • access_mode remains gateway → L4 is brought back in sync via routes.update(previousL4RouteId, payload).
  • access_mode changes AWAY from gateway → L4 is deleted (routes.remove(previousL4RouteId)), gateway_l4_route_id = NULL.
  • access_mode changes TO gateway → new L4 is created.

Failure mode is intentionally non-fatal: a failing L4 sync does NOT roll back the RDP side. The RDP row is saved correctly from the admin's point of view, the sync failure is logged, and the admin can trigger again with another save.

Cascade on delete and toggle

  • remove(id) — first routes.remove(gateway_l4_route_id), then DELETE FROM rdp_routes. The order ensures that no orphan L4 is left behind if the RDP row were suddenly no longer deletable.
  • toggle(id) — the new enabled state is propagated to the L4 route, so that the external listen port is in sync with the RDP status.

Wake-on-LAN

wol_enabled=1 + wol_mac_address activates WoL for the route. Two paths:

Via Home Gateway

With access_mode='gateway': the server calls gateways.notifyWol(gateway_peer_id, {mac, lan_host, timeout_ms}). The gateway sends the magic packet on the LAN and responds with {success, elapsed_ms}. See home-gateway.md.

Via direct peer (no gateway)

If the Windows host is itself a WG peer and WoL would be required: the server can send a magic packet over the tunnel interface, provided the host is in the same L2 broadcast scope as a LAN device. Rarely practical — with classic RDP-via-WG setups, the peer machine is either already on or is woken by another LAN WoL sender.

Maintenance windows

maintenance_enabled=1 + maintenance_schedule (JSON array of lines like "Mo-Fr 08:00-18:00"). Parser:

  • Supports German abbreviations (Mo, Di, Mi, Do, Fr, Sa, So) and English (Mon, Tue, Wed, Thu, Fri, Sat, Sun).
  • Range Mo-Fr or single day.
  • Time range HH:MM-HH:MM, same-day.
  • Multiple lines → union (OR).

isInMaintenanceWindow(routeId) returns true at query time if "now" lies within the window. The desktop client queries this before a connect and shows a warning ("Maintenance until 18:00"); the user can confirm and connect anyway.

Sessions

Separate tracking in rdp_sessions:

POST /api/v1/client/rdp/:id/session        → startSession() → session_id
PATCH /api/v1/client/rdp/session/:id/ping  → heartbeatSession()
DELETE /api/v1/client/rdp/session/:id      → endSession(reason)
  • start sets status='active', logs rdp_session_start in activity.
  • heartbeat updates last_heartbeat; if a session stays N minutes without a heartbeat, it is ended by the sweeper as abandoned.
  • end sets status='ended'|'abandoned'|'admin_disconnect'|'normal', computes duration_s.

The admin dashboard shows:

  • GET /api/v1/rdp — per route the active_sessions count, last access.
  • GET /api/v1/rdp/history — global session history with filters.
  • GET /api/v1/rdp/history/export — CSV/JSON export.
  • POST /api/v1/rdp/:id/sessions/disconnect-all — admin kill for all active sessions of a route.

Access control

Per route:

  • user_ids — JSON array of user IDs, newer policy. If set and non-empty, only these users have access.
  • token_ids — JSON array of API token IDs, legacy. Only checked if user_ids is empty or not set.
  • Without both → route is visible to all tokens/users.

getForToken(tokenId, userId) in src/services/rdp.js implements the priority.

Licensing

  • remote_desktop — all RDP endpoints are behind router.use(requireFeature('remote_desktop')) in src/routes/api/rdp.js.
  • rdp_via_gateway — additionally checked on create/update when access_mode='gateway'. Without the flag, the other three modes remain available.

Monitor and FQDN hint

rdpMonitor.checkAll() does a per-route TCP probe on the configured port. Timeout 2s, results are cached and delivered together with the routes list.

At the connection-info endpoint (/api/v1/client/rdp/:id/connection-info), if the target peer has a hostname in internal DNS and internal_dns is licensed, the FQDN is included:

{
  "peer_hostname": "nas1",
  "peer_fqdn": "nas1.domaincaster.lan",
  "external_hostname": "...",
  ...
}

Background: CredSSP/NLA validates the server certificate of the Windows host against the server name specified. With a plain IP this fails. See internal-dns.md.

See also

  • home-gateway.md — how the gateway companion provides L4 listeners and fires WoL.
  • routing.md — the linked L4 route uses the same mechanism as manual L4 gateway routes.
  • internal-dns.md — FQDN for CredSSP.
  • licensing.mdremote_desktop, rdp_via_gateway.
  • ../API.md/api/v1/rdp/* and /api/v1/client/rdp/*.

Source files

  • src/services/rdp.js — CRUD, validation, _syncLinkedL4Route, credential handling, maintenance parser.
  • src/services/rdpSessions.js — Session start/heartbeat/end, history, CSV export.
  • src/services/rdpMonitor.js — Health probe per route.
  • src/routes/api/rdp.js — Admin endpoints, /pubkey, /rotation/pending.
  • src/routes/api/client.js — Desktop client endpoints, /connection-info, session lifecycle.
  • src/utils/crypto.jsecdhEncrypt, ecdhDecrypt, getServerPublicKey, publicKeyEncrypt.
  • src/db/migrations.js — Migration 31 (create_rdp_routes), 38 (rdp_routes_gateway_link).

Cookie Settings

We use cookies to improve your experience. Essential cookies are always active.

Privacy Policy
ESC
↑↓ navigate open esc close