From c4649571df78d812a666a06a3187a093df79b551 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 10:24:29 +0200 Subject: [PATCH] Wait for the tunnel interface, and prove the gateway answers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two things reported success while the tunnel console was unreachable. The services were restarted the moment the hub container counted as started, but the hub brings wg0 up as part of its start command — so they were binding an address that did not exist yet. They now wait for the interface. And readiness was a container-state check. A container can be up and running while the process inside it listens in a network namespace that was torn down underneath it: nothing errors, the address simply refuses connections, and "running" reports everything fine. It asks the gateway now, and only a real answer counts — because the consequence of getting this wrong is a client config naming a resolver that is not there, which takes the device's whole name resolution with it for as long as the tunnel is up. Co-Authored-By: Claude Opus 5 --- deploy/update.sh | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/deploy/update.sh b/deploy/update.sh index 51d8a2f..51db1ee 100755 --- a/deploy/update.sh +++ b/deploy/update.sh @@ -113,15 +113,25 @@ deployed="$(cat "$STATE_FILE" 2>/dev/null || echo '')" reconcile_vpn_readiness() { grep -qE '^COMPOSE_PROFILES=.*vpn' .env 2>/dev/null || return 0 - sleep 3 - local running vpn_ready - running="$(docker compose --profile vpn ps --status running --services 2>/dev/null || true)" + local host vpn_ready=false + host="$(sed -n 's/^VPN_INTERNAL_HOST=//p' .env | tail -1)" - if grep -q '^vpn-gateway$' <<<"$running" && grep -q '^vpn-dns$' <<<"$running"; then - vpn_ready=true - else - vpn_ready=false - printf '\033[1;33m !\033[0m %s\n' "The tunnel gateway is not running — clients will not be given its resolver." + # ASKED, not assumed. A container can be up and running while the process + # inside it listens in a network namespace that was torn down underneath it + # — nothing errors, the address simply refuses connections, and a + # container-state check reports everything fine. + for _ in $(seq 1 10); do + if docker compose exec -T vpn-gateway sh -c \ + "wget -q --spider --no-check-certificate https://127.0.0.1/ 2>/dev/null \ + || wget -q -O /dev/null --no-check-certificate https://127.0.0.1/ 2>/dev/null" >/dev/null 2>&1; then + vpn_ready=true + break + fi + sleep 2 + done + + if [[ "$vpn_ready" != "true" ]]; then + printf '\033[1;33m !\033[0m %s\n' "The tunnel gateway is not answering on ${host} — clients will not be given its resolver." fi if [[ "$(sed -n 's/^VPN_READY=//p' .env | tail -1)" != "$vpn_ready" ]]; then @@ -322,6 +332,13 @@ docker compose restart queue queue-provisioning scheduler reverb # Nothing errors; connections to the tunnel address are simply refused, which # looks exactly like the gateway never having worked. if grep -qE '^COMPOSE_PROFILES=.*vpn' .env 2>/dev/null; then + # And only once wg0 is back. The hub brings the interface up as part of its + # start command, so restarting these the instant the container is "started" + # has them binding an address that does not exist yet. + for _ in $(seq 1 30); do + docker compose exec -T queue-provisioning ip -4 addr show wg0 2>/dev/null | grep -q 'inet ' && break + sleep 2 + done docker compose --profile vpn restart vpn-dns vpn-gateway >/dev/null 2>&1 || true fi