Merge pull request #101218 from andir/unbound-systemd
This commit is contained in:
commit
5f5d38e88f
@ -172,6 +172,62 @@
|
|||||||
All services should use <xref linkend="opt-systemd.services._name_.startLimitIntervalSec" /> or <literal>StartLimitIntervalSec</literal> in <xref linkend="opt-systemd.services._name_.unitConfig" /> instead.
|
All services should use <xref linkend="opt-systemd.services._name_.startLimitIntervalSec" /> or <literal>StartLimitIntervalSec</literal> in <xref linkend="opt-systemd.services._name_.unitConfig" /> instead.
|
||||||
</para>
|
</para>
|
||||||
</listitem>
|
</listitem>
|
||||||
|
<listitem>
|
||||||
|
<para>
|
||||||
|
The Unbound DNS resolver service (<literal>services.unbound</literal>) has been refactored to allow reloading, control sockets and to fix startup ordering issues.
|
||||||
|
</para>
|
||||||
|
|
||||||
|
<para>
|
||||||
|
It is now possible to enable a local UNIX control socket for unbound by setting the <xref linkend="opt-services.unbound.localControlSocketPath" />
|
||||||
|
option.
|
||||||
|
</para>
|
||||||
|
|
||||||
|
<para>
|
||||||
|
Previously we just applied a very minimal set of restrictions and
|
||||||
|
trusted unbound to properly drop root privs and capabilities.
|
||||||
|
</para>
|
||||||
|
|
||||||
|
<para>
|
||||||
|
As of this we are (for the most part) just using the upstream
|
||||||
|
example unit file for unbound. The main difference is that we start
|
||||||
|
unbound as <literal>unbound</literal> user with the required capabilities instead of
|
||||||
|
letting unbound do the chroot & uid/gid changes.
|
||||||
|
</para>
|
||||||
|
|
||||||
|
<para>
|
||||||
|
The upstream unit configuration this is based on is a lot stricter with
|
||||||
|
all kinds of permissions then our previous variant. It also came with
|
||||||
|
the default of having the <literal>Type</literal> set to <literal>notify</literal>, therefore we are now also
|
||||||
|
using the <literal>unbound-with-systemd</literal> package here. Unbound will start up,
|
||||||
|
read the configuration files and start listening on the configured ports
|
||||||
|
before systemd will declare the unit <literal>active (running)</literal>.
|
||||||
|
This will likely help with startup order and the occasional race condition during system
|
||||||
|
activation where the DNS service is started but not yet ready to answer
|
||||||
|
queries. Services depending on <literal>nss-lookup.target</literal> or <literal>unbound.service</literal>
|
||||||
|
are now be able to use unbound when those targets have been reached.
|
||||||
|
</para>
|
||||||
|
|
||||||
|
<para>
|
||||||
|
Aditionally to the much stricter runtime environmet the
|
||||||
|
<literal>/dev/urandom</literal> mount lines we previously had in the code (that would
|
||||||
|
randomly failed during the stop-phase) have been removed as systemd will take care of those for us.
|
||||||
|
</para>
|
||||||
|
|
||||||
|
<para>
|
||||||
|
The <literal>preStart</literal> script is now only required if we enabled the trust
|
||||||
|
anchor updates (which are still enabled by default).
|
||||||
|
</para>
|
||||||
|
|
||||||
|
<para>
|
||||||
|
Another benefit of the refactoring is that we can now issue reloads via
|
||||||
|
either <literal>pkill -HUP unbound</literal> and <literal>systemctl reload unbound</literal> to reload the
|
||||||
|
running configuration without taking the daemon offline. A prerequisite
|
||||||
|
of this was that unbound configuration is available on a well known path
|
||||||
|
on the file system. We are using the path <literal>/etc/unbound/unbound.conf</literal> as that is the
|
||||||
|
default in the CLI tooling which in turn enables us to use
|
||||||
|
<literal>unbound-control</literal> without passing a custom configuration location.
|
||||||
|
</para>
|
||||||
|
</listitem>
|
||||||
</itemizedlist>
|
</itemizedlist>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
{ config, lib, pkgs, ... }:
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
with lib;
|
with lib;
|
||||||
|
|
||||||
let
|
let
|
||||||
|
|
||||||
cfg = config.services.unbound;
|
cfg = config.services.unbound;
|
||||||
|
|
||||||
stateDir = "/var/lib/unbound";
|
stateDir = "/var/lib/unbound";
|
||||||
@ -17,12 +15,12 @@ let
|
|||||||
forward =
|
forward =
|
||||||
optionalString (any isLocalAddress cfg.forwardAddresses) ''
|
optionalString (any isLocalAddress cfg.forwardAddresses) ''
|
||||||
do-not-query-localhost: no
|
do-not-query-localhost: no
|
||||||
'' +
|
''
|
||||||
optionalString (cfg.forwardAddresses != []) ''
|
+ optionalString (cfg.forwardAddresses != []) ''
|
||||||
forward-zone:
|
forward-zone:
|
||||||
name: .
|
name: .
|
||||||
'' +
|
''
|
||||||
concatMapStringsSep "\n" (x: " forward-addr: ${x}") cfg.forwardAddresses;
|
+ concatMapStringsSep "\n" (x: " forward-addr: ${x}") cfg.forwardAddresses;
|
||||||
|
|
||||||
rootTrustAnchorFile = "${stateDir}/root.key";
|
rootTrustAnchorFile = "${stateDir}/root.key";
|
||||||
|
|
||||||
@ -31,19 +29,25 @@ let
|
|||||||
|
|
||||||
confFile = pkgs.writeText "unbound.conf" ''
|
confFile = pkgs.writeText "unbound.conf" ''
|
||||||
server:
|
server:
|
||||||
|
ip-freebind: yes
|
||||||
directory: "${stateDir}"
|
directory: "${stateDir}"
|
||||||
username: unbound
|
username: unbound
|
||||||
chroot: "${stateDir}"
|
chroot: ""
|
||||||
pidfile: ""
|
pidfile: ""
|
||||||
|
# when running under systemd there is no need to daemonize
|
||||||
|
do-daemonize: no
|
||||||
${interfaces}
|
${interfaces}
|
||||||
${access}
|
${access}
|
||||||
${trustAnchor}
|
${trustAnchor}
|
||||||
|
${lib.optionalString (cfg.localControlSocketPath != null) ''
|
||||||
|
remote-control:
|
||||||
|
control-enable: yes
|
||||||
|
control-interface: ${cfg.localControlSocketPath}
|
||||||
|
''}
|
||||||
${cfg.extraConfig}
|
${cfg.extraConfig}
|
||||||
${forward}
|
${forward}
|
||||||
'';
|
'';
|
||||||
|
|
||||||
in
|
in
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
||||||
###### interface
|
###### interface
|
||||||
@ -55,8 +59,8 @@ in
|
|||||||
|
|
||||||
package = mkOption {
|
package = mkOption {
|
||||||
type = types.package;
|
type = types.package;
|
||||||
default = pkgs.unbound;
|
default = pkgs.unbound-with-systemd;
|
||||||
defaultText = "pkgs.unbound";
|
defaultText = "pkgs.unbound-with-systemd";
|
||||||
description = "The unbound package to use";
|
description = "The unbound package to use";
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -69,11 +73,14 @@ in
|
|||||||
interfaces = mkOption {
|
interfaces = mkOption {
|
||||||
default = [ "127.0.0.1" ] ++ optional config.networking.enableIPv6 "::1";
|
default = [ "127.0.0.1" ] ++ optional config.networking.enableIPv6 "::1";
|
||||||
type = types.listOf types.str;
|
type = types.listOf types.str;
|
||||||
description = "What addresses the server should listen on.";
|
description = ''
|
||||||
|
What addresses the server should listen on. This supports the interface syntax documented in
|
||||||
|
<citerefentry><refentrytitle>unbound.conf</refentrytitle><manvolnum>8</manvolnum></citerefentry>.
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
forwardAddresses = mkOption {
|
forwardAddresses = mkOption {
|
||||||
default = [ ];
|
default = [];
|
||||||
type = types.listOf types.str;
|
type = types.listOf types.str;
|
||||||
description = "What servers to forward queries to.";
|
description = "What servers to forward queries to.";
|
||||||
};
|
};
|
||||||
@ -84,6 +91,28 @@ in
|
|||||||
description = "Use and update root trust anchor for DNSSEC validation.";
|
description = "Use and update root trust anchor for DNSSEC validation.";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
localControlSocketPath = mkOption {
|
||||||
|
default = null;
|
||||||
|
# FIXME: What is the proper type here so users can specify strings,
|
||||||
|
# paths and null?
|
||||||
|
# My guess would be `types.nullOr (types.either types.str types.path)`
|
||||||
|
# but I haven't verified yet.
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
example = "/run/unbound/unbound.ctl";
|
||||||
|
description = ''
|
||||||
|
When not set to <literal>null</literal> this option defines the path
|
||||||
|
at which the unbound remote control socket should be created at. The
|
||||||
|
socket will be owned by the unbound user (<literal>unbound</literal>)
|
||||||
|
and group will be <literal>nogroup</literal>.
|
||||||
|
|
||||||
|
Users that should be permitted to access the socket must be in the
|
||||||
|
<literal>unbound</literal> group.
|
||||||
|
|
||||||
|
If this option is <literal>null</literal> remote control will not be
|
||||||
|
configured at all. Unbounds default values apply.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
extraConfig = mkOption {
|
extraConfig = mkOption {
|
||||||
default = "";
|
default = "";
|
||||||
type = types.lines;
|
type = types.lines;
|
||||||
@ -106,43 +135,85 @@ in
|
|||||||
users.users.unbound = {
|
users.users.unbound = {
|
||||||
description = "unbound daemon user";
|
description = "unbound daemon user";
|
||||||
isSystemUser = true;
|
isSystemUser = true;
|
||||||
|
group = lib.mkIf (cfg.localControlSocketPath != null) (lib.mkDefault "unbound");
|
||||||
|
};
|
||||||
|
|
||||||
|
# We need a group so that we can give users access to the configured
|
||||||
|
# control socket. Unbound allows access to the socket only to the unbound
|
||||||
|
# user and the primary group.
|
||||||
|
users.groups = lib.mkIf (cfg.localControlSocketPath != null) {
|
||||||
|
unbound = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.resolvconf.useLocalResolver = mkDefault true;
|
networking.resolvconf.useLocalResolver = mkDefault true;
|
||||||
|
|
||||||
|
|
||||||
|
environment.etc."unbound/unbound.conf".source = confFile;
|
||||||
|
|
||||||
systemd.services.unbound = {
|
systemd.services.unbound = {
|
||||||
description = "Unbound recursive Domain Name Server";
|
description = "Unbound recursive Domain Name Server";
|
||||||
after = [ "network.target" ];
|
after = [ "network.target" ];
|
||||||
before = [ "nss-lookup.target" ];
|
before = [ "nss-lookup.target" ];
|
||||||
wants = [ "nss-lookup.target" ];
|
wantedBy = [ "multi-user.target" "nss-lookup.target" ];
|
||||||
wantedBy = [ "multi-user.target" ];
|
|
||||||
|
|
||||||
preStart = ''
|
preStart = lib.mkIf cfg.enableRootTrustAnchor ''
|
||||||
mkdir -m 0755 -p ${stateDir}/dev/
|
${cfg.package}/bin/unbound-anchor -a ${rootTrustAnchorFile} || echo "Root anchor updated!"
|
||||||
cp ${confFile} ${stateDir}/unbound.conf
|
|
||||||
${optionalString cfg.enableRootTrustAnchor ''
|
|
||||||
${cfg.package}/bin/unbound-anchor -a ${rootTrustAnchorFile} || echo "Root anchor updated!"
|
|
||||||
chown unbound ${stateDir} ${rootTrustAnchorFile}
|
|
||||||
''}
|
|
||||||
touch ${stateDir}/dev/random
|
|
||||||
${pkgs.utillinux}/bin/mount --bind -n /dev/urandom ${stateDir}/dev/random
|
|
||||||
'';
|
'';
|
||||||
|
|
||||||
serviceConfig = {
|
restartTriggers = [
|
||||||
ExecStart = "${cfg.package}/bin/unbound -d -c ${stateDir}/unbound.conf";
|
confFile
|
||||||
ExecStopPost="${pkgs.utillinux}/bin/umount ${stateDir}/dev/random";
|
];
|
||||||
|
|
||||||
ProtectSystem = true;
|
serviceConfig = {
|
||||||
ProtectHome = true;
|
ExecStart = "${cfg.package}/bin/unbound -p -d -c /etc/unbound/unbound.conf";
|
||||||
|
ExecReload = "+/run/current-system/sw/bin/kill -HUP $MAINPID";
|
||||||
|
|
||||||
|
NotifyAccess = "main";
|
||||||
|
Type = "notify";
|
||||||
|
|
||||||
|
# FIXME: Which of these do we actualy need, can we drop the chroot flag?
|
||||||
|
AmbientCapabilities = [
|
||||||
|
"CAP_NET_BIND_SERVICE"
|
||||||
|
"CAP_NET_RAW"
|
||||||
|
"CAP_SETGID"
|
||||||
|
"CAP_SETUID"
|
||||||
|
"CAP_SYS_CHROOT"
|
||||||
|
"CAP_SYS_RESOURCE"
|
||||||
|
];
|
||||||
|
|
||||||
|
User = "unbound";
|
||||||
|
Group = lib.mkIf (cfg.localControlSocketPath != null) (lib.mkDefault "unbound");
|
||||||
|
|
||||||
|
MemoryDenyWriteExecute = true;
|
||||||
|
NoNewPrivileges = true;
|
||||||
PrivateDevices = true;
|
PrivateDevices = true;
|
||||||
Restart = "always";
|
PrivateTmp = true;
|
||||||
RestartSec = "5s";
|
ProtectHome = true;
|
||||||
|
ProtectControlGroups = true;
|
||||||
|
ProtectKernelModules = true;
|
||||||
|
ProtectSystem = "strict";
|
||||||
|
RuntimeDirectory = "unbound";
|
||||||
|
ConfigurationDirectory = "unbound";
|
||||||
|
StateDirectory = "unbound";
|
||||||
|
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
|
||||||
|
RestrictRealtime = true;
|
||||||
|
SystemCallArchitectures = "native";
|
||||||
|
SystemCallFilter = [
|
||||||
|
"~@clock"
|
||||||
|
"@cpu-emulation"
|
||||||
|
"@debug"
|
||||||
|
"@keyring"
|
||||||
|
"@module"
|
||||||
|
"mount"
|
||||||
|
"@obsolete"
|
||||||
|
"@resources"
|
||||||
|
];
|
||||||
|
RestrictNamespaces = true;
|
||||||
|
LockPersonality = true;
|
||||||
|
RestrictSUIDSGID = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# If networkmanager is enabled, ask it to interface with unbound.
|
# If networkmanager is enabled, ask it to interface with unbound.
|
||||||
networking.networkmanager.dns = "unbound";
|
networking.networkmanager.dns = "unbound";
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -368,6 +368,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 {};
|
||||||
|
278
nixos/tests/unbound.nix
Normal file
278
nixos/tests/unbound.nix
Normal file
@ -0,0 +1,278 @@
|
|||||||
|
/*
|
||||||
|
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.
|
||||||
|
|
||||||
|
Another aspect that we cover is access to the local control UNIX socket. It
|
||||||
|
can optionally be enabled and users can optionally be in a group to gain
|
||||||
|
access. Users that are not in the group (except for root) should not have
|
||||||
|
access to that socket. Also, when there is no socket configured, users
|
||||||
|
shouldn't be able to access the control socket at all. Not even root.
|
||||||
|
*/
|
||||||
|
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, config, ... }: {
|
||||||
|
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" ];
|
||||||
|
localControlSocketPath = "/run/unbound/unbound.ctl";
|
||||||
|
extraConfig = ''
|
||||||
|
include: "/etc/unbound/extra*.conf"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
users.users = {
|
||||||
|
# user that is permitted to access the unix socket
|
||||||
|
someuser.extraGroups = [
|
||||||
|
config.users.users.unbound.group
|
||||||
|
];
|
||||||
|
|
||||||
|
# user that is not permitted to access the unix socket
|
||||||
|
unauthorizeduser = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
with subtest("root is unable to use unbounc-control when the socket is not configured"):
|
||||||
|
resolver.succeed("which unbound-control") # the binary must exist
|
||||||
|
resolver.fail("unbound-control list_forwards") # the invocation must fail
|
||||||
|
|
||||||
|
# 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"])
|
||||||
|
|
||||||
|
with subtest("test that we can use the unbound control socket"):
|
||||||
|
out = local_resolver.succeed(
|
||||||
|
"sudo -u someuser -- unbound-control list_forwards"
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
# Thank you black! Can't really break this line into a readable version.
|
||||||
|
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}"
|
||||||
|
assert out == expected, f"Expected `{expected}` but got `{out}` instead."
|
||||||
|
local_resolver.fail("sudo -u unauthorizeduser -- unbound-control list_forwards")
|
||||||
|
|
||||||
|
|
||||||
|
# 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("unbound-control reload")
|
||||||
|
r = [("A", "3.4.5.6")]
|
||||||
|
test(local_resolver, ["::1", "127.0.0.1"], zone="something.local.", records=r)
|
||||||
|
'';
|
||||||
|
})
|
@ -1,4 +1,24 @@
|
|||||||
{ stdenv, fetchurl, openssl, nettle, expat, libevent, dns-root-data }:
|
{ stdenv
|
||||||
|
, lib
|
||||||
|
, fetchurl
|
||||||
|
, openssl
|
||||||
|
, nettle
|
||||||
|
, expat
|
||||||
|
, libevent
|
||||||
|
, dns-root-data
|
||||||
|
, pkg-config
|
||||||
|
#
|
||||||
|
# By default unbound will not be built with systemd support. Unbound is a very
|
||||||
|
# commmon dependency. The transitive dependency closure of systemd also
|
||||||
|
# contains unbound.
|
||||||
|
# Since most (all?) (lib)unbound users outside of the unbound daemon usage do
|
||||||
|
# not need the systemd integration it is likely best to just default to no
|
||||||
|
# systemd integration.
|
||||||
|
# For the daemon use-case, that needs to notify systemd, use `unbound-with-systemd`.
|
||||||
|
#
|
||||||
|
, withSystemd ? false
|
||||||
|
, systemd ? null
|
||||||
|
}:
|
||||||
|
|
||||||
stdenv.mkDerivation rec {
|
stdenv.mkDerivation rec {
|
||||||
pname = "unbound";
|
pname = "unbound";
|
||||||
@ -11,7 +31,7 @@ stdenv.mkDerivation rec {
|
|||||||
|
|
||||||
outputs = [ "out" "lib" "man" ]; # "dev" would only split ~20 kB
|
outputs = [ "out" "lib" "man" ]; # "dev" would only split ~20 kB
|
||||||
|
|
||||||
buildInputs = [ openssl nettle expat libevent ];
|
buildInputs = [ openssl nettle expat libevent ] ++ lib.optionals withSystemd [ pkg-config systemd ];
|
||||||
|
|
||||||
configureFlags = [
|
configureFlags = [
|
||||||
"--with-ssl=${openssl.dev}"
|
"--with-ssl=${openssl.dev}"
|
||||||
@ -25,6 +45,8 @@ stdenv.mkDerivation rec {
|
|||||||
"--enable-relro-now"
|
"--enable-relro-now"
|
||||||
] ++ stdenv.lib.optional stdenv.hostPlatform.isStatic [
|
] ++ stdenv.lib.optional stdenv.hostPlatform.isStatic [
|
||||||
"--disable-flto"
|
"--disable-flto"
|
||||||
|
] ++ lib.optionals withSystemd [
|
||||||
|
"--enable-systemd"
|
||||||
];
|
];
|
||||||
|
|
||||||
installFlags = [ "configfile=\${out}/etc/unbound/unbound.conf" ];
|
installFlags = [ "configfile=\${out}/etc/unbound/unbound.conf" ];
|
||||||
@ -33,7 +55,7 @@ stdenv.mkDerivation rec {
|
|||||||
make unbound-event-install
|
make unbound-event-install
|
||||||
'';
|
'';
|
||||||
|
|
||||||
preFixup = stdenv.lib.optionalString (stdenv.isLinux && !stdenv.hostPlatform.isMusl) # XXX: revisit
|
preFixup = lib.optionalString (stdenv.isLinux && !stdenv.hostPlatform.isMusl) # XXX: revisit
|
||||||
# Build libunbound again, but only against nettle instead of openssl.
|
# Build libunbound again, but only against nettle instead of openssl.
|
||||||
# This avoids gnutls.out -> unbound.lib -> openssl.out.
|
# This avoids gnutls.out -> unbound.lib -> openssl.out.
|
||||||
# There was some problem with this on Darwin; let's not complicate non-Linux.
|
# There was some problem with this on Darwin; let's not complicate non-Linux.
|
||||||
@ -43,17 +65,17 @@ stdenv.mkDerivation rec {
|
|||||||
buildPhase
|
buildPhase
|
||||||
installPhase
|
installPhase
|
||||||
''
|
''
|
||||||
# get rid of runtime dependencies on $dev outputs
|
# get rid of runtime dependencies on $dev outputs
|
||||||
+ ''substituteInPlace "$lib/lib/libunbound.la" ''
|
+ ''substituteInPlace "$lib/lib/libunbound.la" ''
|
||||||
+ stdenv.lib.concatMapStrings
|
+ lib.concatMapStrings
|
||||||
(pkg: " --replace '-L${pkg.dev}/lib' '-L${pkg.out}/lib' --replace '-R${pkg.dev}/lib' '-R${pkg.out}/lib'")
|
(pkg: lib.optionalString (pkg ? dev) " --replace '-L${pkg.dev}/lib' '-L${pkg.out}/lib' --replace '-R${pkg.dev}/lib' '-R${pkg.out}/lib'")
|
||||||
buildInputs;
|
(builtins.filter (p: p != null) buildInputs);
|
||||||
|
|
||||||
meta = with stdenv.lib; {
|
meta = with lib; {
|
||||||
description = "Validating, recursive, and caching DNS resolver";
|
description = "Validating, recursive, and caching DNS resolver";
|
||||||
license = licenses.bsd3;
|
license = licenses.bsd3;
|
||||||
homepage = "https://www.unbound.net";
|
homepage = "https://www.unbound.net";
|
||||||
maintainers = with maintainers; [ ehmry fpletz globin ];
|
maintainers = with maintainers; [ ehmry fpletz globin ];
|
||||||
platforms = stdenv.lib.platforms.unix;
|
platforms = platforms.unix;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -8097,7 +8097,11 @@ in
|
|||||||
|
|
||||||
unclutter-xfixes = callPackage ../tools/misc/unclutter-xfixes { };
|
unclutter-xfixes = callPackage ../tools/misc/unclutter-xfixes { };
|
||||||
|
|
||||||
unbound = callPackage ../tools/networking/unbound { };
|
unbound = callPackage ../tools/networking/unbound {};
|
||||||
|
|
||||||
|
unbound-with-systemd = unbound.override {
|
||||||
|
withSystemd = true;
|
||||||
|
};
|
||||||
|
|
||||||
unicorn = callPackage ../development/libraries/unicorn { };
|
unicorn = callPackage ../development/libraries/unicorn { };
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user