diff --git a/nixos/doc/manual/release-notes/rl-2105.xml b/nixos/doc/manual/release-notes/rl-2105.xml index 2b0a265cd98..e3e6dc48433 100644 --- a/nixos/doc/manual/release-notes/rl-2105.xml +++ b/nixos/doc/manual/release-notes/rl-2105.xml @@ -829,6 +829,23 @@ environment.systemPackages = [ default in the CLI tooling which in turn enables us to use unbound-control without passing a custom configuration location. + + + The module has also been reworked to be RFC + 0042 compliant. As such, + has been removed and replaced + by . + has been renamed to . + + + + and + have also been changed to + use the new settings interface. You can follow the instructions when + executing nixos-rebuild to upgrade your configuration to + use the new interface. + diff --git a/nixos/modules/services/networking/unbound.nix b/nixos/modules/services/networking/unbound.nix index 622c3d8ea43..a8747e244a9 100644 --- a/nixos/modules/services/networking/unbound.nix +++ b/nixos/modules/services/networking/unbound.nix @@ -4,51 +4,28 @@ with lib; let cfg = config.services.unbound; - stateDir = "/var/lib/unbound"; + yesOrNo = v: if v then "yes" else "no"; - access = concatMapStringsSep "\n " (x: "access-control: ${x} allow") cfg.allowedAccess; + toOption = indent: n: v: "${indent}${toString n}: ${v}"; - interfaces = concatMapStringsSep "\n " (x: "interface: ${x}") cfg.interfaces; + 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"); - isLocalAddress = x: substring 0 3 x == "::1" || substring 0 9 x == "127.0.0.1"; + confFile = pkgs.writeText "unbound.conf" (concatStringsSep "\n" ((mapAttrsToList (toConf "") cfg.settings) ++ [""])); - 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 = "${cfg.stateDir}/root.key"; - 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 -{ +in { ###### interface @@ -64,27 +41,32 @@ 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 - unbound.conf8. + group = mkOption { + type = types.str; + default = "unbound"; + description = "Group under which unbound runs."; + }; + + 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). ''; }; - forwardAddresses = mkOption { - default = []; - type = types.listOf types.str; - description = "What servers to forward queries to."; - }; - enableRootTrustAnchor = mkOption { default = true; type = types.bool; @@ -106,23 +88,66 @@ in and group will be nogroup. Users that should be permitted to access the socket must be in the - unbound group. + config.services.unbound.group group. If this option is null 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 - unbound.conf8 - . + Declarative Unbound configuration + See the unbound.conf + 5 manpage for a list of + available options. ''; }; - }; }; @@ -130,23 +155,56 @@ in 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"); + 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; + }; }; - # 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) { + environment.systemPackages = [ cfg.package ]; + + users.users = mkIf (cfg.user == "unbound") { + unbound = { + description = "unbound daemon user"; + isSystemUser = true; + group = cfg.group; + }; + }; + + 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; @@ -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 = [ @@ -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; @@ -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. + '') + ]; } diff --git a/nixos/tests/unbound.nix b/nixos/tests/unbound.nix index ca9718ac633..e24c3ef6c99 100644 --- a/nixos/tests/unbound.nix +++ b/nixos/tests/unbound.nix @@ -61,13 +61,16 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: 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" - ''; + settings = { + server = { + interface = [ "192.168.0.1" "fd21::1" "::1" "127.0.0.1" ]; + access-control = [ "192.168.0.0/24 allow" "fd21::/64 allow" "::1 allow" "127.0.0.0/8 allow" ]; + local-data = [ + ''"example.local. IN A 1.2.3.4"'' + ''"example.local. IN AAAA abcd::eeff"'' + ]; + }; + }; }; }; @@ -90,19 +93,25 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: 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" - "192.168.0.2@443" "fd21::2@443" "::1@443" "127.0.0.1@443" ]; - 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 - ''; + settings = { + server = { + interface = [ "::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" + "192.168.0.2@443" "fd21::2@443" "::1@443" "127.0.0.1@443" ]; + access-control = [ "192.168.0.0/24 allow" "fd21::/64 allow" "::1 allow" "127.0.0.0/8 allow" ]; + tls-service-pem = "${cert}/cert.pem"; + tls-service-key = "${cert}/key.pem"; + }; + forward-zone = [ + { + name = "."; + forward-addr = [ + (lib.head nodes.authoritative.config.networking.interfaces.eth1.ipv6.addresses).address + (lib.head nodes.authoritative.config.networking.interfaces.eth1.ipv4.addresses).address + ]; + } + ]; + }; }; }; @@ -122,12 +131,14 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: services.unbound = { enable = true; - allowedAccess = [ "::1" "127.0.0.0/8" ]; - interfaces = [ "::1" "127.0.0.1" ]; + settings = { + server = { + interface = [ "::1" "127.0.0.1" ]; + access-control = [ "::1 allow" "127.0.0.0/8 allow" ]; + }; + include = "/etc/unbound/extra*.conf"; + }; localControlSocketPath = "/run/unbound/unbound.ctl"; - extraConfig = '' - include: "/etc/unbound/extra*.conf" - ''; }; users.users = { @@ -143,12 +154,13 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: unauthorizeduser = { isSystemUser = true; }; }; + # Used for testing configuration reloading 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} + 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: