1{ config, pkgs, lib, ... }:
2
3with lib;
4
5let
6 cfg = config.services.nsd;
7
8 username = "nsd";
9 stateDir = "/var/lib/nsd";
10 pidFile = stateDir + "/var/nsd.pid";
11
12 nsdPkg = pkgs.nsd.override {
13 bind8Stats = cfg.bind8Stats;
14 ipv6 = cfg.ipv6;
15 ratelimit = cfg.ratelimit.enable;
16 rootServer = cfg.rootServer;
17 zoneStats = length (collect (x: (x.zoneStats or null) != null) cfg.zones) > 0;
18 };
19
20 zoneFiles = pkgs.stdenv.mkDerivation {
21 preferLocalBuild = true;
22 name = "nsd-env";
23 buildCommand = concatStringsSep "\n"
24 [ "mkdir -p $out"
25 (concatStrings (mapAttrsToList (zoneName: zoneOptions: ''
26 cat > "$out/${zoneName}" <<_EOF_
27 ${zoneOptions.data}
28 _EOF_
29 '') zoneConfigs))
30 ];
31 };
32
33 configFile = pkgs.writeText "nsd.conf" ''
34 server:
35 username: ${username}
36 chroot: "${stateDir}"
37
38 # The directory for zonefile: files. The daemon chdirs here.
39 zonesdir: "${stateDir}"
40
41 # the list of dynamically added zones.
42 zonelistfile: "${stateDir}/var/zone.list"
43 database: "${stateDir}/var/nsd.db"
44 pidfile: "${pidFile}"
45 xfrdfile: "${stateDir}/var/xfrd.state"
46 xfrdir: "${stateDir}/tmp"
47
48 # interfaces
49 ${forEach " ip-address: " cfg.interfaces}
50
51 server-count: ${toString cfg.serverCount}
52 ip-transparent: ${yesOrNo cfg.ipTransparent}
53 do-ip4: ${yesOrNo cfg.ipv4}
54 do-ip6: ${yesOrNo cfg.ipv6}
55 port: ${toString cfg.port}
56 verbosity: ${toString cfg.verbosity}
57 hide-version: ${yesOrNo cfg.hideVersion}
58 identity: "${cfg.identity}"
59 ${maybeString "nsid: " cfg.nsid}
60 tcp-count: ${toString cfg.tcpCount}
61 tcp-query-count: ${toString cfg.tcpQueryCount}
62 tcp-timeout: ${toString cfg.tcpTimeout}
63 ipv4-edns-size: ${toString cfg.ipv4EDNSSize}
64 ipv6-edns-size: ${toString cfg.ipv6EDNSSize}
65 ${if cfg.statistics == null then "" else "statistics: ${toString cfg.statistics}"}
66 xfrd-reload-timeout: ${toString cfg.xfrdReloadTimeout}
67 zonefiles-check: ${yesOrNo cfg.zonefilesCheck}
68
69 rrl-size: ${toString cfg.ratelimit.size}
70 rrl-ratelimit: ${toString cfg.ratelimit.ratelimit}
71 rrl-whitelist-ratelimit: ${toString cfg.ratelimit.whitelistRatelimit}
72 ${maybeString "rrl-slip: " cfg.ratelimit.slip}
73 ${maybeString "rrl-ipv4-prefix-length: " cfg.ratelimit.ipv4PrefixLength}
74 ${maybeString "rrl-ipv6-prefix-length: " cfg.ratelimit.ipv6PrefixLength}
75
76 ${keyConfigFile}
77
78 remote-control:
79 control-enable: ${yesOrNo cfg.remoteControl.enable}
80 ${forEach " control-interface: " cfg.remoteControl.interfaces}
81 control-port: ${toString cfg.port}
82 server-key-file: "${cfg.remoteControl.serverKeyFile}"
83 server-cert-file: "${cfg.remoteControl.serverCertFile}"
84 control-key-file: "${cfg.remoteControl.controlKeyFile}"
85 control-cert-file: "${cfg.remoteControl.controlCertFile}"
86
87 # zone files reside in "${zoneFiles}" linked to "${stateDir}/zones"
88 ${concatStrings (mapAttrsToList zoneConfigFile zoneConfigs)}
89
90 ${cfg.extraConfig}
91 '';
92
93 yesOrNo = b: if b then "yes" else "no";
94 maybeString = pre: s: if s == null then "" else ''${pre} "${s}"'';
95 forEach = pre: l: concatMapStrings (x: pre + x + "\n") l;
96
97
98 keyConfigFile = concatStrings (mapAttrsToList (keyName: keyOptions: ''
99 key:
100 name: "${keyName}"
101 algorithm: "${keyOptions.algorithm}"
102 include: "${stateDir}/private/${keyName}"
103 '') cfg.keys);
104
105 copyKeys = concatStrings (mapAttrsToList (keyName: keyOptions: ''
106 secret=$(cat "${keyOptions.keyFile}")
107 dest="${stateDir}/private/${keyName}"
108 echo " secret: \"$secret\"" > "$dest"
109 ${pkgs.coreutils}/bin/chown ${username}:${username} "$dest"
110 ${pkgs.coreutils}/bin/chmod 0400 "$dest"
111 '') cfg.keys);
112
113
114 zoneConfigFile = name: zone: ''
115 zone:
116 name: "${name}"
117 zonefile: "${stateDir}/zones/${name}"
118 ${maybeString "zonestats: " zone.zoneStats}
119 ${maybeString "outgoing-interface: " zone.outgoingInterface}
120 ${forEach " rrl-whitelist: " zone.rrlWhitelist}
121
122 ${forEach " allow-notify: " zone.allowNotify}
123 ${forEach " request-xfr: " zone.requestXFR}
124 allow-axfr-fallback: ${yesOrNo zone.allowAXFRFallback}
125
126 ${forEach " notify: " zone.notify}
127 notify-retry: ${toString zone.notifyRetry}
128 ${forEach " provide-xfr: " zone.provideXFR}
129 '';
130
131 zoneConfigs = zoneConfigs' {} "" { children = cfg.zones; };
132
133 zoneConfigs' = parent: name: zone:
134 if !(zone ? children) || zone.children == null || zone.children == { }
135 # leaf -> actual zone
136 then listToAttrs [ (nameValuePair name (parent // zone)) ]
137
138 # fork -> pattern
139 else zipAttrsWith (name: head) (
140 mapAttrsToList (name: child: zoneConfigs' (parent // zone // { children = {}; }) name child)
141 zone.children
142 );
143
144 # fighting infinite recursion
145 zoneOptions = zoneOptionsRaw // childConfig zoneOptions1 true;
146 zoneOptions1 = zoneOptionsRaw // childConfig zoneOptions2 false;
147 zoneOptions2 = zoneOptionsRaw // childConfig zoneOptions3 false;
148 zoneOptions3 = zoneOptionsRaw // childConfig zoneOptions4 false;
149 zoneOptions4 = zoneOptionsRaw // childConfig zoneOptions5 false;
150 zoneOptions5 = zoneOptionsRaw // childConfig zoneOptions6 false;
151 zoneOptions6 = zoneOptionsRaw // childConfig null false;
152
153 childConfig = x: v: { options.children = { type = types.attrsOf x; visible = v; }; };
154
155 zoneOptionsRaw = types.submodule {
156 options = {
157 children = mkOption {
158 default = {};
159 description = ''
160 Children zones inherit all options of their parents. Attributes
161 defined in a child will overwrite the ones of its parent. Only
162 leaf zones will be actually served. This way it's possible to
163 define maybe zones which share most attributes without
164 duplicating everything. This mechanism replaces nsd's patterns
165 in a save and functional way.
166 '';
167 };
168
169 allowNotify = mkOption {
170 type = types.listOf types.str;
171 default = [ ];
172 example = [ "192.0.2.0/24 NOKEY" "10.0.0.1-10.0.0.5 my_tsig_key_name"
173 "10.0.3.4&255.255.0.0 BLOCKED"
174 ];
175 description = ''
176 Listed primary servers are allowed to notify this secondary server.
177 <screen><![CDATA[
178 Format: <ip> <key-name | NOKEY | BLOCKED>
179
180 <ip> either a plain IPv4/IPv6 address or range. Valid patters for ranges:
181 * 10.0.0.0/24 # via subnet size
182 * 10.0.0.0&255.255.255.0 # via subnet mask
183 * 10.0.0.1-10.0.0.254 # via range
184
185 A optional port number could be added with a '@':
186 * 2001:1234::1@1234
187
188 <key-name | NOKEY | BLOCKED>
189 * <key-name> will use the specified TSIG key
190 * NOKEY no TSIG signature is required
191 * BLOCKED notifies from non-listed or blocked IPs will be ignored
192 * ]]></screen>
193 '';
194 };
195
196 requestXFR = mkOption {
197 type = types.listOf types.str;
198 default = [];
199 example = [];
200 description = ''
201 Format: <code>[AXFR|UDP] <ip-address> <key-name | NOKEY></code>
202 '';
203 };
204
205 allowAXFRFallback = mkOption {
206 type = types.bool;
207 default = true;
208 description = ''
209 If NSD as secondary server should be allowed to AXFR if the primary
210 server does not allow IXFR.
211 '';
212 };
213
214 notify = mkOption {
215 type = types.listOf types.str;
216 default = [];
217 example = [ "10.0.0.1@3721 my_key" "::5 NOKEY" ];
218 description = ''
219 This primary server will notify all given secondary servers about
220 zone changes.
221 <screen><![CDATA[
222 Format: <ip> <key-name | NOKEY>
223
224 <ip> a plain IPv4/IPv6 address with on optional port number (ip@port)
225
226 <key-name | NOKEY>
227 * <key-name> sign notifies with the specified key
228 * NOKEY don't sign notifies
229 ]]></screen>
230 '';
231 };
232
233 notifyRetry = mkOption {
234 type = types.int;
235 default = 5;
236 description = ''
237 Specifies the number of retries for failed notifies. Set this along with notify.
238 '';
239 };
240
241 provideXFR = mkOption {
242 type = types.listOf types.str;
243 default = [];
244 example = [ "192.0.2.0/24 NOKEY" "192.0.2.0/24 my_tsig_key_name" ];
245 description = ''
246 Allow these IPs and TSIG to transfer zones, addr TSIG|NOKEY|BLOCKED
247 address range 192.0.2.0/24, 1.2.3.4&255.255.0.0, 3.0.2.20-3.0.2.40
248 '';
249 };
250
251 outgoingInterface = mkOption {
252 type = types.nullOr types.str;
253 default = null;
254 example = "2000::1@1234";
255 description = ''
256 This address will be used for zone-transfere requests if configured
257 as a secondary server or notifications in case of a primary server.
258 Supply either a plain IPv4 or IPv6 address with an optional port
259 number (ip@port).
260 '';
261 };
262
263 rrlWhitelist = mkOption {
264 type = types.listOf types.str;
265 default = [];
266 description = ''
267 Whitelists the given rrl-types.
268 The RRL classification types are: nxdomain, error, referral, any,
269 rrsig, wildcard, nodata, dnskey, positive, all
270 '';
271 };
272
273 data = mkOption {
274 type = types.str;
275 default = "";
276 example = "";
277 description = ''
278 The actual zone data. This is the content of your zone file.
279 Use imports or pkgs.lib.readFile if you don't want this data in your config file.
280 '';
281 };
282
283 zoneStats = mkOption {
284 type = types.nullOr types.str;
285 default = null;
286 example = "%s";
287 description = ''
288 When set to something distinct to null NSD is able to collect
289 statistics per zone. All statistics of this zone(s) will be added
290 to the group specified by this given name. Use "%s" to use the zones
291 name as the group. The groups are output from nsd-control stats
292 and stats_noreset.
293 '';
294 };
295 };
296 };
297
298in
299{
300 options = {
301 services.nsd = {
302
303 enable = mkEnableOption "NSD authoritative DNS server";
304 bind8Stats = mkEnableOption "BIND8 like statistics";
305
306 rootServer = mkOption {
307 type = types.bool;
308 default = false;
309 description = ''
310 Wheter if this server will be a root server (a DNS root server, you
311 usually don't want that).
312 '';
313 };
314
315 interfaces = mkOption {
316 type = types.listOf types.str;
317 default = [ "127.0.0.0" "::1" ];
318 description = ''
319 What addresses the server should listen to.
320 '';
321 };
322
323 serverCount = mkOption {
324 type = types.int;
325 default = 1;
326 description = ''
327 Number of NSD servers to fork. Put the number of CPUs to use here.
328 '';
329 };
330
331 ipTransparent = mkOption {
332 type = types.bool;
333 default = false;
334 description = ''
335 Allow binding to non local addresses.
336 '';
337 };
338
339 ipv4 = mkOption {
340 type = types.bool;
341 default = true;
342 description = ''
343 Wheter to listen on IPv4 connections.
344 '';
345 };
346
347 ipv6 = mkOption {
348 type = types.bool;
349 default = true;
350 description = ''
351 Wheter to listen on IPv6 connections.
352 '';
353 };
354
355 port = mkOption {
356 type = types.int;
357 default = 53;
358 description = ''
359 Port the service should bind do.
360 '';
361 };
362
363 verbosity = mkOption {
364 type = types.int;
365 default = 0;
366 description = ''
367 Verbosity level.
368 '';
369 };
370
371 hideVersion = mkOption {
372 type = types.bool;
373 default = true;
374 description = ''
375 Wheter NSD should answer VERSION.BIND and VERSION.SERVER CHAOS class queries.
376 '';
377 };
378
379 identity = mkOption {
380 type = types.str;
381 default = "unidentified server";
382 description = ''
383 Identify the server (CH TXT ID.SERVER entry).
384 '';
385 };
386
387 nsid = mkOption {
388 type = types.nullOr types.str;
389 default = null;
390 description = ''
391 NSID identity (hex string, or "ascii_somestring").
392 '';
393 };
394
395 tcpCount = mkOption {
396 type = types.int;
397 default = 100;
398 description = ''
399 Maximum number of concurrent TCP connections per server.
400 '';
401 };
402
403 tcpQueryCount = mkOption {
404 type = types.int;
405 default = 0;
406 description = ''
407 Maximum number of queries served on a single TCP connection.
408 0 means no maximum.
409 '';
410 };
411
412 tcpTimeout = mkOption {
413 type = types.int;
414 default = 120;
415 description = ''
416 TCP timeout in seconds.
417 '';
418 };
419
420 ipv4EDNSSize = mkOption {
421 type = types.int;
422 default = 4096;
423 description = ''
424 Preferred EDNS buffer size for IPv4.
425 '';
426 };
427
428 ipv6EDNSSize = mkOption {
429 type = types.int;
430 default = 4096;
431 description = ''
432 Preferred EDNS buffer size for IPv6.
433 '';
434 };
435
436 statistics = mkOption {
437 type = types.nullOr types.int;
438 default = null;
439 description = ''
440 Statistics are produced every number of seconds. Prints to log.
441 If null no statistics are logged.
442 '';
443 };
444
445 xfrdReloadTimeout = mkOption {
446 type = types.int;
447 default = 1;
448 description = ''
449 Number of seconds between reloads triggered by xfrd.
450 '';
451 };
452
453 zonefilesCheck = mkOption {
454 type = types.bool;
455 default = true;
456 description = ''
457 Wheter to check mtime of all zone files on start and sighup.
458 '';
459 };
460
461
462 extraConfig = mkOption {
463 type = types.str;
464 default = "";
465 description = ''
466 Extra nsd config.
467 '';
468 };
469
470
471 ratelimit = {
472 enable = mkEnableOption "ratelimit capabilities";
473
474 size = mkOption {
475 type = types.int;
476 default = 1000000;
477 description = ''
478 Size of the hashtable. More buckets use more memory but lower
479 the chance of hash hash collisions.
480 '';
481 };
482
483 ratelimit = mkOption {
484 type = types.int;
485 default = 200;
486 description = ''
487 Max qps allowed from any query source.
488 0 means unlimited. With an verbosity of 2 blocked and
489 unblocked subnets will be logged.
490 '';
491 };
492
493 whitelistRatelimit = mkOption {
494 type = types.int;
495 default = 2000;
496 description = ''
497 Max qps allowed from whitelisted sources.
498 0 means unlimited. Set the rrl-whitelist option for specific
499 queries to apply this limit instead of the default to them.
500 '';
501 };
502
503 slip = mkOption {
504 type = types.nullOr types.int;
505 default = null;
506 description = ''
507 Number of packets that get discarded before replying a SLIP response.
508 0 disables SLIP responses. 1 will make every response a SLIP response.
509 '';
510 };
511
512 ipv4PrefixLength = mkOption {
513 type = types.nullOr types.int;
514 default = null;
515 description = ''
516 IPv4 prefix length. Addresses are grouped by netblock.
517 '';
518 };
519
520 ipv6PrefixLength = mkOption {
521 type = types.nullOr types.int;
522 default = null;
523 description = ''
524 IPv6 prefix length. Addresses are grouped by netblock.
525 '';
526 };
527 };
528
529
530 remoteControl = {
531 enable = mkEnableOption "remote control via nsd-control";
532
533 interfaces = mkOption {
534 type = types.listOf types.str;
535 default = [ "127.0.0.1" "::1" ];
536 description = ''
537 Which interfaces NSD should bind to for remote control.
538 '';
539 };
540
541 port = mkOption {
542 type = types.int;
543 default = 8952;
544 description = ''
545 Port number for remote control operations (uses TLS over TCP).
546 '';
547 };
548
549 serverKeyFile = mkOption {
550 type = types.path;
551 default = "/etc/nsd/nsd_server.key";
552 description = ''
553 Path to the server private key, which is used by the server
554 but not by nsd-control. This file is generated by nsd-control-setup.
555 '';
556 };
557
558 serverCertFile = mkOption {
559 type = types.path;
560 default = "/etc/nsd/nsd_server.pem";
561 description = ''
562 Path to the server self signed certificate, which is used by the server
563 but and by nsd-control. This file is generated by nsd-control-setup.
564 '';
565 };
566
567 controlKeyFile = mkOption {
568 type = types.path;
569 default = "/etc/nsd/nsd_control.key";
570 description = ''
571 Path to the client private key, which is used by nsd-control
572 but not by the server. This file is generated by nsd-control-setup.
573 '';
574 };
575
576 controlCertFile = mkOption {
577 type = types.path;
578 default = "/etc/nsd/nsd_control.pem";
579 description = ''
580 Path to the client certificate signed with the server certificate.
581 This file is used by nsd-control and generated by nsd-control-setup.
582 '';
583 };
584 };
585
586
587 keys = mkOption {
588 type = types.attrsOf (types.submodule {
589 options = {
590 algorithm = mkOption {
591 type = types.str;
592 default = "hmac-sha256";
593 description = ''
594 Authentication algorithm for this key.
595 '';
596 };
597
598 keyFile = mkOption {
599 type = types.path;
600 description = ''
601 Path to the file which contains the actual base64 encoded
602 key. The key will be copied into "${stateDir}/private" before
603 NSD starts. The copied file is only accessibly by the NSD
604 user.
605 '';
606 };
607 };
608 });
609 default = {};
610 example = {
611 "tsig.example.org" = {
612 algorithm = "hmac-md5";
613 secret = "aaaaaabbbbbbccccccdddddd";
614 };
615 };
616 description = ''
617 Define your TSIG keys here.
618 '';
619 };
620
621 zones = mkOption {
622 type = types.attrsOf zoneOptions;
623 default = {};
624 example = literalExample ''
625 { "serverGroup1" = {
626 provideXFR = [ "10.1.2.3 NOKEY" ];
627 children = {
628 "example.com." = {
629 data = '''
630 $ORIGIN example.com.
631 $TTL 86400
632 @ IN SOA a.ns.example.com. admin.example.com. (
633 ...
634 ''';
635 };
636 "example.org." = {
637 data = '''
638 $ORIGIN example.org.
639 $TTL 86400
640 @ IN SOA a.ns.example.com. admin.example.com. (
641 ...
642 ''';
643 };
644 };
645 };
646
647 "example.net." = {
648 provideXFR = [ "10.3.2.1 NOKEY" ];
649 data = '''
650 ...
651 ''';
652 };
653 }
654 '';
655 description = ''
656 Define your zones here. Zones can cascade other zones and therefore
657 inherit settings from parent zones. Look at the definition of
658 children to learn about inheritance and child zones.
659 The given example will define 3 zones (example.(com|org|net).). Both
660 example.com. and example.org. inherit their configuration from
661 serverGroup1.
662 '';
663 };
664
665 };
666 };
667
668 config = mkIf cfg.enable {
669
670 users.extraGroups = singleton {
671 name = username;
672 gid = config.ids.gids.nsd;
673 };
674
675 users.extraUsers = singleton {
676 name = username;
677 description = "NSD service user";
678 home = stateDir;
679 createHome = true;
680 uid = config.ids.uids.nsd;
681 group = username;
682 };
683
684 systemd.services.nsd = {
685 description = "NSD authoritative only domain name service";
686 wantedBy = [ "multi-user.target" ];
687 after = [ "network.target" ];
688
689 serviceConfig = {
690 PIDFile = pidFile;
691 Restart = "always";
692 ExecStart = "${nsdPkg}/sbin/nsd -d -c ${configFile}";
693 };
694
695 preStart = ''
696 ${pkgs.coreutils}/bin/mkdir -m 0700 -p "${stateDir}/private"
697 ${pkgs.coreutils}/bin/mkdir -m 0700 -p "${stateDir}/tmp"
698 ${pkgs.coreutils}/bin/mkdir -m 0700 -p "${stateDir}/var"
699
700 ${pkgs.coreutils}/bin/touch "${stateDir}/don't touch anything in here"
701
702 ${pkgs.coreutils}/bin/rm -f "${stateDir}/private/"*
703 ${pkgs.coreutils}/bin/rm -f "${stateDir}/tmp/"*
704
705 ${pkgs.coreutils}/bin/chown nsd:nsd -R "${stateDir}/private"
706 ${pkgs.coreutils}/bin/chown nsd:nsd -R "${stateDir}/tmp"
707 ${pkgs.coreutils}/bin/chown nsd:nsd -R "${stateDir}/var"
708
709 ${pkgs.coreutils}/bin/rm -rf "${stateDir}/zones"
710 ${pkgs.coreutils}/bin/cp -r "${zoneFiles}" "${stateDir}/zones"
711
712 ${copyKeys}
713 '';
714 };
715
716 };
717}