at 23.11-beta 6.3 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.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})