{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.unbound;
stateDir = "/var/lib/unbound";
access = concatMapStringsSep "\n " (x: "access-control: ${x} allow") cfg.allowedAccess;
interfaces = concatMapStringsSep "\n " (x: "interface: ${x}") cfg.interfaces;
isLocalAddress = x: substring 0 3 x == "::1" || substring 0 9 x == "127.0.0.1";
forward =
optionalString (any isLocalAddress cfg.forwardAddresses) ''
do-not-query-localhost: no
''
+ optionalString (cfg.forwardAddresses != []) ''
forward-zone:
name: .
''
+ concatMapStringsSep "\n" (x: " forward-addr: ${x}") cfg.forwardAddresses;
rootTrustAnchorFile = "${stateDir}/root.key";
trustAnchor = optionalString cfg.enableRootTrustAnchor
"auto-trust-anchor-file: ${rootTrustAnchorFile}";
confFile = pkgs.writeText "unbound.conf" ''
server:
ip-freebind: yes
directory: "${stateDir}"
username: unbound
chroot: ""
pidfile: ""
# when running under systemd there is no need to daemonize
do-daemonize: no
${interfaces}
${access}
${trustAnchor}
${lib.optionalString (cfg.localControlSocketPath != null) ''
remote-control:
control-enable: yes
control-interface: ${cfg.localControlSocketPath}
''}
${cfg.extraConfig}
${forward}
'';
in
{
###### interface
options = {
services.unbound = {
enable = mkEnableOption "Unbound domain name server";
package = mkOption {
type = types.package;
default = pkgs.unbound-with-systemd;
defaultText = "pkgs.unbound-with-systemd";
description = "The unbound package to use";
};
allowedAccess = mkOption {
default = [ "127.0.0.0/24" ];
type = types.listOf types.str;
description = "What networks are allowed to use unbound as a resolver.";
};
interfaces = mkOption {
default = [ "127.0.0.1" ] ++ optional config.networking.enableIPv6 "::1";
type = types.listOf types.str;
description = ''
What addresses the server should listen on. This supports the interface syntax documented in
unbound.conf8.
'';
};
forwardAddresses = mkOption {
default = [];
type = types.listOf types.str;
description = "What servers to forward queries to.";
};
enableRootTrustAnchor = mkOption {
default = true;
type = types.bool;
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 null 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 (unbound)
and group will be nogroup.
Users that should be permitted to access the socket must be in the
unbound group.
If this option is null remote control will not be
configured at all. Unbounds default values apply.
'';
};
extraConfig = mkOption {
default = "";
type = types.lines;
description = ''
Extra unbound config. See
unbound.conf8
.
'';
};
};
};
###### implementation
config = mkIf cfg.enable {
environment.systemPackages = [ cfg.package ];
users.users.unbound = {
description = "unbound daemon user";
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;
environment.etc."unbound/unbound.conf".source = confFile;
systemd.services.unbound = {
description = "Unbound recursive Domain Name Server";
after = [ "network.target" ];
before = [ "nss-lookup.target" ];
wantedBy = [ "multi-user.target" "nss-lookup.target" ];
preStart = lib.mkIf cfg.enableRootTrustAnchor ''
${cfg.package}/bin/unbound-anchor -a ${rootTrustAnchorFile} || echo "Root anchor updated!"
'';
restartTriggers = [
confFile
];
serviceConfig = {
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;
PrivateTmp = true;
ProtectHome = true;
ProtectControlGroups = true;
ProtectKernelModules = true;
ProtectSystem = "strict";
RuntimeDirectory = "unbound";
ConfigurationDirectory = "unbound";
StateDirectory = "unbound";
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_NETLINK" "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.
networking.networkmanager.dns = "unbound";
};
}