1{ config, lib, pkgs, ... }:
2
3with lib;
4let
5 cfg = config.services.unbound;
6
7 yesOrNo = v: if v then "yes" else "no";
8
9 toOption = indent: n: v: "${indent}${toString n}: ${v}";
10
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
20 )
21 ))
22 else throw (traceSeq v "services.unbound.settings: unexpected type");
23
24 confFile = pkgs.writeText "unbound.conf" (concatStringsSep "\n" ((mapAttrsToList (toConf "") cfg.settings) ++ [""]));
25
26 rootTrustAnchorFile = "${cfg.stateDir}/root.key";
27
28in {
29
30 ###### interface
31
32 options = {
33 services.unbound = {
34
35 enable = mkEnableOption "Unbound domain name server";
36
37 package = mkOption {
38 type = types.package;
39 default = pkgs.unbound-with-systemd;
40 defaultText = "pkgs.unbound-with-systemd";
41 description = "The unbound package to use";
42 };
43
44 user = mkOption {
45 type = types.str;
46 default = "unbound";
47 description = "User account under which unbound runs.";
48 };
49
50 group = mkOption {
51 type = types.str;
52 default = "unbound";
53 description = "Group under which unbound runs.";
54 };
55
56 stateDir = mkOption {
57 default = "/var/lib/unbound";
58 description = "Directory holding all state for unbound to run.";
59 };
60
61 resolveLocalQueries = mkOption {
62 type = types.bool;
63 default = true;
64 description = ''
65 Whether unbound should resolve local queries (i.e. add 127.0.0.1 to
66 /etc/resolv.conf).
67 '';
68 };
69
70 enableRootTrustAnchor = mkOption {
71 default = true;
72 type = types.bool;
73 description = "Use and update root trust anchor for DNSSEC validation.";
74 };
75
76 localControlSocketPath = mkOption {
77 default = null;
78 # FIXME: What is the proper type here so users can specify strings,
79 # paths and null?
80 # My guess would be `types.nullOr (types.either types.str types.path)`
81 # but I haven't verified yet.
82 type = types.nullOr types.str;
83 example = "/run/unbound/unbound.ctl";
84 description = ''
85 When not set to <literal>null</literal> this option defines the path
86 at which the unbound remote control socket should be created at. The
87 socket will be owned by the unbound user (<literal>unbound</literal>)
88 and group will be <literal>nogroup</literal>.
89
90 Users that should be permitted to access the socket must be in the
91 <literal>config.services.unbound.group</literal> group.
92
93 If this option is <literal>null</literal> remote control will not be
94 enabled. Unbounds default values apply.
95 '';
96 };
97
98 settings = mkOption {
99 default = {};
100 type = with types; submodule {
101
102 freeformType = let
103 validSettingsPrimitiveTypes = oneOf [ int str bool float ];
104 validSettingsTypes = oneOf [ validSettingsPrimitiveTypes (listOf validSettingsPrimitiveTypes) ];
105 settingsType = oneOf [ str (attrsOf validSettingsTypes) ];
106 in attrsOf (oneOf [ settingsType (listOf settingsType) ])
107 // { description = ''
108 unbound.conf configuration type. The format consist of an attribute
109 set of settings. Each settings can be either one value, a list of
110 values or an attribute set. The allowed values are integers,
111 strings, booleans or floats.
112 '';
113 };
114
115 options = {
116 remote-control.control-enable = mkOption {
117 type = bool;
118 default = false;
119 internal = true;
120 };
121 };
122 };
123 example = literalExample ''
124 {
125 server = {
126 interface = [ "127.0.0.1" ];
127 };
128 forward-zone = [
129 {
130 name = ".";
131 forward-addr = "1.1.1.1@853#cloudflare-dns.com";
132 }
133 {
134 name = "example.org.";
135 forward-addr = [
136 "1.1.1.1@853#cloudflare-dns.com"
137 "1.0.0.1@853#cloudflare-dns.com"
138 ];
139 }
140 ];
141 remote-control.control-enable = true;
142 };
143 '';
144 description = ''
145 Declarative Unbound configuration
146 See the <citerefentry><refentrytitle>unbound.conf</refentrytitle>
147 <manvolnum>5</manvolnum></citerefentry> manpage for a list of
148 available options.
149 '';
150 };
151 };
152 };
153
154 ###### implementation
155
156 config = mkIf cfg.enable {
157
158 services.unbound.settings = {
159 server = {
160 directory = mkDefault cfg.stateDir;
161 username = cfg.user;
162 chroot = ''""'';
163 pidfile = ''""'';
164 # when running under systemd there is no need to daemonize
165 do-daemonize = false;
166 interface = mkDefault ([ "127.0.0.1" ] ++ (optional config.networking.enableIPv6 "::1"));
167 access-control = mkDefault ([ "127.0.0.0/8 allow" ] ++ (optional config.networking.enableIPv6 "::1/128 allow"));
168 auto-trust-anchor-file = mkIf cfg.enableRootTrustAnchor rootTrustAnchorFile;
169 tls-cert-bundle = mkDefault "/etc/ssl/certs/ca-certificates.crt";
170 # prevent race conditions on system startup when interfaces are not yet
171 # configured
172 ip-freebind = mkDefault true;
173 };
174 remote-control = {
175 control-enable = mkDefault false;
176 control-interface = mkDefault ([ "127.0.0.1" ] ++ (optional config.networking.enableIPv6 "::1"));
177 server-key-file = mkDefault "${cfg.stateDir}/unbound_server.key";
178 server-cert-file = mkDefault "${cfg.stateDir}/unbound_server.pem";
179 control-key-file = mkDefault "${cfg.stateDir}/unbound_control.key";
180 control-cert-file = mkDefault "${cfg.stateDir}/unbound_control.pem";
181 } // optionalAttrs (cfg.localControlSocketPath != null) {
182 control-enable = true;
183 control-interface = cfg.localControlSocketPath;
184 };
185 };
186
187 environment.systemPackages = [ cfg.package ];
188
189 users.users = mkIf (cfg.user == "unbound") {
190 unbound = {
191 description = "unbound daemon user";
192 isSystemUser = true;
193 group = cfg.group;
194 };
195 };
196
197 users.groups = mkIf (cfg.group == "unbound") {
198 unbound = {};
199 };
200
201 networking = mkIf cfg.resolveLocalQueries {
202 resolvconf = {
203 useLocalResolver = mkDefault true;
204 };
205
206 networkmanager.dns = "unbound";
207 };
208
209 environment.etc."unbound/unbound.conf".source = confFile;
210
211 systemd.services.unbound = {
212 description = "Unbound recursive Domain Name Server";
213 after = [ "network.target" ];
214 before = [ "nss-lookup.target" ];
215 wantedBy = [ "multi-user.target" "nss-lookup.target" ];
216
217 path = mkIf cfg.settings.remote-control.control-enable [ pkgs.openssl ];
218
219 preStart = ''
220 ${optionalString cfg.enableRootTrustAnchor ''
221 ${cfg.package}/bin/unbound-anchor -a ${rootTrustAnchorFile} || echo "Root anchor updated!"
222 ''}
223 ${optionalString cfg.settings.remote-control.control-enable ''
224 ${cfg.package}/bin/unbound-control-setup -d ${cfg.stateDir}
225 ''}
226 '';
227
228 restartTriggers = [
229 confFile
230 ];
231
232 serviceConfig = {
233 ExecStart = "${cfg.package}/bin/unbound -p -d -c /etc/unbound/unbound.conf";
234 ExecReload = "+/run/current-system/sw/bin/kill -HUP $MAINPID";
235
236 NotifyAccess = "main";
237 Type = "notify";
238
239 # FIXME: Which of these do we actualy need, can we drop the chroot flag?
240 AmbientCapabilities = [
241 "CAP_NET_BIND_SERVICE"
242 "CAP_NET_RAW"
243 "CAP_SETGID"
244 "CAP_SETUID"
245 "CAP_SYS_CHROOT"
246 "CAP_SYS_RESOURCE"
247 ];
248
249 User = cfg.user;
250 Group = cfg.group;
251
252 MemoryDenyWriteExecute = true;
253 NoNewPrivileges = true;
254 PrivateDevices = true;
255 PrivateTmp = true;
256 ProtectHome = true;
257 ProtectControlGroups = true;
258 ProtectKernelModules = true;
259 ProtectSystem = "strict";
260 RuntimeDirectory = "unbound";
261 ConfigurationDirectory = "unbound";
262 StateDirectory = "unbound";
263 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_NETLINK" "AF_UNIX" ];
264 RestrictRealtime = true;
265 SystemCallArchitectures = "native";
266 SystemCallFilter = [
267 "~@clock"
268 "@cpu-emulation"
269 "@debug"
270 "@keyring"
271 "@module"
272 "mount"
273 "@obsolete"
274 "@resources"
275 ];
276 RestrictNamespaces = true;
277 LockPersonality = true;
278 RestrictSUIDSGID = true;
279
280 Restart = "on-failure";
281 RestartSec = "5s";
282 };
283 };
284 };
285
286 imports = [
287 (mkRenamedOptionModule [ "services" "unbound" "interfaces" ] [ "services" "unbound" "settings" "server" "interface" ])
288 (mkChangedOptionModule [ "services" "unbound" "allowedAccess" ] [ "services" "unbound" "settings" "server" "access-control" ] (
289 config: map (value: "${value} allow") (getAttrFromPath [ "services" "unbound" "allowedAccess" ] config)
290 ))
291 (mkRemovedOptionModule [ "services" "unbound" "forwardAddresses" ] ''
292 Add a new setting:
293 services.unbound.settings.forward-zone = [{
294 name = ".";
295 forward-addr = [ # Your current services.unbound.forwardAddresses ];
296 }];
297 If any of those addresses are local addresses (127.0.0.1 or ::1), you must
298 also set services.unbound.settings.server.do-not-query-localhost to false.
299 '')
300 (mkRemovedOptionModule [ "services" "unbound" "extraConfig" ] ''
301 You can use services.unbound.settings to add any configuration you want.
302 '')
303 ];
304}