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 confFileUnchecked = pkgs.writeText "unbound.conf" ''
29 ${optionalString (cfg.settings.server.define-tag != "") (toOption " " "define-tag" cfg.settings.server.define-tag)}
33 confFile = if cfg.checkconf then pkgs.runCommandLocal "unbound-checkconf" { } ''
34 cp ${confFileUnchecked} unbound.conf
36 # fake stateDir which is not accessible in the sandbox
39 -e '/auto-trust-anchor-file/d' \
40 -e "s|${cfg.stateDir}|$PWD/state|"
41 ${cfg.package}/bin/unbound-checkconf unbound.conf
43 cp ${confFileUnchecked} $out
44 '' else confFileUnchecked;
46 rootTrustAnchorFile = "${cfg.stateDir}/root.key";
55 enable = mkEnableOption "Unbound domain name server";
57 package = mkPackageOption pkgs "unbound-with-systemd" { };
62 description = "User account under which unbound runs.";
68 description = "Group under which unbound runs.";
73 default = "/var/lib/unbound";
74 description = "Directory holding all state for unbound to run.";
77 checkconf = mkOption {
79 default = !cfg.settings ? include && !cfg.settings ? remote-control;
80 defaultText = "!services.unbound.settings ? include && !services.unbound.settings ? remote-control";
82 Whether to check the resulting config file with unbound checkconf for syntax errors.
84 If settings.include is used, this options is disabled, as the import can likely not be accessed at build time.
85 If settings.remote-control is used, this option is disabled, too as the control-key-file, server-cert-file and server-key-file cannot be accessed at build time.
89 resolveLocalQueries = mkOption {
93 Whether unbound should resolve local queries (i.e. add 127.0.0.1 to
98 enableRootTrustAnchor = mkOption {
101 description = "Use and update root trust anchor for DNSSEC validation.";
104 localControlSocketPath = mkOption {
106 # FIXME: What is the proper type here so users can specify strings,
108 # My guess would be `types.nullOr (types.either types.str types.path)`
109 # but I haven't verified yet.
110 type = types.nullOr types.str;
111 example = "/run/unbound/unbound.ctl";
113 When not set to `null` this option defines the path
114 at which the unbound remote control socket should be created at. The
115 socket will be owned by the unbound user (`unbound`)
116 and group will be `nogroup`.
118 Users that should be permitted to access the socket must be in the
119 `config.services.unbound.group` group.
121 If this option is `null` remote control will not be
122 enabled. Unbounds default values apply.
126 settings = mkOption {
128 type = with types; submodule {
131 validSettingsPrimitiveTypes = oneOf [ int str bool float ];
132 validSettingsTypes = oneOf [ validSettingsPrimitiveTypes (listOf validSettingsPrimitiveTypes) ];
133 settingsType = oneOf [ str (attrsOf validSettingsTypes) ];
134 in attrsOf (oneOf [ settingsType (listOf settingsType) ])
135 // { description = ''
136 unbound.conf configuration type. The format consist of an attribute
137 set of settings. Each settings can be either one value, a list of
138 values or an attribute set. The allowed values are integers,
139 strings, booleans or floats.
144 remote-control.control-enable = mkOption {
151 example = literalExpression ''
154 interface = [ "127.0.0.1" ];
159 forward-addr = "1.1.1.1@853#cloudflare-dns.com";
162 name = "example.org.";
164 "1.1.1.1@853#cloudflare-dns.com"
165 "1.0.0.1@853#cloudflare-dns.com"
169 remote-control.control-enable = true;
173 Declarative Unbound configuration
174 See the {manpage}`unbound.conf(5)` manpage for a list of
181 ###### implementation
183 config = mkIf cfg.enable {
185 services.unbound.settings = {
187 directory = mkDefault cfg.stateDir;
191 # when running under systemd there is no need to daemonize
192 do-daemonize = false;
193 interface = mkDefault ([ "127.0.0.1" ] ++ (optional config.networking.enableIPv6 "::1"));
194 access-control = mkDefault ([ "127.0.0.0/8 allow" ] ++ (optional config.networking.enableIPv6 "::1/128 allow"));
195 auto-trust-anchor-file = mkIf cfg.enableRootTrustAnchor rootTrustAnchorFile;
196 tls-cert-bundle = mkDefault "/etc/ssl/certs/ca-certificates.crt";
197 # prevent race conditions on system startup when interfaces are not yet
199 ip-freebind = mkDefault true;
200 define-tag = mkDefault "";
203 control-enable = mkDefault false;
204 control-interface = mkDefault ([ "127.0.0.1" ] ++ (optional config.networking.enableIPv6 "::1"));
205 server-key-file = mkDefault "${cfg.stateDir}/unbound_server.key";
206 server-cert-file = mkDefault "${cfg.stateDir}/unbound_server.pem";
207 control-key-file = mkDefault "${cfg.stateDir}/unbound_control.key";
208 control-cert-file = mkDefault "${cfg.stateDir}/unbound_control.pem";
209 } // optionalAttrs (cfg.localControlSocketPath != null) {
210 control-enable = true;
211 control-interface = cfg.localControlSocketPath;
215 environment.systemPackages = [ cfg.package ];
217 users.users = mkIf (cfg.user == "unbound") {
219 description = "unbound daemon user";
225 users.groups = mkIf (cfg.group == "unbound") {
229 networking = mkIf cfg.resolveLocalQueries {
231 useLocalResolver = mkDefault true;
235 environment.etc."unbound/unbound.conf".source = confFile;
237 systemd.services.unbound = {
238 description = "Unbound recursive Domain Name Server";
239 after = [ "network.target" ];
240 before = [ "nss-lookup.target" ];
241 wantedBy = [ "multi-user.target" "nss-lookup.target" ];
243 path = mkIf cfg.settings.remote-control.control-enable [ pkgs.openssl ];
246 ${optionalString cfg.enableRootTrustAnchor ''
247 ${cfg.package}/bin/unbound-anchor -a ${rootTrustAnchorFile} || echo "Root anchor updated!"
249 ${optionalString cfg.settings.remote-control.control-enable ''
250 ${cfg.package}/bin/unbound-control-setup -d ${cfg.stateDir}
259 ExecStart = "${cfg.package}/bin/unbound -p -d -c /etc/unbound/unbound.conf";
260 ExecReload = "+/run/current-system/sw/bin/kill -HUP $MAINPID";
262 NotifyAccess = "main";
265 AmbientCapabilities = [
266 "CAP_NET_BIND_SERVICE"
267 "CAP_NET_RAW" # needed if ip-transparent is set to true
269 CapabilityBoundingSet = [
270 "CAP_NET_BIND_SERVICE"
277 MemoryDenyWriteExecute = true;
278 NoNewPrivileges = true;
279 PrivateDevices = true;
282 ProtectControlGroups = true;
283 ProtectKernelModules = true;
284 ProtectSystem = "strict";
286 ProtectHostname = true;
287 ProtectProc = "invisible";
289 ProtectKernelLogs = true;
290 ProtectKernelTunables = true;
291 RuntimeDirectory = "unbound";
292 ConfigurationDirectory = "unbound";
293 StateDirectory = "unbound";
294 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_NETLINK" "AF_UNIX" ];
295 RestrictRealtime = true;
296 SystemCallArchitectures = "native";
297 SystemCallFilter = [ "@system-service" ];
298 RestrictNamespaces = true;
299 LockPersonality = true;
300 RestrictSUIDSGID = true;
302 ReadWritePaths = [ cfg.stateDir ];
304 Restart = "on-failure";
311 (mkRenamedOptionModule [ "services" "unbound" "interfaces" ] [ "services" "unbound" "settings" "server" "interface" ])
312 (mkChangedOptionModule [ "services" "unbound" "allowedAccess" ] [ "services" "unbound" "settings" "server" "access-control" ] (
313 config: map (value: "${value} allow") (getAttrFromPath [ "services" "unbound" "allowedAccess" ] config)
315 (mkRemovedOptionModule [ "services" "unbound" "forwardAddresses" ] ''
317 services.unbound.settings.forward-zone = [{
319 forward-addr = [ # Your current services.unbound.forwardAddresses ];
321 If any of those addresses are local addresses (127.0.0.1 or ::1), you must
322 also set services.unbound.settings.server.do-not-query-localhost to false.
324 (mkRemovedOptionModule [ "services" "unbound" "extraConfig" ] ''
325 You can use services.unbound.settings to add any configuration you want.