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)
11 In the below test setup we are trying to implement all of those use cases.
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.
19 import ./make-test-python.nix ({ pkgs, lib, ... }:
21 # common client configuration that we can just use for the multitude of
22 # clients we are constructing
23 common = { lib, pkgs, ... }: {
25 environment.systemPackages = [ pkgs.knot-dns ];
27 # disable the root anchor update as we do not have internet access during
29 services.unbound.enableRootTrustAnchor = false;
31 # we want to test the full-variant of the package to also get DoH support
32 services.unbound.package = pkgs.unbound-full;
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'
39 cp key.pem cert.pem $out
44 meta = with pkgs.lib.maintainers; {
45 maintainers = [ andir ];
50 # The server that actually serves our zones, this tests unbounds authoriative mode
51 authoritative = { lib, pkgs, config, ... }: {
53 networking.interfaces.eth1.ipv4.addresses = lib.mkForce [
54 { address = "192.168.0.1"; prefixLength = 24; }
56 networking.interfaces.eth1.ipv6.addresses = lib.mkForce [
57 { address = "fd21::1"; prefixLength = 64; }
59 networking.firewall.allowedTCPPorts = [ 53 ];
60 networking.firewall.allowedUDPPorts = [ 53 ];
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" ];
69 ''"example.local. IN A 1.2.3.4"''
70 ''"example.local. IN AAAA abcd::eeff"''
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, ... }: {
81 networking.interfaces.eth1.ipv4.addresses = lib.mkForce [
82 { address = "192.168.0.2"; prefixLength = 24; }
84 networking.interfaces.eth1.ipv6.addresses = lib.mkForce [
85 { address = "fd21::2"; prefixLength = 64; }
87 networking.firewall.allowedTCPPorts = [
92 networking.firewall.allowedUDPPorts = [ 53 ];
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";
109 (lib.head nodes.authoritative.config.networking.interfaces.eth1.ipv6.addresses).address
110 (lib.head nodes.authoritative.config.networking.interfaces.eth1.ipv4.addresses).address
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; }
124 networking.interfaces.eth1.ipv6.addresses = lib.mkForce [
125 { address = "fd21::3"; prefixLength = 64; }
127 networking.firewall.allowedTCPPorts = [
130 networking.firewall.allowedUDPPorts = [ 53 ];
136 interface = [ "::1" "127.0.0.1" ];
137 access-control = [ "::1 allow" "127.0.0.0/8 allow" ];
139 include = "/etc/unbound/extra*.conf";
141 localControlSocketPath = "/run/unbound/unbound.ctl";
145 # user that is permitted to access the unix socket
150 config.users.users.unbound.group
154 # user that is not permitted to access the unix socket
157 group = "unauthorizeduser";
163 unauthorizeduser = {};
166 # Used for testing configuration reloading
168 "unbound-extra1.conf".text = ''
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}
174 "unbound-extra2.conf".text = ''
176 name: something.local.
177 zonefile: ${pkgs.writeText "zone" ''
178 something.local. IN A 3.4.5.6
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
193 networking.interfaces.eth1.ipv4.addresses = [
194 { address = "192.168.0.10"; prefixLength = 24; }
196 networking.interfaces.eth1.ipv6.addresses = [
197 { address = "fd21::10"; prefixLength = 64; }
202 testScript = { nodes, ... }: ''
205 zone = "example.local."
206 records = [("AAAA", "abcd::eeff"), ("A", "1.2.3.4")]
214 expected: typing.Optional[str] = None,
215 args: typing.Optional[typing.List[str]] = None,
218 Execute a single query and compare the result with expectation
222 text_args = " ".join(args)
224 out = machine.succeed(
225 f"kdig {text_args} {query} {query_type} @{host} +short"
227 machine.log(f"{host} replied with {out}")
229 assert expected == out, f"Expected `{expected}` but got `{out}`"
232 def test(machine, remotes, /, doh=False, zone=zone, records=records, args=[]):
234 Run queries for the given remotes on the given machine.
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)
247 ["+tcp", "+tls"] + args,
260 authoritative.wait_for_unit("unbound.service")
262 # verify that we can resolve locally
263 with subtest("test the authoritative servers local responses"):
264 test(authoritative, ["::1", "127.0.0.1"])
266 resolver.wait_for_unit("unbound.service")
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
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)
276 resolver.wait_for_unit("multi-user.target")
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)
281 # discard the client we do not need anymore
284 local_resolver.wait_for_unit("multi-user.target")
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")
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"])
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"
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")
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")
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)