Files
nixos-router/modules/router/dhcp.nix
T
nilsandClaude Opus 5.5 5ddff44a35 Serve DHCP on LANs plugged in after boot; accept browser logins
Kea skips interfaces without carrier when it opens its sockets and only
looks again on a reload, so a LAN cable plugged in after Kea started got
no DHCP until a restart. networkd-dispatcher now restarts Kea when a DHCP
LAN gets carrier (a restart: a SIGHUP while Kea is starting ends it). The
rule script is the same for every configuration and reads the interfaces
from /etc/router/dhcp-interfaces, so the device never has to rebuild it
(writeShellApplication's shellcheck could not be built offline).

The web UI refused every login from a browser with "forbidden": under
Referrer-Policy no-referrer browsers send "Origin: null" with form posts.
Use http.CrossOriginProtection (Sec-Fetch-Site, then Origin) and a
same-origin referrer policy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
2026-10-29 09:19:14 +01:00

80 lines
2.6 KiB
Nix

{
config,
lib,
pkgs,
...
}:
let
cfg = config.router;
dhcpLans = lib.filter (l: l.dhcp.enable) (lib.attrValues cfg.lans);
ipv4 = (import ./lib.nix { inherit lib; }).ipv4;
in
{
config = lib.mkIf (cfg.enable && dhcpLans != [ ]) {
# Kea skips interfaces without carrier when it opens its sockets and only
# looks again on a reload: a LAN cable plugged in after boot would get no
# DHCP until a restart. So restart it when a DHCP LAN gets carrier.
services.networkd-dispatcher = {
enable = true;
rules.kea-carrier = {
onState = [
"carrier"
"degraded"
"routable"
];
# The same for every configuration (the interfaces are in /etc): the
# module builds it with writeShellApplication, whose shellcheck run
# could not be rebuilt offline on the device.
# Not a reload: a SIGHUP while Kea is still starting ends it (cleanly,
# so systemd does not restart it).
script = ''
if ${pkgs.gnugrep}/bin/grep -qxF "$IFACE" /etc/router/dhcp-interfaces; then
${config.systemd.package}/bin/systemctl try-restart --no-block kea-dhcp4-server.service
fi
'';
};
};
environment.etc."router/dhcp-interfaces".text = lib.concatMapStrings (l: "${l.device}\n") dhcpLans;
services.kea.dhcp4 = {
enable = true;
settings = {
interfaces-config = {
interfaces = map (l: l.device) dhcpLans;
# Interfaces are configured asynchronously by networkd.
re-detect = true;
};
lease-database = {
type = "memfile";
persist = true;
name = "/var/lib/kea/dhcp4.leases";
};
valid-lifetime = 3600;
# The ID keys persistent leases, so derive it from the network address:
# stable when LANs are added or removed (an index would shift).
subnet4 = map (l: {
id = ipv4.networkOf (ipv4.parse l.ipv4.address) l.ipv4.prefixLength;
inherit (l.ipv4) subnet;
interface = l.device;
pools = [ { pool = "${l.dhcp.pool.start} - ${l.dhcp.pool.end}"; } ];
reservations = lib.mapAttrsToList (hostname: r: {
hw-address = lib.toLower r.mac;
ip-address = r.address;
inherit hostname;
}) l.dhcp.reservations;
option-data = [
{
name = "routers";
data = l.ipv4.address;
}
{
name = "domain-name-servers";
data = l.ipv4.address;
}
];
}) dhcpLans;
};
};
};
}