# xlab-gateway DHCP + DDNS + IPv6 SLAAC
# Kea DHCPv4 on bond.lan254, DDNS forwarding to BIND9 at 10.0.0.1:5353
{ config, pkgs, ... }:
let
lanDeviceUnit = "sys-subsystem-net-devices-bond.lan254.device";
waitForLanAddresses = pkgs.writeShellScript "wait-for-xlab-lan-addresses" ''
set -eu
for _ in $(${pkgs.coreutils}/bin/seq 1 30); do
if ${pkgs.iproute2}/bin/ip -4 -brief address show dev bond.lan254 \
| ${pkgs.gnugrep}/bin/grep -Fq "10.253.254.1/24" \
&& ${pkgs.iproute2}/bin/ip -6 -brief address show dev bond.lan254 \
| ${pkgs.gnugrep}/bin/grep -Fq "fd99:23eb:1682:1::1/64"; then
exit 0
fi
${pkgs.coreutils}/bin/sleep 1
done
echo "bond.lan254 did not acquire its configured addresses within 30 seconds" >&2
exit 1
'';
in
{
age.secrets.xlab-ddns-tsig = {
file = ../../secrets/xlab-ddns-tsig.age;
owner = "kea";
group = "kea";
mode = "0400";
};
# ===========================================================================
# Kea DHCPv4
# ===========================================================================
services.kea.dhcp4 = {
enable = true;
settings = {
interfaces-config.interfaces = [ "bond.lan254" ];
lease-database = {
type = "memfile";
name = "/var/lib/kea/kea-leases4.csv";
persist = true;
lfc-interval = 3600;
};
expired-leases-processing = {
reclaim-timer-wait-time = 10;
flush-reclaimed-timer-wait-time = 25;
hold-reclaimed-time = 3600;
max-reclaim-leases = 100;
max-reclaim-time = 250;
};
valid-lifetime = 86400;
renew-timer = 21600;
rebind-timer = 43200;
option-data = [
{ name = "routers"; data = "10.253.254.1"; }
{ name = "domain-name-servers"; data = "10.253.254.1"; }
{ name = "domain-name"; data = "dev.skyw.top"; }
{ name = "domain-search"; data = "dev.skyw.top"; }
];
# Cisco CAPWAP APs -> Catalyst 9800-CL WMI 10.0.10.10 via DHCP option 43
# sub-option 241 (0xf1). Scoped to APs (option-60 "Cisco AP*") so other
# clients never receive it. Mirrors door1's kea-dhcp4 cisco-ap class.
client-classes = [
{
name = "cisco-ap";
test = "substring(option[60].hex,0,8) == 'Cisco AP'";
option-data = [
{ name = "vendor-encapsulated-options"; csv-format = false; data = "F1:04:0A:00:0A:0A"; always-send = true; }
# CAPWAP over the 1420-MTU wg-to-wgnet tunnel: the AP sends 1441B DF
# DTLS packets and ignores PMTUD frag-needed, so the join blackholes.
# Pin the AP's wired MTU to 1420 so it never exceeds the path.
{ name = "interface-mtu"; data = "1420"; always-send = true; }
];
}
];
subnet4 = [
{
id = 1;
subnet = "10.253.254.0/24";
pools = [
{ pool = "10.253.254.100 - 10.253.254.240"; }
];
option-data = [
{ name = "subnet-mask"; data = "255.255.255.0"; }
{ name = "routers"; data = "10.253.254.1"; }
{ name = "domain-name-servers"; data = "10.253.254.1"; }
# Classless static routes: 10.0.0.0/16 via 10.253.254.1, default via 10.253.254.1
{ code = 121; csv-format = false; data = "100A000AFDFE01000AFDFE01"; }
# MS classless static routes (same)
{ code = 249; csv-format = false; data = "100A000AFDFE01000AFDFE01"; }
];
reservations = [];
}
];
ddns-send-updates = true;
ddns-qualifying-suffix = "dev.skyw.top.";
dhcp-ddns = {
enable-updates = true;
max-queue-size = 1024;
ncr-protocol = "UDP";
ncr-format = "JSON";
sender-ip = "127.0.0.1";
sender-port = 0;
server-ip = "127.0.0.1";
server-port = 53001;
};
};
};
# The global network-online target is intentionally disabled on this router,
# so wait for the actual LAN device and addresses before Kea opens sockets.
systemd.services.kea-dhcp4-server = {
requires = [ lanDeviceUnit ];
wants = [ "kea-dhcp-ddns-server.service" ];
after = [
lanDeviceUnit
"systemd-networkd.service"
"kea-dhcp-ddns-server.service"
];
serviceConfig = {
ExecStartPre = [ waitForLanAddresses ];
RestartSec = "2s";
};
};
# ===========================================================================
# Kea DHCP-DDNS (D2) - Forwards DNS updates to BIND9
# ===========================================================================
services.kea.dhcp-ddns = {
enable = true;
settings = {
ip-address = "127.0.0.1";
port = 53001;
tsig-keys = [
{
# The main gateway temporarily accepts both names during rotation;
# xlab uses only the replacement key.
name = "edge-ddns-key-next";
algorithm = "HMAC-SHA256";
secret-file = config.age.secrets.xlab-ddns-tsig.path;
}
];
forward-ddns.ddns-domains = [
{
name = "dev.skyw.top.";
key-name = "edge-ddns-key-next";
dns-servers = [{ ip-address = "10.0.0.1"; port = 5353; }];
}
];
reverse-ddns.ddns-domains = [
{
name = "10.in-addr.arpa.";
key-name = "edge-ddns-key-next";
dns-servers = [{ ip-address = "10.0.0.1"; port = 5353; }];
}
{
name = "2.8.6.1.b.e.3.2.9.9.d.f.ip6.arpa.";
key-name = "edge-ddns-key-next";
dns-servers = [{ ip-address = "10.0.0.1"; port = 5353; }];
}
];
};
};
systemd.services.kea-dhcp-ddns-server.restartTriggers = [
config.age.secrets.xlab-ddns-tsig.file
];
# ===========================================================================
# radvd - IPv6 SLAAC, DNS, and search-domain advertisements
# ===========================================================================
services.radvd = {
enable = true;
config = ''
interface bond.lan254 {
AdvSendAdvert on;
AdvManagedFlag off;
AdvOtherConfigFlag off;
MinRtrAdvInterval 30;
MaxRtrAdvInterval 100;
prefix fd99:23eb:1682:1::/64 {
AdvOnLink on;
AdvAutonomous on;
AdvRouterAddr on;
};
RDNSS fd99:23eb:1682:1::1 {
AdvRDNSSLifetime 3600;
};
DNSSL dev.skyw.top {
AdvDNSSLLifetime 3600;
};
};
'';
};
systemd.services.radvd = {
requires = [ lanDeviceUnit ];
after = [ lanDeviceUnit "systemd-networkd.service" ];
serviceConfig.ExecStartPre = [ waitForLanAddresses ];
};
}