1 { config, lib, pkgs, ... }:
5 cfg = config.services.unbound;
7 yesOrNo = v: if v then "yes" else "no";
9 toOption = indent: n: v: "${indent}${toString n}: ${v}";
11 toConf = indent: n: v:
12 if builtins.isFloat v then (toOption indent n (builtins.toJSON v))
13 else if isInt v then (toOption indent n (toString v))
14 else if isBool v then (toOption indent n (yesOrNo v))
15 else if isString v then (toOption indent n v)
16 else if isList v then (concatMapStringsSep "\n" (toConf indent n) v)
17 else if isAttrs v then (concatStringsSep "\n" (
18 ["${indent}${n}:"] ++ (
19 mapAttrsToList (toConf "${indent} ") v
22 else throw (traceSeq v "services.unbound.settings: unexpected type");
24 confNoServer = concatStringsSep "\n" ((mapAttrsToList (toConf "") (builtins.removeAttrs cfg.settings [ "server" ])) ++ [""]);
25 confServer = concatStringsSep "\n" (mapAttrsToList (toConf " ") (builtins.removeAttrs cfg.settings.server [ "define-tag" ]));
27 confFile = pkgs.writeText "unbound.conf" ''
29 ${optionalString (cfg.settings.server.define-tag != "") (toOption " " "define-tag" cfg.settings.server.define-tag)}
34 rootTrustAnchorFile = "${cfg.stateDir}/root.key";
43 enable = mkEnableOption (lib.mdDoc "Unbound domain name server");
47 default = pkgs.unbound-with-systemd;
48 defaultText = literalExpression "pkgs.unbound-with-systemd";
49 description = lib.mdDoc "The unbound package to use";
55 description = lib.mdDoc "User account under which unbound runs.";
61 description = lib.mdDoc "Group under which unbound runs.";
66 default = "/var/lib/unbound";
67 description = lib.mdDoc "Directory holding all state for unbound to run.";
70 resolveLocalQueries = mkOption {
73 description = lib.mdDoc ''
74 Whether unbound should resolve local queries (i.e. add 127.0.0.1 to
79 enableRootTrustAnchor = mkOption {
82 description = lib.mdDoc "Use and update root trust anchor for DNSSEC validation.";
85 localControlSocketPath = mkOption {
87 # FIXME: What is the proper type here so users can specify strings,
89 # My guess would be `types.nullOr (types.either types.str types.path)`
90 # but I haven't verified yet.
91 type = types.nullOr types.str;
92 example = "/run/unbound/unbound.ctl";
93 description = lib.mdDoc ''
94 When not set to `null` this option defines the path
95 at which the unbound remote control socket should be created at. The
96 socket will be owned by the unbound user (`unbound`)
97 and group will be `nogroup`.
99 Users that should be permitted to access the socket must be in the
100 `config.services.unbound.group` group.
102 If this option is `null` remote control will not be
103 enabled. Unbounds default values apply.
107 settings = mkOption {
109 type = with types; submodule {
112 validSettingsPrimitiveTypes = oneOf [ int str bool float ];
113 validSettingsTypes = oneOf [ validSettingsPrimitiveTypes (listOf validSettingsPrimitiveTypes) ];
114 settingsType = oneOf [ str (attrsOf validSettingsTypes) ];
115 in attrsOf (oneOf [ settingsType (listOf settingsType) ])
116 // { description = ''
117 unbound.conf configuration type. The format consist of an attribute
118 set of settings. Each settings can be either one value, a list of
119 values or an attribute set. The allowed values are integers,
120 strings, booleans or floats.
125 remote-control.control-enable = mkOption {
132 example = literalExpression ''
135 interface = [ "127.0.0.1" ];
140 forward-addr = "1.1.1.1@853#cloudflare-dns.com";
143 name = "example.org.";
145 "1.1.1.1@853#cloudflare-dns.com"
146 "1.0.0.1@853#cloudflare-dns.com"
150 remote-control.control-enable = true;
153 description = lib.mdDoc ''
154 Declarative Unbound configuration
155 See the {manpage}`unbound.conf(5)` manpage for a list of
162 ###### implementation
164 config = mkIf cfg.enable {
166 services.unbound.settings = {
168 directory = mkDefault cfg.stateDir;
172 # when running under systemd there is no need to daemonize
173 do-daemonize = false;
174 interface = mkDefault ([ "127.0.0.1" ] ++ (optional config.networking.enableIPv6 "::1"));
175 access-control = mkDefault ([ "127.0.0.0/8 allow" ] ++ (optional config.networking.enableIPv6 "::1/128 allow"));
176 auto-trust-anchor-file = mkIf cfg.enableRootTrustAnchor rootTrustAnchorFile;
177 tls-cert-bundle = mkDefault "/etc/ssl/certs/ca-certificates.crt";
178 # prevent race conditions on system startup when interfaces are not yet
180 ip-freebind = mkDefault true;
181 define-tag = mkDefault "";
184 control-enable = mkDefault false;
185 control-interface = mkDefault ([ "127.0.0.1" ] ++ (optional config.networking.enableIPv6 "::1"));
186 server-key-file = mkDefault "${cfg.stateDir}/unbound_server.key";
187 server-cert-file = mkDefault "${cfg.stateDir}/unbound_server.pem";
188 control-key-file = mkDefault "${cfg.stateDir}/unbound_control.key";
189 control-cert-file = mkDefault "${cfg.stateDir}/unbound_control.pem";
190 } // optionalAttrs (cfg.localControlSocketPath != null) {
191 control-enable = true;
192 control-interface = cfg.localControlSocketPath;
196 environment.systemPackages = [ cfg.package ];
198 users.users = mkIf (cfg.user == "unbound") {
200 description = "unbound daemon user";
206 users.groups = mkIf (cfg.group == "unbound") {
210 networking = mkIf cfg.resolveLocalQueries {
212 useLocalResolver = mkDefault true;
215 networkmanager.dns = "unbound";
218 environment.etc."unbound/unbound.conf".source = confFile;
220 systemd.services.unbound = {
221 description = "Unbound recursive Domain Name Server";
222 after = [ "network.target" ];
223 before = [ "nss-lookup.target" ];
224 wantedBy = [ "multi-user.target" "nss-lookup.target" ];
226 path = mkIf cfg.settings.remote-control.control-enable [ pkgs.openssl ];
229 ${optionalString cfg.enableRootTrustAnchor ''
230 ${cfg.package}/bin/unbound-anchor -a ${rootTrustAnchorFile} || echo "Root anchor updated!"
232 ${optionalString cfg.settings.remote-control.control-enable ''
233 ${cfg.package}/bin/unbound-control-setup -d ${cfg.stateDir}
242 ExecStart = "${cfg.package}/bin/unbound -p -d -c /etc/unbound/unbound.conf";
243 ExecReload = "+/run/current-system/sw/bin/kill -HUP $MAINPID";
245 NotifyAccess = "main";
248 # FIXME: Which of these do we actualy need, can we drop the chroot flag?
249 AmbientCapabilities = [
250 "CAP_NET_BIND_SERVICE"
261 MemoryDenyWriteExecute = true;
262 NoNewPrivileges = true;
263 PrivateDevices = true;
266 ProtectControlGroups = true;
267 ProtectKernelModules = true;
268 ProtectSystem = "strict";
269 RuntimeDirectory = "unbound";
270 ConfigurationDirectory = "unbound";
271 StateDirectory = "unbound";
272 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_NETLINK" "AF_UNIX" ];
273 RestrictRealtime = true;
274 SystemCallArchitectures = "native";
285 RestrictNamespaces = true;
286 LockPersonality = true;
287 RestrictSUIDSGID = true;
289 Restart = "on-failure";
296 (mkRenamedOptionModule [ "services" "unbound" "interfaces" ] [ "services" "unbound" "settings" "server" "interface" ])
297 (mkChangedOptionModule [ "services" "unbound" "allowedAccess" ] [ "services" "unbound" "settings" "server" "access-control" ] (
298 config: map (value: "${value} allow") (getAttrFromPath [ "services" "unbound" "allowedAccess" ] config)
300 (mkRemovedOptionModule [ "services" "unbound" "forwardAddresses" ] ''
302 services.unbound.settings.forward-zone = [{
304 forward-addr = [ # Your current services.unbound.forwardAddresses ];
306 If any of those addresses are local addresses (127.0.0.1 or ::1), you must
307 also set services.unbound.settings.server.do-not-query-localhost to false.
309 (mkRemovedOptionModule [ "services" "unbound" "extraConfig" ] ''
310 You can use services.unbound.settings to add any configuration you want.