213 lines
8.4 KiB
Bash
213 lines
8.4 KiB
Bash
#!/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 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"
|
|
|
|
# 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 ping -c 1 -W 5 "$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
|