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 = mkOption {
304 type = types.bool;
305 default = false;
306 description = ''
307 Whether to enable the NSD authoritative domain name server.
308 '';
309 };
310
311 bind8Stats = mkOption {
312 type = types.bool;
313 default = false;
314 example = true;
315 description = ''
316 Wheter to enable BIND8 like statisics.
317 '';
318 };
319
320 rootServer = mkOption {
321 type = types.bool;
322 default = false;
323 description = ''
324 Wheter if this server will be a root server (a DNS root server, you
325 usually don't want that).
326 '';
327 };
328
329 interfaces = mkOption {
330 type = types.listOf types.str;
331 default = [ "127.0.0.0" "::1" ];
332 description = ''
333 What addresses the server should listen to.
334 '';
335 };
336
337 serverCount = mkOption {
338 type = types.int;
339 default = 1;
340 description = ''
341 Number of NSD servers to fork. Put the number of CPUs to use here.
342 '';
343 };
344
345 ipTransparent = mkOption {
346 type = types.bool;
347 default = false;
348 description = ''
349 Allow binding to non local addresses.
350 '';
351 };
352
353 ipv4 = mkOption {
354 type = types.bool;
355 default = true;
356 description = ''
357 Wheter to listen on IPv4 connections.
358 '';
359 };
360
361 ipv6 = mkOption {
362 type = types.bool;
363 default = true;
364 description = ''
365 Wheter to listen on IPv6 connections.
366 '';
367 };
368
369 port = mkOption {
370 type = types.int;
371 default = 53;
372 description = ''
373 Port the service should bind do.
374 '';
375 };
376
377 verbosity = mkOption {
378 type = types.int;
379 default = 0;
380 description = ''
381 Verbosity level.
382 '';
383 };
384
385 hideVersion = mkOption {
386 type = types.bool;
387 default = true;
388 description = ''
389 Wheter NSD should answer VERSION.BIND and VERSION.SERVER CHAOS class queries.
390 '';
391 };
392
393 identity = mkOption {
394 type = types.str;
395 default = "unidentified server";
396 description = ''
397 Identify the server (CH TXT ID.SERVER entry).
398 '';
399 };
400
401 nsid = mkOption {
402 type = types.nullOr types.str;
403 default = null;
404 description = ''
405 NSID identity (hex string, or "ascii_somestring").
406 '';
407 };
408
409 tcpCount = mkOption {
410 type = types.int;
411 default = 100;
412 description = ''
413 Maximum number of concurrent TCP connections per server.
414 '';
415 };
416
417 tcpQueryCount = mkOption {
418 type = types.int;
419 default = 0;
420 description = ''
421 Maximum number of queries served on a single TCP connection.
422 0 means no maximum.
423 '';
424 };
425
426 tcpTimeout = mkOption {
427 type = types.int;
428 default = 120;
429 description = ''
430 TCP timeout in seconds.
431 '';
432 };
433
434 ipv4EDNSSize = mkOption {
435 type = types.int;
436 default = 4096;
437 description = ''
438 Preferred EDNS buffer size for IPv4.
439 '';
440 };
441
442 ipv6EDNSSize = mkOption {
443 type = types.int;
444 default = 4096;
445 description = ''
446 Preferred EDNS buffer size for IPv6.
447 '';
448 };
449
450 statistics = mkOption {
451 type = types.nullOr types.int;
452 default = null;
453 description = ''
454 Statistics are produced every number of seconds. Prints to log.
455 If null no statistics are logged.
456 '';
457 };
458
459 xfrdReloadTimeout = mkOption {
460 type = types.int;
461 default = 1;
462 description = ''
463 Number of seconds between reloads triggered by xfrd.
464 '';
465 };
466
467 zonefilesCheck = mkOption {
468 type = types.bool;
469 default = true;
470 description = ''
471 Wheter to check mtime of all zone files on start and sighup.
472 '';
473 };
474
475
476 extraConfig = mkOption {
477 type = types.str;
478 default = "";
479 description = ''
480 Extra nsd config.
481 '';
482 };
483
484
485 ratelimit = {
486 enable = mkOption {
487 type = types.bool;
488 default = false;
489 description = ''
490 Enable ratelimit capabilities.
491 '';
492 };
493
494 size = mkOption {
495 type = types.int;
496 default = 1000000;
497 description = ''
498 Size of the hashtable. More buckets use more memory but lower
499 the chance of hash hash collisions.
500 '';
501 };
502
503 ratelimit = mkOption {
504 type = types.int;
505 default = 200;
506 description = ''
507 Max qps allowed from any query source.
508 0 means unlimited. With an verbosity of 2 blocked and
509 unblocked subnets will be logged.
510 '';
511 };
512
513 whitelistRatelimit = mkOption {
514 type = types.int;
515 default = 2000;
516 description = ''
517 Max qps allowed from whitelisted sources.
518 0 means unlimited. Set the rrl-whitelist option for specific
519 queries to apply this limit instead of the default to them.
520 '';
521 };
522
523 slip = mkOption {
524 type = types.nullOr types.int;
525 default = null;
526 description = ''
527 Number of packets that get discarded before replying a SLIP response.
528 0 disables SLIP responses. 1 will make every response a SLIP response.
529 '';
530 };
531
532 ipv4PrefixLength = mkOption {
533 type = types.nullOr types.int;
534 default = null;
535 description = ''
536 IPv4 prefix length. Addresses are grouped by netblock.
537 '';
538 };
539
540 ipv6PrefixLength = mkOption {
541 type = types.nullOr types.int;
542 default = null;
543 description = ''
544 IPv6 prefix length. Addresses are grouped by netblock.
545 '';
546 };
547 };
548
549
550 remoteControl = {
551 enable = mkOption {
552 type = types.bool;
553 default = false;
554 description = ''
555 Wheter to enable remote control via nsd-control(8).
556 '';
557 };
558
559 interfaces = mkOption {
560 type = types.listOf types.str;
561 default = [ "127.0.0.1" "::1" ];
562 description = ''
563 Which interfaces NSD should bind to for remote control.
564 '';
565 };
566
567 port = mkOption {
568 type = types.int;
569 default = 8952;
570 description = ''
571 Port number for remote control operations (uses TLS over TCP).
572 '';
573 };
574
575 serverKeyFile = mkOption {
576 type = types.path;
577 default = "/etc/nsd/nsd_server.key";
578 description = ''
579 Path to the server private key, which is used by the server
580 but not by nsd-control. This file is generated by nsd-control-setup.
581 '';
582 };
583
584 serverCertFile = mkOption {
585 type = types.path;
586 default = "/etc/nsd/nsd_server.pem";
587 description = ''
588 Path to the server self signed certificate, which is used by the server
589 but and by nsd-control. This file is generated by nsd-control-setup.
590 '';
591 };
592
593 controlKeyFile = mkOption {
594 type = types.path;
595 default = "/etc/nsd/nsd_control.key";
596 description = ''
597 Path to the client private key, which is used by nsd-control
598 but not by the server. This file is generated by nsd-control-setup.
599 '';
600 };
601
602 controlCertFile = mkOption {
603 type = types.path;
604 default = "/etc/nsd/nsd_control.pem";
605 description = ''
606 Path to the client certificate signed with the server certificate.
607 This file is used by nsd-control and generated by nsd-control-setup.
608 '';
609 };
610 };
611
612
613 keys = mkOption {
614 type = types.attrsOf (types.submodule {
615 options = {
616 algorithm = mkOption {
617 type = types.str;
618 default = "hmac-sha256";
619 description = ''
620 Authentication algorithm for this key.
621 '';
622 };
623
624 keyFile = mkOption {
625 type = types.path;
626 description = ''
627 Path to the file which contains the actual base64 encoded
628 key. The key will be copied into "${stateDir}/private" before
629 NSD starts. The copied file is only accessibly by the NSD
630 user.
631 '';
632 };
633 };
634 });
635 default = {};
636 example = {
637 "tsig.example.org" = {
638 algorithm = "hmac-md5";
639 secret = "aaaaaabbbbbbccccccdddddd";
640 };
641 };
642 description = ''
643 Define your TSIG keys here.
644 '';
645 };
646
647 zones = mkOption {
648 type = types.attrsOf zoneOptions;
649 default = {};
650 example = {
651 "serverGroup1" = {
652 provideXFR = [ "10.1.2.3 NOKEY" ];
653 children = {
654 "example.com." = {
655 data = ''
656 $ORIGIN example.com.
657 $TTL 86400
658 @ IN SOA a.ns.example.com. admin.example.com. (
659 ...
660 '';
661 };
662 "example.org." = {
663 data = ''
664 $ORIGIN example.org.
665 $TTL 86400
666 @ IN SOA a.ns.example.com. admin.example.com. (
667 ...
668 '';
669 };
670 };
671 };
672
673 "example.net." = {
674 provideXFR = [ "10.3.2.1 NOKEY" ];
675 data = ''...'';
676 };
677 };
678 description = ''
679 Define your zones here. Zones can cascade other zones and therefore
680 inherit settings from parent zones. Look at the definition of
681 children to learn about inheritance and child zones.
682 The given example will define 3 zones (example.(com|org|net).). Both
683 example.com. and example.org. inherit their configuration from
684 serverGroup1.
685 '';
686 };
687
688 };
689 };
690
691 config = mkIf cfg.enable {
692
693 users.extraGroups = singleton {
694 name = username;
695 gid = config.ids.gids.nsd;
696 };
697
698 users.extraUsers = singleton {
699 name = username;
700 description = "NSD service user";
701 home = stateDir;
702 createHome = true;
703 uid = config.ids.uids.nsd;
704 group = username;
705 };
706
707 systemd.services.nsd = {
708 description = "NSD authoritative only domain name service";
709 wantedBy = [ "multi-user.target" ];
710 after = [ "network.target" ];
711
712 serviceConfig = {
713 PIDFile = pidFile;
714 Restart = "always";
715 ExecStart = "${nsdPkg}/sbin/nsd -d -c ${configFile}";
716 };
717
718 preStart = ''
719 ${pkgs.coreutils}/bin/mkdir -m 0700 -p "${stateDir}/private"
720 ${pkgs.coreutils}/bin/mkdir -m 0700 -p "${stateDir}/tmp"
721 ${pkgs.coreutils}/bin/mkdir -m 0700 -p "${stateDir}/var"
722
723 ${pkgs.coreutils}/bin/touch "${stateDir}/don't touch anything in here"
724
725 ${pkgs.coreutils}/bin/rm -f "${stateDir}/private/"*
726 ${pkgs.coreutils}/bin/rm -f "${stateDir}/tmp/"*
727
728 ${pkgs.coreutils}/bin/chown nsd:nsd -R "${stateDir}/private"
729 ${pkgs.coreutils}/bin/chown nsd:nsd -R "${stateDir}/tmp"
730 ${pkgs.coreutils}/bin/chown nsd:nsd -R "${stateDir}/var"
731
732 ${pkgs.coreutils}/bin/rm -rf "${stateDir}/zones"
733 ${pkgs.coreutils}/bin/cp -r "${zoneFiles}" "${stateDir}/zones"
734
735 ${copyKeys}
736 '';
737 };
738
739 };
740}