1{
2 pkgs,
3 lib,
4 ...
5}:
6let
7 common = {
8 networking.firewall.enable = false;
9 networking.useDHCP = false;
10 };
11 exampleZone = pkgs.writeTextDir "example.com.zone" ''
12 @ SOA ns.example.com. noc.example.com. 2019031301 86400 7200 3600000 172800
13 @ NS ns1
14 @ NS ns2
15 ns1 A 192.168.0.1
16 ns1 AAAA fd00::1
17 ns2 A 192.168.0.2
18 ns2 AAAA fd00::2
19 www A 192.0.2.1
20 www AAAA 2001:DB8::1
21 sub NS ns.example.com.
22 '';
23 delegatedZone = pkgs.writeTextDir "sub.example.com.zone" ''
24 @ SOA ns.example.com. noc.example.com. 2019031301 86400 7200 3600000 172800
25 @ NS ns1.example.com.
26 @ NS ns2.example.com.
27 @ A 192.0.2.2
28 @ AAAA 2001:DB8::2
29 '';
30
31 knotZonesEnv = pkgs.buildEnv {
32 name = "knot-zones";
33 paths = [
34 exampleZone
35 delegatedZone
36 ];
37 };
38 # DO NOT USE pkgs.writeText IN PRODUCTION. This put secrets in the nix store!
39 tsigFile = pkgs.writeText "tsig.conf" ''
40 key:
41 - id: xfr_key
42 algorithm: hmac-sha256
43 secret: zOYgOgnzx3TGe5J5I/0kxd7gTcxXhLYMEq3Ek3fY37s=
44 '';
45in
46{
47 name = "knot";
48 meta = with pkgs.lib.maintainers; {
49 maintainers = [ hexa ];
50 };
51
52 nodes = {
53 primary =
54 { lib, ... }:
55 {
56 imports = [ common ];
57
58 # trigger sched_setaffinity syscall
59 virtualisation.cores = 2;
60
61 networking.interfaces.eth1 = {
62 ipv4.addresses = lib.mkForce [
63 {
64 address = "192.168.0.1";
65 prefixLength = 24;
66 }
67 ];
68 ipv6.addresses = lib.mkForce [
69 {
70 address = "fd00::1";
71 prefixLength = 64;
72 }
73 ];
74 };
75 services.knot.enable = true;
76 services.knot.extraArgs = [ "-v" ];
77 services.knot.keyFiles = [ tsigFile ];
78 services.knot.settings = {
79 server = {
80 listen = [
81 "0.0.0.0@53"
82 "::@53"
83 ];
84 listen-quic = [
85 "0.0.0.0@853"
86 "::@853"
87 ];
88 automatic-acl = true;
89 };
90
91 acl.secondary_acl = {
92 address = "192.168.0.2";
93 key = "xfr_key";
94 action = "transfer";
95 };
96
97 remote.secondary.address = "192.168.0.2@53";
98
99 template.default = {
100 storage = knotZonesEnv;
101 notify = [ "secondary" ];
102 acl = [ "secondary_acl" ];
103 dnssec-signing = true;
104 # Input-only zone files
105 # https://www.knot-dns.cz/docs/2.8/html/operation.html#example-3
106 # prevents modification of the zonefiles, since the zonefiles are immutable
107 zonefile-sync = -1;
108 zonefile-load = "difference";
109 journal-content = "changes";
110 };
111
112 zone = {
113 "example.com".file = "example.com.zone";
114 "sub.example.com".file = "sub.example.com.zone";
115 };
116
117 log.syslog.any = "info";
118 };
119 };
120
121 secondary =
122 { lib, ... }:
123 {
124 imports = [ common ];
125 networking.interfaces.eth1 = {
126 ipv4.addresses = lib.mkForce [
127 {
128 address = "192.168.0.2";
129 prefixLength = 24;
130 }
131 ];
132 ipv6.addresses = lib.mkForce [
133 {
134 address = "fd00::2";
135 prefixLength = 64;
136 }
137 ];
138 };
139 services.knot.enable = true;
140 services.knot.keyFiles = [ tsigFile ];
141 services.knot.extraArgs = [ "-v" ];
142 services.knot.settings = {
143 server = {
144 automatic-acl = true;
145 };
146
147 xdp = {
148 listen = [
149 "eth1"
150 ];
151 tcp = true;
152 };
153
154 remote.primary = {
155 address = "192.168.0.1@53";
156 key = "xfr_key";
157 };
158
159 remote.primary-quic = {
160 address = "192.168.0.1@853";
161 key = "xfr_key";
162 quic = true;
163 };
164
165 template.default = {
166 # zonefileless setup
167 # https://www.knot-dns.cz/docs/2.8/html/operation.html#example-2
168 zonefile-sync = "-1";
169 zonefile-load = "none";
170 journal-content = "all";
171 };
172
173 zone = {
174 "example.com" = {
175 master = "primary";
176 file = "example.com.zone";
177 };
178 "sub.example.com" = {
179 master = "primary-quic";
180 file = "sub.example.com.zone";
181 };
182 };
183
184 log.syslog.any = "debug";
185 };
186 };
187 client =
188 { lib, nodes, ... }:
189 {
190 imports = [ common ];
191 networking.interfaces.eth1 = {
192 ipv4.addresses = [
193 {
194 address = "192.168.0.3";
195 prefixLength = 24;
196 }
197 ];
198 ipv6.addresses = [
199 {
200 address = "fd00::3";
201 prefixLength = 64;
202 }
203 ];
204 };
205 environment.systemPackages = [ pkgs.knot-dns ];
206 };
207 };
208
209 testScript =
210 { nodes, ... }:
211 let
212 primary4 = (lib.head nodes.primary.config.networking.interfaces.eth1.ipv4.addresses).address;
213 primary6 = (lib.head nodes.primary.config.networking.interfaces.eth1.ipv6.addresses).address;
214
215 secondary4 = (lib.head nodes.secondary.config.networking.interfaces.eth1.ipv4.addresses).address;
216 secondary6 = (lib.head nodes.secondary.config.networking.interfaces.eth1.ipv6.addresses).address;
217 in
218 ''
219 import re
220
221 start_all()
222
223 client.wait_for_unit("network.target")
224 primary.wait_for_unit("knot.service")
225 secondary.wait_for_unit("knot.service")
226
227 for zone in ("example.com.", "sub.example.com."):
228 secondary.wait_until_succeeds(
229 f"knotc zone-status {zone} | grep -q 'serial: 2019031302'"
230 )
231
232 def test(host, query_type, query, pattern):
233 out = client.succeed(f"khost -t {query_type} {query} {host}").strip()
234 client.log(f"{host} replied with: {out}")
235 assert re.search(pattern, out), f'Did not match "{pattern}"'
236
237
238 for host in ("${primary4}", "${primary6}", "${secondary4}", "${secondary6}"):
239 with subtest(f"Interrogate {host}"):
240 test(host, "SOA", "example.com", r"start of authority.*noc\.example\.com\.")
241 test(host, "A", "example.com", r"has no [^ ]+ record")
242 test(host, "AAAA", "example.com", r"has no [^ ]+ record")
243
244 test(host, "A", "www.example.com", r"address 192.0.2.1$")
245 test(host, "AAAA", "www.example.com", r"address 2001:db8::1$")
246
247 test(host, "NS", "sub.example.com", r"nameserver is ns\d\.example\.com.$")
248 test(host, "A", "sub.example.com", r"address 192.0.2.2$")
249 test(host, "AAAA", "sub.example.com", r"address 2001:db8::2$")
250
251 test(host, "RRSIG", "www.example.com", r"RR set signature is")
252 test(host, "DNSKEY", "example.com", r"DNSSEC key is")
253
254 primary.log(primary.succeed("systemd-analyze security knot.service | grep -v '✓'"))
255 '';
256}