# `services.nxdns`: the packaged unit, run in file mode against a configuration # that the pinned binary accepted at build time. # # The unit is `deploy/systemd/nxdns.service` from the release tarball, installed # by the package and picked up through `systemd.packages`. This module only adds # a drop-in, so the hardening lives in that one file and a change to it reaches # NixOS with the release that ships it. { self }: { config, lib, pkgs, ... }: let cfg = config.services.nxdns; # Nix treats the ZON as opaque data, so without this a configuration the # pinned binary rejects evaluates, builds and deploys, and fails only when # the service starts. `import` into a throwaway data directory is the offline # validator: it shares the parser, the validator and the reconcile engine # with `run`, and its delete gate is inert on an empty database. `check` is # not usable here: it probes every upstream, and the build sandbox has no # network. validatedConfig = pkgs.runCommand "nxdns-config.zon" { nativeBuildInputs = [ cfg.package ]; src = cfg.configFile; } '' mkdir -p "$TMPDIR/validate" nxdns import --data-dir "$TMPDIR/validate" "$src" cp "$src" "$out" ''; in { options.services.nxdns = { enable = lib.mkEnableOption "nxdns, a DNS sinkhole for a household network"; package = lib.mkOption { type = lib.types.package; default = self.packages.${pkgs.stdenv.hostPlatform.system}.default; defaultText = lib.literalExpression "nxdns.packages.\${pkgs.stdenv.hostPlatform.system}.default"; description = "The nxdns release to run. The default is the release of the flake revision this module comes from."; }; configFile = lib.mkOption { type = lib.types.path; example = lib.literalExpression "./nxdns-config.zon"; description = '' The nxdns configuration, in ZON. nxdns runs in file mode: every start reconciles the database onto this file, and the web interface refuses configuration edits. The build fails when `nxdns import` from `package` rejects the file. ''; }; openFirewall = lib.mkOption { type = lib.types.bool; default = false; description = "Open port 53 over UDP and TCP. The web interface port and the DoH and DoT ports stay closed."; }; }; config = lib.mkIf cfg.enable { environment.etc."nxdns/config.zon".source = validatedConfig; users.users.nxdns = { isSystemUser = true; group = "nxdns"; description = "nxdns DNS sinkhole"; }; users.groups.nxdns = { }; systemd.packages = [ cfg.package ]; systemd.services.nxdns = { wantedBy = [ "multi-user.target" ]; # The unit file does not change when only the configuration does, so # without this a configuration-only switch restarts nothing and the old # configuration keeps serving with no error anywhere. restartTriggers = [ validatedConfig ]; serviceConfig = { # The empty entry clears the packaged ExecStart: a drop-in cannot # replace it otherwise. ExecStart = [ "" "${lib.getExe cfg.package} run --config=/etc/nxdns/config.zon" ]; # `environment.etc` creates /etc/nxdns and owns what is in it. A # ConfigurationDirectory would make systemd adjust the same directory # at every start. ConfigurationDirectory = ""; }; }; networking.firewall = lib.mkIf cfg.openFirewall { allowedUDPPorts = [ 53 ]; allowedTCPPorts = [ 53 ]; }; }; }