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&amp;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] &lt;ip-address&gt; &lt;key-name | NOKEY&gt;</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}