router: let allowWan VLANs out through the staging uplink

With stagingPort set, the box itself had internet over the staging DHCP
uplink but LAN/Wi-Fi clients had none: forward and masquerade were scoped
to ppp0 only. Worse, pppd's `defaultroute` refuses to install its route
while the staging DHCP default route (metric 1024) exists ("not replacing
existing default route"), so even a live PPPoE session was never used.

- firewall: forward-allow + masquerade allowWan VLANs -> stagingPort in a
  separate `router-staging-nat` postrouting chain (networking.nat only
  takes one external interface). Same allowWan set as nixos-nat.
- pppoe: `defaultroute-metric 0`, so pppd only checks for a metric-0
  default route, installs ppp0 as the preferred exit and removes it on
  hangup, leaving the staging route as the fallback.
This commit is contained in:
2026-09-21 04:32:07 +00:00
parent 4146f2c878
commit 8090ab3e6d
3 changed files with 42 additions and 8 deletions
+27 -1
View File
@@ -3,7 +3,8 @@
# other VLANs -> DNS/DHCP on the router + WAN (if allowWan); no inter-VLAN
# WAN (ppp0) -> nothing inbound beyond established/related
# mesh -> admin SSH + metrics scrapes (same trust boundary as the fleet)
# staging -> admin SSH only (pre-cutover uplink into the old LAN)
# staging -> admin SSH only inbound (pre-cutover uplink into the old LAN);
# allowWan VLANs are NATed out through it while ppp0 is down
{ settings }:
{ lib, ... }:
let
@@ -14,6 +15,13 @@ let
lib.filterAttrs (_: vlan: vlan.allowWan) cfg.vlans
);
nonMgmtIfs = lib.filter (i: i != "vlan-mgmt") vlanIfs;
# allowWan VLANs may also leave through the staging uplink. Same set as
# networking.nat.internalInterfaces below, so `allowWan` holds on both
# exits. The kernel picks the exit: ppp0 (metric 0, see pppoe.nix) while
# the session is up, the staging DHCP route (metric 1024) otherwise.
stagingExit = cfg.stagingPort != null && wanVlanIfs != [ ];
wanVlanSet = "{ ${lib.concatMapStringsSep ", " (i: ''"${i}"'') wanVlanIfs} }";
in
{
networking.nftables.enable = true;
@@ -50,6 +58,9 @@ in
extraForwardRules = ''
tcp flags syn tcp option maxseg size set rt mtu comment "MSS clamp for PPPoE mtu 1492"
iifname "vlan-mgmt" accept comment "mgmt reaches all VLANs and the WAN"
''
+ lib.optionalString stagingExit ''
iifname ${wanVlanSet} oifname "${cfg.stagingPort}" accept comment "allowWan VLANs out via the staging uplink"
'';
};
@@ -62,4 +73,19 @@ in
externalInterface = "ppp0";
internalInterfaces = wanVlanIfs;
};
# networking.nat only masquerades on its single externalInterface; the
# staging uplink needs its own postrouting chain (nixos-nat's is
# oifname-scoped to ppp0, so the two never both apply).
networking.nftables.tables = lib.optionalAttrs stagingExit {
router-staging-nat = {
family = "ip";
content = ''
chain post {
type nat hook postrouting priority srcnat;
iifname ${wanVlanSet} oifname "${cfg.stagingPort}" masquerade comment "allowWan VLANs out via the staging uplink"
}
'';
};
};
}