# 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; 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; }; }; 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" ]; }