CallMeTechie
EN Anmelden
Home Produkte Blog Über mich Kontakt

Remote Desktop

v1.0 · Updated vor 1 Monat

Überblick

Eine RDP-Route beschreibt einen Windows-Rechner, den ein Nutzer vom GateControl-Desktop-Client per RDP erreichen soll. Im Gegensatz zu HTTP- oder L4-Routen (die generischen Caddy-Upstream-Verkehr sind) ist eine RDP-Route eine Kombination aus Zieladresse, NLA-Einstellungen, Anmeldeinformationen im Credential-Vault, Wake-on-LAN-Konfiguration, Wartungsfenstern, Session-Tracking und einem optional vorgelagerten Home-Gateway oder Microsoft RD-Gateway.

RDP-Routen haben eine eigene Tabelle rdp_routes und eigene API-Endpoints (/api/v1/rdp/*). Sie werden NICHT in Caddy gerendert — RDP geht am Reverse-Proxy vorbei. Wenn aber der Admin access_mode='gateway' wählt, wird die Server-seitige Port-Brücke automatisch als gelinkte L4-Route erzeugt (und beim Delete/Toggle wieder aufgeräumt).

Das gesamte Feature steckt hinter dem Lizenz-Flag remote_desktop; die Gateway-Variante zusätzlich hinter rdp_via_gateway.

Architektur

  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

Komponenten

Komponente Rolle
rdp_routes Zentrale Tabelle, eigene Migration-Historie
rdp_sessions Session-Log (start/heartbeat/end)
rdpMonitor Health-Check-Service, TCP-Probe auf Port 3389
Credential-Vault AES-GCM-verschlüsselt in username_encrypted / password_encrypted
ECDH-Endpoint E2EE-Download der Creds zum Desktop-Client
Linked L4-Route Auto-erzeugte Caddy-L4-Route bei access_mode='gateway'

Datenmodell

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 Ziel Wie der Client verbindet
internal LAN-Host, erreichbar über VPN-Tunnel <host>:<port> direkt
external Öffentlich exponierter Port-Forward <external_hostname>:<external_port>
both Client darf beide Wege Client-seitige Präferenz
gateway Home-Gateway-Companion leitet in LAN weiter <server-public-host>:<gateway_listen_port> → Server-Caddy-L4 → Gateway → <host>:<port>

Zusätzlich kann bei jedem Mode ein Microsoft RD-Gateway (TSGateway) vorgeschaltet werden über die Felder gateway_host + gateway_port (nicht zu verwechseln mit dem Home-Gateway-Mode). GateControl reicht diese Werte nur als Konfiguration an den Desktop-Client weiter; die TSGateway-Rolle selbst läuft ausserhalb.

Gateway-Mode: die gelinkte L4-Route

Der Sinn von access_mode='gateway': Der Windows-Host hat keinen eigenen WG-Peer, lebt rein im LAN hinter einem Home-Gateway. Der Server muss trotzdem einen externen Port bereitstellen, auf dem der Desktop-Client landet, und den TCP-Stream zum Gateway forwarden.

Auto-Create beim Save

_syncLinkedL4Route(rdpId, previousL4RouteId) in src/services/rdp.js wird bei create und bei relevanten update-Fällen aufgerufen. Bei 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}"`
})

Die zurückerhaltene route.id wandert in rdp_routes.gateway_l4_route_id. Schlägt der L4-Create fehl, wird die RDP-Route wieder gelöscht — kein halb-konfigurierter Zustand bleibt zurück.

Update-Synchronisation

Wenn im RDP-Update eines dieser Felder ändert: access_mode, gateway_peer_id, gateway_listen_port, host, port, enabled_syncLinkedL4Route wird erneut aufgerufen:

  • access_mode bleibt gateway → L4 wird per routes.update(previousL4RouteId, payload) in sync gebracht.
  • access_mode wechselt WEG von gateway → L4 wird gelöscht (routes.remove(previousL4RouteId)), gateway_l4_route_id = NULL.
  • access_mode wechselt AUF gateway → neuer L4 wird erzeugt.

Failure-Mode ist absichtlich non-fatal: Ein fehlschlagendes L4-Sync rollbackt NICHT die RDP-Seite. Das RDP-Row ist aus Sicht des Admins korrekt gespeichert, der Sync-Fehler wird geloggt, der Admin kann per erneutem Save nochmal triggern.

Cascade beim Delete und Toggle

  • remove(id) — zuerst routes.remove(gateway_l4_route_id), dann DELETE FROM rdp_routes. Reihenfolge stellt sicher, dass kein Orphan-L4 übrig bleibt, falls die RDP-Zeile plötzlich nicht mehr löschbar wäre.
  • toggle(id) — neue enabled-State wird auf die L4-Route propagiert, sodass das externe Listen-Port mit dem RDP-Status synchron ist.

Wake-on-LAN

wol_enabled=1 + wol_mac_address aktiviert WoL für die Route. Zwei Pfade:

Via Home-Gateway

Bei access_mode='gateway': Server ruft gateways.notifyWol(gateway_peer_id, {mac, lan_host, timeout_ms}) auf. Der Gateway sendet das Magic-Packet im LAN und antwortet mit {success, elapsed_ms}. Siehe home-gateway.md.

Via direkter Peer (kein Gateway)

Wenn der Windows-Host selbst ein WG-Peer ist und WoL erforderlich wäre: Der Server kann ein Magic-Packet über das Tunnel-Interface senden, sofern der Host im gleichen L2-Broadcast-Scope liegt wie ein LAN-Gerät. Praktisch selten — bei klassischen RDP-via-WG-Setups ist der Peer-Rechner entweder schon an oder wird von einem anderen LAN-WoL-Sender geweckt.

Wartungsfenster

maintenance_enabled=1 + maintenance_schedule (JSON-Array von Zeilen wie "Mo-Fr 08:00-18:00"). Parser:

  • Unterstützt deutsche Abkürzungen (Mo, Di, Mi, Do, Fr, Sa, So) und englische (Mon, Tue, Wed, Thu, Fri, Sat, Sun).
  • Range Mo-Fr oder einzelner Tag.
  • Zeitbereich HH:MM-HH:MM, gleichtagig.
  • Mehrere Zeilen → Union (ODER).

isInMaintenanceWindow(routeId) gibt zur Abfragezeit true zurück, wenn "jetzt" innerhalb des Fensters liegt. Der Desktop-Client fragt das vor einem Connect an und zeigt eine Warnung ("Wartung bis 18:00"); der User kann bestätigen und trotzdem verbinden.

Sessions

Separates 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 setzt status='active', loggt rdp_session_start in Activity.
  • heartbeat aktualisiert last_heartbeat; wenn eine Session N Minuten ohne Heartbeat bleibt, wird sie vom Sweeper als abandoned beendet.
  • end setzt status='ended'|'abandoned'|'admin_disconnect'|'normal', berechnet duration_s.

Admin-Dashboard zeigt:

  • GET /api/v1/rdp — pro Route die active_sessions-Anzahl, letzter Zugriff.
  • GET /api/v1/rdp/history — globale Session-Historie mit Filtern.
  • GET /api/v1/rdp/history/export — CSV/JSON-Export.
  • POST /api/v1/rdp/:id/sessions/disconnect-all — Admin-Kill für alle aktiven Sessions einer Route.

Access-Control

Pro Route:

  • user_ids — JSON-Array von User-IDs, neuere Policy. Wenn gesetzt und non-empty, haben nur diese User Zugriff.
  • token_ids — JSON-Array von API-Token-IDs, Legacy. Nur geprüft wenn user_ids leer oder nicht gesetzt.
  • Ohne beide → Route ist für alle Token/User sichtbar.

getForToken(tokenId, userId) in src/services/rdp.js realisiert die Priorität.

Lizenzierung

  • remote_desktop — alle RDP-Endpoints sind in src/routes/api/rdp.js hinter router.use(requireFeature('remote_desktop')).
  • rdp_via_gateway — wird zusätzlich geprüft bei Create/Update, wenn access_mode='gateway'. Ohne das Flag bleiben die drei anderen Modi verfügbar.

Monitor und FQDN-Hint

rdpMonitor.checkAll() macht pro-Route TCP-Probe auf den konfigurierten Port. Timeout 2s, Ergebnisse werden gecached und zusammen mit der Routes-List ausgeliefert.

Beim Connection-Info-Endpoint (/api/v1/client/rdp/:id/connection-info) wird, wenn der Ziel-Peer im internen DNS einen Hostname hat und internal_dns lizenziert ist, der FQDN mitgeliefert:

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

Hintergrund: CredSSP/NLA validiert das Server-Zertifikat des Windows-Host gegen den angegebenen Server-Namen. Mit reiner IP schlägt das fehl. Siehe internal-dns.md.

Siehe auch

  • home-gateway.md — wie der Gateway-Companion L4-Listener bereitstellt und WoL feuert.
  • routing.md — die gelinkte L4-Route nutzt denselben Mechanismus wie manuelle L4-Gateway-Routen.
  • internal-dns.md — FQDN für CredSSP.
  • licensing.mdremote_desktop, rdp_via_gateway.
  • ../API.md/api/v1/rdp/* und /api/v1/client/rdp/*.

Quelldateien

  • src/services/rdp.js — CRUD, Validierung, _syncLinkedL4Route, Credential-Handling, Maintenance-Parser.
  • src/services/rdpSessions.js — Session-Start/Heartbeat/End, Historie, CSV-Export.
  • src/services/rdpMonitor.js — Health-Probe pro 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

Wir verwenden Cookies, um Ihre Erfahrung zu verbessern. Essentielle Cookies sind immer aktiv.

Datenschutzerklärung
ESC
↑↓ navigate open esc close