diff options
| author | Ahmed Abdelhalim <[email protected]> | 2026-07-08 14:28:33 +0200 |
|---|---|---|
| committer | Ahmed Abdelhalim <[email protected]> | 2026-07-08 16:55:30 +0200 |
| commit | b8f25e9d80c96f7170d51eb5abcd662a66d55317 (patch) | |
| tree | 664374075de9f34ee23ab8f59d50371b4f3522e3 /roles/gateway | |
| parent | 663bce95c096ed43dd24674debf4c8df2be1b604 (diff) | |
Route IPv6 through WireGuard VPN via radvd and static gateway
Add radvd to gateway role to advertise Pi as high-preference IPv6
default router using the stable ULA prefix (fd1e:.../64). With
FritzBox also sending RAs, devices end up with ECMP between Pi and
FritzBox. To solve this, add network_ipv6_gateway (Pi's link-local)
as a static route with metric 100 to all managed hosts — beats RA
metric 425, ensuring all IPv6 default traffic goes through Pi.
Fix IPv6 MASQUERADE in gateway-apply-rules:
- Direct mode: add MASQUERADE on end0 (LAN devices use ULA source
addresses not known to FritzBox, so Pi must NAT them)
- FORWARD rules: restrict to RELATED,ESTABLISHED only — previously
the broad ACCEPT rule passed un-NAT'd packets alongside masqueraded
copies, causing duplicate SYNs, conntrack corruption, and RSTs
- MASQUERADE/clear rules: match by interface not by source subnet
(devices may use any source address, not just the ULA prefix)
- VPN mode return traffic: explicitly restrict to wg+→end0 direction
Add network_ipv6_gateway var (optional) to network role NM templates
(ethernet, wifi, bridge) — injects a static IPv6 default route at
metric 100 when set. Add rpi5 static route to FritzBox link-local so
Pi keeps IPv6 after FritzBox RA is disabled.
Force SSH to IPv4 for *.local hosts (AddressFamily inet) — prevents
Ansible from hanging on mDNS returning multiple IPv6 addresses.
Update gateway and pihole READMEs with two-step IPv6 setup process.
Co-Authored-By: Claude.ai
Diffstat (limited to 'roles/gateway')
| -rw-r--r-- | roles/gateway/README.md | 26 | ||||
| -rw-r--r-- | roles/gateway/templates/gateway-apply-rules.sh.j2 | 11 |
2 files changed, 19 insertions, 18 deletions
diff --git a/roles/gateway/README.md b/roles/gateway/README.md index cfbe49d3..6c46cc9b 100644 --- a/roles/gateway/README.md +++ b/roles/gateway/README.md @@ -19,20 +19,22 @@ gateway_router_interface: "end0" ### IPv6 Support (Optional) -IPv6 gateway is enabled by setting both `network_ipv6_address` and `gateway_local_ipv6_subnet`: +IPv6 gateway is enabled by setting `gateway_local_ipv6_subnet` to the ULA prefix advertised by radvd: ```yaml -network_ipv6_address: "2a02:xxxx:xxxx:xxxx::254/64" # Pi's IPv6 address -gateway_local_ipv6_subnet: "2a02:xxxx:xxxx:xxxx::/64" # LAN IPv6 subnet +gateway_local_ipv6_subnet: "fd00:xxxx:xxxx:xxxx::/64" # ULA prefix (stable, router-independent) ``` -To disable IPv6 entirely, remove both variables from `host_vars`. The role will: -- Omit `accept_ra` sysctl (no IPv6 interface key written) -- Skip IPv6 iptables masquerade rules (`gateway_local_ipv6_subnet` defaults to `""`) -- Leave `net.ipv6.conf.all.forwarding=0` +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 -**Note:** `accept_ra` is set per-interface (`gateway_router_interface`) only when IPv6 is enabled. -This prevents rogue RA acceptance on WireGuard and LAN interfaces. +**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. + +To disable IPv6, leave `gateway_local_ipv6_subnet: ""` (default). The role skips all ip6tables rules and disables radvd. ## Network Architecture @@ -63,9 +65,9 @@ Client Devices (192.168.1.0/24) **Direct Mode Characteristics:** -- All traffic NAT'd through router interface (`end0`) -- Direct IPv4/IPv6 routing to ISP -- Standard internet routing +- IPv4 traffic NAT'd through router interface (`end0`) +- IPv6 forwarded without NAT (globally routable addresses, native routing) +- Standard internet routing via ISP ## Requirements diff --git a/roles/gateway/templates/gateway-apply-rules.sh.j2 b/roles/gateway/templates/gateway-apply-rules.sh.j2 index 0ce9f740..daddb180 100644 --- a/roles/gateway/templates/gateway-apply-rules.sh.j2 +++ b/roles/gateway/templates/gateway-apply-rules.sh.j2 @@ -27,8 +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 -t nat -D POSTROUTING -s "$GATEWAY_SUBNET_V6" -o wg+ -j MASQUERADE 2>/dev/null || true - ip6tables -t nat -D POSTROUTING -s "$GATEWAY_SUBNET_V6" -o "$GATEWAY_INTERFACE" -j MASQUERADE 2>/dev/null || true + ip6tables -t nat -D POSTROUTING -o wg+ -j MASQUERADE 2>/dev/null || true + ip6tables -t nat -D POSTROUTING -o "$GATEWAY_INTERFACE" -j MASQUERADE 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 @@ -45,7 +45,7 @@ apply_vpn_mode() { ip6tables -t nat -A POSTROUTING -o wg+ -j MASQUERADE ip6tables -A OUTPUT -o wg+ -j ACCEPT ip6tables -A FORWARD -i "$GATEWAY_INTERFACE" -o wg+ -j ACCEPT - ip6tables -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT + ip6tables -A FORWARD -i wg+ -o "$GATEWAY_INTERFACE" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT fi log_message "VPN mode rules applied - traffic routed through VPN" } @@ -57,10 +57,9 @@ apply_direct_rules() { iptables -A FORWARD -s "$GATEWAY_SUBNET_V4" -o "$GATEWAY_INTERFACE" -j ACCEPT iptables -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT if [ -n "$GATEWAY_SUBNET_V6" ]; then - # No MASQUERADE for IPv6: LAN devices have public SLAAC addresses, native routing applies + ip6tables -t nat -A POSTROUTING -o "$GATEWAY_INTERFACE" -j MASQUERADE ip6tables -A OUTPUT -o "$GATEWAY_INTERFACE" -j ACCEPT - ip6tables -A FORWARD -i "$GATEWAY_INTERFACE" -o "$GATEWAY_INTERFACE" -j ACCEPT - ip6tables -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT + ip6tables -A FORWARD -i "$GATEWAY_INTERFACE" -o "$GATEWAY_INTERFACE" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT fi log_message "Direct mode rules applied - normal routing active" } |
