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: xfr_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 primary = { lib, ... }: {
47 imports = [ common ];
48
49 # trigger sched_setaffinity syscall
50 virtualisation.cores = 2;
51
52 networking.interfaces.eth1 = {
53 ipv4.addresses = lib.mkForce [
54 { address = "192.168.0.1"; prefixLength = 24; }
55 ];
56 ipv6.addresses = lib.mkForce [
57 { address = "fd00::1"; prefixLength = 64; }
58 ];
59 };
60 services.knot.enable = true;
61 services.knot.extraArgs = [ "-v" ];
62 services.knot.keyFiles = [ tsigFile ];
63 services.knot.settings = {
64 server = {
65 listen = [
66 "0.0.0.0@53"
67 "::@53"
68 ];
69 automatic-acl = true;
70 };
71
72 acl.secondary_acl = {
73 address = "192.168.0.2";
74 key = "xfr_key";
75 action = "transfer";
76 };
77
78 remote.secondary.address = "192.168.0.2@53";
79
80 template.default = {
81 storage = knotZonesEnv;
82 notify = [ "secondary" ];
83 acl = [ "secondary_acl" ];
84 dnssec-signing = true;
85 # Input-only zone files
86 # https://www.knot-dns.cz/docs/2.8/html/operation.html#example-3
87 # prevents modification of the zonefiles, since the zonefiles are immutable
88 zonefile-sync = -1;
89 zonefile-load = "difference";
90 journal-content = "changes";
91 };
92
93 zone = {
94 "example.com".file = "example.com.zone";
95 "sub.example.com".file = "sub.example.com.zone";
96 };
97
98 log.syslog.any = "info";
99 };
100 };
101
102 secondary = { lib, ... }: {
103 imports = [ common ];
104 networking.interfaces.eth1 = {
105 ipv4.addresses = lib.mkForce [
106 { address = "192.168.0.2"; prefixLength = 24; }
107 ];
108 ipv6.addresses = lib.mkForce [
109 { address = "fd00::2"; prefixLength = 64; }
110 ];
111 };
112 services.knot.enable = true;
113 services.knot.keyFiles = [ tsigFile ];
114 services.knot.extraArgs = [ "-v" ];
115 services.knot.settings = {
116 server = {
117 listen = [
118 "0.0.0.0@53"
119 "::@53"
120 ];
121 automatic-acl = true;
122 };
123
124 remote.primary = {
125 address = "192.168.0.1@53";
126 key = "xfr_key";
127 };
128
129 template.default = {
130 master = "primary";
131 # zonefileless setup
132 # https://www.knot-dns.cz/docs/2.8/html/operation.html#example-2
133 zonefile-sync = "-1";
134 zonefile-load = "none";
135 journal-content = "all";
136 };
137
138 zone = {
139 "example.com".file = "example.com.zone";
140 "sub.example.com".file = "sub.example.com.zone";
141 };
142
143 log.syslog.any = "info";
144 };
145 };
146 client = { lib, nodes, ... }: {
147 imports = [ common ];
148 networking.interfaces.eth1 = {
149 ipv4.addresses = [
150 { address = "192.168.0.3"; prefixLength = 24; }
151 ];
152 ipv6.addresses = [
153 { address = "fd00::3"; prefixLength = 64; }
154 ];
155 };
156 environment.systemPackages = [ pkgs.knot-dns ];
157 };
158 };
159
160 testScript = { nodes, ... }: let
161 primary4 = (lib.head nodes.primary.config.networking.interfaces.eth1.ipv4.addresses).address;
162 primary6 = (lib.head nodes.primary.config.networking.interfaces.eth1.ipv6.addresses).address;
163
164 secondary4 = (lib.head nodes.secondary.config.networking.interfaces.eth1.ipv4.addresses).address;
165 secondary6 = (lib.head nodes.secondary.config.networking.interfaces.eth1.ipv6.addresses).address;
166 in ''
167 import re
168
169 start_all()
170
171 client.wait_for_unit("network.target")
172 primary.wait_for_unit("knot.service")
173 secondary.wait_for_unit("knot.service")
174
175
176 def test(host, query_type, query, pattern):
177 out = client.succeed(f"khost -t {query_type} {query} {host}").strip()
178 client.log(f"{host} replied with: {out}")
179 assert re.search(pattern, out), f'Did not match "{pattern}"'
180
181
182 for host in ("${primary4}", "${primary6}", "${secondary4}", "${secondary6}"):
183 with subtest(f"Interrogate {host}"):
184 test(host, "SOA", "example.com", r"start of authority.*noc\.example\.com\.")
185 test(host, "A", "example.com", r"has no [^ ]+ record")
186 test(host, "AAAA", "example.com", r"has no [^ ]+ record")
187
188 test(host, "A", "www.example.com", r"address 192.0.2.1$")
189 test(host, "AAAA", "www.example.com", r"address 2001:db8::1$")
190
191 test(host, "NS", "sub.example.com", r"nameserver is ns\d\.example\.com.$")
192 test(host, "A", "sub.example.com", r"address 192.0.2.2$")
193 test(host, "AAAA", "sub.example.com", r"address 2001:db8::2$")
194
195 test(host, "RRSIG", "www.example.com", r"RR set signature is")
196 test(host, "DNSKEY", "example.com", r"DNSSEC key is")
197
198 primary.log(primary.succeed("systemd-analyze security knot.service | grep -v '✓'"))
199 '';
200})