CallMeTechie
DE Login
Home Products Blog About Contact

Routes & HTTPS

v1.x · Updated 1 month ago

Overview

GateControl doesn't speak HTTP to the outside itself — public termination is handled by Caddy. The Node process builds a Caddy JSON configuration from its database and reloads it via the Caddy admin API. There are two route types (http, l4) and two target kinds (peer, gateway). These two dimensions yield four combinations, which are rendered differently in the config builder.

The central insight, without which much is hard to understand: all HTTP routes land in a single Caddy server srv0 with listen: [':443', ':80']. The per-route listen field is discarded during assembly. Caddy's Automatic HTTPS provisions certificates for every domain in srv0, regardless of the https_enabled flag of the route. This flag only serves as a hint for redirects.

The routing core lives in src/services/caddyConfig.js (~900 lines) and src/services/l4.js. The CRUD part of the routes table sits in src/services/routes.js and delegates the Caddy side entirely to caddyConfig.syncToCaddy.

Architecture

   Client ──HTTPS──▶ Caddy (srv0, :443/:80)
                      │
                      ├──▶ reverse_proxy → Peer-IP:Port        (target_kind=peer)
                      │
                      ├──▶ reverse_proxy → Gateway-IP:8080     (target_kind=gateway)
                      │     mit Headers X-Gateway-Target=…
                      │                    │
                      │                    ▼
                      │           Gateway-HTTP-Proxy → LAN-Host:Port
                      │
                      └──▶ apps.layer4 (eigene L4-Server pro Port)
                                │
                                ├──▶ proxy → Peer-IP:Port
                                └──▶ proxy → Gateway-IP:ListenPort
                                             │
                                             ▼
                                     Gateway-TcpProxy → LAN-Host:Port

Route types and target kinds

route_type target_kind Upstream in Caddy Backend
http peer peer.allowed_ips:route.target_port Peer WG IP, direct TCP
http gateway gateway.allowed_ips:8080 Gateway container over WG tunnel, HTTP proxy
l4 peer peer.allowed_ips:route.target_port Peer WG IP, L4 proxy
l4 gateway gateway.allowed_ips:l4_listen_port Gateway container, own L4 listener

The target_kind column in routes controls the upstream build; target_peer_id points to the gateway peer for gateway routes, and target_lan_host + target_lan_port are passed as instructions for the gateway in the header.

Data model (excerpt)

routes (
  id, domain, route_type,           -- 'http' | 'l4'
  enabled, https_enabled,

  -- Peer-Routen
  peer_id,                          -- FK peers.id, Upstream-Peer
  target_ip, target_port,

  -- Gateway-Routen
  target_kind,                      -- 'peer' | 'gateway'
  target_peer_id,                   -- FK peers.id, Gateway-Peer
  target_lan_host, target_lan_port, -- LAN-Adresse HINTER dem Gateway

  -- HTTP-Features
  basic_auth_enabled, basic_auth_user, basic_auth_password_hash,
  acl_enabled, ip_filter_enabled,
  rate_limit_enabled, rate_limit_requests, rate_limit_window,
  compress_enabled, retry_enabled, retry_count,
  circuit_breaker_enabled, circuit_breaker_status,
  bot_blocker_enabled, bot_blocker_mode, bot_blocker_config,
  backend_https,                    -- nur peer-target
  custom_headers, mirror_enabled, mirror_targets,
  sticky_enabled, backends,          -- Load-Balancing

  -- L4-Felder
  l4_protocol, l4_listen_port, l4_tls_mode,

  -- WoL (hauptsächlich Gateway-Kontext)
  wol_enabled, wol_mac
)

HTTP routing flow

buildCaddyConfig() (in caddyConfig.js) iterates over all enabled routes and builds a Caddy route object per domain. Steps per HTTP route:

  1. Determine upstream

    • target_kind='gateway' → upstream is gatewayPeerIp:8080.
    • Multiple backends → array for load balancing.
    • Otherwise peer IP or target_ip + target_port.
  2. Gateway offline fallback — when the gateway is offline (flag gateway_offline), a static_response with 502 + maintenance HTML is rendered instead of reverse_proxy (template gateway-offline.njk). See patchGatewayRouteHandlers for runtime patches via admin API.

  3. Gateway headers — for target_kind='gateway', these are set in the reverse_proxy headers:

    • X-Gateway-Target: target_lan_host:target_lan_port
    • X-Gateway-Target-Domain: route.domain

    The gateway container interprets these headers and forwards to the LAN address. See home-gateway.md.

  4. Backend HTTPSbackend_https=true enables insecure_skip_verify as transport. Only for target_kind='peer', never for gateway routes: HTTP always goes to the gateway port 8080, TLS would trigger a 502.

  5. Build the handler chain (in order):

    bot_blocker (defender) → trace (debug) → custom_headers (request)
    → rate_limit → mirror → compress → reverse_proxy
    
  6. Circuit breaker — if status='open', the entire chain is replaced by a static_response 503 (with Retry-After header).

  7. Peer ACLmatch.remote_ip.ranges from the route_peer_acl table. Only peers in the allow list may pass.

  8. Route auth / forward-auth — see below.

  9. Mount route config under caddyRoutes[route.domain].

Route auth (forward-auth)

Route auth inserts a second route BEFORE the main chain:

caddyRoutes[domain] = {
  routes: [
    routeAuthProxy,    // match /route-auth/* → Node-Process (Login-UI + /verify)
    routeConfig        // match anything else → forward_auth-Subrequest → upstream
  ]
}

The forwardAuthSubrequest calls 127.0.0.1:3000/route-auth/verify with the request metadata as headers. On 2xx, the request is continued; on 4xx/5xx, Caddy returns a 302 to /route-auth/login?route=…&redirect=…. After login, the Node server sets a session cookie, which the next subrequest accepts again.

Only /route-auth/* is intercepted — earlier versions also claimed /css, /js, /fonts for the login asset bundle; that collided with upstream panels (Synology, Speedport, TR-064). See the comment in caddyConfig.js starting around line 420.

Load balancing

backends is a JSON array with {peer_id, port, weight}. At build time, peer IPs are resolved and disabled peers are filtered out.

Mode Policy
sticky_enabled=true cookie + configurable cookie name + TTL
All weights equal round_robin
Different weights weighted_round_robin

retry_enabled additionally increases load_balancing.retries = retry_count.

Server assembly

After the per-route build, all domains are merged into a single server srv0:

caddyConfig.apps.http.servers.srv0 = {
  listen: [':443', ':80'],
  routes: serverRoutes,             // [{match: {host: [domain]}, handle: [...]}]
  protocols: ['h1', 'h2'],
}

Individual routes are mounted via {match: {host: [domain]}, handle: [...], terminal: true}. Compound routes (route auth) are wrapped in a subroute handler so that the internal route list is preserved.

The management UI route (from GC_BASE_URL) is also inserted automatically, with 127.0.0.1:{config.app.port} as upstream — so the Node process serves its own admin interface through Caddy.

Consequence for TLS

Since srv0 listens on :443 and all host matchers are contained within it, Caddy's Automatic HTTPS provisions a certificate for every domain, regardless of the https_enabled flag of the individual route. In the client UI the flag now only serves as a hint as to whether the admin expects a TLS endpoint — it controls no listener.

L4 routing

L4 routes live under apps.layer4.servers.<name> (Caddy L4 plugin). buildL4Servers in src/services/l4.js groups routes by (protocol, listen_port, tls_mode) and creates a server per group:

servers['l4-tls-8443'] = {
  listen: ['tcp/:8443'],
  routes: [
    { match: [{tls: {sni: ['app.example.com']}}], handle: [{handler: 'tls'}, proxyHandler] },
    ...
  ]
}

TLS modes

l4_tls_mode Behavior
none Pass raw TCP/UDP through, no match, no TLS
passthrough Match via SNI, but Caddy does NOT terminate — certificate lives at the backend
terminate Match via SNI, Caddy terminates TLS (certificate is provisioned), backend sees plaintext

Port conflicts

validatePortConflicts() checks:

  • Reserved ports (via isPortBlocked) — e.g. 22, 53, 443, 51820.
  • Duplicate TLS-none routes on the same port.
  • Overlapping port ranges (8000-8100 vs 8050-8150).

Conflicts throw and abort the config push.

Gateway L4

For target_kind='gateway' + route_type='l4', the upstream is set to gateway-peer-ip:l4_listen_port. The gateway container itself listens on this port (via its TcpProxyManager) and forwards to the LAN host. Caddy on the server only passes the TCP connection through to the gateway.

Sync flow and self-healing

syncToCaddy() in caddyConfig.js:

  1. Previous config fetched via GET /config/ (for rollback).
  2. Build new config with buildCaddyConfig().
  3. Write runtime.json to /data/caddy/runtime.json (atomic rename). This file is the source of truth at Caddy boot via entrypoint.sh.
  4. Live reload via POST /load.
  5. Verify via GET /config/ — the config must have arrived.
  6. TLS canary_verifyLocalTls(managementHost) opens a TLS connection to 127.0.0.1:443 with the management UI domain as SNI. If that fails even though /load was ok, Caddy's listener state is corrupt. Then:
  7. Caddy restart via pkill -TERM -x caddy. Supervisord starts Caddy again from runtime.json.
  8. On verify failure without TLS issue: rollback to previousConfig.

The TLS canary addresses a concrete bug: /load can in certain situations (listener rebind, new cert) respond successfully while the server still aborts every TLS handshake with internal error. A supervisord restart reliably resolves the state.

Partial patches for gateway state

patchGatewayRouteHandlers({peerId, offline, ...}) is called by the gatewayHealth state machine when a gateway transitions from online to offline (or back). Every route gets an @id marker (gc_route_<id>) during assembly, so that PATCH /id/gc_route_<id>/handle can swap out the handler in the live config — without a full /load round trip.

  • offline=true → handler to static_response 502 + maintenance HTML.
  • offline=false/revert (handler back to original).

See also

Source files

  • src/services/caddyConfig.js — Config builder, sync, self-heal, partial patches.
  • src/services/l4.jsbuildL4Servers, buildL4Route, validatePortConflicts.
  • src/services/routes.js — CRUD, delegates sync to caddyConfig.syncToCaddy.
  • src/services/routeAuth.js — Route-auth lookup helper for the forward-auth handler.
  • templates/gateway-offline.njk — Maintenance page for offline gateways.
  • entrypoint.sh — Caddy boot from /data/caddy/runtime.json.

Cookie Settings

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

Privacy Policy
ESC
↑↓ navigate open esc close