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