From e9b225d2d6379330fe3210a686774bf2a717a03c Mon Sep 17 00:00:00 2001 From: beatzaplenty Date: Sun, 26 Jul 2026 10:00:47 +1000 Subject: [PATCH] fix(tailscale-router): add MASQUERADE to POSTROUTING, not nixos-nat-post extraCommands runs after nixos-nat-post is deleted but before it is re-created, so -A nixos-nat-post silently fails every time. POSTROUTING is a built-in chain that always exists; target it directly instead. The -C idempotency check prevents duplicate rules on firewall reloads. Drop networking.nat.enable -- it was only needed for the sub-chain that turned out to be the wrong target. Co-Authored-By: Claude Sonnet 4.6 --- modules/build-types/tailscale-router.nix | 44 +++++++++++------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/modules/build-types/tailscale-router.nix b/modules/build-types/tailscale-router.nix index 764ef38..a1d3c56 100644 --- a/modules/build-types/tailscale-router.nix +++ b/modules/build-types/tailscale-router.nix @@ -16,29 +16,27 @@ # Must also be approved in the Tailscale admin console (Machines → Edit route settings). services.tailscale.extraUpFlags = [ "--advertise-routes=${vars.lanCidr}" ]; - networking = { - # SNAT traffic from LAN machines going out through Tailscale so the remote - # peer sees it sourced from this router's Tailscale IP (100.x.x.x) rather - # than a raw LAN IP. Without this, Tailscale drops the forwarded packets - # because the source is not a recognised Tailscale address. - # - # networking.nat.externalInterface alone does not insert a MASQUERADE rule - # (it only does so when internalInterfaces is also set). We use - # extraCommands to add the rule into the nixos-nat-post chain that - # networking.nat.enable creates, and extraStopCommands to clean it up. - nat.enable = true; + networking.firewall = { + # Forwarded subnet-router traffic arrives on tailscale0 already + # tailscale-authenticated -- the firewall's normal per-port allow-list + # would otherwise drop it. Standard NixOS/Tailscale subnet-router guidance. + trustedInterfaces = [ "tailscale0" ]; - firewall = { - # Forwarded subnet-router traffic arrives on tailscale0 already - # tailscale-authenticated -- the firewall's normal per-port allow-list - # would otherwise drop it. Standard NixOS/Tailscale subnet-router guidance. - trustedInterfaces = [ "tailscale0" ]; - extraCommands = '' - iptables -t nat -A nixos-nat-post -s ${vars.lanCidr} -o tailscale0 -j MASQUERADE - ''; - extraStopCommands = '' - iptables -t nat -D nixos-nat-post -s ${vars.lanCidr} -o tailscale0 -j MASQUERADE 2>/dev/null || true - ''; - }; + # SNAT LAN traffic going into Tailscale so the remote peer sees it as + # coming from this router's Tailscale IP rather than a raw LAN IP. + # Without this, Tailscale drops forwarded packets whose source is not a + # recognised Tailscale address. + # + # We target POSTROUTING directly (always-existing built-in chain) rather + # than nixos-nat-post: extraCommands runs after the old nixos-nat-post is + # deleted but before the new one is created, so -A nixos-nat-post silently + # fails. The -C check makes the rule idempotent across firewall reloads. + extraCommands = '' + iptables -t nat -C POSTROUTING -s ${vars.lanCidr} -o tailscale0 -j MASQUERADE 2>/dev/null || \ + iptables -t nat -A POSTROUTING -s ${vars.lanCidr} -o tailscale0 -j MASQUERADE + ''; + extraStopCommands = '' + iptables -t nat -D POSTROUTING -s ${vars.lanCidr} -o tailscale0 -j MASQUERADE 2>/dev/null || true + ''; }; } -- 2.54.0