#!/bin/sh # ikev2-egress.sh - runs inside ts-lan-ikev2, which shares ts-vpn's network # namespace. Tailscale (kernel mode, --netfilter-mode=off) only installs the # policy routing (default -> tailscale0 via table 52); this loop owns the rest # and is idempotent, so a container restart or a late tailscale0 is harmless. # # 1. ip rules ahead of Tailscale's 52xx rules: replies of the IKE/NAT-T/ESP # server itself must leave via the docker bridge (main table), otherwise # the SYN-ACK-equivalent goes into the tunnel and no client ever connects. # 2. FORWARD accepts for VPN<->tailscale0 ahead of hwdsl2's final -j DROP, and # MASQUERADE everything leaving tailscale0 to the node's own 100.x address - # the exit node's WireGuard AllowedIPs drops any other source. # 3. MSS clamp to the tunnel PMTU (tailscale0 is MTU 1280; hwdsl2 clamps 1360). # 4. Pin the Libreswan conn to eth0's address: hwdsl2 writes left=%defaultroute, # which resolves through Tailscale's policy routing to the tailscale0 address, # so IKE packets arriving on the bridge match no conn (NO_PROPOSAL_CHOSEN). # 5. LAN access: one ip rule sends LAN_NET out the docker bridge (main table) # instead of table 52, plus FORWARD accepts and a MASQUERADE to the bridge # address - LAN hosts have no route back to the VPN pool. Scoped to LAN_NET # only: every other destination, including the docker bridges themselves, # still falls through to table 52 = tailscale0. Set LAN_NET empty to restore # the original egress-only behaviour. # The LAN block sits OUTSIDE the tailscale0 guard on purpose: LAN access does not # depend on the exit node, so a client still reaches the office if the tunnel is # down (internet then fails closed, which is what we want). TS_IF=tailscale0 LAN_IF=eth0 VPN_NET="192.168.43.0/24" # hwdsl2 IKEv2 address pool (rightaddresspool) LAN_NET="192.168.0.0/22" # client-02 head office (Kirochnaya). Branch sites # are NOT routed here - see the deployment note. log() { echo "[ikev2-egress] $*"; } ensure_ipt() { # ensure_ipt t=$1; c=$2; shift 2 iptables -w -t "$t" -C "$c" "$@" 2>/dev/null || { iptables -w -t "$t" -A "$c" "$@" && log "iptables -t $t -A $c $*"; } } ensure_ipt_first() { # like ensure_ipt but inserts at the top (hwdsl2 ends FORWARD with -j DROP) t=$1; c=$2; shift 2 iptables -w -t "$t" -C "$c" "$@" 2>/dev/null || { iptables -w -t "$t" -I "$c" 1 "$@" && log "iptables -t $t -I $c $*"; } } ensure_rule() { # ensure_rule p=$1; shift ip rule list | grep -q "^$p:" || { ip rule add pref "$p" "$@" && log "ip rule $p: $*"; } } fix_left() { conf=/etc/ipsec.d/ikev2.conf [ -f "$conf" ] || return 0 ip4=$(ip -4 -o addr show dev eth0 2>/dev/null | awk '{print $4}' | cut -d/ -f1 | head -1) [ -n "$ip4" ] || return 0 grep -q "^ left=$ip4\$" "$conf" && return 0 sed -i "s|^ left=.*| left=$ip4|" "$conf" && log "ikev2.conf: left=$ip4 (was %defaultroute -> tailscale0)" if ipsec auto --replace ikev2-cp >/dev/null 2>&1; then log "ipsec auto --replace ikev2-cp"; fi } while :; do fix_left if [ -n "$LAN_NET" ] && ip link show "$LAN_IF" >/dev/null 2>&1; then ensure_rule 5010 to "$LAN_NET" lookup main ensure_ipt_first filter FORWARD -s "$VPN_NET" -d "$LAN_NET" -o "$LAN_IF" -j ACCEPT ensure_ipt_first filter FORWARD -d "$VPN_NET" -s "$LAN_NET" -i "$LAN_IF" \ -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT ensure_ipt nat POSTROUTING -s "$VPN_NET" -d "$LAN_NET" -o "$LAN_IF" -j MASQUERADE fi if ip link show "$TS_IF" >/dev/null 2>&1; then ensure_rule 5001 ipproto udp sport 500 lookup main ensure_rule 5002 ipproto udp sport 4500 lookup main ensure_rule 5003 ipproto 50 lookup main ensure_ipt_first filter FORWARD -s "$VPN_NET" -o "$TS_IF" -j ACCEPT ensure_ipt_first filter FORWARD -d "$VPN_NET" -i "$TS_IF" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT ensure_ipt nat POSTROUTING -o "$TS_IF" -j MASQUERADE ensure_ipt mangle FORWARD -o "$TS_IF" -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu fi sleep 30 done