diff --git a/nix/nixos-module/collectd/default.nix b/nix/nixos-module/collectd/default.nix index 945a124..51b813c 100644 --- a/nix/nixos-module/collectd/default.nix +++ b/nix/nixos-module/collectd/default.nix @@ -90,7 +90,7 @@ in Host "inbert.c3d2.de" Host "heise.de" ''; - }) (lib.optionalAttrs config.services.dhcpd4.enable { + }) (lib.optionalAttrs config.services.kea.dhcp4.enable { plugins.exec = let maxTimeout = builtins.foldl' (maxTimeout: net: @@ -117,11 +117,11 @@ in }) ]; - systemd.services.collectd = lib.mkIf config.services.dhcpd4.enable { - after = [ "dhcpd4.service" ]; + systemd.services.collectd = lib.mkIf config.services.kea.dhcp4.enable { + after = [ "kea-dhcp4-server.service" ]; }; - security.wrappers = lib.mkIf config.services.dhcpd4.enable { + security.wrappers = lib.mkIf config.services.kea.dhcp4.enable { collectd-dhcpcount = let dhcpcount = pkgs.runCommand "dhcpcount" { diff --git a/nix/nixos-module/collectd/dhcpcount.rb b/nix/nixos-module/collectd/dhcpcount.rb index 094d9f0..d7ef59d 100755 --- a/nix/nixos-module/collectd/dhcpcount.rb +++ b/nix/nixos-module/collectd/dhcpcount.rb @@ -1,36 +1,26 @@ #!/usr/bin/env ruby -require 'date' +require 'csv' -INTERVAL = 300 -TIMEOUT = ARGV[0].to_i -hostname = IO::readlines("/proc/sys/kernel/hostname").join.strip +INTERVAL = 60 +TIMEOUT = ARGV[0].to_i # TODO: now unused +hostname = CSV::readlines("/proc/sys/kernel/hostname").join.strip STDOUT.sync = true loop do seen = {} count = 0 - addr = nil - starts = nil + header = nil - IO::readlines("/var/lib/dhcpd4/dhcpd.leases").each do |line| - if line =~ /^lease (.+) \{/ - addr = $1 + CSV::readlines("/var/lib/kea/kea-leases4.csv", headers: true).each do |rec| + h = rec.to_h + addr = h["hwaddr"] + next unless addr - starts = nil - elsif line =~ /starts \d+ (.+?);/ - starts = DateTime.parse($1).to_time - elsif line =~ /^\}/ - now = Time.now - if starts and - now >= starts and now < starts + TIMEOUT - - unless seen[addr] - count += 1 - seen[addr] = true - end - end + unless seen[addr] + count += 1 + seen[addr] = true end end puts "PUTVAL \"#{hostname}/exec-dhcpd/current_sessions-leases\" interval=#{INTERVAL} N:#{count}" diff --git a/nix/nixos-module/container/dhcp-server.nix b/nix/nixos-module/container/dhcp-server.nix index e0cd465..cbb6fbc 100644 --- a/nix/nixos-module/container/dhcp-server.nix +++ b/nix/nixos-module/container/dhcp-server.nix @@ -8,98 +8,171 @@ let 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.dhcpd4 = lib.optionalAttrs enabled { + services.kea.dhcp4 = lib.optionalAttrs enabled { enable = true; - interfaces = builtins.attrNames dhcpNets; + settings = { + interfaces-config.interfaces = builtins.attrNames dhcpNets; + dhcp-ddns.enable-updates = true; - extraConfig = '' - ${builtins.concatStringsSep "\n" ( + subnet4 = concatMapDhcpNets (net: { vlan, subnet4, dhcp, domainName, ... }: { + id = vlan; + subnet = subnet4; + pools = [ { + pool = "${dhcp.start} - ${dhcp.end}"; + } ]; + rebind-timer = dhcp.time; + valid-lifetime = dhcp.max-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 = "172.20.73.8, 9.9.9.9"; + } ]; + }); + + match-client-id = false; + host-reservation-identifiers = [ "hw-address" ]; + reservations = concatMapDhcpNets (net: { hosts4, dhcp, ... }: builtins.attrValues ( - builtins.mapAttrs (net: { dhcp, subnet4Net, subnet4Len, domainName, ...}: - '' - ddns-update-style standard; - key dyndns { - algorithm hmac-sha256; - secret ${config.site.dyndnsKey}; - }; - zone ${domainName}. { - primary ${config.site.net.serv.hosts4.dns}; - primary6 ${config.site.net.serv.hosts6.dn42.dns}; - key dyndns; - } - ${lib.concatMapStrings ({ name, dynamic, ... }: - lib.optionalString ( - dynamic && - lib.hasSuffix ".in-addr.arpa" name - ) '' - zone ${name}. { - primary ${config.site.net.serv.hosts4.dns}; - primary6 ${config.site.net.serv.hosts6.dn42.dns}; - key dyndns; - } - '' - ) config.site.dns.localZones} + builtins.mapAttrs (name: hwaddr: { + hostname = "${name}.${net}.zentralwerk.org"; + hw-address = hwaddr; + ip-address = hosts4.${name}; + }) dhcp.fixed-hosts + )); - option guid code 97 = text; - group { - default-lease-time ${toString dhcp.time}; - max-lease-time ${toString dhcp.max-time}; - option routers ${config.site.net.${net}.hosts4.${dhcp.router}}; - option domain-name "${domainName}"; - option domain-name-servers 172.20.73.8, 9.9.9.9; - ddns-domainname "${domainName}"; + # 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"; + } ]; + }; - class "pxeclients" { - match if substring (option vendor-class-identifier, 0, 9) = "PXEClient"; + 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"; + }; - next-server ${config.site.net.serv.hosts4.nfsroot}; - option tftp-server-address ${config.site.net.serv.hosts4.nfsroot}; - if suffix(reverse(1, option guid), 5) = 34:69:50:52:00 { - # RPi4 - option vendor-class-identifier "PXEClient"; - option vendor-encapsulated-options "Raspberry Pi Boot"; - option tftp-server-name "${config.site.net.serv.hosts4.nfsroot}"; - } elsif option pxe-system-type = 00:00 { - filename "netboot.xyz.kpxe"; # BIOS - } elsif option pxe-system-type = 00:07 { - filename "netboot.xyz.efi"; # EFI - option bootfile-name "netboot.xyz.efi"; - } elsif option pxe-system-type = 00:06 { - filename "netboot.xyz.efi"; # ia32_EFI - } - } + 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.dhcp-ddns = lib.optionalAttrs enabled { + enable = true; - subnet ${subnet4Net} netmask ${lib.netmasks.${toString subnet4Len}} { - range ${dhcp.start} ${dhcp.end}; + settings = { + tsig-keys = [ { + name = "dyndns"; + algorithm = "hmac-sha256"; + secret = config.site.dyndnsKey; + } ]; - # always assign the same IP to the same MAC address. - # fixes changing IP for PXE clients. - ignore-client-uids true; - } - - update-static-leases on; - - ${builtins.concatStringsSep "\n" ( - builtins.attrValues ( - builtins.mapAttrs (addr: hwaddr: - '' - host ${addr} { - hardware ethernet ${hwaddr}; - fixed-address ${addr}; - } - '' - ) dhcp.fixed-hosts - ) - )} - } - '' - ) dhcpNets - ) - )} - ''; + 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 + ) config.site.dns.localZones + ); + }; }; }