# ISC DHCP/IPv4 server configuration { hostName, config, lib, ... }: let dhcpNets = lib.filterAttrs (_: { dhcp, ... }: dhcp != null && dhcp.server == hostName ) config.site.net; concatMapDhcpNets = f: lib.pipe dhcpNets [ (builtins.mapAttrs f) builtins.attrValues (map (r: if builtins.isList r then r else [ r ])) builtins.concatLists ]; enabled = builtins.length (builtins.attrNames dhcpNets) > 0; in { services.kea.dhcp4 = lib.mkIf enabled { enable = true; settings = { interfaces-config.interfaces = builtins.attrNames dhcpNets; dhcp-ddns.enable-updates = true; ddns-send-updates = true; # TODO: use with kea >= 2.5.0 # ddns-conflict-resolution-mode = "check-exists-with-dhcid"; ddns-use-conflict-resolution = false; expired-leases-processing.hold-reclaimed-time = builtins.foldl' lib.max 3600 (concatMapDhcpNets (net: { dhcp, ... }: dhcp.max-time)); subnet4 = concatMapDhcpNets (net: { vlan, subnet4, hosts4, dhcp, domainName, ... }: { id = vlan; subnet = subnet4; pools = [ { pool = "${dhcp.start} - ${dhcp.end}"; } ]; renew-timer = builtins.ceil (.5 * dhcp.time); rebind-timer = builtins.ceil (.85 * dhcp.time); valid-lifetime = dhcp.time; option-data = [ { space = "dhcp4"; name = "routers"; code = 3; data = config.site.net.${net}.hosts4.${dhcp.router}; } { space = "dhcp4"; name = "domain-name"; code = 15; data = domainName; } { space = "dhcp4"; name = "domain-name-servers"; code = 6; data = "${config.site.net.serv.hosts4.dnscache}, 9.9.9.9"; } ]; ddns-qualifying-suffix = domainName; reservations = lib.pipe dhcp.fixed-hosts [ (builtins.mapAttrs (fixedAddr: hwaddr: if hosts4 ? ${fixedAddr} then # fixedAddr is a known hostname let name = fixedAddr; addr = hosts4.${fixedAddr}; in { hostname = "${name}.${net}.zentralwerk.org"; hw-address = hwaddr; ip-address = addr; } else let names = builtins.attrNames ( lib.filterAttrs (_: hostAddr: hostAddr == fixedAddr ) hosts4); name = builtins.head names; in if builtins.length names > 0 then { # fixedAddr is IPv4 of a known hostname hostname = "${name}.${net}.zentralwerk.org"; hw-address = hwaddr; ip-address = hosts4.${name}; } # fixedAddr is IPv4? else { hw-address = hwaddr; ip-address = fixedAddr; } )) builtins.attrValues (builtins.filter (r: r != null)) ]; }); match-client-id = false; host-reservation-identifiers = [ "hw-address" ]; # Netbooting option-def = [ { name = "PXEDiscoveryControl"; code = 6; space = "vendor-encapsulated-options-space"; type = "uint8"; array = false; } { name = "PXEMenuPrompt"; code = 10; space = "vendor-encapsulated-options-space"; type = "record"; array = false; record-types = "uint8,string"; } { name = "PXEBootMenu"; code = 9; space = "vendor-encapsulated-options-space"; type = "record"; array = false; record-types = "uint16,uint8,string"; } ]; client-classes = let rpi4Class = { name = "rpi4-pxe"; test = "option[vendor-class-identifier].text == 'PXEClient:Arch:00000:UNDI:002001'"; option-data = [ { name = "boot-file-name"; data = "bootcode.bin"; } { name = "vendor-class-identifier"; data = "PXEClient"; } { name = "vendor-encapsulated-options"; } { name = "PXEBootMenu"; csv-format = true; data = "0,17,Raspberry Pi Boot"; space = "vendor-encapsulated-options-space"; } { name = "PXEDiscoveryControl"; data = "3"; space = "vendor-encapsulated-options-space"; } { name = "PXEMenuPrompt"; csv-format = true; data = "0,PXE"; space = "vendor-encapsulated-options-space"; } ]; }; pxeClassData = { PXE-Legacy = { arch = "00000"; boot-file-name = "netboot.xyz.kpxe"; }; PXE-UEFI-32-1.arch = "00002"; PXE-UEFI-32-2.arch = "00006"; PXE-UEFI-64-1.arch = "00007"; PXE-UEFI-64-2.arch = "00008"; PXE-UEFI-64-3.arch = "00009"; }; makePxe = name: { boot-file-name ? "netboot.xyz.efi", arch }: { inherit name boot-file-name; test = "substring(option[60].hex,0,20) == 'PXEClient:Arch:${arch}'"; next-server = config.site.net.serv.hosts4.nfsroot; }; in [ rpi4Class ] ++ builtins.attrValues ( builtins.mapAttrs makePxe pxeClassData ); }; }; services.kea.dhcp6 = lib.mkIf enabled { enable = true; settings = { interfaces-config.interfaces = builtins.attrNames dhcpNets; dhcp-ddns.enable-updates = true; ddns-override-no-update = true; ddns-override-client-update = true; ddns-replace-client-name = "when-not-present"; # TODO: use with kea >= 2.5.0 # ddns-conflict-resolution-mode = "check-exists-with-dhcid"; ddns-use-conflict-resolution = false; subnet6 = concatMapDhcpNets (net: { vlan, subnets6, dhcp, domainName, ... }: let subnet = subnets6.up4 or subnets6.flpk or null; prefix = builtins.head (builtins.split "::/" subnet); in if subnet != null then { id = vlan; interface = net; inherit subnet; pools = [ { pool = "${prefix}:c3d2:c3d2:c3d2:1000 - ${prefix}:c3d2:c3d2:c3d2:ffff"; #pool = subnet; } ]; valid-lifetime = dhcp.time; max-valid-lifetime = dhcp.max-time; option-data = [ { space = "dhcp6"; name = "domain-search"; code = 24; data = domainName; } { space = "dhcp6"; name = "dns-servers"; code = 23; data = "${config.site.net.serv.hosts6.dn42.dnscache}, 2620:fe::9"; } ]; ddns-generated-prefix = "d"; ddns-qualifying-suffix = domainName; } else [] ); host-reservation-identifiers = [ "hw-address" ]; #reservations = concatMapDhcpNets (net: { hosts6, dhcp, ... }: # builtins.filter (r: r != null) ( # builtins.attrValues ( # builtins.mapAttrs (name: hwaddr: # let # ip-addresses = lib.pipe hosts6 [ # (builtins.mapAttrs (_: hosts6: hosts6.${name} or null)) # builtins.attrValues # (builtins.filter (a: a != null)) # ]; # in # if builtins.trace (lib.generators.toPretty {} ip-addresses) (builtins.length ip-addresses) > 0 # then { # hostname = "${name}.${net}.zentralwerk.org"; # hw-address = hwaddr; # inherit ip-addresses; # } # else null # ) dhcp.fixed-hosts # ))); }; }; services.kea.dhcp-ddns = lib.mkIf enabled { enable = true; settings = { tsig-keys = [ { name = "dyndns"; algorithm = "hmac-sha256"; secret = config.site.dyndnsKey; } ]; forward-ddns.ddns-domains = concatMapDhcpNets (net: { domainName, ... }: { name = "${domainName}."; key-name = "dyndns"; dns-servers = [ { ip-address = config.site.net.serv.hosts4.dns; } { ip-address = config.site.net.serv.hosts6.dn42.dns; } ]; }); reverse-ddns.ddns-domains = map ({ name, ...}: { name = "${name}."; key-name = "dyndns"; dns-servers = [ { ip-address = config.site.net.serv.hosts4.dns; } { ip-address = config.site.net.serv.hosts6.dn42.dns; } ]; }) ( builtins.filter ({ name, dynamic, ... }: dynamic && (lib.hasSuffix ".in-addr.arpa" name || lib.hasSuffix ".ip6.arpa" name) ) config.site.dns.localZones ); }; }; # Increase reliablity # (mostly for kea-dhcp-ddns-server.service) systemd.services = let restartService.serviceConfig = { RestartSec = 4; Restart = "always"; }; in { kea-dhcp4-server = restartService; kea-dhcp6-server = restartService; kea-dhcp-ddns-server = restartService; }; }