{ config, pkgs, lib, ... }: let cfg = config.services.dns; dnsPkg = if cfg.package != null then cfg.package else pkgs.technitium-dns-server; # Build the config.json for Technitium DNS Server. # The server reads this file on startup from its config directory. configJson = { WebServicePort = cfg.webPort; DNSListenerPort = cfg.dnsPort; Recursion = cfg.recursion; Forwarders = cfg.forwarders; Log = false; CachePrefetch = false; AllowTtlOverride = true; } // lib.optionalAttrs (cfg.adminPasswordFile != null) { # Password hash will be set by the activation script on first run # using the value from adminPasswordFile. } // lib.optionalAttrs (cfg.listenAddresses != [ ]) { ListenAddresses = cfg.listenAddresses; } // lib.optionalAttrs (cfg.allowZoneTransfer != [ ]) { AllowZoneTransfer = cfg.allowZoneTransfer; } // cfg.extraConfig; configFile = pkgs.writeText "technitium-dns-config.json" (builtins.toJSON configJson); in { imports = [ ./options.nix ]; config = lib.mkIf cfg.enable { environment.systemPackages = [ dnsPkg ]; # Create the config directory and deploy initial config.json systemd.tmpfiles.rules = [ "d ${cfg.configDir} 0750 dns dns - -" ]; systemd.services.technitium-dns-server = { description = "Technitium DNS Server"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; wants = [ "network-online.target" ]; # Generate a password hash if adminPasswordFile is provided. # The server is stopped on first run if no password hash exists, # so we pre-seed the config with the hashed password. preStart = '' if [ -f "${cfg.configDir}/config.json" ]; then # Config already exists, do not overwrite true else install -m 0640 ${configFile} ${cfg.configDir}/config.json ${lib.optionalString (cfg.adminPasswordFile != null) '' if [ -f "${cfg.adminPasswordFile}" ]; then # .NET-compatible SHA256 hash of the password PASSWORD=$(cat "${cfg.adminPasswordFile}" | tr -d '\n') HASH=$(echo -n "$PASSWORD" | ${pkgs.openssl}/bin/openssl dgst -sha256 -hex | cut -d' ' -f2) ${pkgs.jq}/bin/jq \ ".AdminPassword = \"$HASH\" | .Pbkdf2Iterations = 600000" \ ${cfg.configDir}/config.json > ${cfg.configDir}/config.json.tmp mv ${cfg.configDir}/config.json.tmp ${cfg.configDir}/config.json fi ''} fi ''; serviceConfig = { Type = "simple"; ExecStart = "${dnsPkg}/bin/technitium-dns-server ${cfg.configDir}"; User = "dns"; Group = "dns"; Restart = "on-failure"; RestartSec = "5s"; LimitNOFILE = 1048576; # Protect the system ProtectSystem = "full"; ProtectHome = true; PrivateTmp = true; NoNewPrivileges = true; ReadWritePaths = [ cfg.configDir ]; }; }; # Create the dns system user and group users.users.dns = { description = "Technitium DNS Server daemon user"; group = "dns"; isSystemUser = true; home = cfg.configDir; createHome = true; }; users.groups.dns = { }; # Open firewall ports for DNS (UDP/TCP 53) and optionally the web interface networking.firewall = lib.mkMerge [ { allowedTCPPorts = [ cfg.dnsPort ]; allowedUDPPorts = [ cfg.dnsPort ]; } # Allow web admin access only if listenAddresses restricts it to localhost (lib.mkIf (cfg.listenAddresses == [ ] || builtins.elem "127.0.0.1" cfg.listenAddresses) { allowedTCPPorts = [ cfg.webPort ]; }) ]; # Ensure DNS resolution is available locally before starting networking.nameservers = lib.mkAfter [ "127.0.0.1" ]; }; }