diff options
Diffstat (limited to 'roles')
| -rw-r--r-- | roles/gateway/README.md | 59 | ||||
| -rw-r--r-- | roles/gateway/templates/gateway-apply-rules.sh.j2 | 5 |
2 files changed, 52 insertions, 12 deletions
diff --git a/roles/gateway/README.md b/roles/gateway/README.md index 6c46cc9b..5340cc25 100644 --- a/roles/gateway/README.md +++ b/roles/gateway/README.md @@ -28,11 +28,15 @@ gateway_local_ipv6_subnet: "fd00:xxxx:xxxx:xxxx::/64" # ULA prefix (stable, rou When set, this role: - Enables `net.ipv6.conf.all.forwarding=1` and `accept_ra=2` on `gateway_router_interface` - Deploys radvd advertising the ULA prefix with `AdvDefaultPreference high` — Pi wins as IPv6 default router -- Adds ip6tables MASQUERADE/FORWARD rules matching by interface (not subnet) so all source addresses are handled +- Adds ip6tables MASQUERADE/FORWARD rules so all client IPv6 traffic is forwarded through Pi +- Drops outbound ICMPv6 Redirect messages — prevents Pi from redirecting clients to bypass the gateway -**Prerequisite:** Disable the router's RA (FritzBox: Home Network > IPv6 > Router advertisement > ❌) after -setting `network_ipv6_gateway` in `host_vars` so Pi keeps its static IPv6 default route to the router. -See `roles/pihole/README.md` for the full two-step setup. +**ICMPv6 Redirect suppression is critical.** Pi, the ISP router (FritzBox), and LAN clients share the same L2 +segment. When Pi forwards a packet to an external destination and the kernel determines the ISP router is a +"better" next-hop on the same link, it sends an ICMPv6 Redirect to the client. The client then sends +subsequent connections directly to the ISP router — bypassing Pi's masquerade and content filtering entirely. +The first connection attempt fails (Pi forwarded nothing, just redirected), giving the symptom of "first +request fails every ~30-60s." Dropping Redirect messages in ip6tables OUTPUT fixes this. To disable IPv6, leave `gateway_local_ipv6_subnet: ""` (default). The role skips all ip6tables rules and disables radvd. @@ -61,13 +65,40 @@ Client Devices (192.168.1.0/24) - All traffic NAT'd through `wg+` interfaces - Pi and client traffic both use VPN exit IP -- Most VPN providers IPv4/IPv6 protocol routing automatically +- Most VPN providers handle IPv4/IPv6 protocol routing automatically **Direct Mode Characteristics:** - IPv4 traffic NAT'd through router interface (`end0`) -- IPv6 forwarded without NAT (globally routable addresses, native routing) -- Standard internet routing via ISP +- IPv6 traffic masqueraded through router interface — clients appear as Pi's GUA to the ISP router, + preventing ISP-router-level content filtering from applying per-device rules +- ICMPv6 Redirects suppressed so clients always route through Pi + +## IPv6 Architecture Notes + +### Why masquerade IPv6 in direct mode? + +Clients have globally routable GUA addresses (assigned by ISP router SLAAC). Without masquerade, the ISP +router sees each client's real GUA and can apply per-device content filtering. With masquerade, all client +traffic appears to originate from Pi's GUA — bypassing per-device filtering consistently. + +### Why suppress ICMPv6 Redirects? + +When Pi, the ISP router, and clients share the same L2 segment (single broadcast domain), the Linux kernel +sends ICMPv6 Redirect messages when it detects a "better" next-hop for a destination on the same link. For +external GUA destinations reachable via the ISP router directly, Linux redirects clients from Pi to the ISP +router. This causes: +1. First SYN from client arrives at Pi — Pi sends Redirect, does NOT forward the packet +2. Client receives Redirect, caches ISP router as next-hop for that destination +3. Subsequent connections go directly to ISP router — masquerade and filtering bypassed + +Dropping `icmpv6-type redirect` in ip6tables OUTPUT prevents this entirely. + +### Why radvd with `AdvDefaultPreference high`? + +The ISP router (FritzBox) also sends RAs with default preference `medium` (or `low` if configured). +Pi's radvd sends `AdvDefaultPreference high`, so clients prefer Pi as their IPv6 default router. +Without this, clients split or prefer the ISP router depending on RA timing. ## Requirements @@ -104,10 +135,18 @@ journalctl -u gateway-direct-mode.service -f sudo iptables -L -n -v sudo iptables -t nat -L -n -v -# Check WireGuard interface detection -ip link show | grep wg +# Verify ip6tables rules (redirect DROP must be first in OUTPUT) +sudo ip6tables -L OUTPUT -n -v --line-numbers +sudo ip6tables -t nat -L -n -v + +# Confirm no ICMPv6 Redirects being sent +sudo tcpdump -i end0 -n 'icmp6 and ip6[40] == 137' + +# Verify clients route through Pi (not ISP router) +# On client: ip -6 route show default +# On Pi: sudo tcpdump -i end0 -n -e ip6 and host <client-GUA> # Test traffic routing curl -4 ifconfig.co # Should show VPN IP when VPN active -curl -6 ifconfig.co # Should show VPN IPv6 when VPN active +curl -6 ifconfig.co # Should show Pi's GUA in direct mode, VPN IPv6 in VPN mode ``` diff --git a/roles/gateway/templates/gateway-apply-rules.sh.j2 b/roles/gateway/templates/gateway-apply-rules.sh.j2 index 0cbdd67a..59a960cf 100644 --- a/roles/gateway/templates/gateway-apply-rules.sh.j2 +++ b/roles/gateway/templates/gateway-apply-rules.sh.j2 @@ -27,9 +27,8 @@ clear_rules() { iptables -D OUTPUT -o "$GATEWAY_INTERFACE" -j ACCEPT 2>/dev/null || true if [ -n "$GATEWAY_SUBNET_V6" ]; then ip6tables -F FORWARD 2>/dev/null || true + ip6tables -F OUTPUT 2>/dev/null || true ip6tables -t nat -F POSTROUTING 2>/dev/null || true - ip6tables -D OUTPUT -o wg+ -j ACCEPT 2>/dev/null || true - ip6tables -D OUTPUT -o "$GATEWAY_INTERFACE" -j ACCEPT 2>/dev/null || true fi } @@ -42,6 +41,7 @@ apply_vpn_mode() { iptables -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT if [ -n "$GATEWAY_SUBNET_V6" ]; then ip6tables -t nat -A POSTROUTING -o wg+ -j MASQUERADE + ip6tables -I OUTPUT 1 -p ipv6-icmp --icmpv6-type redirect -j DROP ip6tables -A OUTPUT -o wg+ -j ACCEPT ip6tables -A FORWARD -i "$GATEWAY_INTERFACE" -o wg+ -j ACCEPT ip6tables -A FORWARD -i wg+ -o "$GATEWAY_INTERFACE" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT @@ -57,6 +57,7 @@ apply_direct_rules() { iptables -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT if [ -n "$GATEWAY_SUBNET_V6" ]; then ip6tables -t nat -A POSTROUTING -o "$GATEWAY_INTERFACE" -j MASQUERADE + ip6tables -I OUTPUT 1 -p ipv6-icmp --icmpv6-type redirect -j DROP ip6tables -A OUTPUT -o "$GATEWAY_INTERFACE" -j ACCEPT ip6tables -A FORWARD -s "$GATEWAY_SUBNET_V6" -i "$GATEWAY_INTERFACE" -o "$GATEWAY_INTERFACE" -j ACCEPT ip6tables -A FORWARD -i "$GATEWAY_INTERFACE" -o "$GATEWAY_INTERFACE" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT |
