nixos/systemd-resolved: add support to generate dns-delegate files

See https://www.freedesktop.org/software/systemd/man/258/systemd.dns-delegate.html
This commit is contained in:
r-vdp
2026-02-12 17:38:12 +02:00
parent 6dee6078e8
commit cfc8eb2d3c
2 changed files with 102 additions and 15 deletions

View File

@@ -1,7 +1,6 @@
{
config,
lib,
pkgs,
utils,
...
}:
@@ -14,12 +13,15 @@ let
elem
isList
literalExpression
mapAttrs'
mapAttrsToList
mkIf
mkMerge
mkOption
mkOrder
mkRenamedOptionModule
mkRemovedOptionModule
nameValuePair
optionalAttrs
types
;
@@ -132,6 +134,27 @@ in
};
};
dnsDelegates = mkOption {
description = ''
dns-delegate files to be created.
See {manpage}`systemd.dns-delegate(5)` for more info.
'';
default = { };
type = types.attrsOf (
types.submodule {
options.Delegate = mkOption {
description = ''
Settings option for systemd dns-delegate files.
See {manpage}`systemd.dns-delegate(5)` for all available options.
'';
type = types.submodule {
freeformType = types.attrsOf unitOption;
};
};
}
);
};
};
boot.initrd.services.resolved.enable = mkOption {
@@ -167,7 +190,12 @@ in
systemd.services.systemd-resolved = {
wantedBy = [ "sysinit.target" ];
aliases = [ "dbus-org.freedesktop.resolve1.service" ];
reloadTriggers = [ config.environment.etc."systemd/resolved.conf".source ];
reloadTriggers = [
config.environment.etc."systemd/resolved.conf".source
]
++ mapAttrsToList (
name: _: config.environment.etc."systemd/dns-delegate.d/${name}.dns-delegate".source
) cfg.dnsDelegates;
stopIfChanged = false;
};
@@ -180,7 +208,13 @@ in
}
// optionalAttrs dnsmasqResolve {
"dnsmasq-resolv.conf".source = "/run/systemd/resolve/resolv.conf";
};
}
// mapAttrs' (
name: value:
nameValuePair "systemd/dns-delegate.d/${name}.dns-delegate" {
text = settingsToSections (transformSettings value);
}
) cfg.dnsDelegates;
# If networkmanager is enabled, ask it to interface with resolved.
networking.networkmanager.dns = "systemd-resolved";

View File

@@ -36,13 +36,48 @@
};
};
nodes.delegate_server =
{ lib, config, ... }:
let
delegateZone = pkgs.writeTextDir "delegated.example.org.zone" ''
@ SOA ns.delegated.example.org. noc.delegated.example.org. 2019031301 86400 7200 3600000 172800
test A ${(lib.head config.networking.interfaces.eth1.ipv4.addresses).address}
test AAAA ${(lib.head config.networking.interfaces.eth1.ipv6.addresses).address}
'';
in
{
networking.firewall.enable = false;
networking.useDHCP = false;
networking.interfaces.eth1.ipv6.addresses = lib.mkForce [
{
address = "fd00::3";
prefixLength = 64;
}
];
services.knot = {
enable = true;
settings = {
server.listen = [
"0.0.0.0@53"
"::@53"
];
template.default.storage = delegateZone;
zone."delegated.example.org".file = "delegated.example.org.zone";
};
};
};
nodes.client =
{ nodes, ... }:
let
inherit (lib.head nodes.server.networking.interfaces.eth1.ipv4.addresses) address;
serverAddress = (lib.head nodes.server.networking.interfaces.eth1.ipv4.addresses).address;
delegateAddress =
(lib.head nodes.delegate_server.networking.interfaces.eth1.ipv4.addresses).address;
in
{
networking.nameservers = [ address ];
networking.nameservers = [ serverAddress ];
networking.interfaces.eth1.ipv6.addresses = lib.mkForce [
{
address = "fd00::2";
@@ -51,6 +86,12 @@
];
services.resolved.enable = true;
services.resolved.settings.Resolve.FallbackDNS = [ ];
services.resolved.dnsDelegates.example-org = {
Delegate = {
DNS = delegateAddress;
Domains = [ "delegated.example.org" ];
};
};
networking.useNetworkd = true;
networking.useDHCP = false;
systemd.network.networks."40-eth0".enable = false;
@@ -69,23 +110,35 @@
let
address4 = (lib.head nodes.server.networking.interfaces.eth1.ipv4.addresses).address;
address6 = (lib.head nodes.server.networking.interfaces.eth1.ipv6.addresses).address;
delegateAddress4 =
(lib.head nodes.delegate_server.networking.interfaces.eth1.ipv4.addresses).address;
delegateAddress6 =
(lib.head nodes.delegate_server.networking.interfaces.eth1.ipv6.addresses).address;
in
#python
''
start_all()
server.wait_for_unit("multi-user.target")
delegate_server.wait_for_unit("multi-user.target")
def test_client():
query = client.succeed("resolvectl query example.com")
assert "${address4}" in query
assert "${address6}" in query
client.succeed("ping -4 -c 1 example.com")
client.succeed("ping -6 -c 1 example.com")
def test_resolve(domain, expected_addrs):
query = client.succeed(f"resolvectl query {domain}")
for addr in expected_addrs:
assert addr in query, f"Expected {addr} in: {query}"
client.succeed(f"ping -4 -c 1 {domain}")
client.succeed(f"ping -6 -c 1 {domain}")
with subtest("resolve in initrd"):
client.wait_for_unit("initrd.target")
test_resolve("example.com", ["${address4}", "${address6}"])
client.wait_for_unit("initrd.target")
test_client()
client.switch_root()
client.wait_for_unit("multi-user.target")
test_client()
with subtest("resolve after switch-root"):
client.wait_for_unit("multi-user.target")
test_resolve("example.com", ["${address4}", "${address6}"])
with subtest("dns-delegate resolves delegated subdomain"):
test_resolve("test.delegated.example.org", ["${delegateAddress4}", "${delegateAddress6}"])
'';
}