#!/bin/bash set -e INTERFACE="wg0" MOUNT_CONFIG="/etc/wireguard/${INTERFACE}.conf" CONFIG_DIR="/run/wireguard" CONFIG_FILE="${CONFIG_DIR}/${INTERFACE}.conf" CHECK_INTERVAL="${HEALTH_CHECK_INTERVAL:-10}" CHECK_HOST="${HEALTH_CHECK_HOST:-1.1.1.1}" # ────────────────────────────────────────────── # Validate config exists, copy to writable location # ────────────────────────────────────────────── if [ ! -f "$MOUNT_CONFIG" ]; then echo "[error] WireGuard config not found at ${MOUNT_CONFIG}" echo "[error] Mount your config file: -v /path/to/your.conf:/etc/wireguard/wg0.conf:ro" exit 1 fi mkdir -p "$CONFIG_DIR" cp "$MOUNT_CONFIG" "$CONFIG_FILE" chmod 600 "$CONFIG_FILE" # Extract endpoint IP and port from the config VPN_ENDPOINT=$(grep -i '^Endpoint' "$CONFIG_FILE" | head -1 | sed 's/.*= *//;s/:.*//;s/ //g') VPN_PORT=$(grep -i '^Endpoint' "$CONFIG_FILE" | head -1 | sed 's/.*://;s/ //g') # Extract address VPN_ADDRESS=$(grep -i '^Address' "$CONFIG_FILE" | head -1 | sed 's/.*= *//;s/ //g') if [ -z "$VPN_ENDPOINT" ] || [ -z "$VPN_PORT" ]; then echo "[error] Could not parse Endpoint from ${CONFIG_FILE}" exit 1 fi echo "[init] Config: ${CONFIG_FILE}" echo "[init] Endpoint: ${VPN_ENDPOINT}:${VPN_PORT}" echo "[init] Address: ${VPN_ADDRESS}" # ────────────────────────────────────────────── # Kill switch: only allow traffic through wg0 # ────────────────────────────────────────────── setup_killswitch() { echo "[killswitch] Setting up iptables kill switch..." # Flush existing rules iptables -F iptables -X iptables -t nat -F # Default policy: DROP everything iptables -P INPUT DROP iptables -P FORWARD DROP iptables -P OUTPUT DROP # Allow loopback iptables -A INPUT -i lo -j ACCEPT iptables -A OUTPUT -o lo -j ACCEPT # Allow traffic to/from VPN endpoint (needed to establish tunnel) iptables -A OUTPUT -d "$VPN_ENDPOINT" -p udp --dport "$VPN_PORT" -j ACCEPT iptables -A INPUT -s "$VPN_ENDPOINT" -p udp --sport "$VPN_PORT" -j ACCEPT # Allow all traffic through the WireGuard interface iptables -A INPUT -i "$INTERFACE" -j ACCEPT iptables -A OUTPUT -o "$INTERFACE" -j ACCEPT # Allow DNS to the VPN DNS server (through wg0) iptables -A OUTPUT -o "$INTERFACE" -p udp --dport 53 -j ACCEPT iptables -A OUTPUT -o "$INTERFACE" -p tcp --dport 53 -j ACCEPT # Allow DHCP (for container networking) iptables -A OUTPUT -p udp --dport 67:68 -j ACCEPT iptables -A INPUT -p udp --sport 67:68 -j ACCEPT # Allow established/related connections iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT # ── Allow incoming connections to exposed service ports (e.g. app on 8000) ── # LOCAL_PORTS can be set as env var, e.g. "8000,8080,3000" if [ -n "${LOCAL_PORTS:-}" ]; then for port in $(echo "$LOCAL_PORTS" | tr ',' ' '); do echo "[killswitch] Allowing incoming traffic on port ${port}" iptables -A INPUT -p tcp --dport "$port" -j ACCEPT iptables -A OUTPUT -p tcp --sport "$port" -j ACCEPT done fi # ── FORWARDING (so other containers can use this VPN) ── iptables -A FORWARD -i eth0 -o "$INTERFACE" -j ACCEPT iptables -A FORWARD -i "$INTERFACE" -o eth0 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT # NAT: masquerade traffic from other containers going out through wg0 iptables -t nat -A POSTROUTING -o "$INTERFACE" -j MASQUERADE echo "[killswitch] Kill switch active. Traffic blocked if VPN drops." } # ────────────────────────────────────────────── # Enable IP forwarding so other containers can route through us # ────────────────────────────────────────────── enable_forwarding() { echo "[init] Enabling IP forwarding..." if cat /proc/sys/net/ipv4/ip_forward 2>/dev/null | grep -q 1; then echo "[init] IP forwarding already enabled." elif echo 1 > /proc/sys/net/ipv4/ip_forward 2>/dev/null; then echo "[init] IP forwarding enabled via /proc." else echo "[init] /proc read-only — relying on --sysctl net.ipv4.ip_forward=1" fi } # ────────────────────────────────────────────── # Start WireGuard manually (no wg-quick, avoids sysctl issues) # ────────────────────────────────────────────── start_vpn() { echo "[vpn] Starting WireGuard interface ${INTERFACE}..." # Create the interface ip link add "$INTERFACE" type wireguard # Apply the WireGuard config (keys, peer, endpoint) wg setconf "$INTERFACE" <(grep -v -i '^\(Address\|DNS\|MTU\|Table\|PreUp\|PostUp\|PreDown\|PostDown\|SaveConfig\)' "$CONFIG_FILE") # Assign the address ip -4 address add "$VPN_ADDRESS" dev "$INTERFACE" # Set MTU ip link set mtu 1420 up dev "$INTERFACE" # Find default gateway/interface for the endpoint route DEFAULT_GW=$(ip route | grep '^default' | head -1 | awk '{print $3}') DEFAULT_IF=$(ip route | grep '^default' | head -1 | awk '{print $5}') # Route VPN endpoint through the container's default gateway if [ -n "$DEFAULT_GW" ] && [ -n "$DEFAULT_IF" ]; then ip route add "$VPN_ENDPOINT/32" via "$DEFAULT_GW" dev "$DEFAULT_IF" 2>/dev/null || true fi # Route all traffic through the WireGuard tunnel ip route add 0.0.0.0/1 dev "$INTERFACE" ip route add 128.0.0.0/1 dev "$INTERFACE" # ── Policy routing: ensure responses to incoming LAN traffic go back via eth0 ── if [ -n "$DEFAULT_GW" ] && [ -n "$DEFAULT_IF" ]; then # Get the container's eth0 IP address (BusyBox-compatible, no grep -P) ETH0_IP=$(ip -4 addr show "$DEFAULT_IF" | awk '/inet / {split($2, a, "/"); print a[1]}' | head -1) ETH0_SUBNET=$(ip -4 route show dev "$DEFAULT_IF" | grep -v default | head -1 | awk '{print $1}') if [ -n "$ETH0_IP" ] && [ -n "$ETH0_SUBNET" ]; then echo "[vpn] Setting up policy routing for incoming traffic (${ETH0_IP} on ${DEFAULT_IF})" ip route add default via "$DEFAULT_GW" dev "$DEFAULT_IF" table 100 2>/dev/null || true ip route add "$ETH0_SUBNET" dev "$DEFAULT_IF" table 100 2>/dev/null || true ip rule add from "$ETH0_IP" table 100 priority 100 2>/dev/null || true echo "[vpn] Policy routing active — incoming connections will be routed back via ${DEFAULT_IF}" fi fi # Set up DNS VPN_DNS=$(grep -i '^DNS' "$CONFIG_FILE" | head -1 | sed 's/.*= *//;s/ //g') if [ -n "$VPN_DNS" ]; then echo "nameserver $VPN_DNS" > /etc/resolv.conf echo "[vpn] DNS set to ${VPN_DNS}" fi echo "[vpn] WireGuard interface ${INTERFACE} is up." } # ────────────────────────────────────────────── # Stop WireGuard manually # ────────────────────────────────────────────── stop_vpn() { echo "[vpn] Stopping WireGuard interface ${INTERFACE}..." ip link del "$INTERFACE" 2>/dev/null || true } # ────────────────────────────────────────────── # Health check loop — restarts VPN if tunnel dies # ────────────────────────────────────────────── health_loop() { local failures=0 local max_failures=3 echo "[health] Starting health check (every ${CHECK_INTERVAL}s, target ${CHECK_HOST})..." while true; do sleep "$CHECK_INTERVAL" if curl -sf --max-time 5 "http://$CHECK_HOST" > /dev/null 2>&1; then if [ "$failures" -gt 0 ]; then echo "[health] VPN recovered." failures=0 fi else failures=$((failures + 1)) echo "[health] Ping failed ($failures/$max_failures)" if [ "$failures" -ge "$max_failures" ]; then echo "[health] VPN appears down. Restarting WireGuard..." stop_vpn sleep 2 start_vpn failures=0 echo "[health] WireGuard restarted." fi fi done } # ────────────────────────────────────────────── # Graceful shutdown # ────────────────────────────────────────────── cleanup() { echo "[shutdown] Stopping WireGuard..." stop_vpn echo "[shutdown] Flushing iptables..." iptables -F iptables -t nat -F echo "[shutdown] Done." exit 0 } trap cleanup SIGTERM SIGINT # ── Main ── enable_forwarding setup_killswitch start_vpn health_loop