Gateway-Routen auf Layer 4 (TCP/UDP-Port-Forwarding)
Seit v1.42.0 funktionieren L4-Routen mit target_kind=gateway
end-to-end. Damit lässt sich jeder TCP- oder UDP-Port eines
LAN-Geräts hinter dem Gateway-Container über den öffentlichen
Server veröffentlichen — unabhängig vom Application-Layer-Protokoll.
Typische Use-Cases:
- RDP auf Port 3389 (Windows-Server, Workstation)
- SSH auf Port 22 (Linux-Server, Switches)
- Datenbanken (MySQL 3306, PostgreSQL 5432, MSSQL 1433)
- Mailserver (IMAPS 993, SMTP 587)
- Proprietäre Protokolle (MQTT 1883, Modbus 502, Industrie-Steuerungen)
- Game-Server (Minecraft 25565, Valheim 2456, etc.)
- Medienserver (Plex 32400, Jellyfin 8096 via TCP statt Reverse-Proxy)
- Alles was kein HTTP ist und trotzdem ins LAN soll
Datenfluss
Internet-Client
│
├─ TCP-connect → Server-Public-IP:3389
▼
Caddy Layer-4-Plugin (VPS)
│
│ Layer-4 Proxy (kein HTTP-Parsing, einfach Byte-Stream)
│ Upstream: gateway-tunnel-IP:3389
▼
WireGuard-Tunnel
│
▼
Gateway-Container TcpProxyManager (Heimnetz, tunnel_ip:3389)
│
│ Forwarded zu target_lan_host:target_lan_port
▼
LAN-Gerät (z.B. 192.168.2.100:3389 = Windows-RDP)
Der Listen-Port (an dem der Server im Internet lauscht) und der Port den der Gateway-Container auf der Tunnel-IP öffnet sind identisch — 1:1-Mapping. Der LAN-Host und der LAN-Port dahinter sind frei wählbar und können sich unterscheiden (z.B. Listen 2222 → LAN 192.168.2.100:22, falls dir nicht danach ist, den SSH-Port des Servers mit Port 22 zu belegen).
Voraussetzungen
- Pro-Lizenz: Das Feature-Flag
gateway_tcp_routingist im Community-Fallback auffalse. Ohne Pro-Key blockt der API-Handler den Route-Create mit403 gateway_tcp_routing not licensed. - Listen-Port darf nicht auf der Blocklist stehen. Default:
80, 443, 2019, 3000, 51820(HTTP, HTTPS, Caddy-Admin-API, Node-App, WireGuard). Konfigurierbar via EnvGC_L4_BLOCKED_PORTS. - Listen-Port darf nicht zweimal mit derselben TLS-Einstellung
belegt sein. Konflikte werden bei
buildCaddyConfig()erkannt und als Error geworfen (Route-Create rollt zurück). - Der Gateway-Container muss erreichbar sein — heißt WG-Tunnel up und Handshake aktiv. Ohne Tunnel kein L4-Forwarding.
Anlegen im Admin-UI
- Routes → Neue Route (oder existierende bearbeiten)
- Type auf
L4umschalten (der HTTP/L4-Toggle oben im Create-Form) - Ziel-Typ auf
Home Gateway (LAN)setzen - Home Gateway auswählen (der Gateway-Peer durch den geroutet wird)
- LAN-Host eintragen (die IP des Zielgeräts im Heimnetz, z.B.
192.168.2.100) - LAN-Port eintragen (z.B.
3389für RDP) - L4-Protokoll wählen:
TCPoderUDP - L4-Listen-Port eintragen. Meist identisch zum LAN-Port (z.B.
3389). Kann aber abweichen, wenn der Server-Port bereits belegt ist oder du bewusst eine andere Port-Nummer nach außen geben willst. - L4-TLS-Mode:
- None: Caddy reicht den Stream durch ohne jede TLS-Behandlung. Passt für RDP, SSH, DB-Ports, alles was nativ kein TLS spricht ODER seine eigene TLS-Terminierung macht.
- Passthrough: Caddy liest nur den SNI aus dem Client-Hello zum Routing und leitet den kompletten TLS-Handshake durch. Passt wenn das LAN-Ziel HTTPS macht und du das Cert aufs Ziel-Gerät auslagern willst. Erfordert eine Domain.
- Terminate: Caddy terminiert TLS auf dem Server und spricht plain TCP zum Gateway. ACME-Cert via DNS/HTTP-01. Erfordert eine Domain.
- Domain (nur bei TLS-Modes ≠ None): FQDN der per SNI zum
Routing genutzt wird, z.B.
rdp.example.com. - Speichern
Der Gateway-Container bekommt die Änderung via Config-Sync sofort
(Push-Notification vom Server → /api/config-changed → Gateway pollt
→ TcpProxyManager.setRoutes() startet den neuen Listener).
Port-Ranges
Wenn du einen zusammenhängenden Port-Bereich brauchst (z.B. FTP
Passive-Mode, oder mehrere aufeinanderfolgende Game-Server-Ports),
trägst du den Bereich als L4-Listen-Port ein:
L4-Listen-Port: 2000-2099
LAN-Port: 2000
Caddy öffnet dann alle 100 Ports auf dem Server und forwarded jeden 1:1 an den Gateway. Der Gateway startet einen einzelnen Listener der alle eingehenden Connections an die LAN-Zieladresse weitergibt.
Beispiel Game-Server mit vielen Worker-Ports:
L4-Listen-Port: 27015-27020
LAN-Host: 192.168.2.50
LAN-Port: 27015
L4-Protokoll: UDP
Wildcard "alle Ports eines Geräts" ist nicht möglich
Das ist eine bewusste Design-Entscheidung:
- Der Server selbst braucht seine eigenen Ports (TLS, Admin-UI, SSH, WireGuard). Ein „schluck alles" würde die Servermaschine selbst unerreichbar machen.
- Port-Konflikte mit anderen L4-Routen (z.B. zu einem zweiten Gateway) wären nicht mehr auflösbar.
- DDoS-Exposure wäre enorm.
Stattdessen: explizit die Ports aufzählen, die du wirklich brauchst. Jeder Port = eine Route = ein Firewall-Regel-Äquivalent.
Mehrere Gateways, gleicher Port
Wenn du zwei Gateways hast die beide z.B. SSH (Port 22) freigeben wollen, musst du unterschiedliche Listen-Ports vergeben:
gateway-a.domain: Listen-Port 2222 → Gateway-A tunnel-IP:22 → LAN-A:22
gateway-b.domain: Listen-Port 2223 → Gateway-B tunnel-IP:22 → LAN-B:22
Für HTTP/S-Routen läuft das über SNI (Domain-basiertes Routing auf 443). Für L4 ohne TLS-Mode hat Caddy keine Möglichkeit den Stream auseinanderzuhalten — deshalb pro Server-Listen-Port exakt ein Ziel.
Alternativ: L4 mit TLS-Mode passthrough oder terminate — dann
kann Caddy per SNI zwischen mehreren Diensten auf demselben
Listen-Port unterscheiden (z.B. rdp1.example.com und
rdp2.example.com beide auf 3389, aber der SNI unterscheidet).
Wake-on-LAN
L4-Gateway-Routen unterstützen den WoL-Flag genauso wie HTTP-Gateway-
Routen. Wenn der Gateway beim Connect-Versuch ECONNREFUSED vom
LAN-Target bekommt und der WoL-Flag auf der Route gesetzt ist, schickt
er ein Magic-Packet an die konfigurierte MAC. Kombiniert mit dem
automatischen Retry des Clients (TCP SYN-Retransmission) kommt die
Verbindung meist im zweiten Anlauf durch — das Zielgerät ist dann
aus dem Standby aufgewacht.
Blockierte Ports
Server-Ports die nicht als L4-Listen-Port genutzt werden können (weil der Server sie selbst belegt oder sie sicherheitskritisch sind):
| Port | Zweck |
|---|---|
| 22 | SSH-Daemon des VPS (fast immer belegt) |
| 53 | systemd-resolved / host-dnsmasq (mit network_mode: host geteilt) |
| 80 | HTTP Auto-HTTPS (Caddy) |
| 443 | HTTPS (Caddy) |
| 2019 | Caddy Admin-API (intern) |
| 3000 | Node-App (interne Admin-API) |
| 51820 | WireGuard Server-Endpoint |
Überschreibbar via Env: GC_L4_BLOCKED_PORTS="22,53,80,443,2019,3000,51820".
Praktischer Tipp: Wenn du einen LAN-Dienst auf einem blockierten
Standard-Port hast (z.B. SSH auf 22), nimm einen anderen Listen-Port
nach außen (z.B. 2222) und halte den LAN-Port gleich:
Listen-Port: 2222 (extern, frei wählbar)
LAN-Port: 22 (SSH auf dem Ziel-Gerät)
Der Client ruft dann ssh -p 2222 user@example.com auf.
Nicht empfohlen, die Defaults aufzuweichen — Port-Konflikt mit der
eigenen Infrastruktur wäre die Folge.
TLS-Modes im Detail
none (TCP Passthrough)
Einfachster Fall. Caddy ist pure Byte-Forwarder. Kein TLS-Handling. Domain-Feld optional (kein SNI-Routing). Nur ein Ziel pro Listen-Port erlaubt (keine Mehrdeutigkeit auflösbar).
Client ──TCP──▶ Server:3389 ──TCP──▶ Gateway:3389 ──TCP──▶ LAN:3389
passthrough (SNI-Sniffing ohne Termination)
Caddy liest das Client-Hello um den SNI zu extrahieren und routet basierend darauf. Der komplette TLS-Handshake + verschlüsselter Traffic laufen zum LAN-Ziel. TLS-Cert liegt auf dem LAN-Gerät.
Mehrere Ziele auf demselben Listen-Port möglich, wenn sie unterschiedliche Domains nutzen.
Client ──TLS+SNI──▶ Caddy (liest SNI, Byte-Forward den Rest) ──▶ LAN
terminate (TLS-Termination auf dem Server)
Caddy terminiert TLS, forwarded den entschlüsselten Stream an Gateway/LAN. ACME-Cert wird auf dem Server via DNS-/HTTP-01-Challenge ausgestellt.
Client ──TLS──▶ Caddy (decrypt) ──TCP──▶ Gateway ──TCP──▶ LAN
Passt, wenn das LAN-Ziel kein HTTPS beherrscht oder du die Cert-Verwaltung zentral lassen willst.
Compat / Hashing
L4-Routen sind schon im Config-Hash seit der ersten Gateway-Version. Der Bugfix in v1.42.0 ändert nur die Caddy-Config auf dem Server — das Gateway selbst bekommt denselben L4-Routen-Payload wie zuvor. Kein Hash-Bump, keine Breaking-Changes, keine Migration nötig.
Relevante Dateien
Server
src/services/l4.js— baut Caddy-Layer-4-Config (buildL4Servers,buildL4Route,validatePortConflicts)src/services/caddyConfig.js— Pre-Processing: setzt für Gateway-L4-Routentarget_ip = <gateway-tunnel-ip>undtarget_port = <l4_listen_port>bevorbuildL4Serversläuftsrc/services/gateways.js—getGatewayConfig()liefertl4_routes[]an den Gatewaysrc/routes/api/routes.js— License-Gategateway_tcp_routingim POST + PUT Handler
Gateway
src/proxy/tcp.js—TcpProxyManagerstartet Listener auf tunnel_ip:listen_port und forwarded zu target_lan_host: target_lan_port, Dual-Bind-Overlap bei Port-Änderungensrc/bootstrap.js—tcpMgr.setRoutes(cfg.l4_routes)im config-change Event-Handler
Tests
tests/caddyConfig_gateway.test.jspinnt das neue Verhalten: „L4 gateway route forwards to gateway-peer-ip:listen_port (not the 127.0.0.1 placeholder)"tests/gateways_getConfig.test.jsdeckt die L4-Sync-Payload ab- Gateway:
tests/proxy_tcp.test.jstestet den TcpProxyManager
Troubleshooting
Route zeigt beim Speichern gateway_tcp_routing not licensed
Du hast keine Pro-Lizenz oder der Community-Fallback ist aktiv. Feature ist Pro-only.
Client bekommt Connection-Refused
- Gateway offline? Check
docker psundsudo docker logs gatecontrol-gateway - WG-Tunnel down? Auf dem Server
wg show wg0 | grep handshake— die Zeile fürs Gateway-Peer sollte <2 Minuten alt sein - LAN-Target offline? Gateway checkt das nicht vorab — er versucht die TCP-Verbindung erst beim Client-Connect. WoL auf der Route aktivieren wenn es sich ums Aufwecken handelt.
„Port conflict" beim Save
Ein anderer L4-Route benutzt denselben Listen-Port schon. Server listet beide IDs im Error. Entweder: anderen Listen-Port wählen, oder — falls beide Routen TLS nutzen — sicherstellen dass sie unterschiedliche Domains per SNI haben.
Verbindung klappt von draußen, aber nicht aus dem VPN
Client ist schon im selben WG-Subnet wie der Gateway — nimm den LAN-Pfad direkt zum Zielgerät statt den Umweg über das öffentliche Caddy. Dafür sind Split-Horizon-DNS + direktes WG-Peering da.
„tls: first record does not look like a TLS handshake"
Du hast TLS-Mode auf terminate gesetzt, aber das LAN-Ziel will
selbst TLS terminieren (spricht also nur HTTPS). Wechsle auf
passthrough oder none.