{ config, lib, ... }: let cfg = config.router; v6 = cfg.wan.ipv6.enable; v6ppp = v6 && cfg.wan.ipv6.transport == "ppp"; # IPv6 on the physical link (not inside the PPP session). v6native = v6 && !v6ppp; # The device carrying DHCPv6 / RAs. v6dev = if v6ppp then cfg.wan.ipv4.interface else cfg.wan.interface; pppoe = cfg.wan.ipv4.type == "pppoe"; vlan = cfg.wan.ipv4.pppoe.vlan; vlanDevice = if cfg.wan.ipv4.pppoe.vlanName != null then cfg.wan.ipv4.pppoe.vlanName else "${cfg.wan.interface}.${toString vlan}"; wanVlanOn = parent: lib.optional (pppoe && vlan != null && parent == cfg.wan.interface) vlanDevice; lans = lib.attrValues cfg.lans; vlanLans = lib.filter (l: l.vlan != null) lans; # VLAN devices hang off their parent; the parent's own unit must list them. vlansOn = parent: map (l: l.device) (lib.filter (l: l.interface == parent) vlanLans) ++ wanVlanOn parent; # The WAN link's own unit is only needed when no LAN shares the link. sharedLink = lib.elem cfg.wan.interface (map (l: l.interface) lans); untaggedParents = map (l: l.interface) (lib.filter (l: l.vlan == null) lans); # Parents that carry only tagged LANs still need a unit to be brought up. trunkParents = lib.filter (p: !(lib.elem p untaggedParents)) ( lib.unique (map (l: l.interface) vlanLans) ); dhcp = if pppoe then (if v6native then "ipv6" else "no") else (if v6 then "yes" else "ipv4"); in { config = lib.mkIf cfg.enable { networking.useNetworkd = true; networking.useDHCP = false; # Wait for the LAN devices (VLANs included: Kea only binds interfaces that # exist when it starts) but not for the uplink: an ISP outage or a pulled # cable must not hold up boot or a deployment. systemd.network.wait-online.extraArgs = map (l: "--interface=${l.device}") ( lib.attrValues cfg.lans ); # Rename network cards by MAC address. Only at boot: the interface that is # up cannot be renamed, and router-apply stages such a change for the # next boot instead of activating it (see the names file below). systemd.network.links = lib.mapAttrs' ( name: i: lib.nameValuePair "05-${name}" { matchConfig = { PermanentMACAddress = i.mac; Type = "ether"; }; linkConfig.Name = name; } ) cfg.interfaces; # The names this system gives to devices; router-apply compares the file # between the running and the new system and stages a change of a name for # the next boot (a device that is up cannot be renamed). environment.etc."router/interface-names".text = lib.concatMapStrings (l: l + "\n") ( lib.sort lib.lessThan ( lib.mapAttrsToList (name: i: "nic ${i.mac} ${name}") cfg.interfaces ++ map (l: "vlan ${l.interface} ${toString l.vlan} ${l.device}") vlanLans ++ lib.optional (pppoe && vlan != null) "vlan ${cfg.wan.interface} ${toString vlan} ${vlanDevice}" ) ); boot.kernel.sysctl = { "net.ipv4.conf.all.forwarding" = 1; "net.ipv6.conf.all.forwarding" = 1; }; systemd.network.netdevs = lib.optionalAttrs (pppoe && vlan != null) { "05-wan-pppoe" = { netdevConfig = { Kind = "vlan"; Name = vlanDevice; }; vlanConfig.Id = vlan; }; } // lib.mapAttrs' ( name: lan: lib.nameValuePair "06-lan-${name}" { netdevConfig = { Kind = "vlan"; Name = lan.device; }; vlanConfig.Id = lan.vlan; } ) (lib.filterAttrs (_: l: l.vlan != null) cfg.lans); systemd.network.networks = lib.optionalAttrs (!sharedLink) { "10-wan" = { matchConfig.Name = cfg.wan.interface; networkConfig = { DHCP = dhcp; } // lib.optionalAttrs v6native { IPv6AcceptRA = true; KeepConfiguration = "yes"; } // lib.optionalAttrs (pppoe && !v6native) { # IPv6 (if any) lives inside the PPP session, never on the link. LinkLocalAddressing = "no"; IPv6AcceptRA = false; } // lib.optionalAttrs (pppoe && vlan != null) { VLAN = [ vlanDevice ]; }; } // lib.optionalAttrs (!pppoe) { # The router runs its own resolver; never adopt the ISP's DNS for the host. dhcpV4Config.UseDNS = false; } // lib.optionalAttrs v6native { # The ISP sends RAs (default route) without DHCPv6 flags, so solicit anyway. dhcpV6Config = { WithoutRA = "solicit"; UseDelegatedPrefix = true; UseDNS = false; }; }; } // lib.optionalAttrs v6ppp { # IPv6 inside the PPP session: pppd (IPv4, IPv6CP link-local) creates the # device; networkd takes RAs and DHCPv6 (prefix delegation) on it. "12-wan-ppp" = { matchConfig.Name = cfg.wan.ipv4.interface; networkConfig = { DHCP = "ipv6"; IPv6AcceptRA = true; KeepConfiguration = "yes"; }; dhcpV6Config = { WithoutRA = "solicit"; UseDelegatedPrefix = true; UseDNS = false; }; }; } // lib.optionalAttrs (pppoe && vlan != null) { # pppd configures the session; networkd only has to bring the device up. "11-wan-pppoe" = { matchConfig.Name = vlanDevice; networkConfig = { LinkLocalAddressing = "no"; IPv6AcceptRA = false; }; }; } // lib.mapAttrs' ( name: lan: lib.nameValuePair "20-lan-${name}" ( { matchConfig.Name = lan.device; address = [ "${lan.ipv4.address}/${toString lan.ipv4.prefixLength}" ] ++ lib.optional (lan.ipv6.address != null) lan.ipv6.address; networkConfig = { ConfigureWithoutCarrier = true; # Never accept RAs from the inside. IPv6AcceptRA = false; } // lib.optionalAttrs (lan.vlan == null && vlansOn lan.interface != [ ]) { VLAN = vlansOn lan.interface; } // lib.optionalAttrs lan.ipv6.pd { DHCPPrefixDelegation = true; } // lib.optionalAttrs (lan.ipv6.pd || lan.ipv6.address != null) { IPv6SendRA = true; }; } // lib.optionalAttrs (lan.ipv6.address != null) { # With prefix delegation networkd announces only the delegated # prefix, so announce the ULA explicitly (SLAAC, on-link). ipv6Prefixes = [ { Prefix = (import ./lib.nix { inherit lib; }).ipv6.prefix64 lan.ipv6.address; OnLink = true; AddressAutoconfiguration = true; } ]; } // lib.optionalAttrs lan.ipv6.pd { dhcpPrefixDelegationConfig = { UplinkInterface = v6dev; SubnetId = 0; Announce = true; Assign = true; }; } // lib.optionalAttrs (lan.ipv6.pd || lan.ipv6.address != null) { # RDNSS points hosts at the router's resolver on its stable ULA; the # delegated global address is unknown at evaluation time, so without # a static `ipv6.address` there is nothing to announce. ipv6SendRAConfig = if lan.ipv6.address != null then { EmitDNS = true; DNS = [ (builtins.head (lib.splitString "/" lan.ipv6.address)) ]; } else { EmitDNS = false; }; } ) ) cfg.lans // lib.listToAttrs ( map ( parent: lib.nameValuePair "19-trunk-${parent}" { matchConfig.Name = parent; networkConfig = { LinkLocalAddressing = "no"; IPv6AcceptRA = false; ConfigureWithoutCarrier = true; VLAN = vlansOn parent; }; } ) trunkParents ); }; }