# LAN DHCP (Kea) and DNS (Blocky). Fully declarative: one Kea subnet per VLAN # with dhcp.enable, Blocky as the blocklist resolver every DHCP lease points # at. Blocky's HTTP listener (:4000) serves Prometheus metrics, scraped by # control over the mesh (firewall.nix scopes it to the mesh subnet). { settings }: { lib, ... }: let cfg = settings; dhcpVlans = lib.filterAttrs (_: vlan: vlan.dhcp.enable) cfg.vlans; in { services.kea.dhcp4 = { enable = true; settings = { interfaces-config = { interfaces = lib.mapAttrsToList (name: _: "vlan-${name}") dhcpVlans; # The unit orders after network-online.target, which under networkd # only waits for the WAN carrier (the vlan-* links are # RequiredForOnline=no), so Kea can start before vlan-* have their # addresses. By default it then logs the failed bind and runs with no # socket at all: clients' DISCOVERs reach vlan-lan and nobody answers. # Insist on every socket and keep retrying while networkd catches up; # if it still cannot bind, exit and let systemd restart the unit. service-sockets-require-all = true; service-sockets-max-retries = 60; service-sockets-retry-wait-time = 1000; }; lease-database = { type = "memfile"; persist = true; name = "/var/lib/kea/dhcp4.leases"; }; valid-lifetime = 86400; subnet4 = lib.mapAttrsToList (name: vlan: { id = vlan.id; subnet = vlan.subnet; interface = "vlan-${name}"; valid-lifetime = vlan.dhcp.leaseTime; pools = [ { pool = "${vlan.dhcp.pool.from} - ${vlan.dhcp.pool.to}"; } ]; reservations = lib.mapAttrsToList (host: res: { hostname = host; hw-address = res.hwAddress; ip-address = res.ipAddress; }) vlan.dhcp.reservations; option-data = [ { name = "routers"; data = vlan.address; } { name = "domain-name-servers"; data = vlan.address; } ]; }) dhcpVlans; }; }; systemd.services.kea-dhcp4-server.serviceConfig = { Restart = "on-failure"; RestartSec = 5; }; services.blocky = { enable = true; settings = { ports = { dns = 53; http = 4000; }; upstreams.groups.default = [ "9.9.9.9" "149.112.112.112" "2620:fe::fe" ]; blocking = { denylists.ads = [ "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts" ]; clientGroupsBlock.default = [ "ads" ]; }; caching = { minTime = "5m"; prefetching = true; }; prometheus.enable = true; }; }; # The router itself resolves via public resolvers, not via Blocky, so DNS # for deploys/updates survives a broken local resolver. networkd would # enable systemd-resolved by default, whose stub listener on 127.0.0.53:53 # makes Blocky's wildcard :53 bind fail — plain resolv.conf instead. services.resolved.enable = false; networking.nameservers = [ "9.9.9.9" "1.1.1.1" ]; }