{ config, options, lib, ... }: with lib; let getHostNets = link: let hostConfig = config.site.hosts.${link}; sort = nets: lib.unique ( builtins.sort (net1: net2: config.site.net.${net1}.vlan < config.site.net.${net2}.vlan ) nets ); in if config.site.hosts ? ${link} then sort ( builtins.attrNames hostConfig.interfaces ++ getLinksNets link ) else if config.site.net ? ${link} then [ link ] else builtins.trace "Don't know what nets to configure for link to \"${link}\"" []; # breaks the getHostNets recursion for switches, # requiring any net to be used by at >1 links getLinksNets = hostName: let linksNets = builtins.mapAttrs (link: _: unique ( getLinkNets { "${hostName}" = true; } [ link ] ) ) config.site.hosts.${hostName}.links; allNets = unique ( builtins.concatLists (builtins.attrValues linksNets) ); netLinkCount = net: builtins.foldl' (netLinkCount: nets: if builtins.elem net nets then netLinkCount + 1 else netLinkCount ) 0 (builtins.attrValues linksNets); nets = builtins.filter (net: netLinkCount net > 1 ) allNets; in nets; getLinkNets = seen: links: if links == [] then [] else let link = builtins.head links; seen' = seen // { "${link}" = true; }; onlyUnseen = builtins.filter (link: ! seen' ? ${link}); links' = builtins.tail links; in if config.site.hosts ? ${link} then getLinkNets seen' ( links' ++ ( onlyUnseen (builtins.attrNames config.site.hosts.${link}.links) ) ) else getHostNets link ++ getLinkNets seen' (onlyUnseen links'); dhcpOpts = { start = mkOption { description = "First IP address in pool"; type = types.str; }; end = mkOption { description = "Last IP address in pool"; type = types.str; }; time = mkOption { description = "Renew time in seconds"; type = types.int; }; max-time = mkOption { description = "Max renew time in seconds"; type = types.int; }; server = mkOption { description = "Container that runs the DHCP server"; type = types.str; }; router = mkOption { description = "Gateway"; type = types.str; }; fixed-hosts = mkOption { type = with types; attrsOf str; default = {}; }; }; netOpts = { name, ... }: { options = { vlan = mkOption { description = "VLAN tag number"; type = types.int; }; subnet4 = mkOption { description = "v.w.x.y/z"; type = with types; nullOr str; default = null; }; subnet4Net = mkOption { type = with types; nullOr types.str; default = let inherit (config.site.net.${name}) subnet4; s = lib.splitString "/" subnet4; in if subnet4 != null && builtins.length s == 2 then builtins.head s else null; }; subnet4Len = mkOption { type = with types; nullOr types.int; default = let inherit (config.site.net.${name}) subnet4; s = lib.splitString "/" subnet4; in if subnet4 != null && builtins.length s == 2 then lib.toInt (elemAt s 1) else null; }; subnets6 = mkOption { description = "IPv6 subnets w/o prefixlen (always 64)"; type = with types; attrsOf str; default = {}; }; hosts4 = mkOption { description = "Attribute set of hostnames to IPv4 addresses"; type = with types; attrsOf str; default = {}; }; hosts6 = mkOption { description = "Attribute set of contexts to attribute sets of hostnames to IPv4 addresses"; type = with types; attrsOf (attrsOf str); default = {}; }; ospf = { secret = mkOption { type = with types; nullOr str; default = null; }; }; dhcp = mkOption { type = with types; nullOr (submodule { options = dhcpOpts; }); default = null; }; domainName = mkOption { description = "Domain name option"; type = types.str; default = "${name}.zentralwerk.dn42"; }; dynamicDomain = mkOption { type = types.bool; default = false; description = "Domain updated by DHCP server?"; }; }; }; upstreamOpts = { provider = mkOption { type = types.str; }; link = mkOption { type = with types; nullOr str; default = null; description = "Underlying interface name for eg. PPPoE"; }; staticIpv4Address = mkOption { type = with types; nullOr str; default = null; }; upBandwidth = mkOption { type = with types; nullOr int; default = null; }; noNat.subnets6 = mkOption { type = with types; listOf str; default = []; description = "Do not NAT66 traffic from these public static subnets"; }; }; interfaceOpts = { name, ... }: { options = { hwaddr = mkOption { type = with types; nullOr str; default = null; description = "Static MAC address"; }; type = mkOption { type = types.enum [ "phys" "veth" "pppoe" "bridge" ]; description = '' veth: Virtual ethernet to be attached to a bridge. phys: (Physical) interface from a server moved into the container. Do not use with VLAN interfaces because they won't be moved back after lxc-stop. ''; }; gw4 = mkOption { type = with types; nullOr str; default = null; description = "IPv4 gateway"; }; gw6 = mkOption { type = with types; nullOr str; default = null; description = "IPv6 gateway"; }; upstream = mkOption { type = with types; nullOr (submodule { options = upstreamOpts; }); default = null; description = "Upstream interface configuration"; }; }; }; hostOpts = { name, ... }: { options = { prebuilt = mkOption { type = types.bool; default = false; description = '' Include the container system in the server's `build-container` script. ''; }; firstboot = mkOption { type = types.bool; default = false; description = '' true if host is a newly flashed OpenWRT device with a default address ''; }; role = mkOption { type = types.enum [ "ap" "switch" "server" "container" "client" ]; default = "client"; }; model = mkOption { type = types.str; default = { ap = "unknown"; switch = "unknown"; server = "pc"; container = "lxc"; client = "any"; }."${config.site.hosts.${name}.role}"; }; password = mkOption { type = with types; nullOr str; default = null; }; location = mkOption { type = with types; nullOr str; default = null; }; interfaces = mkOption { default = {}; type = with types; attrsOf (submodule interfaceOpts); description = "Network interfaces"; }; physicalInterfaces = mkOption { default = lib.filterAttrs (_: { type, ... }: builtins.elem type [ "phys" "veth" ] ) config.site.hosts.${name}.interfaces; type = with types; attrsOf (submodule interfaceOpts); description = "Network interfaces that are not virtual (don't set!)"; }; isRouter = mkOption { type = types.bool; # isRouter = Part of the core network? default = config.site.hosts.${name}.interfaces ? core && config.site.net.core.hosts4 ? ${name}; description = "Should this host route?"; }; firewall.enable = mkOption { type = types.bool; default = false; description = "Enable firewall to disallow incoming connections from core"; }; forwardPorts = mkOption { type = with types; listOf (submodule { options = { proto = mkOption { type = types.enum [ "tcp" "udp" ]; }; sourcePort = mkOption { type = types.int; }; destination = mkOption { type = types.str; }; reflect = mkOption { type = types.bool; default = true; description = '' Enable NAT reflection Any forwarded connection will have our static IPv4 address as source so that forwarded services become available internally. Unfortunately, this breaks identification by IPv4 adress. ''; }; }; }); default = []; }; ospf.stubNets4 = mkOption { type = with types; listOf str; default = []; description = "Additional IPv4 networks to announce"; }; ospf.stubNets6 = mkOption { type = with types; listOf str; default = []; description = "Additional IPv6 networks to announce"; }; ospf.allowedUpstreams = mkOption { type = with types; listOf str; default = []; description = "Accept default routes from these OSPF routers, in order of preference"; }; ospf.upstreamInstance = mkOption { type = with types; nullOr int; default = null; description = "OSPF instance for advertising the default route"; }; wireguard = mkOption { default = {}; type = with types; attrsOf (submodule ( { name, ... }: { options = { endpoint = mkOption { type = str; }; publicKey = mkOption { type = str; }; privateKey = mkOption { type = str; }; addresses = mkOption { type = listOf str; }; upBandwidth = mkOption { type = with types; nullOr int; }; }; } )); }; bgp = mkOption { default = null; type = with types; nullOr (submodule { options = bgpOpts; }); }; services.dns = { enable = mkOption { type = types.bool; default = false; }; }; services.dnscache.enable = mkOption { type = types.bool; default = false; }; links = mkOption { description = "Which port is connected to what other device? Keys are either network names or known hostnames."; default = {}; type = with types; attrsOf (submodule linkOpts); }; wifi = mkOption { default = {}; type = with types; attrsOf (submodule ( { name, ... }: { options = { htmode = mkOption { type = enum [ "HT20" "HT40-" "HT40+" "VHT80" ]; }; channel = mkOption { type = int; }; ssids = mkOption { type = attrsOf (submodule ( { name, ... }: { options = { net = mkOption { type = str; }; psk = mkOption { type = nullOr str; default = null; }; }; } )); }; }; } )); }; }; }; bgpOpts = { asn = mkOption { type = types.int; }; peers = mkOption { type = with types; attrsOf (submodule ({ name, ... }: { options = { asn = mkOption { type = types.int; }; }; })); default = {}; }; }; linkOpts = { name, ... }: { options = { ports = mkOption { type = with types; listOf str; description = "Port names"; }; group = mkOption { type = with types; nullOr str; default = null; description = "Link aggregation group with a fixed number"; }; nets = mkOption { type = with types; listOf str; description = "Automatically generated"; default = getHostNets name; }; vlans = mkOption { type = with types; listOf int; description = "Automatically generated, do not set"; default = map (net: config.site.net.${net}.vlan ) (getHostNets name); }; }; }; in { options.site = { net = mkOption { description = "All subnets"; default = {}; type = with types; attrsOf (submodule netOpts); }; hosts = mkOption { description = "All the static hosts"; default = {}; type = with types; attrsOf (submodule hostOpts); }; sshPubKeys = mkOption { type = with types; listOf str; }; }; config.warnings = let findCollisions = getter: xs: (builtins.foldl' ({ known, dup }: k: let ks = builtins.toString k; in if known ? ${ks} then { inherit known; dup = dup ++ [ks]; } else { known = known // { ${ks} = true; }; inherit dup; } ) { known = {}; dup = []; } ( concatMap getter (builtins.attrValues xs) )).dup; reportCollisions = name: getter: xs: map (k: "Duplicate ${name}: ${k}") (findCollisions getter xs); ospfUpstreamXorGw = builtins.concatMap (hostName: let hostConf = config.site.hosts.${hostName}; gwNets = builtins.filter (netName: hostConf.interfaces.${netName}.gw4 != null ) (builtins.attrNames hostConf.interfaces); in if gwNets != [] && hostConf.ospf.allowedUpstreams != [] then [ '' Host ${hostName} has gateway on ${builtins.head gwNets} but accepts default routes from OSPF '' ] else [] ) (builtins.attrNames config.site.hosts); in (reportCollisions "VLAN tag" (x: [x.vlan]) config.site.net) ++ (reportCollisions "IPv4 subnet" (x: if x.subnet4 == null then [] else [x.subnet4]) config.site.net) ++ (reportCollisions "IPv6 subnet" (x: builtins.attrValues x.subnets6) config.site.net) ++ ospfUpstreamXorGw; }