# L2/L3 of the gateway: PPPoE WAN port (optionally on an ISP VLAN), a # VLAN-filtering bridge over the LAN ports, and one L3 interface per VLAN. { settings }: { lib, pkgs, ... }: let cfg = settings; vlanIf = name: "vlan-${name}"; in { assertions = [ { assertion = cfg.vlans ? mgmt && cfg.vlans ? lan; message = "router: every site must define the `mgmt` and `lan` VLANs."; } { assertion = lib.all (v: cfg.vlans ? ${v}) (lib.attrValues cfg.accessPorts); message = "router: every accessPorts value must name a defined VLAN."; } { assertion = lib.all (p: !(cfg.accessPorts ? ${p})) cfg.trunkPorts; message = "router: a port cannot be both a trunk and an access port."; } { assertion = cfg.stagingPort == null || !( cfg.stagingPort == cfg.wan.interface || lib.elem cfg.stagingPort cfg.trunkPorts || cfg.accessPorts ? ${cfg.stagingPort} ); message = "router: stagingPort ${toString cfg.stagingPort} is also the WAN, a trunk or an access port."; } ]; # Router diagnostics toolkit: packets (tcpdump), path (mtr), link # negotiation (ethtool), NAT state (conntrack), DNS (kdig), per-flow # bandwidth (iftop), WAN throughput (librespeed-cli; iperf3 covers LAN). environment.systemPackages = with pkgs; [ tcpdump mtr ethtool conntrack-tools knot-dns iftop librespeed-cli ]; networking.useNetworkd = true; networking.useDHCP = false; systemd.network.enable = true; systemd.network.netdevs = { "20-br0" = { netdevConfig = { Name = "br0"; Kind = "bridge"; }; bridgeConfig.VLANFiltering = true; }; } // lib.optionalAttrs (cfg.wan.vlanId != null) { "15-wan-vlan" = { netdevConfig = { Name = "wan-vlan"; Kind = "vlan"; }; vlanConfig.Id = cfg.wan.vlanId; }; } // lib.mapAttrs' ( name: vlan: lib.nameValuePair "30-${vlanIf name}" { netdevConfig = { Name = vlanIf name; Kind = "vlan"; }; vlanConfig.Id = vlan.id; } ) cfg.vlans; systemd.network.networks = let taggedAll = lib.mapAttrsToList (_: vlan: { VLAN = vlan.id; }) cfg.vlans; in lib.optionalAttrs (cfg.stagingPort != null) { # Staging uplink (see interface.nix): plain DHCPv4 client on a spare # port, no bridge/VLAN membership, so the firewall treats it as untrusted. "05-staging" = { matchConfig.Name = cfg.stagingPort; networkConfig.DHCP = "ipv4"; }; } // { # WAN port carries only the PPPoE session; no IP config of its own. "10-wan" = { matchConfig.Name = cfg.wan.interface; networkConfig.LinkLocalAddressing = "no"; vlan = lib.optional (cfg.wan.vlanId != null) "wan-vlan"; linkConfig = { RequiredForOnline = "carrier"; } # The wan-vlan subinterface (and thus the PPPoE session) inherits # the parent port's MAC, so spoofing here covers both cases. // lib.optionalAttrs (cfg.wan.macAddress != null) { MACAddress = cfg.wan.macAddress; }; }; } // lib.optionalAttrs (cfg.wan.vlanId != null) { # The ISP-side VLAN subinterface pppd dials on (e.g. AIS tags PPPoE). "15-wan-vlan" = { matchConfig.Name = "wan-vlan"; networkConfig.LinkLocalAddressing = "no"; linkConfig.RequiredForOnline = "no"; }; } // { # The bridge itself is L2-only; L3 lives on the vlan-* interfaces, # which hang off the bridge (tagged on the bridge "self" port). "20-br0" = { matchConfig.Name = "br0"; networkConfig.LinkLocalAddressing = "no"; vlan = lib.mapAttrsToList (name: _: vlanIf name) cfg.vlans; bridgeVLANs = taggedAll; linkConfig.RequiredForOnline = "no"; }; } // lib.listToAttrs ( map (port: { name = "25-trunk-${port}"; value = { matchConfig.Name = port; networkConfig.Bridge = "br0"; bridgeVLANs = taggedAll; linkConfig.RequiredForOnline = "no"; }; }) cfg.trunkPorts ) // lib.mapAttrs' ( port: vlanName: lib.nameValuePair "25-access-${port}" { matchConfig.Name = port; networkConfig.Bridge = "br0"; bridgeVLANs = [ { VLAN = cfg.vlans.${vlanName}.id; PVID = cfg.vlans.${vlanName}.id; EgressUntagged = cfg.vlans.${vlanName}.id; } ]; linkConfig.RequiredForOnline = "no"; } ) cfg.accessPorts // lib.mapAttrs' ( name: vlan: lib.nameValuePair "40-${vlanIf name}" { matchConfig.Name = vlanIf name; address = [ "${vlan.address}/${toString vlan.prefixLength}" ]; networkConfig = { IPv6AcceptRA = false; # Announce a /64 carved from the DHCPv6-PD prefix on ppp0 (SLAAC). IPv6SendRA = true; DHCPPrefixDelegation = true; }; dhcpPrefixDelegationConfig.SubnetId = "auto"; linkConfig.RequiredForOnline = "no"; } ) cfg.vlans; }