mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-31 20:43:41 +00:00
The POSTROUTING MASQUERADE rules in forward-port failed to handle two hairpin scenarios: 1. Host-to-target hairpin (OUTPUT DNAT): when sip is a WAN IP (tunnel case), the old rule matched `-s sip` but the actual source of locally-originated packets is a local interface IP, not the WAN IP. Fix: use `-m addrtype --src-type LOCAL -m conntrack --ctorigdst sip` to match any local source while tying the rule to the specific sip. 2. Same-subnet self-hairpin (PREROUTING DNAT): when a WireGuard peer connects to itself via the tunnel's public IP, traffic is DNAT'd back to the peer. Without MASQUERADE the response takes a loopback shortcut, bypassing the tunnel server's conntrack and breaking NAT reversal. Fix: add `-s dip/dprefix -d dip` to masquerade same-subnet traffic, which also subsumes the old bridge_subnet rule. Also bind the hairpin detection socket to the gateway interface and local IP for consistency with the echoip client.
76 lines
3.9 KiB
Bash
Executable File
76 lines
3.9 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
if [ -z "$sip" ] || [ -z "$dip" ] || [ -z "$dprefix" ] || [ -z "$sport" ] || [ -z "$dport" ]; then
|
|
>&2 echo 'missing required env var'
|
|
exit 1
|
|
fi
|
|
|
|
NAME="F$(echo "$sip:$sport -> $dip/$dprefix:$dport ${src_subnet:-any}" | sha256sum | head -c 15)"
|
|
|
|
for kind in INPUT FORWARD ACCEPT; do
|
|
if ! iptables -C $kind -j "${NAME}_${kind}" 2> /dev/null; then
|
|
iptables -N "${NAME}_${kind}" 2> /dev/null
|
|
iptables -A $kind -j "${NAME}_${kind}"
|
|
fi
|
|
done
|
|
for kind in PREROUTING OUTPUT POSTROUTING; do
|
|
if ! iptables -t nat -C $kind -j "${NAME}_${kind}" 2> /dev/null; then
|
|
iptables -t nat -N "${NAME}_${kind}" 2> /dev/null
|
|
iptables -t nat -A $kind -j "${NAME}_${kind}"
|
|
fi
|
|
done
|
|
|
|
err=0
|
|
trap 'err=1' ERR
|
|
|
|
for kind in INPUT FORWARD ACCEPT; do
|
|
iptables -F "${NAME}_${kind}" 2> /dev/null
|
|
done
|
|
for kind in PREROUTING OUTPUT POSTROUTING; do
|
|
iptables -t nat -F "${NAME}_${kind}" 2> /dev/null
|
|
done
|
|
if [ "$UNDO" = 1 ]; then
|
|
conntrack -D -p tcp -d $sip --dport $sport || true # conntrack returns exit 1 if no connections are active
|
|
conntrack -D -p udp -d $sip --dport $sport || true # conntrack returns exit 1 if no connections are active
|
|
exit $err
|
|
fi
|
|
|
|
# DNAT: rewrite destination for incoming packets (external traffic)
|
|
# When src_subnet is set, only forward traffic from that subnet (private forwards)
|
|
if [ -n "$src_subnet" ]; then
|
|
iptables -t nat -A ${NAME}_PREROUTING -s "$src_subnet" -d "$sip" -p tcp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
|
|
iptables -t nat -A ${NAME}_PREROUTING -s "$src_subnet" -d "$sip" -p udp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
|
|
# Also allow containers on the bridge subnet to reach this forward
|
|
if [ -n "$bridge_subnet" ]; then
|
|
iptables -t nat -A ${NAME}_PREROUTING -s "$bridge_subnet" -d "$sip" -p tcp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
|
|
iptables -t nat -A ${NAME}_PREROUTING -s "$bridge_subnet" -d "$sip" -p udp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
|
|
fi
|
|
else
|
|
iptables -t nat -A ${NAME}_PREROUTING -d "$sip" -p tcp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
|
|
iptables -t nat -A ${NAME}_PREROUTING -d "$sip" -p udp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
|
|
fi
|
|
|
|
# DNAT: rewrite destination for locally-originated packets (hairpin from host itself)
|
|
iptables -t nat -A ${NAME}_OUTPUT -d "$sip" -p tcp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
|
|
iptables -t nat -A ${NAME}_OUTPUT -d "$sip" -p udp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
|
|
|
|
# Allow new connections to be forwarded to the destination
|
|
iptables -A ${NAME}_FORWARD -d $dip -p tcp --dport $dport -m state --state NEW -j ACCEPT
|
|
iptables -A ${NAME}_FORWARD -d $dip -p udp --dport $dport -m state --state NEW -j ACCEPT
|
|
|
|
# NAT hairpin: masquerade so replies route back through this host for proper
|
|
# NAT reversal instead of taking a direct path that bypasses conntrack.
|
|
# Host-to-target hairpin: locally-originated packets whose original destination
|
|
# was sip (before OUTPUT DNAT rewrote it to dip). Using --ctorigdst ties the
|
|
# rule to this specific sip, so multiple WAN IPs forwarding the same port to
|
|
# different targets each get their own masquerade.
|
|
iptables -t nat -A ${NAME}_POSTROUTING -m addrtype --src-type LOCAL -m conntrack --ctorigdst "$sip" -d "$dip" -p tcp --dport "$dport" -j MASQUERADE
|
|
iptables -t nat -A ${NAME}_POSTROUTING -m addrtype --src-type LOCAL -m conntrack --ctorigdst "$sip" -d "$dip" -p udp --dport "$dport" -j MASQUERADE
|
|
# Same-subnet hairpin: when traffic originates from the same subnet as the DNAT
|
|
# target (e.g. a container reaching another container, or a WireGuard peer
|
|
# connecting to itself via the tunnel's public IP).
|
|
iptables -t nat -A ${NAME}_POSTROUTING -s "$dip/$dprefix" -d "$dip" -p tcp --dport "$dport" -j MASQUERADE
|
|
iptables -t nat -A ${NAME}_POSTROUTING -s "$dip/$dprefix" -d "$dip" -p udp --dport "$dport" -j MASQUERADE
|
|
|
|
exit $err
|