CallMeTechie
EN Anmelden
Home Produkte Blog Über mich Kontakt

Home-Gateway

v1.0 · Updated vor 1 Monat

Überblick

Ein Home-Gateway ist ein Companion-Container, der in einem LAN (Heimnetz, Zweigstelle, VPS-Niederlassung) läuft und zwei Aufgaben übernimmt: Er terminiert eine WireGuard-Verbindung zum GateControl-Server und stellt den LAN-Geräten hinter sich so zur Verfügung, dass der Server sie erreichen kann — ohne dass jedes LAN-Gerät einen eigenen WireGuard-Peer braucht und ohne Port-Forwarding im Heimrouter.

Das Problem, das er löst: Ein Kunde will seinen NAS, seine Überwachungskamera und sein Synology-Panel über GateControl öffentlich machen. Mit reinen WG-Peers müsste auf jedem Gerät ein WireGuard-Client installiert werden (NAS ja, Kamera nein). Alternativ müsste der Heimrouter Port-Forwarding zum GateControl-Server machen, was bei CG-NAT sowieso nicht geht. Der Home-Gateway sitzt einmal im LAN als WG-Peer, baut von sich aus den Tunnel auf und nimmt LAN-seitig HTTP/TCP entgegen.

Der Gateway ist ein eigenes Projekt (gatecontrol-gateway unter /root/gatecontrol-gateway/), das wiederum Caddy-artig Konfiguration pulled, Routen zur LAN-Seite forwarded und seinen eigenen Health-State an den Server meldet. Der Server sieht den Gateway als speziellen Peer (peer_type='gateway') mit Zusatztabellen (gateway_meta) und verwendet ihn als Upstream-Indirection in Caddy.

Architektur

   Client ──HTTPS──▶ Caddy (Server) ──WG-Tunnel──▶ Gateway-Container ──LAN──▶ LAN-Host
                         │                              │
                         │ reverse_proxy                │ HTTP-Proxy :8080
                         │   → 10.8.0.X:8080            │   (liest X-Gateway-Target
                         │   + X-Gateway-Target         │    und dispatcht)
                         │   + X-Gateway-Target-Domain  │
                         │                              │ TcpProxyManager
                         │ (L4-Routen) ─────────────────▶   :l4_listen_port
                         │                              │   → lan_host:lan_port
                         ▼                              │
                   GateControl-Server                   │
                         ▲                              │
                         │ Heartbeat, Config-Check,     │
                         │ Probe                        │
                         ◀──── Gateway pulls über WG ───┘
                         │
                         │ Push-Kanal (notifyWol,       │
                         │ notifyConfigChanged)         │
                         ────────────────────────────▶  │

Beteiligte Rollen

Rolle Wo Aufgabe
Gateway-Peer (DB-Record) Server peers-Zeile mit peer_type='gateway' + gateway_meta
Gateway-Companion Kundennetz Docker-Container aus gatecontrol-gateway
WireGuard-Tunnel zwischen beiden Gateway = /32, Server = /32 — beide Seiten kennen genau einen Peer
Caddy-Indirection Server Gateway-Routen sind reverse_proxy → 10.8.0.X:8080
HTTP-Proxy Gateway lauscht auf :8080, dispatcht nach X-Gateway-Target
TcpProxyManager Gateway lauscht für L4-Routen auf dedizierten Ports

Datenmodell

Tabelle peers (relevante Spalten)

peers (
  id, name, public_key, private_key_encrypted, preshared_key_encrypted,
  allowed_ips,                            -- "10.8.0.X/32"
  peer_type TEXT DEFAULT 'regular',       -- 'regular' | 'gateway'
  hostname,                               -- (siehe internal-dns.md)
  ...
)

Tabelle gateway_meta

gateway_meta (
  peer_id INTEGER PRIMARY KEY REFERENCES peers(id) ON DELETE CASCADE,
  api_port INTEGER NOT NULL DEFAULT 9876,
  api_token_hash TEXT NOT NULL,           -- sha256: vom API-Token
  push_token_encrypted TEXT NOT NULL,     -- AES-GCM-verschlüsselt
  needs_repair INTEGER DEFAULT 0,         -- Pairing-Invalid-Marker
  last_seen_at INTEGER,                   -- epoch ms des letzten Heartbeats
  last_health TEXT,                       -- JSON des letzten Heartbeat-Bodies
  last_config_hash TEXT,                  -- letzter Hash aus /config/check
  created_at INTEGER
)

Die beiden Tokens haben getrennte Aufgaben:

Token Richtung Verwendung
api_token Gateway → Server Bearer-Auth für /api/v1/gateway/* Endpoints
push_token Server → Gateway Header X-Gateway-Token für Push-Kanal (/api/wol, /api/config-changed)

Der API-Token wird als SHA-256 gespeichert (wie alle Bearer-Tokens). Der Push-Token muss der Server im Klartext abrufen können, darum AES-GCM-verschlüsselt. Der Klartext wird einmal beim Create oder Rotate ausgegeben.

Provisioning-Flow

  1. Admin ruft POST /api/v1/peers mit is_gateway: true auf.
  2. Server prüft Lizenzlimit gateway_peers, erzeugt WG-Keypair, schreibt peers-Zeile mit peer_type='gateway'.
  3. createGateway() generiert API-Token und Push-Token (je 32 Random Bytes), speichert Hash bzw. verschlüsselt.
  4. Server baut eine .env-Datei (Template in buildEnvContent()) mit:
    • Server-URL, API-Token, Push-Token (Klartext, nur jetzt einsichtbar)
    • WireGuard-Privatekey (vom Peer, entschlüsselt aus DB)
    • WG_ADDRESS=10.8.0.X/32, WG_ALLOWED_IPS=10.8.0.1/32 — bewusst /32, damit der Gateway-Host nicht zwei konkurrierende /24-Routen für dasselbe VPN hat (siehe Code-Kommentar buildEnvContent).
  5. Admin lädt die .env, legt sie in den gatecontrol-gateway-Compose-Stack und startet docker compose up -d.
  6. Gateway-Container baut WG-Tunnel auf, sendet ersten Heartbeat. State-Machine geht nach 4/5 Erfolgen auf online.

Token-Rotation

POST /api/v1/peers/:id/gateway-env/rotate ruft rotateGatewayTokens() auf:

  • Neue Tokens + Hashes werden generiert.
  • gateway_meta.api_token_hash und push_token_encrypted werden überschrieben.
  • needs_repair=0 (falls der Gateway vorher als repair-needed markiert war).
  • buildEnvContent() liefert die neue .env-Datei.

Der laufende Container ist ab sofort mit seinem alten API-Token gegen den Server gesperrt. Der Admin muss die neue .env ausrollen und neustarten.

Heartbeat und Health

Protokoll

Der Gateway sendet alle ~30 Sekunden (GC_HEARTBEAT_INTERVAL_S) einen POST /api/v1/gateway/heartbeat mit Bearer-Auth:

{
  "uptime_s": 12345,
  "hostname": "nas1",
  "http_proxy_healthy": true,
  "tcp_listeners": [
    {"port": 3389, "status": "listening"},
    {"port": 8000, "status": "listener_failed", "error": "EADDRINUSE"}
  ],
  "route_reachability": [
    {"route_id": 17, "target": "192.168.1.50:80", "reachable": true, "elapsed_ms": 3}
  ]
}

Seiteneffekte im Server

handleHeartbeat(peerId, health) in src/services/gateways.js:

  1. gateway_meta.last_seen_at und last_health aktualisieren.
  2. _getSm(peerId).recordHeartbeat(healthy) — StateMachine tickt.
  3. Bei Status-Transition: _onStatusTransition → Caddy-Partial-Patch, Activity-Log, Email-Alert, Webhook.
  4. Bei Flap (>4 Transitions/h): gateway_flap_warning loggen.
  5. Bei aktivem internal_dns: Hostname-Feld als Agent-Report an peers.setHostname(peerId, body.hostname, 'agent'). Sticky-Admin-Policy bleibt eingehalten (siehe internal-dns.md).

_isHeartbeatHealthy — warum diese Priorität

Die Funktion entscheidet, ob ein Heartbeat einen "healthy tick" auslöst. Die Prioritätsreihenfolge:

  1. route_reachability — wenn vorhanden und nicht leer: alle Einträge müssen reachable:true sein.
  2. Self-Check-Fallback (http_proxy_healthy + tcp_listeners) — wenn keine Reachability-Probe ging: http_proxy_healthy muss true sein und kein listener_failed.
  3. Bare Heartbeat — wenn weder Reachability noch Self-Check-Feld da ist: als healthy werten. Der Prozess ist offensichtlich am Leben.

Hintergrund: In NAS1-artigen Deployments bindet der HTTP-Proxy auf einer bestimmten Netzwerk-Schnittstelle, während der Self-Check auf Localhost geht und schlägt fehl — obwohl die konfigurierten LAN-Targets tatsächlich antworten. Die alte Logik hätte diese Gateways fälschlich offline markiert. route_reachability ist die empirische Ground-Truth; der Self-Check nur Fallback.

State-Machine

src/services/gatewayHealth.jsStateMachine-Klasse:

Parameter Default Bedeutung
windowSize 5 Grösse des Sliding-Window
offlineThreshold 3 Fails im Fenster → offline
onlineThreshold 4 Erfolge im Fenster → online
cooldownMs 5 min Mindestzeit zwischen Transitions
  • Aus unknown geht es ohne Cooldown nach online/offline.
  • Stable-state → stable-state nur nach Cooldown, um Flapping zu dämpfen.
  • flapCountLastHour() zählt Transitions im letzten 60-Minuten-Fenster (ohne die unknown-Warmup-Transition).
  • StateMachines werden in _smCache pro peerId gehalten (Memory, nicht persistiert). Nach Server-Restart geht der State auf unknown bis erste Heartbeats kommen.

Config-Pull

Der Gateway pulled seine Config, statt sie gepusht zu bekommen:

  1. GET /api/v1/gateway/config/check?hash=sha256:… — Gateway sendet den Hash seiner aktuellen Config.
  2. Server ruft computeConfigHash(peerId) auf. Bei Match: 304 Not Modified. Sonst: 200 + neuer Hash.
  3. Bei Mismatch pulled der Gateway GET /api/v1/gateway/config und übernimmt das JSON-Payload.

getGatewayConfig(peerId) liefert alle aktiven HTTP- und L4-Routen mit target_peer_id = peerId. Das JSON-Schema ist per CONFIG_HASH_VERSION versioniert und in einer geteilten Library @callmetechie/gatecontrol-config-hash implementiert — der Gateway nutzt dieselbe Bibliothek und berechnet seinen lokalen Hash byte-identisch.

Das Polling-Intervall (GC_POLL_INTERVAL_S=300) ist die Fallback-Frequenz. Für schnellere Updates nutzt der Server den Push-Kanal.

Push-Kanal

Der Server kann dem Gateway aktiv sagen, dass was passiert ist — über seinen API-Port (Default 9876), der über den WG-Tunnel zurück zum Gateway-Container erreichbar ist.

notifyConfigChanged(peerId)

Feuert nach jedem Route-Save, das einen Gateway betrifft. Body-less POST an http://<gateway-ip>:<api_port>/api/config-changed mit Header X-Gateway-Token: <push-token>. Der Gateway debounced 500ms und pulled dann frische Config.

Best-effort: Timeout 2s, Fehler werden geloggt aber nicht retried. Das nächste normale Poll-Interval würde es sowieso erledigen.

notifyWol(peerId, {mac, lan_host, timeout_ms})

Feuert, wenn eine Route mit wol_enabled=true aufgerufen wird oder der Admin explizit WoL triggert. POST an /api/wol mit JSON-Body; der Gateway sendet im LAN das Magic-Packet und antwortet mit {success, elapsed_ms}.

Timeout ist timeout_ms + 5000 (Gateway braucht Zeit zum Warten auf Boot-Response).

Caddy-Integration

In caddyConfig.buildCaddyConfig():

  • Bei target_kind='gateway' wird der Upstream auf gatewayPeerIp:8080 gesetzt (gatewayProxyPort, konfigurierbar).
  • Headers werden injiziert: X-Gateway-Target: <lan_host>:<lan_port>, X-Gateway-Target-Domain: <domain>.
  • backend_https wird bewusst ignoriert, weil zum Gateway-Port immer HTTP spricht. Wenn das LAN-Target HTTPS will, regelt das der Gateway selbst (via Flag backend_https im Gateway-Config, der dann LAN-seitig TLS spricht mit insecure_skip_verify).
  • Bei Status-Transition patcht patchGatewayRouteHandlers den Live-Caddy: offline → static_response 502 mit Maintenance-HTML, online → revert.

L4-Gateway-Routen

L4-Gateway-Routen laufen NICHT über Caddys HTTP-Server, sondern über apps.layer4. Upstream ist gateway-peer-ip:l4_listen_port — der Gateway-Container hat auf diesem Port seinen eigenen TCP-Listener (TcpProxyManager), der zum LAN-Host forwardet. Der Server-Caddy reicht nur die TCP-Verbindung weiter. Siehe routing.md für Details.

Config-Hash

Zentrales Instrument für "sind Server und Gateway in sync?". computeConfigHash(peerId) ruft getGatewayConfig(peerId) und delegiert an die geteilte Library, die aus dem JSON eine kanonische Form (Keys sortiert, keine Zahlentypen-Drift) baut und SHA-256 darüber bildet. Server und Gateway benutzen dieselbe Library — bei Mismatch weiss man, dass einer von beiden out-of-sync ist.

Im Admin-UI wird der Hash als kurzes Kürzel (erste 8 Zeichen) neben dem Gateway-Panel angezeigt.

Lizenzierung

  • gateway_peers — Limit für Anzahl Gateway-Peers. Enforced in createGateway().
  • gateway_http_targets — Limit für HTTP-Routen pro Gateway (enforced im Route-Create).
  • gateway_tcp_routing — Feature-Flag für L4-Routen auf Gateways.
  • gateway_wol — Feature-Flag für WoL-Trigger via Gateway.

Siehe licensing.md.

Siehe auch

  • routing.md — wie Caddy Gateway-Upstreams rendert.
  • internal-dns.md — Hostname-Reports via Heartbeat.
  • rdp-routes.md — RDP-Gateway-Mode nutzt den Push-Kanal für WoL und erzeugt L4-Gateway-Routen.
  • licensing.mdgateway_*-Feature-Flags.
  • ../API.md/api/v1/gateway/* Companion-Endpoints und /api/v1/gateways Admin-View.

Quelldateien

  • src/services/gateways.jscreateGateway, handleHeartbeat, _isHeartbeatHealthy, notifyWol, notifyConfigChanged, computeConfigHash, buildEnvContent, rotateGatewayTokens.
  • src/services/gatewayHealth.jsStateMachine-Klasse mit Sliding-Window-Hysterese.
  • src/routes/api/gateway.js — Companion-Endpoints (/heartbeat, /config, /config/check, /status, /probe).
  • src/routes/api/gateways.js — Admin-View /api/v1/gateways mit Status + Routes pro Gateway.
  • src/middleware/gatewayAuth.js — Bearer-Token-Auth für Companion-Endpoints.
  • src/db/migrations.js — Migration 36 (add_gateway_support), 37 (gateway_meta_last_health).
  • /root/gatecontrol-gateway/ — Companion-Projekt (separates Repo).

Cookie Settings

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

Datenschutzerklärung
ESC
↑↓ navigate open esc close