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 mkZoneFileName = name: if name == "." then "root" else name;
22
23 # replaces include: directives for keys with fake keys for nsd-checkconf
24 injectFakeKeys = keys: concatStrings
25 (mapAttrsToList
26 (keyName: keyOptions: ''
27 fakeKey="$(${pkgs.bind}/bin/tsig-keygen -a ${escapeShellArgs [ keyOptions.algorithm keyName ]} | grep -oP "\s*secret \"\K.*(?=\";)")"
28 sed "s@^\s*include:\s*\"${stateDir}/private/${keyName}\"\$@secret: $fakeKey@" -i $out/nsd.conf
29 '')
30 keys);
31
32 nsdEnv = pkgs.buildEnv {
33 name = "nsd-env";
34
35 paths = [ configFile ]
36 ++ mapAttrsToList (name: zone: writeZoneData name zone.data) zoneConfigs;
37
38 postBuild = ''
39 echo "checking zone files"
40 cd $out/zones
41
42 for zoneFile in *; do
43 echo "|- checking zone '$out/zones/$zoneFile'"
44 ${nsdPkg}/sbin/nsd-checkzone "$zoneFile" "$zoneFile" || {
45 if grep -q \\\\\\$ "$zoneFile"; then
46 echo zone "$zoneFile" contains escaped dollar signs \\\$
47 echo Escaping them is not needed any more. Please make sure \
48 to unescape them where they prefix a variable name.
49 fi
50
51 exit 1
52 }
53 done
54
55 echo "checking configuration file"
56 # Save original config file including key references...
57 cp $out/nsd.conf{,.orig}
58 # ...inject mock keys into config
59 ${injectFakeKeys cfg.keys}
60 # ...do the checkconf
61 ${nsdPkg}/sbin/nsd-checkconf $out/nsd.conf
62 # ... and restore original config file.
63 mv $out/nsd.conf{.orig,}
64 '';
65 };
66
67 writeZoneData = name: text: pkgs.writeTextFile {
68 name = "nsd-zone-${mkZoneFileName name}";
69 inherit text;
70 destination = "/zones/${mkZoneFileName name}";
71 };
72
73
74 # options are ordered alphanumerically by the nixos option name
75 configFile = pkgs.writeTextDir "nsd.conf" ''
76 server:
77 chroot: "${stateDir}"
78 username: ${username}
79
80 # The directory for zonefile: files. The daemon chdirs here.
81 zonesdir: "${stateDir}"
82
83 # the list of dynamically added zones.
84 database: "${stateDir}/var/nsd.db"
85 pidfile: "${pidFile}"
86 xfrdfile: "${stateDir}/var/xfrd.state"
87 xfrdir: "${stateDir}/tmp"
88 zonelistfile: "${stateDir}/var/zone.list"
89
90 # interfaces
91 ${forEach " ip-address: " cfg.interfaces}
92
93 ip-freebind: ${yesOrNo cfg.ipFreebind}
94 hide-version: ${yesOrNo cfg.hideVersion}
95 identity: "${cfg.identity}"
96 ip-transparent: ${yesOrNo cfg.ipTransparent}
97 do-ip4: ${yesOrNo cfg.ipv4}
98 ipv4-edns-size: ${toString cfg.ipv4EDNSSize}
99 do-ip6: ${yesOrNo cfg.ipv6}
100 ipv6-edns-size: ${toString cfg.ipv6EDNSSize}
101 log-time-ascii: ${yesOrNo cfg.logTimeAscii}
102 ${maybeString "nsid: " cfg.nsid}
103 port: ${toString cfg.port}
104 reuseport: ${yesOrNo cfg.reuseport}
105 round-robin: ${yesOrNo cfg.roundRobin}
106 server-count: ${toString cfg.serverCount}
107 ${maybeToString "statistics: " cfg.statistics}
108 tcp-count: ${toString cfg.tcpCount}
109 tcp-query-count: ${toString cfg.tcpQueryCount}
110 tcp-timeout: ${toString cfg.tcpTimeout}
111 verbosity: ${toString cfg.verbosity}
112 ${maybeString "version: " cfg.version}
113 xfrd-reload-timeout: ${toString cfg.xfrdReloadTimeout}
114 zonefiles-check: ${yesOrNo cfg.zonefilesCheck}
115
116 ${maybeString "rrl-ipv4-prefix-length: " cfg.ratelimit.ipv4PrefixLength}
117 ${maybeString "rrl-ipv6-prefix-length: " cfg.ratelimit.ipv6PrefixLength}
118 rrl-ratelimit: ${toString cfg.ratelimit.ratelimit}
119 ${maybeString "rrl-slip: " cfg.ratelimit.slip}
120 rrl-size: ${toString cfg.ratelimit.size}
121 rrl-whitelist-ratelimit: ${toString cfg.ratelimit.whitelistRatelimit}
122
123 ${keyConfigFile}
124
125 remote-control:
126 control-enable: ${yesOrNo cfg.remoteControl.enable}
127 control-key-file: "${cfg.remoteControl.controlKeyFile}"
128 control-cert-file: "${cfg.remoteControl.controlCertFile}"
129 ${forEach " control-interface: " cfg.remoteControl.interfaces}
130 control-port: ${toString cfg.remoteControl.port}
131 server-key-file: "${cfg.remoteControl.serverKeyFile}"
132 server-cert-file: "${cfg.remoteControl.serverCertFile}"
133
134 ${concatStrings (mapAttrsToList zoneConfigFile zoneConfigs)}
135
136 ${cfg.extraConfig}
137 '';
138
139 yesOrNo = b: if b then "yes" else "no";
140 maybeString = prefix: x: if x == null then "" else ''${prefix} "${x}"'';
141 maybeToString = prefix: x: if x == null then "" else ''${prefix} ${toString x}'';
142 forEach = pre: l: concatMapStrings (x: pre + x + "\n") l;
143
144
145 keyConfigFile = concatStrings (mapAttrsToList (keyName: keyOptions: ''
146 key:
147 name: "${keyName}"
148 algorithm: "${keyOptions.algorithm}"
149 include: "${stateDir}/private/${keyName}"
150 '') cfg.keys);
151
152 copyKeys = concatStrings (mapAttrsToList (keyName: keyOptions: ''
153 secret=$(cat "${keyOptions.keyFile}")
154 dest="${stateDir}/private/${keyName}"
155 echo " secret: \"$secret\"" > "$dest"
156 chown ${username}:${username} "$dest"
157 chmod 0400 "$dest"
158 '') cfg.keys);
159
160
161 # options are ordered alphanumerically by the nixos option name
162 zoneConfigFile = name: zone: ''
163 zone:
164 name: "${name}"
165 zonefile: "${stateDir}/zones/${mkZoneFileName name}"
166 ${maybeString "outgoing-interface: " zone.outgoingInterface}
167 ${forEach " rrl-whitelist: " zone.rrlWhitelist}
168 ${maybeString "zonestats: " zone.zoneStats}
169
170 ${maybeToString "max-refresh-time: " zone.maxRefreshSecs}
171 ${maybeToString "min-refresh-time: " zone.minRefreshSecs}
172 ${maybeToString "max-retry-time: " zone.maxRetrySecs}
173 ${maybeToString "min-retry-time: " zone.minRetrySecs}
174
175 allow-axfr-fallback: ${yesOrNo zone.allowAXFRFallback}
176 ${forEach " allow-notify: " zone.allowNotify}
177 ${forEach " request-xfr: " zone.requestXFR}
178
179 ${forEach " notify: " zone.notify}
180 notify-retry: ${toString zone.notifyRetry}
181 ${forEach " provide-xfr: " zone.provideXFR}
182 '';
183
184 zoneConfigs = zoneConfigs' {} "" { children = cfg.zones; };
185
186 zoneConfigs' = parent: name: zone:
187 if !(zone ? children) || zone.children == null || zone.children == { }
188 # leaf -> actual zone
189 then listToAttrs [ (nameValuePair name (parent // zone)) ]
190
191 # fork -> pattern
192 else zipAttrsWith (name: head) (
193 mapAttrsToList (name: child: zoneConfigs' (parent // zone // { children = {}; }) name child)
194 zone.children
195 );
196
197 # fighting infinite recursion
198 zoneOptions = zoneOptionsRaw // childConfig zoneOptions1 true;
199 zoneOptions1 = zoneOptionsRaw // childConfig zoneOptions2 false;
200 zoneOptions2 = zoneOptionsRaw // childConfig zoneOptions3 false;
201 zoneOptions3 = zoneOptionsRaw // childConfig zoneOptions4 false;
202 zoneOptions4 = zoneOptionsRaw // childConfig zoneOptions5 false;
203 zoneOptions5 = zoneOptionsRaw // childConfig zoneOptions6 false;
204 zoneOptions6 = zoneOptionsRaw // childConfig null false;
205
206 childConfig = x: v: { options.children = { type = types.attrsOf x; visible = v; }; };
207
208 # options are ordered alphanumerically
209 zoneOptionsRaw = types.submodule {
210 options = {
211
212 allowAXFRFallback = mkOption {
213 type = types.bool;
214 default = true;
215 description = ''
216 If NSD as secondary server should be allowed to AXFR if the primary
217 server does not allow IXFR.
218 '';
219 };
220
221 allowNotify = mkOption {
222 type = types.listOf types.str;
223 default = [ ];
224 example = [ "192.0.2.0/24 NOKEY" "10.0.0.1-10.0.0.5 my_tsig_key_name"
225 "10.0.3.4&255.255.0.0 BLOCKED"
226 ];
227 description = ''
228 Listed primary servers are allowed to notify this secondary server.
229 <screen><![CDATA[
230 Format: <ip> <key-name | NOKEY | BLOCKED>
231
232 <ip> either a plain IPv4/IPv6 address or range. Valid patters for ranges:
233 * 10.0.0.0/24 # via subnet size
234 * 10.0.0.0&255.255.255.0 # via subnet mask
235 * 10.0.0.1-10.0.0.254 # via range
236
237 A optional port number could be added with a '@':
238 * 2001:1234::1@1234
239
240 <key-name | NOKEY | BLOCKED>
241 * <key-name> will use the specified TSIG key
242 * NOKEY no TSIG signature is required
243 * BLOCKED notifies from non-listed or blocked IPs will be ignored
244 * ]]></screen>
245 '';
246 };
247
248 children = mkOption {
249 default = {};
250 description = ''
251 Children zones inherit all options of their parents. Attributes
252 defined in a child will overwrite the ones of its parent. Only
253 leaf zones will be actually served. This way it's possible to
254 define maybe zones which share most attributes without
255 duplicating everything. This mechanism replaces nsd's patterns
256 in a save and functional way.
257 '';
258 };
259
260 data = mkOption {
261 type = types.lines;
262 default = "";
263 example = "";
264 description = ''
265 The actual zone data. This is the content of your zone file.
266 Use imports or pkgs.lib.readFile if you don't want this data in your config file.
267 '';
268 };
269
270 dnssec = mkEnableOption "DNSSEC";
271
272 dnssecPolicy = {
273 algorithm = mkOption {
274 type = types.str;
275 default = "RSASHA256";
276 description = "Which algorithm to use for DNSSEC";
277 };
278 keyttl = mkOption {
279 type = types.str;
280 default = "1h";
281 description = "TTL for dnssec records";
282 };
283 coverage = mkOption {
284 type = types.str;
285 default = "1y";
286 description = ''
287 The length of time to ensure that keys will be correct; no action will be taken to create new keys to be activated after this time.
288 '';
289 };
290 zsk = mkOption {
291 type = keyPolicy;
292 default = { keySize = 2048;
293 prePublish = "1w";
294 postPublish = "1w";
295 rollPeriod = "1mo";
296 };
297 description = "Key policy for zone signing keys";
298 };
299 ksk = mkOption {
300 type = keyPolicy;
301 default = { keySize = 4096;
302 prePublish = "1mo";
303 postPublish = "1mo";
304 rollPeriod = "0";
305 };
306 description = "Key policy for key signing keys";
307 };
308 };
309
310 maxRefreshSecs = mkOption {
311 type = types.nullOr types.int;
312 default = null;
313 description = ''
314 Limit refresh time for secondary zones. This is the timer which
315 checks to see if the zone has to be refetched when it expires.
316 Normally the value from the SOA record is used, but this option
317 restricts that value.
318 '';
319 };
320
321 minRefreshSecs = mkOption {
322 type = types.nullOr types.int;
323 default = null;
324 description = ''
325 Limit refresh time for secondary zones.
326 '';
327 };
328
329 maxRetrySecs = mkOption {
330 type = types.nullOr types.int;
331 default = null;
332 description = ''
333 Limit retry time for secondary zones. This is the timeout after
334 a failed fetch attempt for the zone. Normally the value from
335 the SOA record is used, but this option restricts that value.
336 '';
337 };
338
339 minRetrySecs = mkOption {
340 type = types.nullOr types.int;
341 default = null;
342 description = ''
343 Limit retry time for secondary zones.
344 '';
345 };
346
347
348 notify = mkOption {
349 type = types.listOf types.str;
350 default = [];
351 example = [ "10.0.0.1@3721 my_key" "::5 NOKEY" ];
352 description = ''
353 This primary server will notify all given secondary servers about
354 zone changes.
355 <screen><![CDATA[
356 Format: <ip> <key-name | NOKEY>
357
358 <ip> a plain IPv4/IPv6 address with on optional port number (ip@port)
359
360 <key-name | NOKEY>
361 * <key-name> sign notifies with the specified key
362 * NOKEY don't sign notifies
363 ]]></screen>
364 '';
365 };
366
367 notifyRetry = mkOption {
368 type = types.int;
369 default = 5;
370 description = ''
371 Specifies the number of retries for failed notifies. Set this along with notify.
372 '';
373 };
374
375 outgoingInterface = mkOption {
376 type = types.nullOr types.str;
377 default = null;
378 example = "2000::1@1234";
379 description = ''
380 This address will be used for zone-transfere requests if configured
381 as a secondary server or notifications in case of a primary server.
382 Supply either a plain IPv4 or IPv6 address with an optional port
383 number (ip@port).
384 '';
385 };
386
387 provideXFR = mkOption {
388 type = types.listOf types.str;
389 default = [];
390 example = [ "192.0.2.0/24 NOKEY" "192.0.2.0/24 my_tsig_key_name" ];
391 description = ''
392 Allow these IPs and TSIG to transfer zones, addr TSIG|NOKEY|BLOCKED
393 address range 192.0.2.0/24, 1.2.3.4&255.255.0.0, 3.0.2.20-3.0.2.40
394 '';
395 };
396
397 requestXFR = mkOption {
398 type = types.listOf types.str;
399 default = [];
400 example = [];
401 description = ''
402 Format: <code>[AXFR|UDP] <ip-address> <key-name | NOKEY></code>
403 '';
404 };
405
406 rrlWhitelist = mkOption {
407 type = with types; listOf (enum [ "nxdomain" "error" "referral" "any" "rrsig" "wildcard" "nodata" "dnskey" "positive" "all" ]);
408 default = [];
409 description = ''
410 Whitelists the given rrl-types.
411 '';
412 };
413
414 zoneStats = mkOption {
415 type = types.nullOr types.str;
416 default = null;
417 example = "%s";
418 description = ''
419 When set to something distinct to null NSD is able to collect
420 statistics per zone. All statistics of this zone(s) will be added
421 to the group specified by this given name. Use "%s" to use the zones
422 name as the group. The groups are output from nsd-control stats
423 and stats_noreset.
424 '';
425 };
426 };
427 };
428
429 keyPolicy = types.submodule {
430 options = {
431 keySize = mkOption {
432 type = types.int;
433 description = "Key size in bits";
434 };
435 prePublish = mkOption {
436 type = types.str;
437 description = "How long in advance to publish new keys";
438 };
439 postPublish = mkOption {
440 type = types.str;
441 description = "How long after deactivation to keep a key in the zone";
442 };
443 rollPeriod = mkOption {
444 type = types.str;
445 description = "How frequently to change keys";
446 };
447 };
448 };
449
450 dnssecZones = (filterAttrs (n: v: if v ? dnssec then v.dnssec else false) zoneConfigs);
451
452 dnssec = dnssecZones != {};
453
454 dnssecTools = pkgs.bind.override { enablePython = true; };
455
456 signZones = optionalString dnssec ''
457 mkdir -p ${stateDir}/dnssec
458 chown ${username}:${username} ${stateDir}/dnssec
459 chmod 0600 ${stateDir}/dnssec
460
461 ${concatStrings (mapAttrsToList signZone dnssecZones)}
462 '';
463 signZone = name: zone: ''
464 ${dnssecTools}/bin/dnssec-keymgr -g ${dnssecTools}/bin/dnssec-keygen -s ${dnssecTools}/bin/dnssec-settime -K ${stateDir}/dnssec -c ${policyFile name zone.dnssecPolicy} ${name}
465 ${dnssecTools}/bin/dnssec-signzone -S -K ${stateDir}/dnssec -o ${name} -O full -N date ${stateDir}/zones/${name}
466 ${nsdPkg}/sbin/nsd-checkzone ${name} ${stateDir}/zones/${name}.signed && mv -v ${stateDir}/zones/${name}.signed ${stateDir}/zones/${name}
467 '';
468 policyFile = name: policy: pkgs.writeText "${name}.policy" ''
469 zone ${name} {
470 algorithm ${policy.algorithm};
471 key-size zsk ${toString policy.zsk.keySize};
472 key-size ksk ${toString policy.ksk.keySize};
473 keyttl ${policy.keyttl};
474 pre-publish zsk ${policy.zsk.prePublish};
475 pre-publish ksk ${policy.ksk.prePublish};
476 post-publish zsk ${policy.zsk.postPublish};
477 post-publish ksk ${policy.ksk.postPublish};
478 roll-period zsk ${policy.zsk.rollPeriod};
479 roll-period ksk ${policy.ksk.rollPeriod};
480 coverage ${policy.coverage};
481 };
482 '';
483in
484{
485 # options are ordered alphanumerically
486 options.services.nsd = {
487
488 enable = mkEnableOption "NSD authoritative DNS server";
489
490 bind8Stats = mkEnableOption "BIND8 like statistics";
491
492 dnssecInterval = mkOption {
493 type = types.str;
494 default = "1h";
495 description = ''
496 How often to check whether dnssec key rollover is required
497 '';
498 };
499
500 extraConfig = mkOption {
501 type = types.lines;
502 default = "";
503 description = ''
504 Extra nsd config.
505 '';
506 };
507
508 hideVersion = mkOption {
509 type = types.bool;
510 default = true;
511 description = ''
512 Whether NSD should answer VERSION.BIND and VERSION.SERVER CHAOS class queries.
513 '';
514 };
515
516 identity = mkOption {
517 type = types.str;
518 default = "unidentified server";
519 description = ''
520 Identify the server (CH TXT ID.SERVER entry).
521 '';
522 };
523
524 interfaces = mkOption {
525 type = types.listOf types.str;
526 default = [ "127.0.0.0" "::1" ];
527 description = ''
528 What addresses the server should listen to.
529 '';
530 };
531
532 ipFreebind = mkOption {
533 type = types.bool;
534 default = false;
535 description = ''
536 Whether to bind to nonlocal addresses and interfaces that are down.
537 Similar to ip-transparent.
538 '';
539 };
540
541 ipTransparent = mkOption {
542 type = types.bool;
543 default = false;
544 description = ''
545 Allow binding to non local addresses.
546 '';
547 };
548
549 ipv4 = mkOption {
550 type = types.bool;
551 default = true;
552 description = ''
553 Whether to listen on IPv4 connections.
554 '';
555 };
556
557 ipv4EDNSSize = mkOption {
558 type = types.int;
559 default = 4096;
560 description = ''
561 Preferred EDNS buffer size for IPv4.
562 '';
563 };
564
565 ipv6 = mkOption {
566 type = types.bool;
567 default = true;
568 description = ''
569 Whether to listen on IPv6 connections.
570 '';
571 };
572
573 ipv6EDNSSize = mkOption {
574 type = types.int;
575 default = 4096;
576 description = ''
577 Preferred EDNS buffer size for IPv6.
578 '';
579 };
580
581 logTimeAscii = mkOption {
582 type = types.bool;
583 default = true;
584 description = ''
585 Log time in ascii, if false then in unix epoch seconds.
586 '';
587 };
588
589 nsid = mkOption {
590 type = types.nullOr types.str;
591 default = null;
592 description = ''
593 NSID identity (hex string, or "ascii_somestring").
594 '';
595 };
596
597 port = mkOption {
598 type = types.int;
599 default = 53;
600 description = ''
601 Port the service should bind do.
602 '';
603 };
604
605 reuseport = mkOption {
606 type = types.bool;
607 default = pkgs.stdenv.isLinux;
608 description = ''
609 Whether to enable SO_REUSEPORT on all used sockets. This lets multiple
610 processes bind to the same port. This speeds up operation especially
611 if the server count is greater than one and makes fast restarts less
612 prone to fail
613 '';
614 };
615
616 rootServer = mkOption {
617 type = types.bool;
618 default = false;
619 description = ''
620 Whether this server will be a root server (a DNS root server, you
621 usually don't want that).
622 '';
623 };
624
625 roundRobin = mkEnableOption "round robin rotation of records";
626
627 serverCount = mkOption {
628 type = types.int;
629 default = 1;
630 description = ''
631 Number of NSD servers to fork. Put the number of CPUs to use here.
632 '';
633 };
634
635 statistics = mkOption {
636 type = types.nullOr types.int;
637 default = null;
638 description = ''
639 Statistics are produced every number of seconds. Prints to log.
640 If null no statistics are logged.
641 '';
642 };
643
644 tcpCount = mkOption {
645 type = types.int;
646 default = 100;
647 description = ''
648 Maximum number of concurrent TCP connections per server.
649 '';
650 };
651
652 tcpQueryCount = mkOption {
653 type = types.int;
654 default = 0;
655 description = ''
656 Maximum number of queries served on a single TCP connection.
657 0 means no maximum.
658 '';
659 };
660
661 tcpTimeout = mkOption {
662 type = types.int;
663 default = 120;
664 description = ''
665 TCP timeout in seconds.
666 '';
667 };
668
669 verbosity = mkOption {
670 type = types.int;
671 default = 0;
672 description = ''
673 Verbosity level.
674 '';
675 };
676
677 version = mkOption {
678 type = types.nullOr types.str;
679 default = null;
680 description = ''
681 The version string replied for CH TXT version.server and version.bind
682 queries. Will use the compiled package version on null.
683 See hideVersion for enabling/disabling this responses.
684 '';
685 };
686
687 xfrdReloadTimeout = mkOption {
688 type = types.int;
689 default = 1;
690 description = ''
691 Number of seconds between reloads triggered by xfrd.
692 '';
693 };
694
695 zonefilesCheck = mkOption {
696 type = types.bool;
697 default = true;
698 description = ''
699 Whether to check mtime of all zone files on start and sighup.
700 '';
701 };
702
703
704 keys = mkOption {
705 type = types.attrsOf (types.submodule {
706 options = {
707
708 algorithm = mkOption {
709 type = types.str;
710 default = "hmac-sha256";
711 description = ''
712 Authentication algorithm for this key.
713 '';
714 };
715
716 keyFile = mkOption {
717 type = types.path;
718 description = ''
719 Path to the file which contains the actual base64 encoded
720 key. The key will be copied into "${stateDir}/private" before
721 NSD starts. The copied file is only accessibly by the NSD
722 user.
723 '';
724 };
725
726 };
727 });
728 default = {};
729 example = literalExample ''
730 { "tsig.example.org" = {
731 algorithm = "hmac-md5";
732 keyFile = "/path/to/my/key";
733 };
734 }
735 '';
736 description = ''
737 Define your TSIG keys here.
738 '';
739 };
740
741
742 ratelimit = {
743
744 enable = mkEnableOption "ratelimit capabilities";
745
746 ipv4PrefixLength = mkOption {
747 type = types.nullOr types.int;
748 default = null;
749 description = ''
750 IPv4 prefix length. Addresses are grouped by netblock.
751 '';
752 };
753
754 ipv6PrefixLength = mkOption {
755 type = types.nullOr types.int;
756 default = null;
757 description = ''
758 IPv6 prefix length. Addresses are grouped by netblock.
759 '';
760 };
761
762 ratelimit = mkOption {
763 type = types.int;
764 default = 200;
765 description = ''
766 Max qps allowed from any query source.
767 0 means unlimited. With an verbosity of 2 blocked and
768 unblocked subnets will be logged.
769 '';
770 };
771
772 slip = mkOption {
773 type = types.nullOr types.int;
774 default = null;
775 description = ''
776 Number of packets that get discarded before replying a SLIP response.
777 0 disables SLIP responses. 1 will make every response a SLIP response.
778 '';
779 };
780
781 size = mkOption {
782 type = types.int;
783 default = 1000000;
784 description = ''
785 Size of the hashtable. More buckets use more memory but lower
786 the chance of hash hash collisions.
787 '';
788 };
789
790 whitelistRatelimit = mkOption {
791 type = types.int;
792 default = 2000;
793 description = ''
794 Max qps allowed from whitelisted sources.
795 0 means unlimited. Set the rrl-whitelist option for specific
796 queries to apply this limit instead of the default to them.
797 '';
798 };
799
800 };
801
802
803 remoteControl = {
804
805 enable = mkEnableOption "remote control via nsd-control";
806
807 controlCertFile = mkOption {
808 type = types.path;
809 default = "/etc/nsd/nsd_control.pem";
810 description = ''
811 Path to the client certificate signed with the server certificate.
812 This file is used by nsd-control and generated by nsd-control-setup.
813 '';
814 };
815
816 controlKeyFile = mkOption {
817 type = types.path;
818 default = "/etc/nsd/nsd_control.key";
819 description = ''
820 Path to the client private key, which is used by nsd-control
821 but not by the server. This file is generated by nsd-control-setup.
822 '';
823 };
824
825 interfaces = mkOption {
826 type = types.listOf types.str;
827 default = [ "127.0.0.1" "::1" ];
828 description = ''
829 Which interfaces NSD should bind to for remote control.
830 '';
831 };
832
833 port = mkOption {
834 type = types.int;
835 default = 8952;
836 description = ''
837 Port number for remote control operations (uses TLS over TCP).
838 '';
839 };
840
841 serverCertFile = mkOption {
842 type = types.path;
843 default = "/etc/nsd/nsd_server.pem";
844 description = ''
845 Path to the server self signed certificate, which is used by the server
846 but and by nsd-control. This file is generated by nsd-control-setup.
847 '';
848 };
849
850 serverKeyFile = mkOption {
851 type = types.path;
852 default = "/etc/nsd/nsd_server.key";
853 description = ''
854 Path to the server private key, which is used by the server
855 but not by nsd-control. This file is generated by nsd-control-setup.
856 '';
857 };
858
859 };
860
861 zones = mkOption {
862 type = types.attrsOf zoneOptions;
863 default = {};
864 example = literalExample ''
865 { "serverGroup1" = {
866 provideXFR = [ "10.1.2.3 NOKEY" ];
867 children = {
868 "example.com." = {
869 data = '''
870 $ORIGIN example.com.
871 $TTL 86400
872 @ IN SOA a.ns.example.com. admin.example.com. (
873 ...
874 ''';
875 };
876 "example.org." = {
877 data = '''
878 $ORIGIN example.org.
879 $TTL 86400
880 @ IN SOA a.ns.example.com. admin.example.com. (
881 ...
882 ''';
883 };
884 };
885 };
886
887 "example.net." = {
888 provideXFR = [ "10.3.2.1 NOKEY" ];
889 data = '''
890 ...
891 ''';
892 };
893 }
894 '';
895 description = ''
896 Define your zones here. Zones can cascade other zones and therefore
897 inherit settings from parent zones. Look at the definition of
898 children to learn about inheritance and child zones.
899 The given example will define 3 zones (example.(com|org|net).). Both
900 example.com. and example.org. inherit their configuration from
901 serverGroup1.
902 '';
903 };
904 };
905
906 config = mkIf cfg.enable {
907
908 assertions = singleton {
909 assertion = zoneConfigs ? "." -> cfg.rootServer;
910 message = "You have a root zone configured. If this is really what you "
911 + "want, please enable 'services.nsd.rootServer'.";
912 };
913
914 environment = {
915 systemPackages = [ nsdPkg ];
916 etc."nsd/nsd.conf".source = "${configFile}/nsd.conf";
917 };
918
919 users.groups.${username}.gid = config.ids.gids.nsd;
920
921 users.users.${username} = {
922 description = "NSD service user";
923 home = stateDir;
924 createHome = true;
925 uid = config.ids.uids.nsd;
926 group = username;
927 };
928
929 systemd.services.nsd = {
930 description = "NSD authoritative only domain name service";
931
932 after = [ "network.target" ];
933 wantedBy = [ "multi-user.target" ];
934
935 startLimitBurst = 4;
936 startLimitIntervalSec = 5 * 60; # 5 mins
937 serviceConfig = {
938 ExecStart = "${nsdPkg}/sbin/nsd -d -c ${nsdEnv}/nsd.conf";
939 StandardError = "null";
940 PIDFile = pidFile;
941 Restart = "always";
942 RestartSec = "4s";
943 };
944
945 preStart = ''
946 rm -Rf "${stateDir}/private/"
947 rm -Rf "${stateDir}/tmp/"
948
949 mkdir -m 0700 -p "${stateDir}/private"
950 mkdir -m 0700 -p "${stateDir}/tmp"
951 mkdir -m 0700 -p "${stateDir}/var"
952
953 cat > "${stateDir}/don't touch anything in here" << EOF
954 Everything in this directory except NSD's state in var and dnssec
955 is automatically generated and will be purged and redeployed by
956 the nsd.service pre-start script.
957 EOF
958
959 chown ${username}:${username} -R "${stateDir}/private"
960 chown ${username}:${username} -R "${stateDir}/tmp"
961 chown ${username}:${username} -R "${stateDir}/var"
962
963 rm -rf "${stateDir}/zones"
964 cp -rL "${nsdEnv}/zones" "${stateDir}/zones"
965
966 ${copyKeys}
967 '';
968 };
969
970 systemd.timers.nsd-dnssec = mkIf dnssec {
971 description = "Automatic DNSSEC key rollover";
972
973 wantedBy = [ "nsd.service" ];
974
975 timerConfig = {
976 OnActiveSec = cfg.dnssecInterval;
977 OnUnitActiveSec = cfg.dnssecInterval;
978 };
979 };
980
981 systemd.services.nsd-dnssec = mkIf dnssec {
982 description = "DNSSEC key rollover";
983
984 wantedBy = [ "nsd.service" ];
985 before = [ "nsd.service" ];
986
987 script = signZones;
988
989 postStop = ''
990 /run/current-system/systemd/bin/systemctl kill -s SIGHUP nsd.service
991 '';
992 };
993
994 };
995
996 meta.maintainers = with lib.maintainers; [ hrdinka ];
997}