|
|
|
|
@@ -72,9 +72,11 @@ setup_killswitch() {
|
|
|
|
|
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 DNS (VPN DNS servers are routed through wg0; allow before routing decision)
|
|
|
|
|
iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
|
|
|
|
|
iptables -A OUTPUT -p tcp --dport 53 -j ACCEPT
|
|
|
|
|
iptables -A INPUT -p udp --sport 53 -j ACCEPT
|
|
|
|
|
iptables -A INPUT -p tcp --sport 53 -j ACCEPT
|
|
|
|
|
|
|
|
|
|
# Allow DHCP (for container networking)
|
|
|
|
|
iptables -A OUTPUT -p udp --dport 67:68 -j ACCEPT
|
|
|
|
|
@@ -128,46 +130,46 @@ start_vpn() {
|
|
|
|
|
ip link add "$INTERFACE" type wireguard
|
|
|
|
|
|
|
|
|
|
# Apply the WireGuard config (keys, peer, endpoint)
|
|
|
|
|
# We filter out Address/DNS/MTU/PreUp/PostUp/PreDown/PostDown/SaveConfig
|
|
|
|
|
# AllowedIPs is kept because WireGuard needs it to know which traffic to tunnel.
|
|
|
|
|
# We remove the auto-created default route afterwards and set our own.
|
|
|
|
|
wg setconf "$INTERFACE" <(grep -v -i '^\(Address\|DNS\|MTU\|PreUp\|PostUp\|PreDown\|PostDown\|SaveConfig\)' "$CONFIG_FILE")
|
|
|
|
|
# Filter out wg-quick directives that wg setconf doesn't understand
|
|
|
|
|
wg setconf "$INTERFACE" <(grep -v -i '^\(Address\|DNS\|MTU\|Table\|PreUp\|PostUp\|PreDown\|PostDown\|SaveConfig\)' "$CONFIG_FILE")
|
|
|
|
|
|
|
|
|
|
# Log public key so it can be verified against the server's peer list
|
|
|
|
|
local PUBKEY
|
|
|
|
|
PUBKEY=$(wg show "$INTERFACE" public-key 2>/dev/null || echo "unknown")
|
|
|
|
|
echo "[vpn] Public key: ${PUBKEY}"
|
|
|
|
|
|
|
|
|
|
# Assign the address
|
|
|
|
|
ip -4 address add "$VPN_ADDRESS" dev "$INTERFACE"
|
|
|
|
|
|
|
|
|
|
# Set MTU
|
|
|
|
|
# Set MTU and bring up
|
|
|
|
|
ip link set mtu 1420 up dev "$INTERFACE"
|
|
|
|
|
|
|
|
|
|
# Remove the auto-created default route by wg setconf (if AllowedIPs = 0.0.0.0/0)
|
|
|
|
|
# We set our own routes manually to avoid breaking the endpoint connection
|
|
|
|
|
# ── fwmark-based routing (mirrors wg-quick behavior) ──
|
|
|
|
|
# WireGuard marks its own encapsulated UDP packets with this fwmark.
|
|
|
|
|
# Policy rules then ensure:
|
|
|
|
|
# - Normal packets (no mark) → VPN routing table → wg0
|
|
|
|
|
# - WireGuard-encapsulated packets (marked) → main table → eth0
|
|
|
|
|
local FW_MARK=51820
|
|
|
|
|
local FW_TABLE=51820
|
|
|
|
|
wg set "$INTERFACE" fwmark "$FW_MARK"
|
|
|
|
|
|
|
|
|
|
# Remove any auto-created default route on wg0
|
|
|
|
|
ip route del default dev "$INTERFACE" 2>/dev/null || true
|
|
|
|
|
|
|
|
|
|
# Find default gateway/interface for the endpoint route
|
|
|
|
|
# VPN routing table: send everything through the tunnel
|
|
|
|
|
ip -4 route add default dev "$INTERFACE" table "$FW_TABLE"
|
|
|
|
|
|
|
|
|
|
# Policy rules:
|
|
|
|
|
# 1. Packets NOT marked by WireGuard use the VPN table (→ wg0)
|
|
|
|
|
# 2. suppress_prefixlength 0: ignore bare default routes in main table,
|
|
|
|
|
# but keep more-specific routes (e.g. LAN, endpoint) working
|
|
|
|
|
ip -4 rule add not fwmark "$FW_MARK" table "$FW_TABLE"
|
|
|
|
|
ip -4 rule add table main suppress_prefixlength 0
|
|
|
|
|
|
|
|
|
|
# Find default gateway/interface
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
# Parse AllowedIPs from config and add routes dynamically
|
|
|
|
|
ALLOWED_IPS=$(grep -i '^AllowedIPs' "$CONFIG_FILE" | head -1 | sed 's/.*= *//;s/ //g')
|
|
|
|
|
|
|
|
|
|
if [ -n "$ALLOWED_IPS" ]; then
|
|
|
|
|
for ip in $(echo "$ALLOWED_IPS" | tr ',' ' '); do
|
|
|
|
|
if [ "$ip" = "0.0.0.0/0" ]; then
|
|
|
|
|
# Use the split route trick to avoid overriding the default route
|
|
|
|
|
# (which would break the endpoint connection)
|
|
|
|
|
ip route add 0.0.0.0/1 dev "$INTERFACE" 2>/dev/null || true
|
|
|
|
|
ip route add 128.0.0.0/1 dev "$INTERFACE" 2>/dev/null || true
|
|
|
|
|
else
|
|
|
|
|
ip route add "$ip" dev "$INTERFACE" 2>/dev/null || true
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# ── 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)
|
|
|
|
|
@@ -194,6 +196,14 @@ start_vpn() {
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
echo "[vpn] WireGuard interface ${INTERFACE} is up."
|
|
|
|
|
echo "[vpn] Main routes:"
|
|
|
|
|
ip route show | sed 's/^/[vpn] /'
|
|
|
|
|
echo "[vpn] VPN table ($FW_TABLE):"
|
|
|
|
|
ip route show table "$FW_TABLE" 2>/dev/null | sed 's/^/[vpn] /'
|
|
|
|
|
echo "[vpn] Policy rules:"
|
|
|
|
|
ip rule show | sed 's/^/[vpn] /'
|
|
|
|
|
echo "[vpn] WireGuard status:"
|
|
|
|
|
wg show "$INTERFACE" 2>/dev/null | sed 's/^/[vpn] /'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ──────────────────────────────────────────────
|
|
|
|
|
@@ -202,23 +212,19 @@ start_vpn() {
|
|
|
|
|
stop_vpn() {
|
|
|
|
|
echo "[vpn] Stopping WireGuard interface ${INTERFACE}..."
|
|
|
|
|
|
|
|
|
|
# Remove routes added for AllowedIPs
|
|
|
|
|
ALLOWED_IPS=$(grep -i '^AllowedIPs' "$CONFIG_FILE" | head -1 | sed 's/.*= *//;s/ //g')
|
|
|
|
|
if [ -n "$ALLOWED_IPS" ]; then
|
|
|
|
|
for ip in $(echo "$ALLOWED_IPS" | tr ',' ' '); do
|
|
|
|
|
if [ "$ip" = "0.0.0.0/0" ]; then
|
|
|
|
|
ip route del 0.0.0.0/1 dev "$INTERFACE" 2>/dev/null || true
|
|
|
|
|
ip route del 128.0.0.0/1 dev "$INTERFACE" 2>/dev/null || true
|
|
|
|
|
else
|
|
|
|
|
ip route del "$ip" dev "$INTERFACE" 2>/dev/null || true
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
fi
|
|
|
|
|
local FW_MARK=51820
|
|
|
|
|
local FW_TABLE=51820
|
|
|
|
|
|
|
|
|
|
# Remove endpoint route
|
|
|
|
|
if [ -n "$VPN_ENDPOINT" ]; then
|
|
|
|
|
ip route del "$VPN_ENDPOINT/32" 2>/dev/null || true
|
|
|
|
|
fi
|
|
|
|
|
# Remove fwmark-based policy rules
|
|
|
|
|
ip -4 rule del not fwmark "$FW_MARK" table "$FW_TABLE" 2>/dev/null || true
|
|
|
|
|
ip -4 rule del table main suppress_prefixlength 0 2>/dev/null || true
|
|
|
|
|
|
|
|
|
|
# Flush VPN routing table
|
|
|
|
|
ip -4 route flush table "$FW_TABLE" 2>/dev/null || true
|
|
|
|
|
|
|
|
|
|
# Remove LAN policy routing
|
|
|
|
|
ip -4 rule del table 100 2>/dev/null || true
|
|
|
|
|
ip -4 route flush table 100 2>/dev/null || true
|
|
|
|
|
|
|
|
|
|
ip link del "$INTERFACE" 2>/dev/null || true
|
|
|
|
|
}
|
|
|
|
|
@@ -235,14 +241,19 @@ health_loop() {
|
|
|
|
|
while true; do
|
|
|
|
|
sleep "$CHECK_INTERVAL"
|
|
|
|
|
|
|
|
|
|
if curl -sf --max-time 5 "http://$CHECK_HOST" > /dev/null 2>&1; then
|
|
|
|
|
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)"
|
|
|
|
|
echo "[health] Check failed ($failures/$max_failures) — ping ${CHECK_HOST} failed"
|
|
|
|
|
# Dump WireGuard stats to show if handshake is stale and how much data flows
|
|
|
|
|
echo "[health] wg stats:"
|
|
|
|
|
wg show "$INTERFACE" 2>/dev/null | grep -E 'latest handshake|transfer|endpoint' | sed 's/^/[health] /' || echo "[health] wg0 not found"
|
|
|
|
|
echo "[health] routes:"
|
|
|
|
|
ip route show | grep -E 'wg0|default' | sed 's/^/[health] /'
|
|
|
|
|
|
|
|
|
|
if [ "$failures" -ge "$max_failures" ]; then
|
|
|
|
|
echo "[health] VPN appears down. Restarting WireGuard..."
|
|
|
|
|
@@ -271,8 +282,81 @@ cleanup() {
|
|
|
|
|
|
|
|
|
|
trap cleanup SIGTERM SIGINT
|
|
|
|
|
|
|
|
|
|
# ──────────────────────────────────────────────
|
|
|
|
|
# Startup connectivity checks — diagnose issues early
|
|
|
|
|
# ──────────────────────────────────────────────
|
|
|
|
|
check_vpn_connectivity() {
|
|
|
|
|
echo "[check] ── Startup connectivity checks ──"
|
|
|
|
|
|
|
|
|
|
# 1. Wait for WireGuard handshake (up to 15s)
|
|
|
|
|
local elapsed=0
|
|
|
|
|
local handshake_ts=0
|
|
|
|
|
echo "[check] Waiting for WireGuard handshake (up to 15s)..."
|
|
|
|
|
while [ "$elapsed" -lt 15 ]; do
|
|
|
|
|
handshake_ts=$(wg show "$INTERFACE" latest-handshakes 2>/dev/null | awk '{print $2}' | head -1)
|
|
|
|
|
if [ -n "$handshake_ts" ] && [ "$handshake_ts" != "0" ]; then
|
|
|
|
|
local age=$(( $(date +%s) - handshake_ts ))
|
|
|
|
|
echo "[check] OK Handshake established ${age}s ago"
|
|
|
|
|
break
|
|
|
|
|
fi
|
|
|
|
|
sleep 1
|
|
|
|
|
elapsed=$((elapsed + 1))
|
|
|
|
|
done
|
|
|
|
|
if [ "$elapsed" -ge 15 ]; then
|
|
|
|
|
echo "[check] FAIL No WireGuard handshake after 15s — tunnel is not up"
|
|
|
|
|
echo "[check] This container's public key (must be on the server):"
|
|
|
|
|
echo "[check] PublicKey = $(wg show "$INTERFACE" public-key 2>/dev/null || echo 'unknown')"
|
|
|
|
|
echo "[check] AllowedIPs = ${VPN_ADDRESS}"
|
|
|
|
|
echo "[check] Verify on server: wg show"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# 2. Check whether traffic actually flows through the tunnel
|
|
|
|
|
echo "[check] Testing traffic through tunnel (ping ${CHECK_HOST})..."
|
|
|
|
|
local rx_before
|
|
|
|
|
rx_before=$(wg show "$INTERFACE" transfer 2>/dev/null | awk '{print $2}' | head -1)
|
|
|
|
|
|
|
|
|
|
if ping -c 1 -W 8 "${CHECK_HOST}" > /dev/null 2>&1; then
|
|
|
|
|
echo "[check] OK Traffic flows — tunnel is fully working"
|
|
|
|
|
else
|
|
|
|
|
local rx_after
|
|
|
|
|
rx_after=$(wg show "$INTERFACE" transfer 2>/dev/null | awk '{print $2}' | head -1)
|
|
|
|
|
echo "[check] FAIL ping ${CHECK_HOST} unreachable through tunnel"
|
|
|
|
|
|
|
|
|
|
if [ -n "$rx_before" ] && [ -n "$rx_after" ]; then
|
|
|
|
|
if [ "$rx_after" -le "$rx_before" ]; then
|
|
|
|
|
echo "[check] RX bytes unchanged (${rx_before} → ${rx_after})"
|
|
|
|
|
echo "[check] Server receives packets but does NOT route them back"
|
|
|
|
|
echo "[check] Fix on VPN server (${VPN_ENDPOINT}):"
|
|
|
|
|
echo "[check] sysctl net.ipv4.ip_forward # must output 1"
|
|
|
|
|
echo "[check] iptables -t nat -L POSTROUTING -v -n # must have MASQUERADE"
|
|
|
|
|
echo "[check] wg show # check peer + AllowedIPs"
|
|
|
|
|
else
|
|
|
|
|
echo "[check] RX increased (${rx_before} → ${rx_after}) — tunnel passes data"
|
|
|
|
|
echo "[check] Issue may be specific to ${CHECK_HOST} or DNS"
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
local transfer
|
|
|
|
|
transfer=$(wg show "$INTERFACE" transfer 2>/dev/null | awk '{printf "rx=%s tx=%s", $2, $3}')
|
|
|
|
|
echo "[check] wg transfer: ${transfer}"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# 3. DNS check
|
|
|
|
|
echo "[check] Testing DNS resolution..."
|
|
|
|
|
if nslookup 1.1.1.1 > /dev/null 2>&1 || nslookup google.com > /dev/null 2>&1; then
|
|
|
|
|
echo "[check] OK DNS resolves"
|
|
|
|
|
else
|
|
|
|
|
echo "[check] FAIL DNS resolution failed"
|
|
|
|
|
echo "[check] resolv.conf: $(tr '\n' ' ' < /etc/resolv.conf)"
|
|
|
|
|
echo "[check] Check that DNS servers are reachable through wg0"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
echo "[check] ── End of checks ──"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ── Main ──
|
|
|
|
|
enable_forwarding
|
|
|
|
|
setup_killswitch
|
|
|
|
|
start_vpn
|
|
|
|
|
check_vpn_connectivity
|
|
|
|
|
health_loop
|
|
|
|
|
|