at 23.05-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.runCommand "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 group = "someuser"; 149 extraGroups = [ 150 config.users.users.unbound.group 151 ]; 152 }; 153 154 # user that is not permitted to access the unix socket 155 unauthorizeduser = { 156 isSystemUser = true; 157 group = "unauthorizeduser"; 158 }; 159 160 }; 161 users.groups = { 162 someuser = {}; 163 unauthorizeduser = {}; 164 }; 165 166 # Used for testing configuration reloading 167 environment.etc = { 168 "unbound-extra1.conf".text = '' 169 forward-zone: 170 name: "example.local." 171 forward-addr: ${(lib.head nodes.resolver.config.networking.interfaces.eth1.ipv6.addresses).address} 172 forward-addr: ${(lib.head nodes.resolver.config.networking.interfaces.eth1.ipv4.addresses).address} 173 ''; 174 "unbound-extra2.conf".text = '' 175 auth-zone: 176 name: something.local. 177 zonefile: ${pkgs.writeText "zone" '' 178 something.local. IN A 3.4.5.6 179 ''} 180 ''; 181 }; 182 }; 183 184 185 # plain node that only has network access and doesn't run any part of the 186 # resolver software locally 187 client = { lib, nodes, ... }: { 188 imports = [ common ]; 189 networking.nameservers = [ 190 (lib.head nodes.resolver.config.networking.interfaces.eth1.ipv6.addresses).address 191 (lib.head nodes.resolver.config.networking.interfaces.eth1.ipv4.addresses).address 192 ]; 193 networking.interfaces.eth1.ipv4.addresses = [ 194 { address = "192.168.0.10"; prefixLength = 24; } 195 ]; 196 networking.interfaces.eth1.ipv6.addresses = [ 197 { address = "fd21::10"; prefixLength = 64; } 198 ]; 199 }; 200 }; 201 202 testScript = { nodes, ... }: '' 203 import typing 204 205 zone = "example.local." 206 records = [("AAAA", "abcd::eeff"), ("A", "1.2.3.4")] 207 208 209 def query( 210 machine, 211 host: str, 212 query_type: str, 213 query: str, 214 expected: typing.Optional[str] = None, 215 args: typing.Optional[typing.List[str]] = None, 216 ): 217 """ 218 Execute a single query and compare the result with expectation 219 """ 220 text_args = "" 221 if args: 222 text_args = " ".join(args) 223 224 out = machine.succeed( 225 f"kdig {text_args} {query} {query_type} @{host} +short" 226 ).strip() 227 machine.log(f"{host} replied with {out}") 228 if expected: 229 assert expected == out, f"Expected `{expected}` but got `{out}`" 230 231 232 def test(machine, remotes, /, doh=False, zone=zone, records=records, args=[]): 233 """ 234 Run queries for the given remotes on the given machine. 235 """ 236 for query_type, expected in records: 237 for remote in remotes: 238 query(machine, remote, query_type, zone, expected, args) 239 query(machine, remote, query_type, zone, expected, ["+tcp"] + args) 240 if doh: 241 query( 242 machine, 243 remote, 244 query_type, 245 zone, 246 expected, 247 ["+tcp", "+tls"] + args, 248 ) 249 query( 250 machine, 251 remote, 252 query_type, 253 zone, 254 expected, 255 ["+https"] + args, 256 ) 257 258 259 client.start() 260 authoritative.wait_for_unit("unbound.service") 261 262 # verify that we can resolve locally 263 with subtest("test the authoritative servers local responses"): 264 test(authoritative, ["::1", "127.0.0.1"]) 265 266 resolver.wait_for_unit("unbound.service") 267 268 with subtest("root is unable to use unbounc-control when the socket is not configured"): 269 resolver.succeed("which unbound-control") # the binary must exist 270 resolver.fail("unbound-control list_forwards") # the invocation must fail 271 272 # verify that the resolver is able to resolve on all the local protocols 273 with subtest("test that the resolver resolves on all protocols and transports"): 274 test(resolver, ["::1", "127.0.0.1"], doh=True) 275 276 resolver.wait_for_unit("multi-user.target") 277 278 with subtest("client should be able to query the resolver"): 279 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) 280 281 # discard the client we do not need anymore 282 client.shutdown() 283 284 local_resolver.wait_for_unit("multi-user.target") 285 286 # link a new config file to /etc/unbound/extra.conf 287 local_resolver.succeed("ln -s /etc/unbound-extra1.conf /etc/unbound/extra1.conf") 288 289 # reload the server & ensure the forwarding works 290 with subtest("test that the local resolver resolves on all protocols and transports"): 291 local_resolver.succeed("systemctl reload unbound") 292 print(local_resolver.succeed("journalctl -u unbound -n 1000")) 293 test(local_resolver, ["::1", "127.0.0.1"], args=["+timeout=60"]) 294 295 with subtest("test that we can use the unbound control socket"): 296 out = local_resolver.succeed( 297 "sudo -u someuser -- unbound-control list_forwards" 298 ).strip() 299 300 # Thank you black! Can't really break this line into a readable version. 301 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}" 302 assert out == expected, f"Expected `{expected}` but got `{out}` instead." 303 local_resolver.fail("sudo -u unauthorizeduser -- unbound-control list_forwards") 304 305 306 # link a new config file to /etc/unbound/extra.conf 307 local_resolver.succeed("ln -sf /etc/unbound-extra2.conf /etc/unbound/extra2.conf") 308 309 # reload the server & ensure the new local zone works 310 with subtest("test that we can query the new local zone"): 311 local_resolver.succeed("unbound-control reload") 312 r = [("A", "3.4.5.6")] 313 test(local_resolver, ["::1", "127.0.0.1"], zone="something.local.", records=r) 314 ''; 315 })