From d14e0a94eb7cebc02acedd60f5ce4351a5fcbcaf Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 16 Sep 2026 19:28:34 -0700 Subject: [PATCH] hermes vhost: dial netavark's host address, not the LAN IP (fixes the 502) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hermes-webui is Network=host, so 8787 is a bare host-netns bind, and netavark DNATs only *published* ports. From Caddy's bridge netns 192.168.0.143:8787 and 10.89.1.1:8787 are both refused, while host.containers.internal:8787 (169.254.1.2) answers 200 — measured live 2026-09-16 from inside the Caddy container. The dashboard's published 9119 answers on that same LAN IP, which is what made the old upstream look sound. The vhost comment asserted "192.168.0.143:8787 is open" from that bridge; it is not, so the claim is replaced with the measurement, and the hermes-webui quadlet comment carries the same correction. The seam guarding this invariant accepted any numeric upstream, so the LAN IP passed it; it now pins the measured-working form and fails on the LAN IP (teeth verified by substitution). --- dankclaw/Caddyfile | 13 ++++++----- .../hermes/quadlet/hermes-webui.container | 10 +++++---- tests/hermes_webui.bats | 22 ++++++++++++++----- 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/dankclaw/Caddyfile b/dankclaw/Caddyfile index 6673111..26a4d99 100644 --- a/dankclaw/Caddyfile +++ b/dankclaw/Caddyfile @@ -176,12 +176,15 @@ pihole.franksdank.cloud { # here enters the http://:8016 auth chain. Internal-only by DNS absence: there is # no public Porkbun A record for this name, so only LAN clients (Pi-hole # addn-hosts → 192.168.0.143) can resolve it at all; DNS-01 issuance via the -# Porkbun module needs no public record. Upstream is the host LAN IP rather than -# localhost because Caddy itself lives on dankclaw_default: from that bridge the -# gateway address 10.89.1.1 has nothing serving 8787 (connection refused), while -# 192.168.0.143:8787 is open. +# Porkbun module needs no public record. Upstream is `host.containers.internal` +# — netavark's host address, 169.254.1.2 — NOT the host LAN IP: hermes-webui is +# `Network=host`, so 8787 is a host-netns bind and not a published port, and +# netavark DNATs only published ports (192.168.0.143:9119, the dashboard below, +# answers from this bridge; 192.168.0.143:8787 does not). Measured live +# 2026-09-16 inside the Caddy container: 192.168.0.143:8787 and 10.89.1.1:8787 +# both REFUSED, host.containers.internal:8787 → 200. hermes.franksdank.cloud { - reverse_proxy 192.168.0.143:8787 + reverse_proxy host.containers.internal:8787 } # --- Hermes dashboard (admin UI; published 9119 on dankclaw_default) -------- diff --git a/dankclaw/hermes/quadlet/hermes-webui.container b/dankclaw/hermes/quadlet/hermes-webui.container index f58c905..b122fef 100644 --- a/dankclaw/hermes/quadlet/hermes-webui.container +++ b/dankclaw/hermes/quadlet/hermes-webui.container @@ -16,10 +16,12 @@ # can drive the browser; a bridge-network client could not reach it at all. # The server binds 0.0.0.0:8787 of the host netns, and that is deliberate: # this unit is host-networked (the CDP daemon above needs a shared loopback), so -# Caddy cannot reach it by container name and fronts the guarded vhost -# hermes.franksdank.cloud with `reverse_proxy 192.168.0.143:8787` (see the -# hermes block in dankclaw/Caddyfile, which records the same constraint). A -# loopback bind was tried on 2026-09-16 for a tighter posture and broke that +# Caddy cannot reach it by container name and fronts the vhost +# hermes.franksdank.cloud with `reverse_proxy host.containers.internal:8787` +# (netavark's host address, 169.254.1.2) — not the host LAN IP, which Caddy's +# bridge netns cannot reach for a bare host-netns bind; the measured asymmetry +# with published ports is recorded in the hermes block of dankclaw/Caddyfile. +# A loopback bind was tried on 2026-09-16 for a tighter posture and broke that # vhost, because a bind is also the only way an off-host process can reach a # host-networked container: the LAN address and the tunnel's localhost hop are # both served by the wildcard, and a loopback-only bind serves only the latter. diff --git a/tests/hermes_webui.bats b/tests/hermes_webui.bats index c41db53..b45866d 100644 --- a/tests/hermes_webui.bats +++ b/tests/hermes_webui.bats @@ -44,14 +44,24 @@ webui_service_yaml() { caddy="$REPO_ROOT/dankclaw/Caddyfile" grep -q 'HERMES_WEBUI_PASSWORD' <<< "$y" \ || { echo "password gate missing"; return 1; } - # This unit is host-networked, so Caddy cannot reach it by container name: - # hermes.franksdank.cloud proxies to the host's LAN address. The app's bind - # is therefore load-bearing for that vhost — a loopback-only bind was tried - # on 2026-09-16 and broke it (502), while host networking means there is no - # port forwarder to bridge the gap. + # This unit is host-networked, so Caddy cannot reach it by container name. + # The app's bind is load-bearing for that vhost — a loopback-only bind was + # tried on 2026-09-16 and broke it (502), while host networking means there + # is no port forwarder to bridge the gap. + # + # The upstream must be netavark's HOST ADDRESS, not the host's LAN IP. + # 8787 is a bare host-netns bind and netavark DNATs only *published* ports, + # so from Caddy's bridge netns 192.168.0.143:8787 and 10.89.1.1:8787 are + # both refused, while host.containers.internal:8787 (169.254.1.2) answers + # 200 — measured 2026-09-16 from inside the Caddy container. The dashboard's + # published 9119 is reachable on the LAN IP precisely because it IS + # published. Pin the measured-working form so reverting to the LAN IP cannot + # pass silently. local upstream - upstream=$(grep -A 6 '^hermes\.franksdank\.cloud {' "$caddy" | grep -oE 'reverse_proxy [0-9.]+:[0-9]+' | head -1) + upstream=$(grep -A 6 '^hermes\.franksdank\.cloud {' "$caddy" | grep -oE 'reverse_proxy [^ {]+' | head -1) [ -n "$upstream" ] || { echo "no hermes vhost upstream in the Caddyfile"; return 1; } + grep -qE '^reverse_proxy (host\.containers\.internal|169\.254\.1\.2):8787$' <<< "$upstream" \ + || { echo "hermes upstream is '$upstream', not netavark's host address (host.containers.internal:8787)"; return 1; } grep -q 'Environment=HERMES_WEBUI_HOST=0.0.0.0' <<< "$y" \ || { echo "webui must bind all interfaces: $upstream cannot reach loopback"; return 1; } }