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