wifi.* settings turn the gateway's own radios into the site AP (hostapd): SSIDs are defined once in wifi.networks and act as untagged access ports of their VLAN (each BSS joins br0 with the VLAN's PVID), radios pick what they broadcast, passphrases are vars prompts. Extra SSIDs on a radio get BSSIDs derived from its hardware address. A udev rule puts configured radios in AP mode on appearance, since networkd cannot bridge a station-mode interface and gives up before hostapd switches it. wifi.enable gates all of it. The VM test grows a hwsim radio with two SSIDs and a WPA3 station in its own netns that must get a Kea lease on the SSID's VLAN. Making the client deterministic (route metrics, loose rp-filter, a guard against a vacuous negative check) exposed that allowWan was never enforced: networking.nat opens forward-to-WAN for all of its internalInterfaces, which listed every VLAN. It now lists only the allowWan VLANs; the duplicate custom rule is gone. No behavioural change on gw-cnx-1 (all its VLANs allow WAN).
59 lines
1.9 KiB
Nix
59 lines
1.9 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)
|
|
{ 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;
|
|
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.
|
|
interfaces = lib.genAttrs nonMgmtIfs (_: {
|
|
allowedTCPPorts = [ 53 ];
|
|
allowedUDPPorts = [
|
|
53
|
|
67
|
|
];
|
|
});
|
|
|
|
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"
|
|
'';
|
|
};
|
|
|
|
# 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;
|
|
};
|
|
}
|