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.
92 lines
3.5 KiB
Nix
92 lines
3.5 KiB
Nix
# Router firewall/NAT policy (nftables). Trust model:
|
|
# mgmt VLAN -> trusted: router services, all VLANs, WAN
|
|
# 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 inbound (pre-cutover uplink into the old LAN);
|
|
# allowWan VLANs are NATed out through it while ppp0 is down
|
|
{ settings }:
|
|
{ lib, ... }:
|
|
let
|
|
cfg = settings;
|
|
|
|
vlanIfs = lib.mapAttrsToList (name: _: "vlan-${name}") cfg.vlans;
|
|
wanVlanIfs = lib.mapAttrsToList (name: _: "vlan-${name}") (
|
|
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;
|
|
|
|
# SSH reachable only from the mgmt VLAN (trusted) and the mesh — never
|
|
# from the WAN or the other VLANs.
|
|
services.openssh.openFirewall = false;
|
|
|
|
networking.firewall = {
|
|
enable = true;
|
|
filterForward = true;
|
|
trustedInterfaces = [ "vlan-mgmt" ];
|
|
|
|
# Non-mgmt VLANs may only talk to the router's DNS and DHCP; the staging
|
|
# uplink (old LAN, pre-cutover) gets admin SSH so the box can be reached
|
|
# before the mgmt VLAN or the mesh are up.
|
|
interfaces =
|
|
lib.genAttrs nonMgmtIfs (_: {
|
|
allowedTCPPorts = [ 53 ];
|
|
allowedUDPPorts = [
|
|
53
|
|
67
|
|
];
|
|
})
|
|
// lib.optionalAttrs (cfg.stagingPort != null) {
|
|
${cfg.stagingPort}.allowedTCPPorts = [ 22 ];
|
|
};
|
|
|
|
extraInputRules = ''
|
|
ip6 saddr ${cfg.mesh.subnet} tcp dport 22 accept comment "admin ssh over the mesh"
|
|
ip6 saddr ${cfg.mesh.subnet} tcp dport 4000 accept comment "blocky metrics scrape from control"
|
|
'';
|
|
|
|
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"
|
|
'';
|
|
};
|
|
|
|
# networking.nat both masquerades and opens forward-to-WAN for exactly its
|
|
# internalInterfaces — so this list, not a rule of our own, is where
|
|
# `allowWan` is enforced. Listing every VLAN here would silently let
|
|
# allowWan = false VLANs out.
|
|
networking.nat = {
|
|
enable = true;
|
|
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"
|
|
}
|
|
'';
|
|
};
|
|
};
|
|
}
|