#!/bin/sh set -eu SOURCE=${WG_CONFIG_SOURCE:-/run/secrets/fritz-athena.conf} RUNTIME=/run/wireguard/wg0.conf [ -s "$SOURCE" ] || { echo "WireGuard configuration is missing" >&2; exit 1; } install -d -m 0700 /run/wireguard # Fritzbox exports global DNS directives and wg-quick hooks. DNS is assigned # per application container by Docker; executable hooks are deliberately not # accepted from a secret file. All cryptographic values remain untouched. awk ' /^[[:space:]]*(DNS|Table|PreUp|PostUp|PreDown|PostDown|SaveConfig)[[:space:]]*=/ { next } /^\[Interface\][[:space:]]*$/ { print; print "Table = off"; next } { print } ' "$SOURCE" >"$RUNTIME" chmod 0600 "$RUNTIME" # Docker deliberately keeps /proc/sys read-only inside this narrowly # privileged container. Table=off prevents wg-quick from trying to modify # global policy-routing sysctls; the two required routes are installed below. physical_default=$(ip -4 route show default | head -n 1) physical_gateway=$(printf '%s\n' "$physical_default" | awk '{for (i=1; i<=NF; i++) if ($i == "via") print $(i+1)}') physical_device=$(printf '%s\n' "$physical_default" | awk '{for (i=1; i<=NF; i++) if ($i == "dev") print $(i+1)}') wg-quick up "$RUNTIME" # Keep the encrypted peer itself reachable over Docker's physical network, # then make the tunnel the namespace default. Connected Docker routes remain # intact for the reverse proxies and internal service discovery. endpoint=$(wg show wg0 endpoints | awk 'NR == 1 { print $2 }') case "$endpoint" in \[*\]:*) endpoint_ip=${endpoint#\[}; endpoint_ip=${endpoint_ip%%\]*} ;; *:*) endpoint_ip=${endpoint%:*} ;; *) endpoint_ip= ;; esac if [ -n "$endpoint_ip" ] && [ -n "$physical_gateway" ] && [ -n "$physical_device" ]; then case "$endpoint_ip" in *:*) : ;; # Docker gateway networks are intentionally IPv4-only. *) ip -4 route replace "$endpoint_ip/32" via "$physical_gateway" dev "$physical_device" ;; esac fi ip -4 route replace default dev wg0 ip -6 route replace default dev wg0 2>/dev/null || true cleanup() { kill "${proxy_ui_pid:-}" "${proxy_router_pid:-}" "${proxy_ssh_pid:-}" 2>/dev/null || true wg-quick down "$RUNTIME" 2>/dev/null || true } trap cleanup EXIT INT TERM # Forward only explicitly selected Docker networks into the tunnel. The # gateway itself is the only container that receives NET_ADMIN. iptables -P FORWARD DROP iptables -A FORWARD -o wg0 -j ACCEPT iptables -A FORWARD -i wg0 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE # Nothing is published on the physical host. These listeners exist only in # the WireGuard container namespace and forward VPN clients to internal names. socat TCP-LISTEN:8080,bind=0.0.0.0,reuseaddr,fork TCP:open-webui:8080 & proxy_ui_pid=$! socat TCP-LISTEN:8081,bind=0.0.0.0,reuseaddr,fork TCP:router:8081 & proxy_router_pid=$! # Emergency SSH path for unattended operation. Bind explicitly to WireGuard's # IPv4 address, never to a Docker-facing interface or the physical host. The # target is the host-side gateway of the fixed frontend bridge. Host sshd still # enforces its normal key-only authentication policy. wg_ipv4=$(ip -4 -o address show dev wg0 | awk 'NR == 1 { split($4, address, "/"); print address[1] }') [ -n "$wg_ipv4" ] || { echo "WireGuard IPv4 address is missing" >&2; exit 1; } socat TCP-LISTEN:22,bind="$wg_ipv4",reuseaddr,fork TCP:172.30.10.1:22 & proxy_ssh_pid=$! wait "$proxy_ui_pid"