{ config, lib, ... }: let cfg = config.router; ipv4 = (import ./lib.nix { inherit lib; }).ipv4; lansByName = cfg.lans; lans = lib.attrValues lansByName; validIp = s: ipv4.parse s != null; # Assertions about a single LAN; skipped (reported once) when its addresses # do not even parse, so we never feed null into the integer helpers. perLan = name: lan: let where = "router.lans.${name}"; addrOk = validIp lan.ipv4.address; startOk = validIp lan.dhcp.pool.start; endOk = validIp lan.dhcp.pool.end; addr = ipv4.parse lan.ipv4.address; start = ipv4.parse lan.dhcp.pool.start; end = ipv4.parse lan.dhcp.pool.end; net = { address = addr; inherit (lan.ipv4) prefixLength; }; parsed = addrOk && startOk && endOk; in [ { assertion = addrOk; message = "router: ${where}.ipv4.address `${lan.ipv4.address}` is not a valid IPv4 address."; } ] ++ lib.optionals lan.dhcp.enable ( [ { assertion = startOk && endOk; message = "router: ${where}.dhcp.pool.start/end must be valid IPv4 addresses."; } ] ++ lib.optionals parsed [ { assertion = start <= end; message = "router: ${where}.dhcp.pool starts after it ends."; } { assertion = ipv4.inSubnet net start && ipv4.inSubnet net end; message = "router: ${where}.dhcp.pool is not inside ${lan.ipv4.subnet}."; } { assertion = !(start <= addr && addr <= end); message = "router: ${where}.dhcp.pool contains the router's own address ${lan.ipv4.address}."; } ] ); overlapPairs = lib.concatLists ( lib.imap0 ( i: a: map (b: { inherit a b; }) (lib.drop (i + 1) (lib.attrNames lansByName)) ) (lib.attrNames lansByName) ); in { config = lib.mkIf cfg.enable { assertions = [ { assertion = lans != [ ]; message = "router: at least one LAN must be defined."; } { assertion = lib.any (l: l.allowManagement) lans; message = "router: no LAN has allowManagement = true; you would be locked out of the router."; } { assertion = !(lib.elem cfg.wan.interface (map (l: l.interface) lans)); message = "router: the WAN interface `${cfg.wan.interface}` is also used by a LAN."; } { assertion = cfg.wan.ipv4.type != "pppoe" || cfg.wan.ipv4.pppoe.username != ""; message = "router: router.wan.ipv4.pppoe.username must be set for a PPPoE uplink."; } { assertion = cfg.wan.ipv4.type != "pppoe" || cfg.wan.ipv4.pppoe.passwordFile != null; message = "router: router.wan.ipv4.pppoe.passwordFile must be set for a PPPoE uplink."; } { assertion = lib.length (lib.filter (l: l.ipv6.pd) lans) <= 1; message = "router: more than one LAN sets ipv6.pd, but the delegated /64 cannot be split."; } { assertion = !(lib.any (l: l.ipv6.pd) lans) || cfg.wan.ipv6.enable; message = "router: a LAN sets ipv6.pd but router.wan.ipv6.enable is false."; } { assertion = let ifaces = map (l: l.interface) lans; in lib.length ifaces == lib.length (lib.unique ifaces); message = "router: two LANs share the same interface."; } ] ++ lib.concatLists (lib.mapAttrsToList perLan lansByName) ++ map ( { a, b }: let la = lansByName.${a}; lb = lansByName.${b}; in { # Only comparable when both addresses parse; the per-LAN assertion reports the rest. assertion = !(validIp la.ipv4.address && validIp lb.ipv4.address) || !(ipv4.overlaps { address = ipv4.parse la.ipv4.address; inherit (la.ipv4) prefixLength; } { address = ipv4.parse lb.ipv4.address; inherit (lb.ipv4) prefixLength; } ); message = "router: LANs `${a}` (${la.ipv4.subnet}) and `${b}` (${lb.ipv4.subnet}) overlap."; } ) overlapPairs; }; }