From a040a8a2e3e598d24be81d36b66fd8c195c019da Mon Sep 17 00:00:00 2001 From: Andreas Rammhold Date: Wed, 21 Oct 2020 01:34:24 +0200 Subject: nixos/tests/unbound: init --- nixos/tests/all-tests.nix | 1 + nixos/tests/unbound.nix | 247 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 248 insertions(+) create mode 100644 nixos/tests/unbound.nix (limited to 'nixos/tests') diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 4e4d8b5e68943..f1d14c9f2d5d6 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -362,6 +362,7 @@ in trezord = handleTest ./trezord.nix {}; trickster = handleTest ./trickster.nix {}; tuptime = handleTest ./tuptime.nix {}; + unbound = handleTest ./unbound.nix {}; udisks2 = handleTest ./udisks2.nix {}; unit-php = handleTest ./web-servers/unit-php.nix {}; upnp = handleTest ./upnp.nix {}; diff --git a/nixos/tests/unbound.nix b/nixos/tests/unbound.nix new file mode 100644 index 0000000000000..bbccfa9c043ab --- /dev/null +++ b/nixos/tests/unbound.nix @@ -0,0 +1,247 @@ +/* + Test that our unbound module indeed works as most users would expect. + There are a few settings that we must consider when modifying the test. The + ususal use-cases for unbound are + * running a recursive DNS resolver on the local machine + * running a recursive DNS resolver on the local machine, forwarding to a local DNS server via UDP/53 & TCP/53 + * running a recursive DNS resolver on the local machine, forwarding to a local DNS server via TCP/853 (DoT) + * running a recursive DNS resolver on a machine in the network awaiting input from clients over TCP/53 & UDP/53 + * running a recursive DNS resolver on a machine in the network awaiting input from clients over TCP/853 (DoT) + + In the below test setup we are trying to implement all of those use cases + without creating a bazillion machines. +*/ +import ./make-test-python.nix ({ pkgs, lib, ... }: + let + # common client configuration that we can just use for the multitude of + # clients we are constructing + common = { lib, pkgs, ... }: { + config = { + environment.systemPackages = [ pkgs.knot-dns ]; + + # disable the root anchor update as we do not have internet access during + # the test execution + services.unbound.enableRootTrustAnchor = false; + }; + }; + + cert = pkgs.runCommandNoCC "selfSignedCerts" { buildInputs = [ pkgs.openssl ]; } '' + openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -nodes -subj '/CN=dns.example.local' + mkdir -p $out + cp key.pem cert.pem $out + ''; + in + { + name = "unbound"; + meta = with pkgs.stdenv.lib.maintainers; { + maintainers = [ andir ]; + }; + + nodes = { + + # The server that actually serves our zones, this tests unbounds authoriative mode + authoritative = { lib, pkgs, config, ... }: { + imports = [ common ]; + networking.interfaces.eth1.ipv4.addresses = lib.mkForce [ + { address = "192.168.0.1"; prefixLength = 24; } + ]; + networking.interfaces.eth1.ipv6.addresses = lib.mkForce [ + { address = "fd21::1"; prefixLength = 64; } + ]; + networking.firewall.allowedTCPPorts = [ 53 ]; + networking.firewall.allowedUDPPorts = [ 53 ]; + + services.unbound = { + enable = true; + interfaces = [ "192.168.0.1" "fd21::1" "::1" "127.0.0.1" ]; + allowedAccess = [ "192.168.0.0/24" "fd21::/64" "::1" "127.0.0.0/8" ]; + extraConfig = '' + server: + local-data: "example.local. IN A 1.2.3.4" + local-data: "example.local. IN AAAA abcd::eeff" + ''; + }; + }; + + # The resolver that knows that fowards (only) to the authoritative server + # and listens on UDP/53, TCP/53 & TCP/853. + resolver = { lib, nodes, ... }: { + imports = [ common ]; + networking.interfaces.eth1.ipv4.addresses = lib.mkForce [ + { address = "192.168.0.2"; prefixLength = 24; } + ]; + networking.interfaces.eth1.ipv6.addresses = lib.mkForce [ + { address = "fd21::2"; prefixLength = 64; } + ]; + networking.firewall.allowedTCPPorts = [ + 53 # regular DNS + 853 # DNS over TLS + ]; + networking.firewall.allowedUDPPorts = [ 53 ]; + + services.unbound = { + enable = true; + allowedAccess = [ "192.168.0.0/24" "fd21::/64" "::1" "127.0.0.0/8" ]; + interfaces = [ "::1" "127.0.0.1" "192.168.0.2" "fd21::2" "192.168.0.2@853" "fd21::2@853" "::1@853" "127.0.0.1@853" ]; + forwardAddresses = [ + (lib.head nodes.authoritative.config.networking.interfaces.eth1.ipv6.addresses).address + (lib.head nodes.authoritative.config.networking.interfaces.eth1.ipv4.addresses).address + ]; + extraConfig = '' + server: + tls-service-pem: ${cert}/cert.pem + tls-service-key: ${cert}/key.pem + ''; + }; + }; + + # machine that runs a local unbound that will be reconfigured during test execution + local_resolver = { lib, nodes, ... }: { + imports = [ common ]; + networking.interfaces.eth1.ipv4.addresses = lib.mkForce [ + { address = "192.168.0.3"; prefixLength = 24; } + ]; + networking.interfaces.eth1.ipv6.addresses = lib.mkForce [ + { address = "fd21::3"; prefixLength = 64; } + ]; + networking.firewall.allowedTCPPorts = [ + 53 # regular DNS + ]; + networking.firewall.allowedUDPPorts = [ 53 ]; + + services.unbound = { + enable = true; + allowedAccess = [ "::1" "127.0.0.0/8" ]; + interfaces = [ "::1" "127.0.0.1" ]; + extraConfig = '' + include: "/etc/unbound/extra*.conf" + ''; + }; + + environment.etc = { + "unbound-extra1.conf".text = '' + forward-zone: + name: "example.local." + forward-addr: ${(lib.head nodes.resolver.config.networking.interfaces.eth1.ipv6.addresses).address} + forward-addr: ${(lib.head nodes.resolver.config.networking.interfaces.eth1.ipv4.addresses).address} + ''; + "unbound-extra2.conf".text = '' + auth-zone: + name: something.local. + zonefile: ${pkgs.writeText "zone" '' + something.local. IN A 3.4.5.6 + ''} + ''; + }; + }; + + + # plain node that only has network access and doesn't run any part of the + # resolver software locally + client = { lib, nodes, ... }: { + imports = [ common ]; + networking.nameservers = [ + (lib.head nodes.resolver.config.networking.interfaces.eth1.ipv6.addresses).address + (lib.head nodes.resolver.config.networking.interfaces.eth1.ipv4.addresses).address + ]; + networking.interfaces.eth1.ipv4.addresses = [ + { address = "192.168.0.10"; prefixLength = 24; } + ]; + networking.interfaces.eth1.ipv6.addresses = [ + { address = "fd21::10"; prefixLength = 64; } + ]; + }; + }; + + testScript = { nodes, ... }: '' + import typing + import json + + zone = "example.local." + records = [("AAAA", "abcd::eeff"), ("A", "1.2.3.4")] + + + def query( + machine, + host: str, + query_type: str, + query: str, + expected: typing.Optional[str] = None, + args: typing.Optional[typing.List[str]] = None, + ): + """ + Execute a single query and compare the result with expectation + """ + text_args = "" + if args: + text_args = " ".join(args) + + out = machine.succeed( + f"kdig {text_args} {query} {query_type} @{host} +short" + ).strip() + machine.log(f"{host} replied with {out}") + if expected: + assert expected == out, f"Expected `{expected}` but got `{out}`" + + + def test(machine, remotes, /, doh=False, zone=zone, records=records, args=[]): + """ + Run queries for the given remotes on the given machine. + """ + for query_type, expected in records: + for remote in remotes: + query(machine, remote, query_type, zone, expected, args) + query(machine, remote, query_type, zone, expected, ["+tcp"] + args) + if doh: + query( + machine, + remote, + query_type, + zone, + expected, + ["+tcp", "+tls"] + args, + ) + + + client.start() + authoritative.wait_for_unit("unbound.service") + + # verify that we can resolve locally + with subtest("test the authoritative servers local responses"): + test(authoritative, ["::1", "127.0.0.1"]) + + resolver.wait_for_unit("unbound.service") + + # verify that the resolver is able to resolve on all the local protocols + with subtest("test that the resolver resolves on all protocols and transports"): + test(resolver, ["::1", "127.0.0.1"], doh=True) + + resolver.wait_for_unit("multi-user.target") + + with subtest("client should be able to query the resolver"): + 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) + + # discard the client we do not need anymore + client.shutdown() + + local_resolver.wait_for_unit("multi-user.target") + + # link a new config file to /etc/unbound/extra.conf + local_resolver.succeed("ln -s /etc/unbound-extra1.conf /etc/unbound/extra1.conf") + + # reload the server & ensure the forwarding works + with subtest("test that the local resolver resolves on all protocols and transports"): + local_resolver.succeed("systemctl reload unbound") + print(local_resolver.succeed("journalctl -u unbound -n 1000")) + test(local_resolver, ["::1", "127.0.0.1"], args=["+timeout=60"]) + + # link a new config file to /etc/unbound/extra.conf + local_resolver.succeed("ln -sf /etc/unbound-extra2.conf /etc/unbound/extra2.conf") + + # reload the server & ensure the new local zone works + with subtest("test that we can query the new local zone"): + local_resolver.succeed("systemctl reload unbound") + r = [("A", "3.4.5.6")] + test(local_resolver, ["::1", "127.0.0.1"], zone="something.local.", records=r) + ''; + }) -- cgit 1.4.1