Web UI: a WireGuard page (tunnels, peers, keys made on the router, client configs with a QR code, import of wg-quick configs, live status), a DNS page (records, wildcards, forward zones), interface dropdowns with VLAN devices, IPv6 options and a local domain. router-setup asks about VLANs, IPv6 and the domain. The router publishes its own name in DNS and hands the local domain to DHCP clients; wg and jq are on the router. Includes a script that takes over antigua's tunnels. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
91 lines
2.9 KiB
Nix
91 lines
2.9 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;
|
|
}
|
|
]
|
|
# The local domain: clients use it for short names (nas -> nas.home...).
|
|
++ lib.optionals (cfg.dns.domain != null) [
|
|
{
|
|
name = "domain-name";
|
|
data = cfg.dns.domain;
|
|
}
|
|
{
|
|
name = "domain-search";
|
|
data = cfg.dns.domain;
|
|
}
|
|
];
|
|
}) dhcpLans;
|
|
};
|
|
};
|
|
};
|
|
}
|