nixos/tests/unbound: init

This commit is contained in:
Andreas Rammhold 2020-10-21 01:34:24 +02:00
parent aadc07618a
commit a040a8a2e3
No known key found for this signature in database
GPG key ID: E432E410B5E48C86
2 changed files with 248 additions and 0 deletions

View file

@ -362,6 +362,7 @@ in
trezord = handleTest ./trezord.nix {}; trezord = handleTest ./trezord.nix {};
trickster = handleTest ./trickster.nix {}; trickster = handleTest ./trickster.nix {};
tuptime = handleTest ./tuptime.nix {}; tuptime = handleTest ./tuptime.nix {};
unbound = handleTest ./unbound.nix {};
udisks2 = handleTest ./udisks2.nix {}; udisks2 = handleTest ./udisks2.nix {};
unit-php = handleTest ./web-servers/unit-php.nix {}; unit-php = handleTest ./web-servers/unit-php.nix {};
upnp = handleTest ./upnp.nix {}; upnp = handleTest ./upnp.nix {};

247
nixos/tests/unbound.nix Normal file
View file

@ -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)
'';
})