Skip to content

Commit

Permalink
nixos/unbound: deprecate extraConfig in favor of settings
Browse files Browse the repository at this point in the history
Follow RFC 42 by having a settings option that is
then converted into an unbound configuration file
instead of having an extraConfig option.

Existing options have been renamed or kept if
possible.

An enableRemoteAccess has been added. It sets remote-control setting to
true in unbound.conf which in turn enables the new wrapping of
unbound-control to access the server locally.  Also includes options
'remoteAccessInterfaces' and 'remoteAccessPort' for remote access.

Signed-off-by: Marc 'risson' Schmitt <[email protected]>
  • Loading branch information
rissson committed May 3, 2021
1 parent 62f241d commit 52f6733
Show file tree
Hide file tree
Showing 3 changed files with 226 additions and 112 deletions.
17 changes: 17 additions & 0 deletions nixos/doc/manual/release-notes/rl-2105.xml
Original file line number Diff line number Diff line change
Expand Up @@ -829,6 +829,23 @@ environment.systemPackages = [
default in the CLI tooling which in turn enables us to use
<literal>unbound-control</literal> without passing a custom configuration location.
</para>

<para>
The module has also been reworked to be <link
xlink:href="https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md">RFC
0042</link> compliant. As such,
<option>sevices.unbound.extraConfig</option> has been removed and replaced
by <xref linkend="opt-services.unbound.settings"/>. <option>services.unbound.interfaces</option>
has been renamed to <option>services.unbound.settings.server.interface</option>.
</para>

<para>
<option>services.unbound.forwardAddresses</option> and
<option>services.unbound.allowedAccess</option> have also been changed to
use the new settings interface. You can follow the instructions when
executing <literal>nixos-rebuild</literal> to upgrade your configuration to
use the new interface.
</para>
</listitem>
<listitem>
<para>
Expand Down
253 changes: 169 additions & 84 deletions nixos/modules/services/networking/unbound.nix
Original file line number Diff line number Diff line change
Expand Up @@ -4,51 +4,28 @@ 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
{
yesOrNo = v: if v then "yes" else "no";

toOption = indent: n: v: "${indent}${toString n}: ${v}";

toConf = indent: n: v:
if builtins.isFloat v then (toOption indent n (builtins.toJSON v))
else if isInt v then (toOption indent n (toString v))
else if isBool v then (toOption indent n (yesOrNo v))
else if isString v then (toOption indent n v)
else if isList v then (concatMapStringsSep "\n" (toConf indent n) v)
else if isAttrs v then (concatStringsSep "\n" (
["${indent}${n}:"] ++ (
mapAttrsToList (toConf "${indent} ") v
)
))
else throw (traceSeq v "services.unbound.settings: unexpected type");

confFile = pkgs.writeText "unbound.conf" (concatStringsSep "\n" ((mapAttrsToList (toConf "") cfg.settings) ++ [""]));

rootTrustAnchorFile = "${cfg.stateDir}/root.key";

in {

###### interface

Expand All @@ -64,25 +41,30 @@ in
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.";
user = mkOption {
type = types.str;
default = "unbound";
description = "User account under which unbound runs.";
};

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
<citerefentry><refentrytitle>unbound.conf</refentrytitle><manvolnum>8</manvolnum></citerefentry>.
'';
group = mkOption {
type = types.str;
default = "unbound";
description = "Group under which unbound runs.";
};

forwardAddresses = mkOption {
default = [];
type = types.listOf types.str;
description = "What servers to forward queries to.";
stateDir = mkOption {
default = "/var/lib/unbound";
description = "Directory holding all state for unbound to run.";
};

resolveLocalQueries = mkOption {
type = types.bool;
default = true;
description = ''
Whether unbound should resolve local queries (i.e. add 127.0.0.1 to
/etc/resolv.conf).
'';
};

enableRootTrustAnchor = mkOption {
Expand All @@ -106,47 +88,123 @@ in
and group will be <literal>nogroup</literal>.
Users that should be permitted to access the socket must be in the
<literal>unbound</literal> group.
<literal>config.services.unbound.group</literal> group.
If this option is <literal>null</literal> remote control will not be
configured at all. Unbounds default values apply.
enabled. Unbounds default values apply.
'';
};

extraConfig = mkOption {
default = "";
type = types.lines;
settings = mkOption {
default = {};
type = with types; submodule {

freeformType = let
validSettingsPrimitiveTypes = oneOf [ int str bool float ];
validSettingsTypes = oneOf [ validSettingsPrimitiveTypes (listOf validSettingsPrimitiveTypes) ];
settingsType = (attrsOf validSettingsTypes);
in attrsOf (oneOf [ string settingsType (listOf settingsType) ])
// { description = ''
unbound.conf configuration type. The format consist of an attribute
set of settings. Each settings can be either one value, a list of
values or an attribute set. The allowed values are integers,
strings, booleans or floats.
'';
};

options = {
remote-control.control-enable = mkOption {
type = bool;
default = false;
internal = true;
};
};
};
example = literalExample ''
{
server = {
interface = [ "127.0.0.1" ];
};
forward-zone = [
{
name = ".";
forward-addr = "1.1.1.1@853#cloudflare-dns.com";
}
{
name = "example.org.";
forward-addr = [
"1.1.1.1@853#cloudflare-dns.com"
"1.0.0.1@853#cloudflare-dns.com"
];
}
];
remote-control.control-enable = true;
};
'';
description = ''
Extra unbound config. See
<citerefentry><refentrytitle>unbound.conf</refentrytitle><manvolnum>8
</manvolnum></citerefentry>.
Declarative Unbound configuration
See the <citerefentry><refentrytitle>unbound.conf</refentrytitle>
<manvolnum>5</manvolnum></citerefentry> manpage for a list of
available options.
'';
};

};
};

###### implementation

config = mkIf cfg.enable {

services.unbound.settings = {
server = {
directory = mkDefault cfg.stateDir;
username = cfg.user;
chroot = ''""'';
pidfile = ''""'';
# when running under systemd there is no need to daemonize
do-daemonize = false;
interface = mkDefault ([ "127.0.0.1" ] ++ (optional config.networking.enableIPv6 "::1"));
access-control = mkDefault ([ "127.0.0.0/8 allow" ] ++ (optional config.networking.enableIPv6 "::1/128 allow"));
auto-trust-anchor-file = mkIf cfg.enableRootTrustAnchor rootTrustAnchorFile;
tls-cert-bundle = mkDefault "/etc/ssl/certs/ca-certificates.crt";
# prevent race conditions on system startup when interfaces are not yet
# configured
ip-freebind = mkDefault true;
};
remote-control = {
control-enable = mkDefault false;
control-interface = mkDefault ([ "127.0.0.1" ] ++ (optional config.networking.enableIPv6 "::1"));
server-key-file = mkDefault "${cfg.stateDir}/unbound_server.key";
server-cert-file = mkDefault "${cfg.stateDir}/unbound_server.pem";
control-key-file = mkDefault "${cfg.stateDir}/unbound_control.key";
control-cert-file = mkDefault "${cfg.stateDir}/unbound_control.pem";
} // optionalAttrs (cfg.localControlSocketPath != null) {
control-enable = true;
control-interface = cfg.localControlSocketPath;
};
};

environment.systemPackages = [ cfg.package ];

users.users.unbound = {
description = "unbound daemon user";
isSystemUser = true;
group = lib.mkIf (cfg.localControlSocketPath != null) (lib.mkDefault "unbound");
users.users = mkIf (cfg.user == "unbound") {
unbound = {
description = "unbound daemon user";
isSystemUser = true;
group = cfg.group;
};
};

# 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) {
users.groups = mkIf (cfg.group == "unbound") {
unbound = {};
};

networking.resolvconf.useLocalResolver = mkDefault true;
networking = mkIf cfg.resolveLocalQueries {
resolvconf = {
useLocalResolver = mkDefault true;
};

networkmanager.dns = "unbound";
};

environment.etc."unbound/unbound.conf".source = confFile;

Expand All @@ -156,8 +214,15 @@ in
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!"
path = mkIf cfg.settings.remote-control.control-enable [ pkgs.openssl ];

preStart = ''
${optionalString cfg.enableRootTrustAnchor ''
${cfg.package}/bin/unbound-anchor -a ${rootTrustAnchorFile} || echo "Root anchor updated!"
''}
${optionalString cfg.settings.remote-control.control-enable ''
${cfg.package}/bin/unbound-control-setup -d ${cfg.stateDir}
''}
'';

restartTriggers = [
Expand All @@ -181,8 +246,8 @@ in
"CAP_SYS_RESOURCE"
];

User = "unbound";
Group = lib.mkIf (cfg.localControlSocketPath != null) (lib.mkDefault "unbound");
User = cfg.user;
Group = cfg.group;

MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
Expand Down Expand Up @@ -211,9 +276,29 @@ in
RestrictNamespaces = true;
LockPersonality = true;
RestrictSUIDSGID = true;

Restart = "on-failure";
RestartSec = "5s";
};
};
# If networkmanager is enabled, ask it to interface with unbound.
networking.networkmanager.dns = "unbound";
};

imports = [
(mkRenamedOptionModule [ "services" "unbound" "interfaces" ] [ "services" "unbound" "settings" "server" "interface" ])
(mkChangedOptionModule [ "services" "unbound" "allowedAccess" ] [ "services" "unbound" "settings" "server" "access-control" ] (
config: map (value: "${value} allow") (getAttrFromPath [ "services" "unbound" "allowedAccess" ] config)
))
(mkRemovedOptionModule [ "services" "unbound" "forwardAddresses" ] ''
Add a new setting:
services.unbound.settings.forward-zone = [{
name = ".";
forward-addr = [ # Your current services.unbound.forwardAddresses ];
}];
If any of those addresses are local addresses (127.0.0.1 or ::1), you must
also set services.unbound.settings.server.do-not-query-localhost to false.
'')
(mkRemovedOptionModule [ "services" "unbound" "extraConfig" ] ''
You can use services.unbound.settings to add any configuration you want.
'')
];
}
Loading

0 comments on commit 52f6733

Please sign in to comment.