# 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). { config, lib, ... }: let cfg = config.cnx.router; dhcpVlans = lib.filterAttrs (_: vlan: vlan.dhcp.enable) cfg.vlans; in { config = lib.mkIf cfg.enable { 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. networking.nameservers = [ "9.9.9.9" "1.1.1.1" ]; }; }