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