diff options
Diffstat (limited to 'nixos/modules/services/networking/unbound.nix')
-rw-r--r-- | nixos/modules/services/networking/unbound.nix | 311 |
1 files changed, 238 insertions, 73 deletions
diff --git a/nixos/modules/services/networking/unbound.nix b/nixos/modules/services/networking/unbound.nix index baed83591e1..6d7178047ea 100644 --- a/nixos/modules/services/networking/unbound.nix +++ b/nixos/modules/services/networking/unbound.nix @@ -1,50 +1,39 @@ { 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"; + yesOrNo = v: if v then "yes" else "no"; - 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; + toOption = indent: n: v: "${indent}${toString n}: ${v}"; - rootTrustAnchorFile = "${stateDir}/root.key"; + 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"); - trustAnchor = optionalString cfg.enableRootTrustAnchor - "auto-trust-anchor-file: ${rootTrustAnchorFile}"; + confNoServer = concatStringsSep "\n" ((mapAttrsToList (toConf "") (builtins.removeAttrs cfg.settings [ "server" ])) ++ [""]); + confServer = concatStringsSep "\n" (mapAttrsToList (toConf " ") (builtins.removeAttrs cfg.settings.server [ "define-tag" ])); confFile = pkgs.writeText "unbound.conf" '' server: - directory: "${stateDir}" - username: unbound - chroot: "${stateDir}" - pidfile: "" - ${interfaces} - ${access} - ${trustAnchor} - ${cfg.extraConfig} - ${forward} + ${optionalString (cfg.settings.server.define-tag != "") (toOption " " "define-tag" cfg.settings.server.define-tag)} + ${confServer} + ${confNoServer} ''; -in + rootTrustAnchorFile = "${cfg.stateDir}/root.key"; -{ +in { ###### interface @@ -55,27 +44,35 @@ in package = mkOption { type = types.package; - default = pkgs.unbound; - defaultText = "pkgs.unbound"; + 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."; + 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."; + 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 { @@ -84,16 +81,81 @@ in description = "Use and update root trust anchor for DNSSEC validation."; }; - extraConfig = mkOption { - default = ""; - type = types.lines; + 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 = '' - Extra unbound config. See - <citerefentry><refentrytitle>unbound.conf</refentrytitle><manvolnum>8 - </manvolnum></citerefentry>. + 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>config.services.unbound.group</literal> group. + + If this option is <literal>null</literal> remote control will not be + enabled. Unbounds default values apply. ''; }; + settings = mkOption { + default = {}; + type = with types; submodule { + + freeformType = let + validSettingsPrimitiveTypes = oneOf [ int str bool float ]; + validSettingsTypes = oneOf [ validSettingsPrimitiveTypes (listOf validSettingsPrimitiveTypes) ]; + settingsType = oneOf [ str (attrsOf validSettingsTypes) ]; + in attrsOf (oneOf [ 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 = '' + Declarative Unbound configuration + See the <citerefentry><refentrytitle>unbound.conf</refentrytitle> + <manvolnum>5</manvolnum></citerefentry> manpage for a list of + available options. + ''; + }; }; }; @@ -101,48 +163,151 @@ in 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; + define-tag = mkDefault ""; + }; + 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; + users.users = mkIf (cfg.user == "unbound") { + unbound = { + description = "unbound daemon user"; + isSystemUser = true; + group = cfg.group; + }; + }; + + users.groups = mkIf (cfg.group == "unbound") { + unbound = {}; + }; + + networking = mkIf cfg.resolveLocalQueries { + resolvconf = { + useLocalResolver = mkDefault true; + }; + + networkmanager.dns = "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" ]; - wants = [ "nss-lookup.target" ]; - wantedBy = [ "multi-user.target" ]; + wantedBy = [ "multi-user.target" "nss-lookup.target" ]; + + path = mkIf cfg.settings.remote-control.control-enable [ pkgs.openssl ]; preStart = '' - mkdir -m 0755 -p ${stateDir}/dev/ - 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 + ${optionalString cfg.settings.remote-control.control-enable '' + ${cfg.package}/bin/unbound-control-setup -d ${cfg.stateDir} + ''} ''; - serviceConfig = { - ExecStart = "${cfg.package}/bin/unbound -d -c ${stateDir}/unbound.conf"; - ExecStopPost="${pkgs.utillinux}/bin/umount ${stateDir}/dev/random"; + restartTriggers = [ + confFile + ]; - ProtectSystem = true; - ProtectHome = true; + 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 = cfg.user; + Group = cfg.group; + + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; PrivateDevices = true; - Restart = "always"; + 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; + + 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. + '') + ]; } |