at master 7.3 kB view raw
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}