{ config, pkgs, lib, ... }: # Define your DNS records declaratively let dnsRecords = { "lan." = { ttl = 86400; # Time-to-live for records in this zone ns = [ "hoardfrost.lan." ]; # Nameserver record soa = { nameServer = "hoardfrost.lan."; adminEmail = "root.lan."; serial = "2025012001"; # Increment serial for changes refresh = "3600"; retry = "1800"; expire = "604800"; minimum = "86400"; }; # Define your host records (A for IPv4, AAAA for IPv6) records = { # Your NixOS server acting as the DNS server hoardfrost = [ { type = "A"; content = "10.0.0.217"; } { type = "AAAA"; content = "2601:282:180:630::cc03"; } ]; # Other devices on your local network router = [ { type = "A"; content = "10.0.0.1"; } { type = "AAAA"; content = "fe80::1"; } ]; yukigekko = [ { type = "A"; content = "10.0.0.210"; } { type = "AAAA"; content = "2601:282:180:630::e2e"; } ]; wesbos = [ { type = "A"; content = "10.0.0.110";} # { type = "AAAA"; content = "";} ]; wsl-hive = [ {type = "A"; content = "172.18.84.193";} {type = "AAAA"; content = "fe80::215:5dff:fec1:e813";} ]; }; }; }; # This function generates the zone file string from the Nix expression generateZoneFile = zoneName: zoneConfig: let soaRecord = "${zoneName} ${toString zoneConfig.ttl} IN SOA ${zoneConfig.soa.nameServer} ${zoneConfig.soa.adminEmail} ( ${zoneConfig.soa.serial} ${zoneConfig.soa.refresh} ${zoneConfig.soa.retry} ${zoneConfig.soa.expire} ${zoneConfig.soa.minimum} )"; nsRecords = lib.concatStringsSep "\n" (map (ns: "${zoneName} ${toString zoneConfig.ttl} IN NS ${ns}") zoneConfig.ns); hostRecords = lib.concatStringsSep "\n" (lib.flatten (lib.mapAttrsToList (hostname: records: map (record: "${hostname}.${zoneName} ${toString zoneConfig.ttl} IN ${record.type} ${record.content}") records ) zoneConfig.records)); in '' $ORIGIN ${zoneName} ${soaRecord} ${nsRecords} ${hostRecords} ''; # Generate the zone file using the `generateZoneFile` function lanZoneFile = pkgs.writeText "lan.zone" (generateZoneFile "lan." dnsRecords."lan."); # Configure hickory-dns with the generated zone file hickoryConfig = pkgs.writeText "hickory-config.toml" '' listen_addrs_ipv4 = ["127.0.0.1", "10.0.0.217"] # Change to your NixOS server's IPv4 listen_addrs_ipv6 = ["::1", "2601:282:180:630::cc03"] # Change to your NixOS server's IPv6 # Configure as an authoritative server for the local zone [[zones]] zone = "lan." zone_type = "Primary" file = "${lanZoneFile}" # Note: Recursive resolution is not supported in Hickory DNS 0.25.2 # This server will only serve the local zone ''; in { systemd.services.hickory-dns = { enable = true; description = "Hickory DNS authoritative server for local zone"; wantedBy = [ "multi-user.target" ]; serviceConfig = { Type = "simple"; User = "hickory"; Group = "hickory"; ExecStart = "${pkgs.hickory-dns}/bin/hickory-dns -c ${hickoryConfig}"; Restart = "on-failure"; AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; DynamicUser = true; StateDirectory = "hickory"; }; }; users.groups.hickory = {}; users.users.hickory = { isSystemUser = true; group = "hickory"; }; environment.systemPackages = [ pkgs.hickory-dns ]; # Configure systemd-resolved to use Unbound services.resolved.enable = true; services.resolved.dnssec = "false"; services.resolved.extraConfig = '' DNS=127.0.0.1@5353 ''; # Configure Unbound as recursive resolver for external queries services.unbound = { enable = true; settings = { server = { interface = [ "127.0.0.1" ]; port = 5353; # Use different port to avoid conflict with Hickory DNS access-control = [ "127.0.0.1 allow" ]; hide-identity = true; hide-version = true; }; forward-zone = [ { name = "lan."; forward-addr = [ "127.0.0.1@53" ]; # Forward lan. queries to Hickory DNS } { name = "."; forward-addr = [ "8.8.8.8#dns.google" "2001:4860:4860::8888#dns.google" ]; } ]; }; }; # Configure NetworkManager to use systemd-resolved and ignore DHCP DNS networking.networkmanager.dns = "systemd-resolved"; networking.networkmanager.settings.main.dns = "none"; # Set the system to use local DNS networking.nameservers = [ "127.0.0.1" ]; }