diff --git a/nixos/modules/misc/ids.nix b/nixos/modules/misc/ids.nix
index db50a010e7d..f1028a479df 100644
--- a/nixos/modules/misc/ids.nix
+++ b/nixos/modules/misc/ids.nix
@@ -133,6 +133,7 @@
spiped = 123;
teamspeak = 124;
influxdb = 125;
+ nsd = 126;
# When adding a uid, make sure it doesn't match an existing gid. And don't use uids above 399!
@@ -240,6 +241,7 @@
spiped = 123;
teamspeak = 124;
influxdb = 125;
+ nsd = 126;
# When adding a gid, make sure it doesn't match an existing uid. And don't use gids above 399!
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index cae2e61c2cf..f4f1abba4de 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -207,6 +207,7 @@
./services/networking/networkmanager.nix
./services/networking/ngircd.nix
./services/networking/notbit.nix
+ ./services/networking/nsd.nix
./services/networking/ntopng.nix
./services/networking/ntpd.nix
./services/networking/oidentd.nix
diff --git a/nixos/modules/services/networking/nsd.nix b/nixos/modules/services/networking/nsd.nix
new file mode 100644
index 00000000000..adfee1caec5
--- /dev/null
+++ b/nixos/modules/services/networking/nsd.nix
@@ -0,0 +1,751 @@
+{ config, pkgs, ... }:
+
+with pkgs.lib;
+
+let
+ cfg = config.services.nsd;
+
+ username = "nsd";
+ stateDir = "/var/lib/nsd";
+ pidFile = stateDir + "/var/nsd.pid";
+
+ zoneFiles = pkgs.stdenv.mkDerivation {
+ preferLocalBuild = true;
+ name = "nsd-env";
+ buildCommand = concatStringsSep "\n"
+ [ "mkdir -p $out"
+ (concatStrings (mapAttrsToList (zoneName: zoneOptions: ''
+ cat > "$out/${zoneName}" <<_EOF_
+ ${zoneOptions.data}
+ _EOF_
+ '') zoneConfigs))
+ ];
+ };
+
+ configFile = pkgs.writeText "nsd.conf" ''
+ server:
+ username: ${username}
+ chroot: "${stateDir}"
+
+ # 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"
+ logfile: "${stateDir}/var/nsd.log"
+ pidfile: "${pidFile}"
+ xfrdfile: "${stateDir}/var/xfrd.state"
+ xfrdir: "${stateDir}/tmp"
+
+ # interfaces
+ ${forEach " ip-address: " cfg.interfaces}
+
+ server-count: ${toString cfg.serverCount}
+ ip-transparent: ${yesOrNo cfg.ipTransparent}
+ do-ip4: ${yesOrNo cfg.ipv4}
+ do-ip6: ${yesOrNo cfg.ipv6}
+ port: ${toString cfg.port}
+ verbosity: ${toString cfg.verbosity}
+ hide-version: ${yesOrNo cfg.hideVersion}
+ identity: "${cfg.identity}"
+ ${maybeString "nsid: " cfg.nsid}
+ 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}"}
+ 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}
+
+ ${keyConfigFile}
+
+ remote-control:
+ control-enable: ${yesOrNo cfg.remoteControl.enable}
+ ${forEach " control-interface: " cfg.remoteControl.interfaces}
+ control-port: ${toString cfg.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";
+ maybeString = pre: s: if s == null then "" else ''${pre} "${s}"'';
+ forEach = pre: l: concatMapStrings (x: pre + x + "\n") l;
+
+
+ keyConfigFile = concatStrings (mapAttrsToList (keyName: keyOptions: ''
+ key:
+ name: "${keyName}"
+ algorithm: "${keyOptions.algorithm}"
+ include: "${stateDir}/private/${keyName}"
+ '') cfg.keys);
+
+ copyKeys = concatStrings (mapAttrsToList (keyName: keyOptions: ''
+ 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"
+ '') cfg.keys);
+
+
+ zoneConfigFile = name: zone: ''
+ zone:
+ name: "${name}"
+ zonefile: "${stateDir}/zones/${name}"
+ ${maybeString "outgoing-interface: " zone.outgoingInterface}
+ ${forEach " rrl-whitelist: " zone.rrlWhitelist}
+
+ ${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}
+ ${forEach " provide-xfr: " zone.provideXFR}
+
+ '';
+
+ zoneConfigs = zoneConfigs' {} "" { children = cfg.zones; };
+
+ zoneConfigs' = parent: name: zone:
+ if !(zone ? children) || zone.children == null || zone.children == { }
+ # leaf -> actual zone
+ then listToAttrs [ (nameValuePair name (parent // zone)) ]
+
+ # fork -> pattern
+ else zipAttrsWith (name: head) (
+ mapAttrsToList (name: child: zoneConfigs' (parent // zone // { children = {}; }) name child)
+ zone.children
+ );
+
+ # fighting infinite recursion
+ zoneOptions = zoneOptionsRaw // childConfig zoneOptions1 true;
+ zoneOptions1 = zoneOptionsRaw // childConfig zoneOptions2 false;
+ zoneOptions2 = zoneOptionsRaw // childConfig zoneOptions3 false;
+ zoneOptions3 = zoneOptionsRaw // childConfig zoneOptions4 false;
+ zoneOptions4 = zoneOptionsRaw // childConfig zoneOptions5 false;
+ zoneOptions5 = zoneOptionsRaw // childConfig zoneOptions6 false;
+ zoneOptions6 = zoneOptionsRaw // childConfig null false;
+
+ childConfig = x: v: { options.children = { type = types.attrsOf x; visible = v; }; };
+
+ zoneOptionsRaw = types.submodule (
+ { options, ... }:
+ { options = {
+ children = mkOption {
+ default = {};
+ 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.
+ '';
+ };
+
+ 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"
+ ];
+ description = ''
+ Listed primary servers are allowed to notify this secondary server.
+
+
+ either a plain IPv4/IPv6 address or range. Valid patters for ranges:
+ * 10.0.0.0/24 # via subnet size
+ * 10.0.0.0&255.255.255.0 # via subnet mask
+ * 10.0.0.1-10.0.0.254 # via range
+
+ A optional port number could be added with a '@':
+ * 2001:1234::1@1234
+
+
+ * will use the specified TSIG key
+ * NOKEY no TSIG signature is required
+ * BLOCKED notifies from non-listed or blocked IPs will be ignored
+ * ]]>
+ '';
+ };
+
+ requestXFR = mkOption {
+ type = types.listOf types.str;
+ default = [];
+ example = [];
+ description = ''
+ Format: [AXFR|UDP] <ip-address> <key-name | NOKEY>
+ '';
+ };
+
+ allowAXFRFallback = mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ If NSD as secondary server should be allowed to AXFR if the primary
+ server does not allow IXFR.
+ '';
+ };
+
+ notify = mkOption {
+ 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.
+
+
+ a plain IPv4/IPv6 address with on optional port number (ip@port)
+
+
+ * sign notifies with the specified key
+ * NOKEY don't sign notifies
+ ]]>
+ '';
+ };
+
+ notifyRetry = mkOption {
+ type = types.int;
+ default = 5;
+ description = ''
+ Specifies the number of retries for failed notifies. Set this along with notify.
+ '';
+ };
+
+ provideXFR = mkOption {
+ 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&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";
+ 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).
+ '';
+ };
+
+ rrlWhitelist = mkOption {
+ type = types.listOf types.str;
+ default = [];
+ description = ''
+ Whitelists the given rrl-types.
+ The RRL classification types are: nxdomain, error, referral, any,
+ rrsig, wildcard, nodata, dnskey, positive, all
+ '';
+ };
+
+ 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.
+ '';
+ };
+
+ };
+ }
+ );
+
+in
+{
+ options = {
+ services.nsd = {
+
+ enable = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Whether to enable the NSD authoritative domain name 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).
+ '';
+ };
+
+ interfaces = mkOption {
+ type = types.listOf types.str;
+ default = [ "127.0.0.0" "::1" ];
+ description = ''
+ What addresses the server should listen to.
+ '';
+ };
+
+ serverCount = mkOption {
+ type = types.int;
+ default = 1;
+ description = ''
+ Number of NSD servers to fork. Put the number of CPUs to use here.
+ '';
+ };
+
+ ipTransparent = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Allow binding to non local addresses.
+ '';
+ };
+
+ ipv4 = mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ Wheter to listen on IPv4 connections.
+ '';
+ };
+
+ ipv6 = mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ Wheter to listen on IPv6 connections.
+ '';
+ };
+
+ port = mkOption {
+ type = types.int;
+ default = 53;
+ description = ''
+ Port the service should bind do.
+ '';
+ };
+
+ verbosity = mkOption {
+ type = types.int;
+ default = 0;
+ description = ''
+ Verbosity level.
+ '';
+ };
+
+ hideVersion = mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ Wheter NSD should answer VERSION.BIND and VERSION.SERVER CHAOS class queries.
+ '';
+ };
+
+ identity = mkOption {
+ type = types.str;
+ default = "unidentified server";
+ description = ''
+ Identify the server (CH TXT ID.SERVER entry).
+ '';
+ };
+
+ nsid = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ NSID identity (hex string, or "ascii_somestring").
+ '';
+ };
+
+ 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.
+ '';
+ };
+
+ ipv4EDNSSize = mkOption {
+ type = types.int;
+ default = 4096;
+ description = ''
+ Preferred EDNS buffer size for IPv4.
+ '';
+ };
+
+ ipv6EDNSSize = mkOption {
+ type = types.int;
+ default = 4096;
+ description = ''
+ Preferred EDNS buffer size for IPv6.
+ '';
+ };
+
+ 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.
+ '';
+ };
+
+ 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.
+ '';
+ };
+
+
+ extraConfig = mkOption {
+ type = types.str;
+ default = "";
+ description = ''
+ Extra nsd config.
+ '';
+ };
+
+
+ ratelimit = mkOption {
+ type = types.submodule (
+ { options, ... }:
+ { options = {
+
+ enable = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Enable 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.
+ '';
+ };
+
+ };
+ });
+ default = {
+ };
+ example = {};
+ description = ''
+ '';
+ };
+
+
+ remoteControl = mkOption {
+ type = types.submodule (
+ { config, options, ... }:
+ { options = {
+
+ enable = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Wheter to enable remote control via nsd-control(8).
+ '';
+ };
+
+ 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.
+ '';
+ };
+
+ };
+
+ });
+ default = {
+ };
+ example = {};
+ description = ''
+ '';
+ };
+
+
+ keys = mkOption {
+ type = types.attrsOf (types.submodule (
+ { options, ... }:
+ { 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" = {
+ provideXFR = [ "10.1.2.3 NOKEY" ];
+ children = {
+ "example.com." = {
+ data = ''
+ $ORIGIN example.com.
+ $TTL 86400
+ @ IN SOA a.ns.example.com. admin.example.com. (
+ ...
+ '';
+ };
+ "example.org." = {
+ data = ''
+ $ORIGIN example.org.
+ $TTL 86400
+ @ IN SOA a.ns.example.com. admin.example.com. (
+ ...
+ '';
+ };
+ };
+ };
+
+ "example.net." = {
+ provideXFR = [ "10.3.2.1 NOKEY" ];
+ 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.
+ '';
+ };
+
+ };
+ };
+
+ config = mkIf cfg.enable {
+
+ # this is not working :(
+ nixpkgs.config.nsd = {
+ ipv6 = cfg.ipv6;
+ ratelimit = cfg.ratelimit.enable;
+ rootServer = cfg.rootServer;
+ };
+
+ users.extraGroups = singleton {
+ name = username;
+ gid = config.ids.gids.nsd;
+ };
+
+ users.extraUsers = singleton {
+ name = username;
+ description = "NSD service user";
+ home = stateDir;
+ createHome = true;
+ uid = config.ids.uids.nsd;
+ group = username;
+ };
+
+ systemd.services.nsd = {
+ description = "NSD authoritative only domain name service";
+ wantedBy = [ "multi-user.target" ];
+ after = [ "network.target" ];
+
+ serviceConfig = {
+ Type = "forking";
+ PIDFile = pidFile;
+ Restart = "always";
+ ExecStart = "${pkgs.nsd}/sbin/nsd -c ${configFile}";
+ };
+
+ 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"
+
+ ${pkgs.coreutils}/bin/touch "${stateDir}/don't touch anything in here"
+
+ ${pkgs.coreutils}/bin/rm -f "${stateDir}/private/"*
+ ${pkgs.coreutils}/bin/rm -f "${stateDir}/tmp/"*
+
+ ${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"
+
+ ${pkgs.coreutils}/bin/rm -rf "${stateDir}/zones"
+ ${pkgs.coreutils}/bin/cp -r "${zoneFiles}" "${stateDir}/zones"
+
+ ${copyKeys}
+ '';
+ };
+
+ };
+}