summary refs log tree commit diff
path: root/nixos/modules/services/networking/nsd.nix
diff options
context:
space:
mode:
Diffstat (limited to 'nixos/modules/services/networking/nsd.nix')
-rw-r--r--nixos/modules/services/networking/nsd.nix872
1 files changed, 476 insertions, 396 deletions
diff --git a/nixos/modules/services/networking/nsd.nix b/nixos/modules/services/networking/nsd.nix
index e85f2681125..b3f2730e672 100644
--- a/nixos/modules/services/networking/nsd.nix
+++ b/nixos/modules/services/networking/nsd.nix
@@ -7,92 +7,118 @@ let
 
   username = "nsd";
   stateDir = "/var/lib/nsd";
-  pidFile  = stateDir + "/var/nsd.pid";
+  pidFile = stateDir + "/var/nsd.pid";
 
+  # build nsd with the options needed for the given config
   nsdPkg = pkgs.nsd.override {
     bind8Stats = cfg.bind8Stats;
-    ipv6       = cfg.ipv6;
-    ratelimit  = cfg.ratelimit.enable;
+    ipv6 = cfg.ipv6;
+    ratelimit = cfg.ratelimit.enable;
     rootServer = cfg.rootServer;
-    zoneStats  = length (collect (x: (x.zoneStats or null) != null) cfg.zones) > 0;
+    zoneStats = length (collect (x: (x.zoneStats or null) != null) cfg.zones) > 0;
   };
 
-  zoneFiles = pkgs.stdenv.mkDerivation {
-    preferLocalBuild = true;
+
+  nsdEnv = pkgs.buildEnv {
     name = "nsd-env";
-    buildCommand = concatStringsSep "\n"
-      [ "mkdir -p $out"
-        (concatStrings (mapAttrsToList (zoneName: zoneOptions: ''
-          cat > "$out/${zoneName}" <<_EOF_
-          ${zoneOptions.data}
-          _EOF_
-        '') zoneConfigs))
-      ];
+
+    paths = [ configFile ]
+      ++ mapAttrsToList (name: zone: writeZoneData name zone.data) zoneConfigs;
+
+    postBuild = ''
+      echo "checking zone files"
+      cd $out/zones
+
+      for zoneFile in *; do
+        ${nsdPkg}/sbin/nsd-checkzone "$zoneFile" "$zoneFile" || {
+          if grep -q \\\\\\$ "$zoneFile"; then
+            echo zone "$zoneFile" contains escaped dollar signes \\\$
+            echo Escaping them is not needed any more. Please make shure \
+                 to unescape them where they prefix a variable name
+          fi
+
+          exit 1
+        }
+      done
+
+      echo "checking configuration file"
+      ${nsdPkg}/sbin/nsd-checkconf $out/nsd.conf
+    '';
+  };
+
+  writeZoneData = name: text: pkgs.writeTextFile {
+    inherit name text;
+    destination = "/zones/${name}";
   };
 
-  configFile = pkgs.writeText "nsd.conf" ''
+
+  # options are ordered alphanumerically by the nixos option name
+  configFile = pkgs.writeTextDir "nsd.conf" ''
     server:
-      username: ${username}
       chroot:   "${stateDir}"
+      username: ${username}
 
       # The directory for zonefile: files. The daemon chdirs here.
       zonesdir: "${stateDir}"
 
       # the list of dynamically added zones.
-      zonelistfile: "${stateDir}/var/zone.list"
       database:     "${stateDir}/var/nsd.db"
       pidfile:      "${pidFile}"
       xfrdfile:     "${stateDir}/var/xfrd.state"
       xfrdir:       "${stateDir}/tmp"
+      zonelistfile: "${stateDir}/var/zone.list"
 
       # interfaces
     ${forEach "  ip-address: " cfg.interfaces}
 
-      server-count:        ${toString cfg.serverCount}
+      hide-version:        ${yesOrNo  cfg.hideVersion}
+      identity:            "${cfg.identity}"
       ip-transparent:      ${yesOrNo  cfg.ipTransparent}
       do-ip4:              ${yesOrNo  cfg.ipv4}
+      ipv4-edns-size:      ${toString cfg.ipv4EDNSSize}
       do-ip6:              ${yesOrNo  cfg.ipv6}
-      port:                ${toString cfg.port}
-      verbosity:           ${toString cfg.verbosity}
-      hide-version:        ${yesOrNo  cfg.hideVersion}
-      identity:            "${cfg.identity}"
+      ipv6-edns-size:      ${toString cfg.ipv6EDNSSize}
+      log-time-ascii:      ${yesOrNo  cfg.logTimeAscii}
       ${maybeString "nsid: " cfg.nsid}
+      port:                ${toString cfg.port}
+      reuseport:           ${yesOrNo  cfg.reuseport}
+      round-robin:         ${yesOrNo  cfg.roundRobin}
+      server-count:        ${toString cfg.serverCount}
+      ${if cfg.statistics == null then "" else "statistics:          ${toString cfg.statistics}"}
       tcp-count:           ${toString cfg.tcpCount}
       tcp-query-count:     ${toString cfg.tcpQueryCount}
       tcp-timeout:         ${toString cfg.tcpTimeout}
-      ipv4-edns-size:      ${toString cfg.ipv4EDNSSize}
-      ipv6-edns-size:      ${toString cfg.ipv6EDNSSize}
-      ${if cfg.statistics == null then "" else "statistics:          ${toString cfg.statistics}"}
+      verbosity:           ${toString cfg.verbosity}
+      ${maybeString "version: " cfg.version}
       xfrd-reload-timeout: ${toString cfg.xfrdReloadTimeout}
       zonefiles-check:     ${yesOrNo  cfg.zonefilesCheck}
 
-      rrl-size:                ${toString cfg.ratelimit.size}
-      rrl-ratelimit:           ${toString cfg.ratelimit.ratelimit}
-      rrl-whitelist-ratelimit: ${toString cfg.ratelimit.whitelistRatelimit}
-      ${maybeString "rrl-slip: "               cfg.ratelimit.slip}
       ${maybeString "rrl-ipv4-prefix-length: " cfg.ratelimit.ipv4PrefixLength}
       ${maybeString "rrl-ipv6-prefix-length: " cfg.ratelimit.ipv6PrefixLength}
+      rrl-ratelimit:           ${toString cfg.ratelimit.ratelimit}
+      ${maybeString "rrl-slip: "               cfg.ratelimit.slip}
+      rrl-size:                ${toString cfg.ratelimit.size}
+      rrl-whitelist-ratelimit: ${toString cfg.ratelimit.whitelistRatelimit}
 
     ${keyConfigFile}
 
     remote-control:
       control-enable:    ${yesOrNo  cfg.remoteControl.enable}
+      control-key-file:  "${cfg.remoteControl.controlKeyFile}"
+      control-cert-file: "${cfg.remoteControl.controlCertFile}"
     ${forEach "  control-interface: " cfg.remoteControl.interfaces}
-      control-port:      ${toString cfg.port}
+      control-port:      ${toString cfg.remoteControl.port}
       server-key-file:   "${cfg.remoteControl.serverKeyFile}"
       server-cert-file:  "${cfg.remoteControl.serverCertFile}"
-      control-key-file:  "${cfg.remoteControl.controlKeyFile}"
-      control-cert-file: "${cfg.remoteControl.controlCertFile}"
 
-    # zone files reside in "${zoneFiles}" linked to "${stateDir}/zones"
     ${concatStrings (mapAttrsToList zoneConfigFile zoneConfigs)}
 
     ${cfg.extraConfig}
   '';
 
-  yesOrNo     = b: if b then "yes" else "no";
+  yesOrNo = b: if b then "yes" else "no";
   maybeString = pre: s: if s == null then "" else ''${pre} "${s}"'';
-  forEach     = pre: l: concatMapStrings (x: pre + x + "\n") l;
+  forEach = pre: l: concatMapStrings (x: pre + x + "\n") l;
 
 
   keyConfigFile = concatStrings (mapAttrsToList (keyName: keyOptions: ''
@@ -106,22 +132,23 @@ let
     secret=$(cat "${keyOptions.keyFile}")
     dest="${stateDir}/private/${keyName}"
     echo "  secret: \"$secret\"" > "$dest"
-    ${pkgs.coreutils}/bin/chown ${username}:${username} "$dest"
-    ${pkgs.coreutils}/bin/chmod 0400 "$dest"
+    chown ${username}:${username} "$dest"
+    chmod 0400 "$dest"
   '') cfg.keys);
 
 
+  # options are ordered alphanumerically by the nixos option name
   zoneConfigFile = name: zone: ''
     zone:
       name:         "${name}"
       zonefile:     "${stateDir}/zones/${name}"
-      ${maybeString "zonestats: "          zone.zoneStats}
       ${maybeString "outgoing-interface: " zone.outgoingInterface}
     ${forEach     "  rrl-whitelist: "      zone.rrlWhitelist}
+      ${maybeString "zonestats: "          zone.zoneStats}
 
+      allow-axfr-fallback: ${yesOrNo       zone.allowAXFRFallback}
     ${forEach     "  allow-notify: "       zone.allowNotify}
     ${forEach     "  request-xfr: "        zone.requestXFR}
-      allow-axfr-fallback: ${yesOrNo       zone.allowAXFRFallback}
 
     ${forEach     "  notify: "             zone.notify}
       notify-retry:                        ${toString zone.notifyRetry}
@@ -142,7 +169,7 @@ let
       );
 
   # fighting infinite recursion
-  zoneOptions  = zoneOptionsRaw // childConfig zoneOptions1 true;
+  zoneOptions = zoneOptionsRaw // childConfig zoneOptions1 true;
   zoneOptions1 = zoneOptionsRaw // childConfig zoneOptions2 false;
   zoneOptions2 = zoneOptionsRaw // childConfig zoneOptions3 false;
   zoneOptions3 = zoneOptionsRaw // childConfig zoneOptions4 false;
@@ -152,26 +179,25 @@ let
 
   childConfig = x: v: { options.children = { type = types.attrsOf x; visible = v; }; };
 
+  # options are ordered alphanumerically
   zoneOptionsRaw = types.submodule {
     options = {
-      children = mkOption {
-        default     = {};
+
+      allowAXFRFallback = mkOption {
+        type = types.bool;
+        default = true;
         description = ''
-          Children zones inherit all options of their parents. Attributes
-          defined in a child will overwrite the ones of its parent. Only
-          leaf zones will be actually served. This way it's possible to
-          define maybe zones which share most attributes without
-          duplicating everything. This mechanism replaces nsd's patterns
-          in a save and functional way.
+          If NSD as secondary server should be allowed to AXFR if the primary
+          server does not allow IXFR.
         '';
       };
 
       allowNotify = mkOption {
-        type        = types.listOf types.str;
-        default     = [ ];
-        example     = [ "192.0.2.0/24 NOKEY" "10.0.0.1-10.0.0.5 my_tsig_key_name"
-                        "10.0.3.4&255.255.0.0 BLOCKED"
-                      ];
+        type = types.listOf types.str;
+        default = [ ];
+        example = [ "192.0.2.0/24 NOKEY" "10.0.0.1-10.0.0.5 my_tsig_key_name"
+                    "10.0.3.4&255.255.0.0 BLOCKED"
+                  ];
         description = ''
           Listed primary servers are allowed to notify this secondary server.
           <screen><![CDATA[
@@ -193,28 +219,32 @@ let
         '';
       };
 
-      requestXFR = mkOption {
-        type        = types.listOf types.str;
-        default     = [];
-        example     = [];
+      children = mkOption {
+        default = {};
         description = ''
-          Format: <code>[AXFR|UDP] &lt;ip-address&gt; &lt;key-name | NOKEY&gt;</code>
+          Children zones inherit all options of their parents. Attributes
+          defined in a child will overwrite the ones of its parent. Only
+          leaf zones will be actually served. This way it's possible to
+          define maybe zones which share most attributes without
+          duplicating everything. This mechanism replaces nsd's patterns
+          in a save and functional way.
         '';
       };
 
-      allowAXFRFallback = mkOption {
-        type        = types.bool;
-        default     = true;
+      data = mkOption {
+        type = types.str;
+        default = "";
+        example = "";
         description = ''
-          If NSD as secondary server should be allowed to AXFR if the primary
-          server does not allow IXFR.
+          The actual zone data. This is the content of your zone file.
+          Use imports or pkgs.lib.readFile if you don't want this data in your config file.
         '';
       };
 
       notify = mkOption {
-        type        = types.listOf types.str;
-        default     = [];
-        example     = [ "10.0.0.1@3721 my_key" "::5 NOKEY" ];
+        type = types.listOf types.str;
+        default = [];
+        example = [ "10.0.0.1@3721 my_key" "::5 NOKEY" ];
         description = ''
           This primary server will notify all given secondary servers about
           zone changes.
@@ -231,38 +261,47 @@ let
       };
 
       notifyRetry = mkOption {
-        type        = types.int;
-        default     = 5;
+        type = types.int;
+        default = 5;
         description = ''
           Specifies the number of retries for failed notifies. Set this along with notify.
         '';
       };
 
+      outgoingInterface = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "2000::1@1234";
+        description = ''
+          This address will be used for zone-transfere requests if configured
+          as a secondary server or notifications in case of a primary server.
+          Supply either a plain IPv4 or IPv6 address with an optional port
+          number (ip@port).
+        '';
+      };
+
       provideXFR = mkOption {
-        type        = types.listOf types.str;
-        default     = [];
-        example     = [ "192.0.2.0/24 NOKEY" "192.0.2.0/24 my_tsig_key_name" ];
+        type = types.listOf types.str;
+        default = [];
+        example = [ "192.0.2.0/24 NOKEY" "192.0.2.0/24 my_tsig_key_name" ];
         description = ''
           Allow these IPs and TSIG to transfer zones, addr TSIG|NOKEY|BLOCKED
           address range 192.0.2.0/24, 1.2.3.4&amp;255.255.0.0, 3.0.2.20-3.0.2.40
         '';
       };
 
-      outgoingInterface = mkOption {
-        type        = types.nullOr types.str;
-        default     = null;
-        example     = "2000::1@1234";
+      requestXFR = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [];
         description = ''
-          This address will be used for zone-transfere requests if configured
-          as a secondary server or notifications in case of a primary server.
-          Supply either a plain IPv4 or IPv6 address with an optional port
-          number (ip@port).
+          Format: <code>[AXFR|UDP] &lt;ip-address&gt; &lt;key-name | NOKEY&gt;</code>
         '';
       };
 
       rrlWhitelist = mkOption {
-        type        = types.listOf types.str;
-        default     = [];
+        type = types.listOf types.str;
+        default = [];
         description = ''
           Whitelists the given rrl-types.
           The RRL classification types are:  nxdomain,  error, referral, any,
@@ -270,20 +309,10 @@ let
         '';
       };
 
-      data = mkOption {
-        type        = types.str;
-        default     = "";
-        example     = "";
-        description = ''
-          The actual zone data. This is the content of your zone file.
-          Use imports or pkgs.lib.readFile if you don't want this data in your config file.
-        '';
-      };
-
       zoneStats = mkOption {
-        type        = types.nullOr types.str;
-        default     = null;
-        example     = "%s";
+        type = types.nullOr types.str;
+        default = null;
+        example = "%s";
         description = ''
           When set to something distinct to null NSD is able to collect
           statistics per zone. All statistics of this zone(s) will be added
@@ -292,419 +321,470 @@ let
           and stats_noreset.
         '';
       };
+
     };
   };
 
 in
 {
-  options = {
-    services.nsd = {
+  # options are ordered alphanumerically
+  options.services.nsd = {
 
-      enable = mkEnableOption "NSD authoritative DNS server";
-      bind8Stats = mkEnableOption "BIND8 like statistics";
+    enable = mkEnableOption "NSD authoritative DNS server";
 
-      rootServer = mkOption {
-        type        = types.bool;
-        default     = false;
-        description = ''
-          Wheter if this server will be a root server (a DNS root server, you
-          usually don't want that).
-        '';
-      };
+    bind8Stats = mkEnableOption "BIND8 like statistics";
 
-      interfaces = mkOption {
-        type        = types.listOf types.str;
-        default     = [ "127.0.0.0" "::1" ];
-        description = ''
-          What addresses the server should listen to.
-        '';
-      };
+    extraConfig = mkOption {
+      type = types.str;
+      default = "";
+      description = ''
+        Extra nsd config.
+      '';
+    };
 
-      serverCount = mkOption {
-        type        = types.int;
-        default     = 1;
-        description = ''
-          Number of NSD servers to fork. Put the number of CPUs to use here.
-        '';
-      };
+    hideVersion = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Wheter NSD should answer VERSION.BIND and VERSION.SERVER CHAOS class queries.
+      '';
+    };
 
-      ipTransparent = mkOption {
-        type        = types.bool;
-        default     = false;
-        description = ''
-          Allow binding to non local addresses.
-        '';
-      };
+    identity = mkOption {
+      type = types.str;
+      default = "unidentified server";
+      description = ''
+        Identify the server (CH TXT ID.SERVER entry).
+      '';
+    };
 
-      ipv4 = mkOption {
-        type        = types.bool;
-        default     = true;
-        description = ''
-          Wheter to listen on IPv4 connections.
-        '';
-      };
+    interfaces = mkOption {
+      type = types.listOf types.str;
+      default = [ "127.0.0.0" "::1" ];
+      description = ''
+        What addresses the server should listen to.
+      '';
+    };
 
-      ipv6 = mkOption {
-        type        = types.bool;
-        default     = true;
-        description = ''
-          Wheter to listen on IPv6 connections.
-        '';
-      };
+    ipTransparent = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Allow binding to non local addresses.
+      '';
+    };
 
-      port = mkOption {
-        type        = types.int;
-        default     = 53;
-        description = ''
-          Port the service should bind do.
-        '';
-      };
+    ipv4 = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Wheter to listen on IPv4 connections.
+      '';
+    };
 
-      verbosity = mkOption {
-        type        = types.int;
-        default     = 0;
-        description = ''
-          Verbosity level.
-        '';
-      };
+    ipv4EDNSSize = mkOption {
+      type = types.int;
+      default = 4096;
+      description = ''
+        Preferred EDNS buffer size for IPv4.
+      '';
+    };
+
+    ipv6 = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Wheter to listen on IPv6 connections.
+      '';
+    };
+
+    ipv6EDNSSize = mkOption {
+      type = types.int;
+      default = 4096;
+      description = ''
+        Preferred EDNS buffer size for IPv6.
+      '';
+    };
+
+    logTimeAscii = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Log time in ascii, if false then in unix epoch seconds.
+      '';
+    };
+
+    nsid = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        NSID identity (hex string, or "ascii_somestring").
+      '';
+    };
+
+    port = mkOption {
+      type = types.int;
+      default = 53;
+      description = ''
+        Port the service should bind do.
+      '';
+    };
+
+    reuseport = mkOption {
+      type = types.bool;
+      default = pkgs.stdenv.isLinux;
+      description = ''
+        Wheter to enable SO_REUSEPORT on all used sockets. This lets multiple
+        processes bind to the same port. This speeds up operation especially
+        if the server count is greater than one and makes fast restarts less
+        prone to fail
+      '';
+    };
+
+    rootServer = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Wheter if this server will be a root server (a DNS root server, you
+        usually don't want that).
+      '';
+    };
+
+    roundRobin = mkEnableOption "round robin rotation of records";
+
+    serverCount = mkOption {
+      type = types.int;
+      default = 1;
+      description = ''
+        Number of NSD servers to fork. Put the number of CPUs to use here.
+      '';
+    };
+
+    statistics = mkOption {
+      type = types.nullOr types.int;
+      default = null;
+      description = ''
+        Statistics are produced every number of seconds. Prints to log.
+        If null no statistics are logged.
+      '';
+    };
+
+    tcpCount = mkOption {
+      type = types.int;
+      default = 100;
+      description = ''
+        Maximum number of concurrent TCP connections per server.
+      '';
+    };
+
+    tcpQueryCount = mkOption {
+      type = types.int;
+      default = 0;
+      description = ''
+        Maximum number of queries served on a single TCP connection.
+        0 means no maximum.
+      '';
+    };
+
+    tcpTimeout = mkOption {
+      type = types.int;
+      default = 120;
+      description = ''
+        TCP timeout in seconds.
+      '';
+    };
+
+    verbosity = mkOption {
+      type = types.int;
+      default = 0;
+      description = ''
+        Verbosity level.
+      '';
+    };
+
+    version = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        The version string replied for CH TXT version.server and version.bind
+        queries. Will use the compiled package version on null.
+        See hideVersion for enabling/disabling this responses.
+      '';
+    };
+
+    xfrdReloadTimeout = mkOption {
+      type = types.int;
+      default = 1;
+      description = ''
+        Number of seconds between reloads triggered by xfrd.
+      '';
+    };
+
+    zonefilesCheck = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Wheter to check mtime of all zone files on start and sighup.
+      '';
+    };
+
+
+    keys = mkOption {
+      type = types.attrsOf (types.submodule {
+        options = {
+
+          algorithm = mkOption {
+            type = types.str;
+            default = "hmac-sha256";
+            description = ''
+              Authentication algorithm for this key.
+            '';
+          };
+
+          keyFile = mkOption {
+            type = types.path;
+            description = ''
+              Path to the file which contains the actual base64 encoded
+              key. The key will be copied into "${stateDir}/private" before
+              NSD starts. The copied file is only accessibly by the NSD
+              user.
+            '';
+          };
+
+        };
+      });
+      default = {};
+      example = literalExample ''
+        { "tsig.example.org" = {
+            algorithm = "hmac-md5";
+            keyFile = "/path/to/my/key";
+          };
+        };
+      '';
+      description = ''
+        Define your TSIG keys here.
+      '';
+    };
+
+
+    ratelimit = {
 
-      hideVersion = mkOption {
-        type        = types.bool;
-        default     = true;
+      enable = mkEnableOption "ratelimit capabilities";
+
+      ipv4PrefixLength = mkOption {
+        type = types.nullOr types.int;
+        default = null;
         description = ''
-          Wheter NSD should answer VERSION.BIND and VERSION.SERVER CHAOS class queries.
+          IPv4 prefix length. Addresses are grouped by netblock.
         '';
       };
 
-      identity = mkOption {
-        type        = types.str;
-        default     = "unidentified server";
+      ipv6PrefixLength = mkOption {
+        type = types.nullOr types.int;
+        default = null;
         description = ''
-          Identify the server (CH TXT ID.SERVER entry).
+          IPv6 prefix length. Addresses are grouped by netblock.
         '';
       };
 
-      nsid = mkOption {
-        type        = types.nullOr types.str;
-        default     = null;
+      ratelimit = mkOption {
+        type = types.int;
+        default = 200;
         description = ''
-          NSID identity (hex string, or "ascii_somestring").
+          Max qps allowed from any query source.
+          0 means unlimited. With an verbosity of 2 blocked and
+          unblocked subnets will be logged.
         '';
       };
 
-      tcpCount = mkOption {
-        type        = types.int;
-        default     = 100;
+      slip = mkOption {
+        type = types.nullOr types.int;
+        default = null;
         description = ''
-          Maximum number of concurrent TCP connections per server.
+          Number of packets that get discarded before replying a SLIP response.
+          0 disables SLIP responses. 1 will make every response a SLIP response.
         '';
       };
 
-      tcpQueryCount = mkOption {
-        type        = types.int;
-        default     = 0;
+      size = mkOption {
+        type = types.int;
+        default = 1000000;
         description = ''
-          Maximum number of queries served on a single TCP connection.
-          0 means no maximum.
+          Size of the hashtable. More buckets use more memory but lower
+          the chance of hash hash collisions.
         '';
       };
 
-      tcpTimeout = mkOption {
-        type        = types.int;
-        default     = 120;
+      whitelistRatelimit = mkOption {
+        type = types.int;
+        default = 2000;
         description = ''
-          TCP timeout in seconds.
+          Max qps allowed from whitelisted sources.
+          0 means unlimited. Set the rrl-whitelist option for specific
+          queries to apply this limit instead of the default to them.
         '';
       };
 
-      ipv4EDNSSize = mkOption {
-        type        = types.int;
-        default     = 4096;
+    };
+
+
+    remoteControl = {
+
+      enable = mkEnableOption "remote control via nsd-control";
+
+      controlCertFile = mkOption {
+        type = types.path;
+        default = "/etc/nsd/nsd_control.pem";
         description = ''
-          Preferred EDNS buffer size for IPv4.
+          Path to the client certificate signed with the server certificate.
+          This file is used by nsd-control and generated by nsd-control-setup.
         '';
       };
 
-      ipv6EDNSSize = mkOption {
-        type        = types.int;
-        default     = 4096;
+      controlKeyFile = mkOption {
+        type = types.path;
+        default = "/etc/nsd/nsd_control.key";
         description = ''
-          Preferred EDNS buffer size for IPv6.
+          Path to the client private key, which is used by nsd-control
+          but not by the server. This file is generated by nsd-control-setup.
         '';
       };
 
-      statistics = mkOption {
-        type        = types.nullOr types.int;
-        default     = null;
+      interfaces = mkOption {
+        type = types.listOf types.str;
+        default = [ "127.0.0.1" "::1" ];
         description = ''
-          Statistics are produced every number of seconds. Prints to log.
-          If null no statistics are logged.
+          Which interfaces NSD should bind to for remote control.
         '';
       };
 
-      xfrdReloadTimeout = mkOption {
-        type        = types.int;
-        default     = 1;
+      port = mkOption {
+        type = types.int;
+        default = 8952;
         description = ''
-          Number of seconds between reloads triggered by xfrd.
+          Port number for remote control operations (uses TLS over TCP).
         '';
       };
 
-      zonefilesCheck = mkOption {
-        type        = types.bool;
-        default     = true;
+      serverCertFile = mkOption {
+        type = types.path;
+        default = "/etc/nsd/nsd_server.pem";
         description = ''
-          Wheter to check mtime of all zone files on start and sighup.
+          Path to the server self signed certificate, which is used by the server
+          but and by nsd-control. This file is generated by nsd-control-setup.
         '';
       };
 
-
-      extraConfig = mkOption {
-        type        = types.str;
-        default     = "";
+      serverKeyFile = mkOption {
+        type = types.path;
+        default = "/etc/nsd/nsd_server.key";
         description = ''
-          Extra nsd config.
+          Path to the server private key, which is used by the server
+          but not by nsd-control. This file is generated by nsd-control-setup.
         '';
       };
 
+    };
 
-      ratelimit = {
-        enable = mkEnableOption "ratelimit capabilities";
-
-        size = mkOption {
-          type        = types.int;
-          default     = 1000000;
-          description = ''
-            Size of the hashtable. More buckets use more memory but lower
-            the chance of hash hash collisions.
-          '';
-        };
-
-        ratelimit = mkOption {
-          type        = types.int;
-          default     = 200;
-          description = ''
-            Max qps allowed from any query source.
-            0 means unlimited. With an verbosity of 2 blocked and
-            unblocked subnets will be logged.
-          '';
-        };
-
-        whitelistRatelimit = mkOption {
-          type        = types.int;
-          default     = 2000;
-          description = ''
-            Max qps allowed from whitelisted sources.
-            0 means unlimited. Set the rrl-whitelist option for specific
-            queries to apply this limit instead of the default to them.
-          '';
-        };
-
-        slip = mkOption {
-          type        = types.nullOr types.int;
-          default     = null;
-          description = ''
-            Number of packets that get discarded before replying a SLIP response.
-            0 disables SLIP responses. 1 will make every response a SLIP response.
-          '';
-        };
-
-        ipv4PrefixLength = mkOption {
-          type        = types.nullOr types.int;
-          default     = null;
-          description = ''
-            IPv4 prefix length. Addresses are grouped by netblock.
-          '';
-        };
-
-        ipv6PrefixLength = mkOption {
-          type        = types.nullOr types.int;
-          default     = null;
-          description = ''
-            IPv6 prefix length. Addresses are grouped by netblock.
-          '';
-        };
-      };
-
-
-      remoteControl = {
-        enable = mkEnableOption "remote control via nsd-control";
-
-        interfaces = mkOption {
-          type        = types.listOf types.str;
-          default     = [ "127.0.0.1" "::1" ];
-          description = ''
-            Which interfaces NSD should bind to for remote control.
-          '';
-        };
-
-        port = mkOption {
-          type        = types.int;
-          default     = 8952;
-          description = ''
-            Port number for remote control operations (uses TLS over TCP).
-          '';
-        };
-
-        serverKeyFile = mkOption {
-          type        = types.path;
-          default     = "/etc/nsd/nsd_server.key";
-          description = ''
-            Path to the server private key, which is used by the server
-            but not by nsd-control. This file is generated by nsd-control-setup.
-          '';
-        };
-
-        serverCertFile = mkOption {
-          type        = types.path;
-          default     = "/etc/nsd/nsd_server.pem";
-          description = ''
-            Path to the server self signed certificate, which is used by the server
-            but and by nsd-control. This file is generated by nsd-control-setup.
-          '';
-        };
-
-        controlKeyFile = mkOption {
-          type        = types.path;
-          default     = "/etc/nsd/nsd_control.key";
-          description = ''
-            Path to the client private key, which is used by nsd-control
-            but not by the server. This file is generated by nsd-control-setup.
-          '';
-        };
-
-        controlCertFile = mkOption {
-          type        = types.path;
-          default     = "/etc/nsd/nsd_control.pem";
-          description = ''
-            Path to the client certificate signed with the server certificate.
-            This file is used by nsd-control and generated by nsd-control-setup.
-          '';
-        };
-      };
-
-
-      keys = mkOption {
-        type = types.attrsOf (types.submodule {
-          options = {
-            algorithm = mkOption {
-              type        = types.str;
-              default     = "hmac-sha256";
-              description = ''
-                Authentication algorithm for this key.
-              '';
-            };
-
-            keyFile = mkOption {
-              type        = types.path;
-              description = ''
-                Path to the file which contains the actual base64 encoded
-                key. The key will be copied into "${stateDir}/private" before
-                NSD starts. The copied file is only accessibly by the NSD
-                user.
-              '';
-            };
-          };
-        });
-        default = {};
-        example = {
-          "tsig.example.org" = {
-            algorithm = "hmac-md5";
-            secret    = "aaaaaabbbbbbccccccdddddd";
-          };
-        };
-        description = ''
-          Define your TSIG keys here.
-        '';
-      };
 
-      zones = mkOption {
-        type        = types.attrsOf zoneOptions;
-        default     = {};
-        example     = {
-          "serverGroup1" = {
+    zones = mkOption {
+      type = types.attrsOf zoneOptions;
+      default = {};
+      example = literalExample ''
+        { "serverGroup1" = {
             provideXFR = [ "10.1.2.3 NOKEY" ];
             children = {
               "example.com." = {
-                data = ''
+                data = '''
                   $ORIGIN example.com.
                   $TTL    86400
                   @ IN SOA a.ns.example.com. admin.example.com. (
                   ...
-                '';
+                ''';
               };
               "example.org." = {
-                data = ''
+                data = '''
                   $ORIGIN example.org.
                   $TTL    86400
                   @ IN SOA a.ns.example.com. admin.example.com. (
                   ...
-                '';
+                ''';
               };
             };
           };
 
           "example.net." = {
             provideXFR = [ "10.3.2.1 NOKEY" ];
-            data = ''...'';
+            data = '''
+              ...
+            ''';
           };
         };
-        description = ''
-          Define your zones here. Zones can cascade other zones and therefore
-          inherit settings from parent zones. Look at the definition of
-          children to learn about inheritance and child zones.
-          The given example will define 3 zones (example.(com|org|net).). Both
-          example.com. and example.org. inherit their configuration from
-          serverGroup1.
-        '';
-      };
-
+      '';
+      description = ''
+        Define your zones here. Zones can cascade other zones and therefore
+        inherit settings from parent zones. Look at the definition of
+        children to learn about inheritance and child zones.
+        The given example will define 3 zones (example.(com|org|net).). Both
+        example.com. and example.org. inherit their configuration from
+        serverGroup1.
+      '';
     };
+
   };
 
   config = mkIf cfg.enable {
 
     users.extraGroups = singleton {
       name = username;
-      gid  = config.ids.gids.nsd;
+      gid = config.ids.gids.nsd;
     };
 
     users.extraUsers = singleton {
-      name        = username;
+      name = username;
       description = "NSD service user";
-      home        = stateDir;
+      home = stateDir;
       createHome  = true;
-      uid         = config.ids.uids.nsd;
-      group       = username;
+      uid = config.ids.uids.nsd;
+      group = username;
     };
 
     systemd.services.nsd = {
       description = "NSD authoritative only domain name service";
-      wantedBy    = [ "multi-user.target" ];
-      after       = [ "network.target" ];
+
+      after = [ "keys.target" "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "keys.target" ];
 
       serviceConfig = {
-        PIDFile   = pidFile;
-        Restart   = "always";
-        ExecStart = "${nsdPkg}/sbin/nsd -d -c ${configFile}";
+        ExecStart = "${nsdPkg}/sbin/nsd -d -c ${nsdEnv}/nsd.conf";
+        PIDFile = pidFile;
+        Restart = "always";
+        RestartSec = "4s";
+        StartLimitBurst = 4;
+        StartLimitInterval = "5min";
       };
 
       preStart = ''
-        ${pkgs.coreutils}/bin/mkdir -m 0700 -p "${stateDir}/private"
-        ${pkgs.coreutils}/bin/mkdir -m 0700 -p "${stateDir}/tmp"
-        ${pkgs.coreutils}/bin/mkdir -m 0700 -p "${stateDir}/var"
+        rm -Rf "${stateDir}/private/"
+        rm -Rf "${stateDir}/tmp/"
 
-        ${pkgs.coreutils}/bin/touch "${stateDir}/don't touch anything in here"
+        mkdir -m 0700 -p "${stateDir}/private"
+        mkdir -m 0700 -p "${stateDir}/tmp"
+        mkdir -m 0700 -p "${stateDir}/var"
 
-        ${pkgs.coreutils}/bin/rm -f "${stateDir}/private/"*
-        ${pkgs.coreutils}/bin/rm -f "${stateDir}/tmp/"*
+        cat > "${stateDir}/don't touch anything in here" << EOF
+        Everything in this directory except NSD's state in var is
+        automatically generated and will be purged and redeployed
+        by the nsd.service pre-start script.
+        EOF
 
-        ${pkgs.coreutils}/bin/chown nsd:nsd -R "${stateDir}/private"
-        ${pkgs.coreutils}/bin/chown nsd:nsd -R "${stateDir}/tmp"
-        ${pkgs.coreutils}/bin/chown nsd:nsd -R "${stateDir}/var"
+        chown ${username}:${username} -R "${stateDir}/private"
+        chown ${username}:${username} -R "${stateDir}/tmp"
+        chown ${username}:${username} -R "${stateDir}/var"
 
-        ${pkgs.coreutils}/bin/rm -rf "${stateDir}/zones"
-        ${pkgs.coreutils}/bin/cp -r  "${zoneFiles}" "${stateDir}/zones"
+        rm -rf "${stateDir}/zones"
+        cp -rL "${nsdEnv}/zones" "${stateDir}/zones"
 
         ${copyKeys}
       '';