nix/nixos-module/container/dhcp-server: migrate from isc-dhcpd to kea-dhcp4

This commit is contained in:
Astro 2023-10-24 00:57:25 +02:00
parent db71886898
commit 9b39803076
3 changed files with 170 additions and 107 deletions

View File

@ -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" {

View File

@ -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}"

View File

@ -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
);
};
};
}