diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 2f94ee53f9b77..b2f2c1ac8cbce 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -578,6 +578,7 @@ ./services/networking/keepalived/default.nix ./services/networking/keybase.nix ./services/networking/kippo.nix + ./services/networking/knot.nix ./services/networking/kresd.nix ./services/networking/lambdabot.nix ./services/networking/libreswan.nix diff --git a/nixos/modules/services/networking/knot.nix b/nixos/modules/services/networking/knot.nix new file mode 100644 index 0000000000000..1cc1dd3f2f62b --- /dev/null +++ b/nixos/modules/services/networking/knot.nix @@ -0,0 +1,95 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.knot; + + configFile = pkgs.writeText "knot.conf" cfg.extraConfig; + socketFile = "/run/knot/knot.sock"; + + knotConfCheck = file: pkgs.runCommand "knot-config-checked" + { buildInputs = [ cfg.package ]; } '' + ln -s ${configFile} $out + knotc --config=${configFile} conf-check + ''; + + knot-cli-wrappers = pkgs.stdenv.mkDerivation { + name = "knot-cli-wrappers"; + buildInputs = [ pkgs.makeWrapper ]; + buildCommand = '' + mkdir -p $out/bin + makeWrapper ${cfg.package}/bin/knotc "$out/bin/knotc" \ + --add-flags "--config=${configFile}" \ + --add-flags "--socket=${socketFile}" + makeWrapper ${cfg.package}/bin/keymgr "$out/bin/keymgr" \ + --add-flags "--config=${configFile}" + for executable in kdig khost kjournalprint knsec3hash knsupdate kzonecheck + do + ln -s "${cfg.package}/bin/$executable" "$out/bin/$executable" + done + mkdir -p "$out/share" + ln -s '${cfg.package}/share/man' "$out/share/" + ''; + }; +in { + options = { + services.knot = { + enable = mkEnableOption "Knot authoritative-only DNS server"; + + extraArgs = mkOption { + type = types.listOf types.str; + default = []; + description = '' + List of additional command line paramters for knotd + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Extra lines to be added verbatim to knot.conf + ''; + }; + + package = mkOption { + type = types.package; + default = pkgs.knot-dns; + description = '' + Which Knot DNS package to use + ''; + }; + }; + }; + + config = mkIf config.services.knot.enable { + systemd.services.knot = { + unitConfig.Documentation = "man:knotd(8) man:knot.conf(5) man:knotc(8) https://www.knot-dns.cz/docs/${cfg.package.version}/html/"; + description = cfg.package.meta.description; + wantedBy = [ "multi-user.target" ]; + wants = [ "network.target" ]; + after = ["network.target" ]; + + serviceConfig = { + Type = "notify"; + ExecStart = "${cfg.package}/bin/knotd --config=${knotConfCheck configFile} --socket=${socketFile} ${concatStringsSep " " cfg.extraArgs}"; + ExecReload = "${knot-cli-wrappers}/bin/knotc reload"; + CapabilityBoundingSet = "CAP_NET_BIND_SERVICE CAP_SETPCAP"; + AmbientCapabilities = "CAP_NET_BIND_SERVICE CAP_SETPCAP"; + NoNewPrivileges = true; + DynamicUser = "yes"; + RuntimeDirectory = "knot"; + StateDirectory = "knot"; + StateDirectoryMode = "0700"; + PrivateDevices = true; + RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6"; + SystemCallArchitectures = "native"; + Restart = "on-abort"; + }; + }; + + environment.systemPackages = [ knot-cli-wrappers ]; + }; +} + diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 2ddb54bcc3d7e..de5b8bbb7c06d 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -117,6 +117,7 @@ in kernel-latest = handleTest ./kernel-latest.nix {}; kernel-lts = handleTest ./kernel-lts.nix {}; keymap = handleTest ./keymap.nix {}; + knot = handleTest ./knot.nix {}; kubernetes.dns = handleTestOn ["x86_64-linux"] ./kubernetes/dns.nix {}; # kubernetes.e2e should eventually replace kubernetes.rbac when it works #kubernetes.e2e = handleTestOn ["x86_64-linux"] ./kubernetes/e2e.nix {}; diff --git a/nixos/tests/knot.nix b/nixos/tests/knot.nix new file mode 100644 index 0000000000000..e46159836ccc0 --- /dev/null +++ b/nixos/tests/knot.nix @@ -0,0 +1,197 @@ +import ./make-test.nix ({ pkgs, lib, ...} : +let + common = { + networking.firewall.enable = false; + networking.useDHCP = false; + }; + exampleZone = pkgs.writeTextDir "example.com.zone" '' + @ SOA ns.example.com. noc.example.com. 2019031301 86400 7200 3600000 172800 + @ NS ns1 + @ NS ns2 + ns1 A 192.168.0.1 + ns1 AAAA fd00::1 + ns2 A 192.168.0.2 + ns2 AAAA fd00::2 + www A 192.0.2.1 + www AAAA 2001:DB8::1 + sub NS ns.example.com. + ''; + delegatedZone = pkgs.writeTextDir "sub.example.com.zone" '' + @ SOA ns.example.com. noc.example.com. 2019031301 86400 7200 3600000 172800 + @ NS ns1.example.com. + @ NS ns2.example.com. + @ A 192.0.2.2 + @ AAAA 2001:DB8::2 + ''; + + knotZonesEnv = pkgs.buildEnv { + name = "knot-zones"; + paths = [ exampleZone delegatedZone ]; + }; +in { + name = "knot"; + + nodes = { + master = { lib, ... }: { + imports = [ common ]; + networking.interfaces.eth1 = { + ipv4.addresses = lib.mkForce [ + { address = "192.168.0.1"; prefixLength = 24; } + ]; + ipv6.addresses = lib.mkForce [ + { address = "fd00::1"; prefixLength = 64; } + ]; + }; + services.knot.enable = true; + services.knot.extraArgs = [ "-v" ]; + services.knot.extraConfig = '' + server: + listen: 0.0.0.0@53 + listen: ::@53 + + acl: + - id: slave_acl + address: 192.168.0.2 + action: transfer + + remote: + - id: slave + address: 192.168.0.2@53 + + template: + - id: default + storage: ${knotZonesEnv} + notify: [slave] + acl: [slave_acl] + dnssec-signing: on + # Input-only zone files + # https://www.knot-dns.cz/docs/2.8/html/operation.html#example-3 + # prevents modification of the zonefiles, since the zonefiles are immutable + zonefile-sync: -1 + zonefile-load: difference + journal-content: changes + # move databases below the state directory, because they need to be writable + journal-db: /var/lib/knot/journal + kasp-db: /var/lib/knot/kasp + timer-db: /var/lib/knot/timer + + zone: + - domain: example.com + file: example.com.zone + + - domain: sub.example.com + file: sub.example.com.zone + + log: + - target: syslog + any: info + ''; + }; + + slave = { lib, ... }: { + imports = [ common ]; + networking.interfaces.eth1 = { + ipv4.addresses = lib.mkForce [ + { address = "192.168.0.2"; prefixLength = 24; } + ]; + ipv6.addresses = lib.mkForce [ + { address = "fd00::2"; prefixLength = 64; } + ]; + }; + services.knot.enable = true; + services.knot.extraArgs = [ "-v" ]; + services.knot.extraConfig = '' + server: + listen: 0.0.0.0@53 + listen: ::@53 + + acl: + - id: notify_from_master + address: 192.168.0.1 + action: notify + + remote: + - id: master + address: 192.168.0.1@53 + + template: + - id: default + master: master + acl: [notify_from_master] + # zonefileless setup + # https://www.knot-dns.cz/docs/2.8/html/operation.html#example-2 + zonefile-sync: -1 + zonefile-load: none + journal-content: all + # move databases below the state directory, because they need to be writable + journal-db: /var/lib/knot/journal + kasp-db: /var/lib/knot/kasp + timer-db: /var/lib/knot/timer + + zone: + - domain: example.com + file: example.com.zone + + - domain: sub.example.com + file: sub.example.com.zone + + log: + - target: syslog + any: info + ''; + }; + client = { lib, nodes, ... }: { + imports = [ common ]; + networking.interfaces.eth1 = { + ipv4.addresses = [ + { address = "192.168.0.3"; prefixLength = 24; } + ]; + ipv6.addresses = [ + { address = "fd00::3"; prefixLength = 64; } + ]; + }; + environment.systemPackages = [ pkgs.knot-dns ]; + }; + }; + + testScript = { nodes, ... }: let + master4 = (lib.head nodes.master.config.networking.interfaces.eth1.ipv4.addresses).address; + master6 = (lib.head nodes.master.config.networking.interfaces.eth1.ipv6.addresses).address; + + slave4 = (lib.head nodes.slave.config.networking.interfaces.eth1.ipv4.addresses).address; + slave6 = (lib.head nodes.slave.config.networking.interfaces.eth1.ipv6.addresses).address; + in '' + startAll; + + $client->waitForUnit("network.target"); + $master->waitForUnit("knot.service"); + $slave->waitForUnit("knot.service"); + + sub assertResponse { + my ($knot, $query_type, $query, $expected) = @_; + my $out = $client->succeed("khost -t $query_type $query $knot"); + $client->log("$knot replies with: $out"); + chomp $out; + die "DNS query for $query ($query_type) against $knot gave '$out' instead of '$expected'" + if ($out !~ $expected); + } + + foreach ("${master4}", "${master6}", "${slave4}", "${slave6}") { + subtest $_, sub { + assertResponse($_, "SOA", "example.com", qr/start of authority.*?noc\.example\.com/); + assertResponse($_, "A", "example.com", qr/has no [^ ]+ record/); + assertResponse($_, "AAAA", "example.com", qr/has no [^ ]+ record/); + + assertResponse($_, "A", "www.example.com", qr/address 192.0.2.1$/); + assertResponse($_, "AAAA", "www.example.com", qr/address 2001:db8::1$/); + + assertResponse($_, "NS", "sub.example.com", qr/nameserver is ns\d\.example\.com.$/); + assertResponse($_, "A", "sub.example.com", qr/address 192.0.2.2$/); + assertResponse($_, "AAAA", "sub.example.com", qr/address 2001:db8::2$/); + + assertResponse($_, "RRSIG", "www.example.com", qr/RR set signature is/); + assertResponse($_, "DNSKEY", "example.com", qr/DNSSEC key is/); + }; + } + ''; +})