Add fail-closed WireGuard container gateway

This commit is contained in:
Mikei386
2026-08-22 20:48:49 +02:00
parent 2f3bdde8b0
commit 0e5876f1a9
17 changed files with 365 additions and 109 deletions
@@ -0,0 +1,68 @@
#!/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:-}" 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=$!
wait "$proxy_ui_pid"