1 import ./make-test-python.nix ({ pkgs, lib, ...} :
4 networking.firewall.enable = false;
5 networking.useDHCP = false;
7 exampleZone = pkgs.writeTextDir "example.com.zone" ''
8 @ SOA ns.example.com. noc.example.com. 2019031301 86400 7200 3600000 172800
17 sub NS ns.example.com.
19 delegatedZone = pkgs.writeTextDir "sub.example.com.zone" ''
20 @ SOA ns.example.com. noc.example.com. 2019031301 86400 7200 3600000 172800
27 knotZonesEnv = pkgs.buildEnv {
29 paths = [ exampleZone delegatedZone ];
31 # DO NOT USE pkgs.writeText IN PRODUCTION. This put secrets in the nix store!
32 tsigFile = pkgs.writeText "tsig.conf" ''
35 algorithm: hmac-sha256
36 secret: zOYgOgnzx3TGe5J5I/0kxd7gTcxXhLYMEq3Ek3fY37s=
40 meta = with pkgs.lib.maintainers; {
41 maintainers = [ hexa ];
46 primary = { lib, ... }: {
49 # trigger sched_setaffinity syscall
50 virtualisation.cores = 2;
52 networking.interfaces.eth1 = {
53 ipv4.addresses = lib.mkForce [
54 { address = "192.168.0.1"; prefixLength = 24; }
56 ipv6.addresses = lib.mkForce [
57 { address = "fd00::1"; prefixLength = 64; }
60 services.knot.enable = true;
61 services.knot.extraArgs = [ "-v" ];
62 services.knot.keyFiles = [ tsigFile ];
63 services.knot.settings = {
77 address = "192.168.0.2";
82 remote.secondary.address = "192.168.0.2@53";
85 storage = knotZonesEnv;
86 notify = [ "secondary" ];
87 acl = [ "secondary_acl" ];
88 dnssec-signing = true;
89 # Input-only zone files
90 # https://www.knot-dns.cz/docs/2.8/html/operation.html#example-3
91 # prevents modification of the zonefiles, since the zonefiles are immutable
93 zonefile-load = "difference";
94 journal-content = "changes";
98 "example.com".file = "example.com.zone";
99 "sub.example.com".file = "sub.example.com.zone";
102 log.syslog.any = "info";
106 secondary = { lib, ... }: {
107 imports = [ common ];
108 networking.interfaces.eth1 = {
109 ipv4.addresses = lib.mkForce [
110 { address = "192.168.0.2"; prefixLength = 24; }
112 ipv6.addresses = lib.mkForce [
113 { address = "fd00::2"; prefixLength = 64; }
116 services.knot.enable = true;
117 services.knot.keyFiles = [ tsigFile ];
118 services.knot.extraArgs = [ "-v" ];
119 services.knot.settings = {
121 automatic-acl = true;
132 address = "192.168.0.1@53";
136 remote.primary-quic = {
137 address = "192.168.0.1@853";
144 # https://www.knot-dns.cz/docs/2.8/html/operation.html#example-2
145 zonefile-sync = "-1";
146 zonefile-load = "none";
147 journal-content = "all";
153 file = "example.com.zone";
155 "sub.example.com" = {
156 master = "primary-quic";
157 file = "sub.example.com.zone";
161 log.syslog.any = "debug";
164 client = { lib, nodes, ... }: {
165 imports = [ common ];
166 networking.interfaces.eth1 = {
168 { address = "192.168.0.3"; prefixLength = 24; }
171 { address = "fd00::3"; prefixLength = 64; }
174 environment.systemPackages = [ pkgs.knot-dns ];
178 testScript = { nodes, ... }: let
179 primary4 = (lib.head nodes.primary.config.networking.interfaces.eth1.ipv4.addresses).address;
180 primary6 = (lib.head nodes.primary.config.networking.interfaces.eth1.ipv6.addresses).address;
182 secondary4 = (lib.head nodes.secondary.config.networking.interfaces.eth1.ipv4.addresses).address;
183 secondary6 = (lib.head nodes.secondary.config.networking.interfaces.eth1.ipv6.addresses).address;
189 client.wait_for_unit("network.target")
190 primary.wait_for_unit("knot.service")
191 secondary.wait_for_unit("knot.service")
193 for zone in ("example.com.", "sub.example.com."):
194 secondary.wait_until_succeeds(
195 f"knotc zone-status {zone} | grep -q 'serial: 2019031302'"
198 def test(host, query_type, query, pattern):
199 out = client.succeed(f"khost -t {query_type} {query} {host}").strip()
200 client.log(f"{host} replied with: {out}")
201 assert re.search(pattern, out), f'Did not match "{pattern}"'
204 for host in ("${primary4}", "${primary6}", "${secondary4}", "${secondary6}"):
205 with subtest(f"Interrogate {host}"):
206 test(host, "SOA", "example.com", r"start of authority.*noc\.example\.com\.")
207 test(host, "A", "example.com", r"has no [^ ]+ record")
208 test(host, "AAAA", "example.com", r"has no [^ ]+ record")
210 test(host, "A", "www.example.com", r"address 192.0.2.1$")
211 test(host, "AAAA", "www.example.com", r"address 2001:db8::1$")
213 test(host, "NS", "sub.example.com", r"nameserver is ns\d\.example\.com.$")
214 test(host, "A", "sub.example.com", r"address 192.0.2.2$")
215 test(host, "AAAA", "sub.example.com", r"address 2001:db8::2$")
217 test(host, "RRSIG", "www.example.com", r"RR set signature is")
218 test(host, "DNSKEY", "example.com", r"DNSSEC key is")
220 primary.log(primary.succeed("systemd-analyze security knot.service | grep -v '✓'"))