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