1import ./make-test-python.nix ({ pkgs, lib, ...} :
2let
3 common = {
4 networking.firewall.enable = false;
5 networking.useDHCP = false;
6 };
7 exampleZone = pkgs.writeTextDir "example.com.zone" ''
8 @ SOA ns.example.com. noc.example.com. 2019031301 86400 7200 3600000 172800
9 @ NS ns1
10 @ NS ns2
11 ns1 A 192.168.0.1
12 ns1 AAAA fd00::1
13 ns2 A 192.168.0.2
14 ns2 AAAA fd00::2
15 www A 192.0.2.1
16 www AAAA 2001:DB8::1
17 sub NS ns.example.com.
18 '';
19 delegatedZone = pkgs.writeTextDir "sub.example.com.zone" ''
20 @ SOA ns.example.com. noc.example.com. 2019031301 86400 7200 3600000 172800
21 @ NS ns1.example.com.
22 @ NS ns2.example.com.
23 @ A 192.0.2.2
24 @ AAAA 2001:DB8::2
25 '';
26
27 knotZonesEnv = pkgs.buildEnv {
28 name = "knot-zones";
29 paths = [ exampleZone delegatedZone ];
30 };
31 # DO NOT USE pkgs.writeText IN PRODUCTION. This put secrets in the nix store!
32 tsigFile = pkgs.writeText "tsig.conf" ''
33 key:
34 - id: slave_key
35 algorithm: hmac-sha256
36 secret: zOYgOgnzx3TGe5J5I/0kxd7gTcxXhLYMEq3Ek3fY37s=
37 '';
38in {
39 name = "knot";
40 meta = with pkgs.lib.maintainers; {
41 maintainers = [ hexa ];
42 };
43
44
45 nodes = {
46 master = { lib, ... }: {
47 imports = [ common ];
48 networking.interfaces.eth1 = {
49 ipv4.addresses = lib.mkForce [
50 { address = "192.168.0.1"; prefixLength = 24; }
51 ];
52 ipv6.addresses = lib.mkForce [
53 { address = "fd00::1"; prefixLength = 64; }
54 ];
55 };
56 services.knot.enable = true;
57 services.knot.extraArgs = [ "-v" ];
58 services.knot.keyFiles = [ tsigFile ];
59 services.knot.extraConfig = ''
60 server:
61 listen: 0.0.0.0@53
62 listen: ::@53
63
64 acl:
65 - id: slave_acl
66 address: 192.168.0.2
67 key: slave_key
68 action: transfer
69
70 remote:
71 - id: slave
72 address: 192.168.0.2@53
73
74 template:
75 - id: default
76 storage: ${knotZonesEnv}
77 notify: [slave]
78 acl: [slave_acl]
79 dnssec-signing: on
80 # Input-only zone files
81 # https://www.knot-dns.cz/docs/2.8/html/operation.html#example-3
82 # prevents modification of the zonefiles, since the zonefiles are immutable
83 zonefile-sync: -1
84 zonefile-load: difference
85 journal-content: changes
86 # move databases below the state directory, because they need to be writable
87 journal-db: /var/lib/knot/journal
88 kasp-db: /var/lib/knot/kasp
89 timer-db: /var/lib/knot/timer
90
91 zone:
92 - domain: example.com
93 file: example.com.zone
94
95 - domain: sub.example.com
96 file: sub.example.com.zone
97
98 log:
99 - target: syslog
100 any: info
101 '';
102 };
103
104 slave = { lib, ... }: {
105 imports = [ common ];
106 networking.interfaces.eth1 = {
107 ipv4.addresses = lib.mkForce [
108 { address = "192.168.0.2"; prefixLength = 24; }
109 ];
110 ipv6.addresses = lib.mkForce [
111 { address = "fd00::2"; prefixLength = 64; }
112 ];
113 };
114 services.knot.enable = true;
115 services.knot.keyFiles = [ tsigFile ];
116 services.knot.extraArgs = [ "-v" ];
117 services.knot.extraConfig = ''
118 server:
119 listen: 0.0.0.0@53
120 listen: ::@53
121
122 acl:
123 - id: notify_from_master
124 address: 192.168.0.1
125 action: notify
126
127 remote:
128 - id: master
129 address: 192.168.0.1@53
130 key: slave_key
131
132 template:
133 - id: default
134 master: master
135 acl: [notify_from_master]
136 # zonefileless setup
137 # https://www.knot-dns.cz/docs/2.8/html/operation.html#example-2
138 zonefile-sync: -1
139 zonefile-load: none
140 journal-content: all
141 # move databases below the state directory, because they need to be writable
142 journal-db: /var/lib/knot/journal
143 kasp-db: /var/lib/knot/kasp
144 timer-db: /var/lib/knot/timer
145
146 zone:
147 - domain: example.com
148 file: example.com.zone
149
150 - domain: sub.example.com
151 file: sub.example.com.zone
152
153 log:
154 - target: syslog
155 any: info
156 '';
157 };
158 client = { lib, nodes, ... }: {
159 imports = [ common ];
160 networking.interfaces.eth1 = {
161 ipv4.addresses = [
162 { address = "192.168.0.3"; prefixLength = 24; }
163 ];
164 ipv6.addresses = [
165 { address = "fd00::3"; prefixLength = 64; }
166 ];
167 };
168 environment.systemPackages = [ pkgs.knot-dns ];
169 };
170 };
171
172 testScript = { nodes, ... }: let
173 master4 = (lib.head nodes.master.config.networking.interfaces.eth1.ipv4.addresses).address;
174 master6 = (lib.head nodes.master.config.networking.interfaces.eth1.ipv6.addresses).address;
175
176 slave4 = (lib.head nodes.slave.config.networking.interfaces.eth1.ipv4.addresses).address;
177 slave6 = (lib.head nodes.slave.config.networking.interfaces.eth1.ipv6.addresses).address;
178 in ''
179 import re
180
181 start_all()
182
183 client.wait_for_unit("network.target")
184 master.wait_for_unit("knot.service")
185 slave.wait_for_unit("knot.service")
186
187
188 def test(host, query_type, query, pattern):
189 out = client.succeed(f"khost -t {query_type} {query} {host}").strip()
190 client.log(f"{host} replied with: {out}")
191 assert re.search(pattern, out), f'Did not match "{pattern}"'
192
193
194 for host in ("${master4}", "${master6}", "${slave4}", "${slave6}"):
195 with subtest(f"Interrogate {host}"):
196 test(host, "SOA", "example.com", r"start of authority.*noc\.example\.com\.")
197 test(host, "A", "example.com", r"has no [^ ]+ record")
198 test(host, "AAAA", "example.com", r"has no [^ ]+ record")
199
200 test(host, "A", "www.example.com", r"address 192.0.2.1$")
201 test(host, "AAAA", "www.example.com", r"address 2001:db8::1$")
202
203 test(host, "NS", "sub.example.com", r"nameserver is ns\d\.example\.com.$")
204 test(host, "A", "sub.example.com", r"address 192.0.2.2$")
205 test(host, "AAAA", "sub.example.com", r"address 2001:db8::2$")
206
207 test(host, "RRSIG", "www.example.com", r"RR set signature is")
208 test(host, "DNSKEY", "example.com", r"DNSSEC key is")
209 '';
210})