nix: a services.nxdns module that runs the packaged unit in file mode against a configuration the pinned binary accepted
This commit is contained in:
+105
@@ -0,0 +1,105 @@
|
||||
# `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 ];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
# The acceptance checks for `services.nxdns`. Neither runs in CI: the VM test
|
||||
# needs KVM, and both need a nixpkgs closure the shared runner should not
|
||||
# download on every push. AGENTS.md says when to run them.
|
||||
{ self, pkgs }:
|
||||
let
|
||||
lib = pkgs.lib;
|
||||
|
||||
# A local record answers without any upstream, which the test VM cannot reach.
|
||||
configWith =
|
||||
address:
|
||||
pkgs.writeText "nxdns-test.zon" ''
|
||||
.{
|
||||
.groups = .{ .{ .name = "default" } },
|
||||
.upstreams = .{ .{ .url = "tls://192.0.2.1:853", .tls_name = "dns.example" } },
|
||||
.local_records = .{ .{ .name = "nas.lan", .rtype = .a, .value = "${address}" } },
|
||||
}
|
||||
'';
|
||||
|
||||
rejected = pkgs.writeText "nxdns-rejected.zon" ''
|
||||
.{
|
||||
.groups = .{ .{ .name = "default" } },
|
||||
.upstreams = .{ .{ .url = "tls://192.0.2.1:853", .tls_name = "dns.example" } },
|
||||
.rules = .{ .{ .group = "default", .pattern = "(", .kind = .regex, .action = .block } },
|
||||
}
|
||||
'';
|
||||
|
||||
machineWith = configFile: {
|
||||
imports = [ self.nixosModules.default ];
|
||||
services.nxdns = {
|
||||
enable = true;
|
||||
inherit configFile;
|
||||
};
|
||||
boot.loader.grub.enable = false;
|
||||
fileSystems."/" = {
|
||||
device = "none";
|
||||
fsType = "tmpfs";
|
||||
};
|
||||
system.stateVersion = lib.trivial.release;
|
||||
};
|
||||
in
|
||||
{
|
||||
nixos-module = pkgs.testers.runNixOSTest {
|
||||
name = "nxdns-module";
|
||||
|
||||
nodes.machine =
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
imports = [ self.nixosModules.default ];
|
||||
services.nxdns = {
|
||||
enable = true;
|
||||
configFile = configWith "192.0.2.5";
|
||||
openFirewall = true;
|
||||
};
|
||||
environment.systemPackages = [ pkgs.dnsutils ];
|
||||
specialisation.changed.configuration.services.nxdns.configFile = lib.mkForce (
|
||||
configWith "192.0.2.6"
|
||||
);
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
machine.wait_for_unit("nxdns.service")
|
||||
machine.wait_until_succeeds("dig +short @127.0.0.1 nas.lan A | grep -x 192.0.2.5")
|
||||
|
||||
# The packaged unit runs, with the module's drop-in on top of it.
|
||||
unit = machine.succeed("systemctl cat nxdns.service")
|
||||
assert "/lib/systemd/system/nxdns.service" in unit, unit
|
||||
assert "ProtectSystem=strict" in unit, unit
|
||||
assert "run --config=/etc/nxdns/config.zon" in unit, unit
|
||||
machine.succeed("systemctl show -p User --value nxdns.service | grep -x nxdns")
|
||||
machine.succeed("test -z \"$(systemctl show -p ConfigurationDirectory --value nxdns.service)\"")
|
||||
machine.succeed("systemctl show -p ReadOnlyPaths --value nxdns.service | tr ' ' '\\n' | grep -Fx /etc/nxdns")
|
||||
machine.succeed("stat -c %U /etc/nxdns | grep -x root")
|
||||
machine.succeed("iptables -S nixos-fw | grep -- '-p udp .*--dport 53 '")
|
||||
machine.succeed("iptables -S nixos-fw | grep -- '-p tcp .*--dport 53 '")
|
||||
|
||||
# A switch that changes only the configuration restarts the service.
|
||||
before = machine.succeed("systemctl show -p InvocationID --value nxdns.service").strip()
|
||||
machine.succeed("/run/current-system/specialisation/changed/bin/switch-to-configuration test")
|
||||
machine.wait_for_unit("nxdns.service")
|
||||
after = machine.succeed("systemctl show -p InvocationID --value nxdns.service").strip()
|
||||
assert before != after, "a configuration-only switch did not restart nxdns"
|
||||
machine.wait_until_succeeds("dig +short @127.0.0.1 nas.lan A | grep -x 192.0.2.6")
|
||||
'';
|
||||
};
|
||||
|
||||
# A configuration the pinned binary rejects must fail the system build, and
|
||||
# the build log must carry the binary's own reason.
|
||||
nixos-module-rejects-config =
|
||||
let
|
||||
system = pkgs.nixos (machineWith rejected);
|
||||
failure = pkgs.testers.testBuildFailure system.config.environment.etc."nxdns/config.zon".source;
|
||||
in
|
||||
pkgs.runCommand "nxdns-module-rejects-config" { } ''
|
||||
grep -F 'FAIL rules[0].pattern' ${failure}/testBuildFailure.log
|
||||
touch "$out"
|
||||
'';
|
||||
}
|
||||
Reference in New Issue
Block a user