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