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