at 25.11-pre 14 kB view raw
1/* 2 Test that our unbound module indeed works as most users would expect. 3 There are a few settings that we must consider when modifying the test. The 4 usual use-cases for unbound are 5 * running a recursive DNS resolver on the local machine 6 * running a recursive DNS resolver on the local machine, forwarding to a local DNS server via UDP/53 & TCP/53 7 * running a recursive DNS resolver on the local machine, forwarding to a local DNS server via TCP/853 (DoT) 8 * running a recursive DNS resolver on a machine in the network awaiting input from clients over TCP/53 & UDP/53 9 * running a recursive DNS resolver on a machine in the network awaiting input from clients over TCP/853 (DoT) 10 11 In the below test setup we are trying to implement all of those use cases. 12 13 Another aspect that we cover is access to the local control UNIX socket. It 14 can optionally be enabled and users can optionally be in a group to gain 15 access. Users that are not in the group (except for root) should not have 16 access to that socket. Also, when there is no socket configured, users 17 shouldn't be able to access the control socket at all. Not even root. 18*/ 19import ./make-test-python.nix ( 20 { pkgs, lib, ... }: 21 let 22 # common client configuration that we can just use for the multitude of 23 # clients we are constructing 24 common = 25 { lib, pkgs, ... }: 26 { 27 config = { 28 environment.systemPackages = [ pkgs.knot-dns ]; 29 30 # disable the root anchor update as we do not have internet access during 31 # the test execution 32 services.unbound.enableRootTrustAnchor = false; 33 34 # we want to test the full-variant of the package to also get DoH support 35 services.unbound.package = pkgs.unbound-full; 36 }; 37 }; 38 39 cert = pkgs.runCommand "selfSignedCerts" { buildInputs = [ pkgs.openssl ]; } '' 40 openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -nodes -subj '/CN=dns.example.local' 41 mkdir -p $out 42 cp key.pem cert.pem $out 43 ''; 44 in 45 { 46 name = "unbound"; 47 meta = with pkgs.lib.maintainers; { 48 maintainers = [ andir ]; 49 }; 50 51 nodes = { 52 53 # The server that actually serves our zones, this tests unbounds authoriative mode 54 authoritative = 55 { 56 lib, 57 pkgs, 58 config, 59 ... 60 }: 61 { 62 imports = [ common ]; 63 networking.interfaces.eth1.ipv4.addresses = lib.mkForce [ 64 { 65 address = "192.168.0.1"; 66 prefixLength = 24; 67 } 68 ]; 69 networking.interfaces.eth1.ipv6.addresses = lib.mkForce [ 70 { 71 address = "fd21::1"; 72 prefixLength = 64; 73 } 74 ]; 75 networking.firewall.allowedTCPPorts = [ 53 ]; 76 networking.firewall.allowedUDPPorts = [ 53 ]; 77 78 services.unbound = { 79 enable = true; 80 settings = { 81 server = { 82 interface = [ 83 "192.168.0.1" 84 "fd21::1" 85 "::1" 86 "127.0.0.1" 87 ]; 88 access-control = [ 89 "192.168.0.0/24 allow" 90 "fd21::/64 allow" 91 "::1 allow" 92 "127.0.0.0/8 allow" 93 ]; 94 local-data = [ 95 ''"example.local. IN A 1.2.3.4"'' 96 ''"example.local. IN AAAA abcd::eeff"'' 97 ]; 98 }; 99 }; 100 }; 101 }; 102 103 # The resolver that knows that forwards (only) to the authoritative server 104 # and listens on UDP/53, TCP/53 & TCP/853. 105 resolver = 106 { lib, nodes, ... }: 107 { 108 imports = [ common ]; 109 networking.interfaces.eth1.ipv4.addresses = lib.mkForce [ 110 { 111 address = "192.168.0.2"; 112 prefixLength = 24; 113 } 114 ]; 115 networking.interfaces.eth1.ipv6.addresses = lib.mkForce [ 116 { 117 address = "fd21::2"; 118 prefixLength = 64; 119 } 120 ]; 121 networking.firewall.allowedTCPPorts = [ 122 53 # regular DNS 123 853 # DNS over TLS 124 443 # DNS over HTTPS 125 ]; 126 networking.firewall.allowedUDPPorts = [ 53 ]; 127 128 services.unbound = { 129 enable = true; 130 settings = { 131 server = { 132 interface = [ 133 "::1" 134 "127.0.0.1" 135 "192.168.0.2" 136 "fd21::2" 137 "192.168.0.2@853" 138 "fd21::2@853" 139 "::1@853" 140 "127.0.0.1@853" 141 "192.168.0.2@443" 142 "fd21::2@443" 143 "::1@443" 144 "127.0.0.1@443" 145 ]; 146 access-control = [ 147 "192.168.0.0/24 allow" 148 "fd21::/64 allow" 149 "::1 allow" 150 "127.0.0.0/8 allow" 151 ]; 152 tls-service-pem = "${cert}/cert.pem"; 153 tls-service-key = "${cert}/key.pem"; 154 }; 155 forward-zone = [ 156 { 157 name = "."; 158 forward-addr = [ 159 (lib.head nodes.authoritative.networking.interfaces.eth1.ipv6.addresses).address 160 (lib.head nodes.authoritative.networking.interfaces.eth1.ipv4.addresses).address 161 ]; 162 } 163 ]; 164 }; 165 }; 166 }; 167 168 # machine that runs a local unbound that will be reconfigured during test execution 169 local_resolver = 170 { 171 lib, 172 nodes, 173 config, 174 ... 175 }: 176 { 177 imports = [ common ]; 178 networking.interfaces.eth1.ipv4.addresses = lib.mkForce [ 179 { 180 address = "192.168.0.3"; 181 prefixLength = 24; 182 } 183 ]; 184 networking.interfaces.eth1.ipv6.addresses = lib.mkForce [ 185 { 186 address = "fd21::3"; 187 prefixLength = 64; 188 } 189 ]; 190 networking.firewall.allowedTCPPorts = [ 191 53 # regular DNS 192 ]; 193 networking.firewall.allowedUDPPorts = [ 53 ]; 194 195 services.unbound = { 196 enable = true; 197 settings = { 198 server = { 199 interface = [ 200 "::1" 201 "127.0.0.1" 202 ]; 203 access-control = [ 204 "::1 allow" 205 "127.0.0.0/8 allow" 206 ]; 207 }; 208 include = "/etc/unbound/extra*.conf"; 209 }; 210 localControlSocketPath = "/run/unbound/unbound.ctl"; 211 }; 212 213 users.users = { 214 # user that is permitted to access the unix socket 215 someuser = { 216 isSystemUser = true; 217 group = "someuser"; 218 extraGroups = [ 219 config.users.users.unbound.group 220 ]; 221 }; 222 223 # user that is not permitted to access the unix socket 224 unauthorizeduser = { 225 isSystemUser = true; 226 group = "unauthorizeduser"; 227 }; 228 229 }; 230 users.groups = { 231 someuser = { }; 232 unauthorizeduser = { }; 233 }; 234 235 # Used for testing configuration reloading 236 environment.etc = { 237 "unbound-extra1.conf".text = '' 238 forward-zone: 239 name: "example.local." 240 forward-addr: ${(lib.head nodes.resolver.networking.interfaces.eth1.ipv6.addresses).address} 241 forward-addr: ${(lib.head nodes.resolver.networking.interfaces.eth1.ipv4.addresses).address} 242 ''; 243 "unbound-extra2.conf".text = '' 244 auth-zone: 245 name: something.local. 246 zonefile: ${pkgs.writeText "zone" '' 247 something.local. IN A 3.4.5.6 248 ''} 249 ''; 250 }; 251 }; 252 253 # plain node that only has network access and doesn't run any part of the 254 # resolver software locally 255 client = 256 { lib, nodes, ... }: 257 { 258 imports = [ common ]; 259 networking.nameservers = [ 260 (lib.head nodes.resolver.networking.interfaces.eth1.ipv6.addresses).address 261 (lib.head nodes.resolver.networking.interfaces.eth1.ipv4.addresses).address 262 ]; 263 networking.interfaces.eth1.ipv4.addresses = [ 264 { 265 address = "192.168.0.10"; 266 prefixLength = 24; 267 } 268 ]; 269 networking.interfaces.eth1.ipv6.addresses = [ 270 { 271 address = "fd21::10"; 272 prefixLength = 64; 273 } 274 ]; 275 }; 276 }; 277 278 testScript = 279 { nodes, ... }: 280 '' 281 import typing 282 283 zone = "example.local." 284 records = [("AAAA", "abcd::eeff"), ("A", "1.2.3.4")] 285 286 287 def query( 288 machine, 289 host: str, 290 query_type: str, 291 query: str, 292 expected: typing.Optional[str] = None, 293 args: typing.Optional[typing.List[str]] = None, 294 ): 295 """ 296 Execute a single query and compare the result with expectation 297 """ 298 text_args = "" 299 if args: 300 text_args = " ".join(args) 301 302 out = machine.succeed( 303 f"kdig {text_args} {query} {query_type} @{host} +short" 304 ).strip() 305 machine.log(f"{host} replied with {out}") 306 if expected: 307 assert expected == out, f"Expected `{expected}` but got `{out}`" 308 309 310 def test(machine, remotes, /, doh=False, zone=zone, records=records, args=[]): 311 """ 312 Run queries for the given remotes on the given machine. 313 """ 314 for query_type, expected in records: 315 for remote in remotes: 316 query(machine, remote, query_type, zone, expected, args) 317 query(machine, remote, query_type, zone, expected, ["+tcp"] + args) 318 if doh: 319 query( 320 machine, 321 remote, 322 query_type, 323 zone, 324 expected, 325 ["+tcp", "+tls"] + args, 326 ) 327 query( 328 machine, 329 remote, 330 query_type, 331 zone, 332 expected, 333 ["+https"] + args, 334 ) 335 336 337 client.start() 338 authoritative.wait_for_unit("unbound.service") 339 340 # verify that we can resolve locally 341 with subtest("test the authoritative servers local responses"): 342 test(authoritative, ["::1", "127.0.0.1"]) 343 344 resolver.wait_for_unit("unbound.service") 345 346 with subtest("root is unable to use unbounc-control when the socket is not configured"): 347 resolver.succeed("which unbound-control") # the binary must exist 348 resolver.fail("unbound-control list_forwards") # the invocation must fail 349 350 # verify that the resolver is able to resolve on all the local protocols 351 with subtest("test that the resolver resolves on all protocols and transports"): 352 test(resolver, ["::1", "127.0.0.1"], doh=True) 353 354 resolver.wait_for_unit("multi-user.target") 355 356 with subtest("client should be able to query the resolver"): 357 test(client, ["${(lib.head nodes.resolver.networking.interfaces.eth1.ipv6.addresses).address}", "${(lib.head nodes.resolver.networking.interfaces.eth1.ipv4.addresses).address}"], doh=True) 358 359 # discard the client we do not need anymore 360 client.shutdown() 361 362 local_resolver.wait_for_unit("multi-user.target") 363 364 # link a new config file to /etc/unbound/extra.conf 365 local_resolver.succeed("ln -s /etc/unbound-extra1.conf /etc/unbound/extra1.conf") 366 367 # reload the server & ensure the forwarding works 368 with subtest("test that the local resolver resolves on all protocols and transports"): 369 local_resolver.succeed("systemctl reload unbound") 370 print(local_resolver.succeed("journalctl -u unbound -n 1000")) 371 test(local_resolver, ["::1", "127.0.0.1"], args=["+timeout=60"]) 372 373 with subtest("test that we can use the unbound control socket"): 374 out = local_resolver.succeed( 375 "sudo -u someuser -- unbound-control list_forwards" 376 ).strip() 377 378 # Thank you black! Can't really break this line into a readable version. 379 expected = "example.local. IN forward ${(lib.head nodes.resolver.networking.interfaces.eth1.ipv6.addresses).address} ${(lib.head nodes.resolver.networking.interfaces.eth1.ipv4.addresses).address}" 380 assert out == expected, f"Expected `{expected}` but got `{out}` instead." 381 local_resolver.fail("sudo -u unauthorizeduser -- unbound-control list_forwards") 382 383 384 # link a new config file to /etc/unbound/extra.conf 385 local_resolver.succeed("ln -sf /etc/unbound-extra2.conf /etc/unbound/extra2.conf") 386 387 # reload the server & ensure the new local zone works 388 with subtest("test that we can query the new local zone"): 389 local_resolver.succeed("unbound-control reload") 390 r = [("A", "3.4.5.6")] 391 test(local_resolver, ["::1", "127.0.0.1"], zone="something.local.", records=r) 392 ''; 393 } 394)