1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8
9 cfg = config.services.bind;
10
11 bindPkg = config.services.bind.package;
12
13 bindUser = "named";
14
15 bindZoneCoerce =
16 list:
17 builtins.listToAttrs (
18 lib.forEach list (zone: {
19 name = zone.name;
20 value = zone;
21 })
22 );
23
24 bindZoneOptions =
25 { name, config, ... }:
26 {
27 options = {
28 name = lib.mkOption {
29 type = lib.types.str;
30 default = name;
31 description = "Name of the zone.";
32 };
33 master = lib.mkOption {
34 description = "Master=false means slave server";
35 type = lib.types.bool;
36 };
37 file = lib.mkOption {
38 type = lib.types.either lib.types.str lib.types.path;
39 description = "Zone file resource records contain columns of data, separated by whitespace, that define the record.";
40 };
41 masters = lib.mkOption {
42 type = lib.types.listOf lib.types.str;
43 description = "List of servers for inclusion in stub and secondary zones.";
44 };
45 slaves = lib.mkOption {
46 type = lib.types.listOf lib.types.str;
47 description = "Addresses who may request zone transfers.";
48 default = [ ];
49 };
50 allowQuery = lib.mkOption {
51 type = lib.types.listOf lib.types.str;
52 description = ''
53 List of address ranges allowed to query this zone. Instead of the address(es), this may instead
54 contain the single string "any".
55 '';
56 default = [ "any" ];
57 };
58 extraConfig = lib.mkOption {
59 type = lib.types.lines;
60 description = "Extra zone config to be appended at the end of the zone section.";
61 default = "";
62 };
63 };
64 };
65
66 confFile = pkgs.writeText "named.conf" ''
67 include "/etc/bind/rndc.key";
68 controls {
69 inet 127.0.0.1 allow {localhost;} keys {"rndc-key";};
70 };
71
72 acl cachenetworks { ${lib.concatMapStrings (entry: " ${entry}; ") cfg.cacheNetworks} };
73 acl badnetworks { ${lib.concatMapStrings (entry: " ${entry}; ") cfg.blockedNetworks} };
74
75 options {
76 listen-on port ${toString cfg.listenOnPort} { ${
77 lib.concatMapStrings (entry: " ${entry}; ") cfg.listenOn
78 } };
79 listen-on-v6 port ${toString cfg.listenOnIpv6Port} { ${
80 lib.concatMapStrings (entry: " ${entry}; ") cfg.listenOnIpv6
81 } };
82 allow-query-cache { cachenetworks; };
83 blackhole { badnetworks; };
84 forward ${cfg.forward};
85 forwarders { ${lib.concatMapStrings (entry: " ${entry}; ") cfg.forwarders} };
86 directory "${cfg.directory}";
87 pid-file "/run/named/named.pid";
88 ${cfg.extraOptions}
89 };
90
91 ${cfg.extraConfig}
92
93 ${lib.concatMapStrings (
94 {
95 name,
96 file,
97 master ? true,
98 slaves ? [ ],
99 masters ? [ ],
100 allowQuery ? [ ],
101 extraConfig ? "",
102 }:
103 ''
104 zone "${name}" {
105 type ${if master then "master" else "slave"};
106 file "${file}";
107 ${
108 if master then
109 ''
110 allow-transfer {
111 ${lib.concatMapStrings (ip: "${ip};\n") slaves}
112 };
113 ''
114 else
115 ''
116 masters {
117 ${lib.concatMapStrings (ip: "${ip};\n") masters}
118 };
119 ''
120 }
121 allow-query { ${lib.concatMapStrings (ip: "${ip}; ") allowQuery}};
122 ${extraConfig}
123 };
124 ''
125 ) (lib.attrValues cfg.zones)}
126 '';
127
128in
129
130{
131
132 ###### interface
133
134 options = {
135
136 services.bind = {
137
138 enable = lib.mkEnableOption "BIND domain name server";
139
140 package = lib.mkPackageOption pkgs "bind" { };
141
142 cacheNetworks = lib.mkOption {
143 default = [
144 "127.0.0.0/24"
145 "::1/128"
146 ];
147 type = lib.types.listOf lib.types.str;
148 description = ''
149 What networks are allowed to use us as a resolver. Note
150 that this is for recursive queries -- all networks are
151 allowed to query zones configured with the `zones` option
152 by default (although this may be overridden within each
153 zone's configuration, via the `allowQuery` option).
154 It is recommended that you limit cacheNetworks to avoid your
155 server being used for DNS amplification attacks.
156 '';
157 };
158
159 blockedNetworks = lib.mkOption {
160 default = [ ];
161 type = lib.types.listOf lib.types.str;
162 description = ''
163 What networks are just blocked.
164 '';
165 };
166
167 ipv4Only = lib.mkOption {
168 default = false;
169 type = lib.types.bool;
170 description = ''
171 Only use ipv4, even if the host supports ipv6.
172 '';
173 };
174
175 forwarders = lib.mkOption {
176 default = config.networking.nameservers;
177 defaultText = lib.literalExpression "config.networking.nameservers";
178 type = lib.types.listOf lib.types.str;
179 description = ''
180 List of servers we should forward requests to.
181 '';
182 };
183
184 forward = lib.mkOption {
185 default = "first";
186 type = lib.types.enum [
187 "first"
188 "only"
189 ];
190 description = ''
191 Whether to forward 'first' (try forwarding but lookup directly if forwarding fails) or 'only'.
192 '';
193 };
194
195 listenOn = lib.mkOption {
196 default = [ "any" ];
197 type = lib.types.listOf lib.types.str;
198 description = ''
199 Interfaces to listen on.
200 '';
201 };
202
203 listenOnPort = lib.mkOption {
204 default = 53;
205 type = lib.types.port;
206 description = ''
207 Port to listen on.
208 '';
209 };
210
211 listenOnIpv6 = lib.mkOption {
212 default = [ "any" ];
213 type = lib.types.listOf lib.types.str;
214 description = ''
215 Ipv6 interfaces to listen on.
216 '';
217 };
218
219 listenOnIpv6Port = lib.mkOption {
220 default = 53;
221 type = lib.types.port;
222 description = ''
223 Ipv6 port to listen on.
224 '';
225 };
226
227 directory = lib.mkOption {
228 type = lib.types.str;
229 default = "/run/named";
230 description = "Working directory of BIND.";
231 };
232
233 zones = lib.mkOption {
234 default = [ ];
235 type =
236 with lib.types;
237 coercedTo (listOf attrs) bindZoneCoerce (attrsOf (lib.types.submodule bindZoneOptions));
238 description = ''
239 List of zones we claim authority over.
240 '';
241 example = {
242 "example.com" = {
243 master = false;
244 file = "/var/dns/example.com";
245 masters = [ "192.168.0.1" ];
246 slaves = [ ];
247 extraConfig = "";
248 };
249 };
250 };
251
252 extraConfig = lib.mkOption {
253 type = lib.types.lines;
254 default = "";
255 description = ''
256 Extra lines to be added verbatim to the generated named configuration file.
257 '';
258 };
259
260 extraOptions = lib.mkOption {
261 type = lib.types.lines;
262 default = "";
263 description = ''
264 Extra lines to be added verbatim to the options section of the
265 generated named configuration file.
266 '';
267 };
268
269 extraArgs = lib.mkOption {
270 type = lib.types.listOf lib.types.str;
271 default = [ ];
272 description = ''
273 Additional command-line arguments to pass to named.
274 '';
275 example = [
276 "-n"
277 "4"
278 ];
279 };
280
281 configFile = lib.mkOption {
282 type = lib.types.path;
283 default = confFile;
284 defaultText = lib.literalExpression "confFile";
285 description = ''
286 Overridable config file to use for named. By default, that
287 generated by nixos.
288 '';
289 };
290
291 };
292
293 };
294
295 ###### implementation
296
297 config = lib.mkIf cfg.enable {
298
299 networking.resolvconf.useLocalResolver = lib.mkDefault true;
300
301 users.users.${bindUser} = {
302 group = bindUser;
303 description = "BIND daemon user";
304 isSystemUser = true;
305 };
306 users.groups.${bindUser} = { };
307
308 systemd.tmpfiles.settings."bind" = lib.mkIf (cfg.directory != "/run/named") {
309 ${cfg.directory} = {
310 d = {
311 user = bindUser;
312 group = bindUser;
313 age = "-";
314 };
315 };
316 };
317 systemd.services.bind = {
318 description = "BIND Domain Name Server";
319 after = [ "network.target" ];
320 wantedBy = [ "multi-user.target" ];
321
322 preStart = ''
323 if ! [ -f "/etc/bind/rndc.key" ]; then
324 ${bindPkg.out}/sbin/rndc-confgen -c /etc/bind/rndc.key -a -A hmac-sha256 2>/dev/null
325 fi
326 '';
327
328 serviceConfig = {
329 Type = "forking"; # Set type to forking, see https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=900788
330 ExecStart = "${bindPkg.out}/sbin/named ${lib.optionalString cfg.ipv4Only "-4"} -c ${cfg.configFile} ${lib.concatStringsSep " " cfg.extraArgs}";
331 ExecReload = "${bindPkg.out}/sbin/rndc -k '/etc/bind/rndc.key' reload";
332 ExecStop = "${bindPkg.out}/sbin/rndc -k '/etc/bind/rndc.key' stop";
333 User = bindUser;
334 RuntimeDirectory = "named";
335 RuntimeDirectoryPreserve = "yes";
336 ConfigurationDirectory = "bind";
337 ReadWritePaths = [
338 (lib.mapAttrsToList (
339 name: config: if (lib.hasPrefix "/" config.file) then ("-${dirOf config.file}") else ""
340 ) cfg.zones)
341 cfg.directory
342 ];
343 CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
344 AmbientCapabilities = "CAP_NET_BIND_SERVICE";
345 # Security
346 NoNewPrivileges = true;
347 # Sandboxing
348 ProtectSystem = "strict";
349 ReadOnlyPaths = "/sys";
350 ProtectHome = true;
351 PrivateTmp = true;
352 PrivateDevices = true;
353 PrivateMounts = true;
354 ProtectHostname = true;
355 ProtectClock = true;
356 ProtectKernelTunables = true;
357 ProtectKernelModules = true;
358 ProtectKernelLogs = true;
359 ProtectControlGroups = true;
360 ProtectProc = "invisible";
361 ProcSubset = "pid";
362 RemoveIPC = true;
363 RestrictAddressFamilies = [ "AF_UNIX AF_INET AF_INET6 AF_NETLINK" ];
364 LockPersonality = true;
365 MemoryDenyWriteExecute = true;
366 RestrictRealtime = true;
367 RestrictSUIDSGID = true;
368 RestrictNamespaces = true;
369 # System Call Filtering
370 SystemCallArchitectures = "native";
371 SystemCallFilter = "~@mount @debug @clock @reboot @resources @privileged @obsolete acct modify_ldt add_key adjtimex clock_adjtime delete_module fanotify_init finit_module get_mempolicy init_module io_destroy io_getevents iopl ioperm io_setup io_submit io_cancel kcmp kexec_load keyctl lookup_dcookie migrate_pages move_pages open_by_handle_at perf_event_open process_vm_readv process_vm_writev ptrace remap_file_pages request_key set_mempolicy swapoff swapon uselib vmsplice";
372 };
373
374 unitConfig.Documentation = "man:named(8)";
375 };
376 };
377}