2014-05-05 19:58:51 +01:00
|
|
|
{ config, pkgs, lib, ... }:
|
2014-06-12 10:20:43 +01:00
|
|
|
|
2014-05-05 19:58:51 +01:00
|
|
|
with lib;
|
2014-06-12 10:20:43 +01:00
|
|
|
|
|
|
|
let
|
|
|
|
cfg = config.services.nsd;
|
|
|
|
|
|
|
|
username = "nsd";
|
|
|
|
stateDir = "/var/lib/nsd";
|
2016-01-19 09:34:58 +00:00
|
|
|
pidFile = stateDir + "/var/nsd.pid";
|
2014-06-12 10:20:43 +01:00
|
|
|
|
2016-01-19 20:53:16 +00:00
|
|
|
# build nsd with the options needed for the given config
|
2015-03-19 11:10:55 +00:00
|
|
|
nsdPkg = pkgs.nsd.override {
|
2017-12-28 13:29:52 +00:00
|
|
|
configFile = "${configFile}/nsd.conf";
|
|
|
|
|
2015-03-19 11:10:55 +00:00
|
|
|
bind8Stats = cfg.bind8Stats;
|
2016-01-19 09:34:58 +00:00
|
|
|
ipv6 = cfg.ipv6;
|
|
|
|
ratelimit = cfg.ratelimit.enable;
|
2015-03-19 11:10:55 +00:00
|
|
|
rootServer = cfg.rootServer;
|
2016-01-19 09:34:58 +00:00
|
|
|
zoneStats = length (collect (x: (x.zoneStats or null) != null) cfg.zones) > 0;
|
2015-03-19 11:10:55 +00:00
|
|
|
};
|
|
|
|
|
2016-01-19 20:53:16 +00:00
|
|
|
|
|
|
|
nsdEnv = pkgs.buildEnv {
|
2014-07-23 16:04:21 +01:00
|
|
|
name = "nsd-env";
|
2016-01-19 20:53:16 +00:00
|
|
|
|
|
|
|
paths = [ configFile ]
|
|
|
|
++ mapAttrsToList (name: zone: writeZoneData name zone.data) zoneConfigs;
|
|
|
|
|
|
|
|
postBuild = ''
|
|
|
|
echo "checking zone files"
|
|
|
|
cd $out/zones
|
|
|
|
|
|
|
|
for zoneFile in *; do
|
2017-05-16 11:40:09 +01:00
|
|
|
echo "|- checking zone '$out/zones/$zoneFile'"
|
2016-01-19 20:53:16 +00:00
|
|
|
${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}";
|
2014-06-12 10:20:43 +01:00
|
|
|
};
|
|
|
|
|
2016-01-19 20:53:16 +00:00
|
|
|
|
2016-01-19 09:34:58 +00:00
|
|
|
# options are ordered alphanumerically by the nixos option name
|
2016-01-19 20:53:16 +00:00
|
|
|
configFile = pkgs.writeTextDir "nsd.conf" ''
|
2014-06-12 10:20:43 +01:00
|
|
|
server:
|
|
|
|
chroot: "${stateDir}"
|
2016-01-19 09:34:58 +00:00
|
|
|
username: ${username}
|
2014-06-12 10:20:43 +01:00
|
|
|
|
|
|
|
# The directory for zonefile: files. The daemon chdirs here.
|
|
|
|
zonesdir: "${stateDir}"
|
|
|
|
|
|
|
|
# the list of dynamically added zones.
|
|
|
|
database: "${stateDir}/var/nsd.db"
|
|
|
|
pidfile: "${pidFile}"
|
|
|
|
xfrdfile: "${stateDir}/var/xfrd.state"
|
|
|
|
xfrdir: "${stateDir}/tmp"
|
2016-01-19 09:34:58 +00:00
|
|
|
zonelistfile: "${stateDir}/var/zone.list"
|
2014-06-12 10:20:43 +01:00
|
|
|
|
|
|
|
# interfaces
|
|
|
|
${forEach " ip-address: " cfg.interfaces}
|
|
|
|
|
2016-09-26 23:10:39 +01:00
|
|
|
ip-freebind: ${yesOrNo cfg.ipFreebind}
|
2016-01-19 09:34:58 +00:00
|
|
|
hide-version: ${yesOrNo cfg.hideVersion}
|
|
|
|
identity: "${cfg.identity}"
|
2014-06-12 10:20:43 +01:00
|
|
|
ip-transparent: ${yesOrNo cfg.ipTransparent}
|
|
|
|
do-ip4: ${yesOrNo cfg.ipv4}
|
2016-01-19 09:34:58 +00:00
|
|
|
ipv4-edns-size: ${toString cfg.ipv4EDNSSize}
|
2014-06-12 10:20:43 +01:00
|
|
|
do-ip6: ${yesOrNo cfg.ipv6}
|
2016-01-19 09:34:58 +00:00
|
|
|
ipv6-edns-size: ${toString cfg.ipv6EDNSSize}
|
2016-01-19 20:53:16 +00:00
|
|
|
log-time-ascii: ${yesOrNo cfg.logTimeAscii}
|
2014-06-12 10:20:43 +01:00
|
|
|
${maybeString "nsid: " cfg.nsid}
|
2016-01-19 09:34:58 +00:00
|
|
|
port: ${toString cfg.port}
|
2016-01-19 10:03:57 +00:00
|
|
|
reuseport: ${yesOrNo cfg.reuseport}
|
|
|
|
round-robin: ${yesOrNo cfg.roundRobin}
|
2016-01-19 09:34:58 +00:00
|
|
|
server-count: ${toString cfg.serverCount}
|
2016-09-26 23:10:39 +01:00
|
|
|
${maybeToString "statistics: " cfg.statistics}
|
2014-06-12 10:20:43 +01:00
|
|
|
tcp-count: ${toString cfg.tcpCount}
|
|
|
|
tcp-query-count: ${toString cfg.tcpQueryCount}
|
|
|
|
tcp-timeout: ${toString cfg.tcpTimeout}
|
2016-01-19 09:34:58 +00:00
|
|
|
verbosity: ${toString cfg.verbosity}
|
2016-01-19 10:03:57 +00:00
|
|
|
${maybeString "version: " cfg.version}
|
2014-06-12 10:20:43 +01:00
|
|
|
xfrd-reload-timeout: ${toString cfg.xfrdReloadTimeout}
|
|
|
|
zonefiles-check: ${yesOrNo cfg.zonefilesCheck}
|
|
|
|
|
|
|
|
${maybeString "rrl-ipv4-prefix-length: " cfg.ratelimit.ipv4PrefixLength}
|
|
|
|
${maybeString "rrl-ipv6-prefix-length: " cfg.ratelimit.ipv6PrefixLength}
|
2016-01-19 09:34:58 +00:00
|
|
|
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}
|
2014-06-12 10:20:43 +01:00
|
|
|
|
|
|
|
${keyConfigFile}
|
|
|
|
|
|
|
|
remote-control:
|
|
|
|
control-enable: ${yesOrNo cfg.remoteControl.enable}
|
2016-01-19 09:34:58 +00:00
|
|
|
control-key-file: "${cfg.remoteControl.controlKeyFile}"
|
|
|
|
control-cert-file: "${cfg.remoteControl.controlCertFile}"
|
2014-06-12 10:20:43 +01:00
|
|
|
${forEach " control-interface: " cfg.remoteControl.interfaces}
|
2016-01-19 09:34:58 +00:00
|
|
|
control-port: ${toString cfg.remoteControl.port}
|
2014-06-12 10:20:43 +01:00
|
|
|
server-key-file: "${cfg.remoteControl.serverKeyFile}"
|
|
|
|
server-cert-file: "${cfg.remoteControl.serverCertFile}"
|
|
|
|
|
|
|
|
${concatStrings (mapAttrsToList zoneConfigFile zoneConfigs)}
|
|
|
|
|
|
|
|
${cfg.extraConfig}
|
|
|
|
'';
|
|
|
|
|
2016-01-19 09:34:58 +00:00
|
|
|
yesOrNo = b: if b then "yes" else "no";
|
2016-11-10 21:40:09 +00:00
|
|
|
maybeString = prefix: x: if x == null then "" else ''${prefix} "${x}"'';
|
|
|
|
maybeToString = prefix: x: if x == null then "" else ''${prefix} ${toString x}'';
|
2016-01-19 09:34:58 +00:00
|
|
|
forEach = pre: l: concatMapStrings (x: pre + x + "\n") l;
|
2014-06-12 10:20:43 +01:00
|
|
|
|
|
|
|
|
|
|
|
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"
|
2016-01-19 20:53:16 +00:00
|
|
|
chown ${username}:${username} "$dest"
|
|
|
|
chmod 0400 "$dest"
|
2014-06-12 10:20:43 +01:00
|
|
|
'') cfg.keys);
|
|
|
|
|
|
|
|
|
2016-01-19 09:34:58 +00:00
|
|
|
# options are ordered alphanumerically by the nixos option name
|
2014-06-12 10:20:43 +01:00
|
|
|
zoneConfigFile = name: zone: ''
|
2014-07-23 16:04:21 +01:00
|
|
|
zone:
|
|
|
|
name: "${name}"
|
|
|
|
zonefile: "${stateDir}/zones/${name}"
|
|
|
|
${maybeString "outgoing-interface: " zone.outgoingInterface}
|
|
|
|
${forEach " rrl-whitelist: " zone.rrlWhitelist}
|
2016-01-19 09:34:58 +00:00
|
|
|
${maybeString "zonestats: " zone.zoneStats}
|
2014-07-23 16:04:21 +01:00
|
|
|
|
2016-09-26 23:10:39 +01:00
|
|
|
${maybeToString "max-refresh-time: " zone.maxRefreshSecs}
|
|
|
|
${maybeToString "min-refresh-time: " zone.minRefreshSecs}
|
|
|
|
${maybeToString "max-retry-time: " zone.maxRetrySecs}
|
|
|
|
${maybeToString "min-retry-time: " zone.minRetrySecs}
|
|
|
|
|
2016-01-19 09:34:58 +00:00
|
|
|
allow-axfr-fallback: ${yesOrNo zone.allowAXFRFallback}
|
2014-07-23 16:04:21 +01:00
|
|
|
${forEach " allow-notify: " zone.allowNotify}
|
|
|
|
${forEach " request-xfr: " zone.requestXFR}
|
|
|
|
|
|
|
|
${forEach " notify: " zone.notify}
|
|
|
|
notify-retry: ${toString zone.notifyRetry}
|
|
|
|
${forEach " provide-xfr: " zone.provideXFR}
|
|
|
|
'';
|
2014-06-12 10:20:43 +01:00
|
|
|
|
|
|
|
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) (
|
2014-07-23 16:04:21 +01:00
|
|
|
mapAttrsToList (name: child: zoneConfigs' (parent // zone // { children = {}; }) name child)
|
|
|
|
zone.children
|
2014-06-12 10:20:43 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
# fighting infinite recursion
|
2016-01-19 09:34:58 +00:00
|
|
|
zoneOptions = zoneOptionsRaw // childConfig zoneOptions1 true;
|
2014-06-12 10:20:43 +01:00
|
|
|
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; }; };
|
|
|
|
|
2016-01-19 09:34:58 +00:00
|
|
|
# options are ordered alphanumerically
|
2014-07-23 16:04:21 +01:00
|
|
|
zoneOptionsRaw = types.submodule {
|
|
|
|
options = {
|
2016-01-19 09:34:58 +00:00
|
|
|
|
|
|
|
allowAXFRFallback = mkOption {
|
|
|
|
type = types.bool;
|
|
|
|
default = true;
|
2014-07-23 16:04:21 +01:00
|
|
|
description = ''
|
2016-01-19 09:34:58 +00:00
|
|
|
If NSD as secondary server should be allowed to AXFR if the primary
|
|
|
|
server does not allow IXFR.
|
2014-07-23 16:04:21 +01:00
|
|
|
'';
|
|
|
|
};
|
2014-06-12 10:20:43 +01:00
|
|
|
|
2014-07-23 16:04:21 +01:00
|
|
|
allowNotify = mkOption {
|
2016-01-19 09:34:58 +00:00
|
|
|
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"
|
|
|
|
];
|
2014-07-23 16:04:21 +01:00
|
|
|
description = ''
|
|
|
|
Listed primary servers are allowed to notify this secondary server.
|
|
|
|
<screen><![CDATA[
|
|
|
|
Format: <ip> <key-name | NOKEY | BLOCKED>
|
|
|
|
|
|
|
|
<ip> 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
|
|
|
|
|
|
|
|
<key-name | NOKEY | BLOCKED>
|
|
|
|
* <key-name> will use the specified TSIG key
|
|
|
|
* NOKEY no TSIG signature is required
|
|
|
|
* BLOCKED notifies from non-listed or blocked IPs will be ignored
|
|
|
|
* ]]></screen>
|
|
|
|
'';
|
|
|
|
};
|
2014-06-12 10:20:43 +01:00
|
|
|
|
2016-01-19 09:34:58 +00:00
|
|
|
children = mkOption {
|
|
|
|
default = {};
|
2014-07-23 16:04:21 +01:00
|
|
|
description = ''
|
2016-01-19 09:34:58 +00:00
|
|
|
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.
|
2014-07-23 16:04:21 +01:00
|
|
|
'';
|
|
|
|
};
|
2014-06-12 10:20:43 +01:00
|
|
|
|
2016-01-19 09:34:58 +00:00
|
|
|
data = mkOption {
|
|
|
|
type = types.str;
|
|
|
|
default = "";
|
|
|
|
example = "";
|
2014-07-23 16:04:21 +01:00
|
|
|
description = ''
|
2016-01-19 09:34:58 +00:00
|
|
|
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.
|
2014-07-23 16:04:21 +01:00
|
|
|
'';
|
|
|
|
};
|
2014-06-12 10:20:43 +01:00
|
|
|
|
2016-09-26 23:10:39 +01:00
|
|
|
maxRefreshSecs = mkOption {
|
|
|
|
type = types.nullOr types.int;
|
|
|
|
default = null;
|
|
|
|
description = ''
|
|
|
|
Limit refresh time for secondary zones. This is the timer which
|
|
|
|
checks to see if the zone has to be refetched when it expires.
|
|
|
|
Normally the value from the SOA record is used, but this option
|
|
|
|
restricts that value.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
minRefreshSecs = mkOption {
|
|
|
|
type = types.nullOr types.int;
|
|
|
|
default = null;
|
|
|
|
description = ''
|
|
|
|
Limit refresh time for secondary zones.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
maxRetrySecs = mkOption {
|
|
|
|
type = types.nullOr types.int;
|
|
|
|
default = null;
|
|
|
|
description = ''
|
|
|
|
Limit retry time for secondary zones. This is the timeout after
|
|
|
|
a failed fetch attempt for the zone. Normally the value from
|
|
|
|
the SOA record is used, but this option restricts that value.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
minRetrySecs = mkOption {
|
|
|
|
type = types.nullOr types.int;
|
|
|
|
default = null;
|
|
|
|
description = ''
|
|
|
|
Limit retry time for secondary zones.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2014-07-23 16:04:21 +01:00
|
|
|
notify = mkOption {
|
2016-01-19 09:34:58 +00:00
|
|
|
type = types.listOf types.str;
|
|
|
|
default = [];
|
|
|
|
example = [ "10.0.0.1@3721 my_key" "::5 NOKEY" ];
|
2014-07-23 16:04:21 +01:00
|
|
|
description = ''
|
|
|
|
This primary server will notify all given secondary servers about
|
|
|
|
zone changes.
|
|
|
|
<screen><![CDATA[
|
|
|
|
Format: <ip> <key-name | NOKEY>
|
2014-06-12 10:20:43 +01:00
|
|
|
|
2014-07-23 16:04:21 +01:00
|
|
|
<ip> a plain IPv4/IPv6 address with on optional port number (ip@port)
|
2014-06-12 10:20:43 +01:00
|
|
|
|
2014-07-23 16:04:21 +01:00
|
|
|
<key-name | NOKEY>
|
|
|
|
* <key-name> sign notifies with the specified key
|
|
|
|
* NOKEY don't sign notifies
|
|
|
|
]]></screen>
|
|
|
|
'';
|
|
|
|
};
|
2014-06-12 10:20:43 +01:00
|
|
|
|
2014-07-23 16:04:21 +01:00
|
|
|
notifyRetry = mkOption {
|
2016-01-19 09:34:58 +00:00
|
|
|
type = types.int;
|
|
|
|
default = 5;
|
2014-07-23 16:04:21 +01:00
|
|
|
description = ''
|
|
|
|
Specifies the number of retries for failed notifies. Set this along with notify.
|
|
|
|
'';
|
|
|
|
};
|
2014-06-12 10:20:43 +01:00
|
|
|
|
2016-01-19 09:34:58 +00:00
|
|
|
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).
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2014-07-23 16:04:21 +01:00
|
|
|
provideXFR = mkOption {
|
2016-01-19 09:34:58 +00:00
|
|
|
type = types.listOf types.str;
|
|
|
|
default = [];
|
|
|
|
example = [ "192.0.2.0/24 NOKEY" "192.0.2.0/24 my_tsig_key_name" ];
|
2014-07-23 16:04:21 +01:00
|
|
|
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
|
|
|
|
'';
|
|
|
|
};
|
2014-06-12 10:20:43 +01:00
|
|
|
|
2016-01-19 09:34:58 +00:00
|
|
|
requestXFR = mkOption {
|
|
|
|
type = types.listOf types.str;
|
|
|
|
default = [];
|
|
|
|
example = [];
|
2014-07-23 16:04:21 +01:00
|
|
|
description = ''
|
2016-01-19 09:34:58 +00:00
|
|
|
Format: <code>[AXFR|UDP] <ip-address> <key-name | NOKEY></code>
|
2014-07-23 16:04:21 +01:00
|
|
|
'';
|
|
|
|
};
|
2014-06-12 10:20:43 +01:00
|
|
|
|
2014-07-23 16:04:21 +01:00
|
|
|
rrlWhitelist = mkOption {
|
2016-11-16 13:37:14 +00:00
|
|
|
type = with types; listOf (enum [ "nxdomain" "error" "referral" "any" "rrsig" "wildcard" "nodata" "dnskey" "positive" "all" ]);
|
2016-01-19 09:34:58 +00:00
|
|
|
default = [];
|
2014-07-23 16:04:21 +01:00
|
|
|
description = ''
|
|
|
|
Whitelists the given rrl-types.
|
|
|
|
'';
|
|
|
|
};
|
2014-06-12 10:20:43 +01:00
|
|
|
|
2015-03-18 20:01:35 +00:00
|
|
|
zoneStats = mkOption {
|
2016-01-19 09:34:58 +00:00
|
|
|
type = types.nullOr types.str;
|
|
|
|
default = null;
|
|
|
|
example = "%s";
|
2015-03-18 20:01:35 +00:00
|
|
|
description = ''
|
2015-03-19 11:10:55 +00:00
|
|
|
When set to something distinct to null NSD is able to collect
|
2015-03-18 20:01:35 +00:00
|
|
|
statistics per zone. All statistics of this zone(s) will be added
|
|
|
|
to the group specified by this given name. Use "%s" to use the zones
|
|
|
|
name as the group. The groups are output from nsd-control stats
|
|
|
|
and stats_noreset.
|
|
|
|
'';
|
|
|
|
};
|
2016-01-19 09:34:58 +00:00
|
|
|
|
2014-07-23 16:04:21 +01:00
|
|
|
};
|
|
|
|
};
|
2014-06-12 10:20:43 +01:00
|
|
|
|
|
|
|
in
|
|
|
|
{
|
2016-01-19 09:34:58 +00:00
|
|
|
# options are ordered alphanumerically
|
|
|
|
options.services.nsd = {
|
2014-06-12 10:20:43 +01:00
|
|
|
|
2016-01-19 09:34:58 +00:00
|
|
|
enable = mkEnableOption "NSD authoritative DNS server";
|
2015-03-19 11:10:55 +00:00
|
|
|
|
2016-01-19 09:34:58 +00:00
|
|
|
bind8Stats = mkEnableOption "BIND8 like statistics";
|
2014-06-12 10:20:43 +01:00
|
|
|
|
2016-01-19 09:34:58 +00:00
|
|
|
extraConfig = mkOption {
|
|
|
|
type = types.str;
|
|
|
|
default = "";
|
|
|
|
description = ''
|
|
|
|
Extra nsd config.
|
|
|
|
'';
|
|
|
|
};
|
2014-06-12 10:20:43 +01:00
|
|
|
|
2016-01-19 09:34:58 +00:00
|
|
|
hideVersion = mkOption {
|
|
|
|
type = types.bool;
|
|
|
|
default = true;
|
|
|
|
description = ''
|
2016-03-06 23:54:38 +00:00
|
|
|
Whether NSD should answer VERSION.BIND and VERSION.SERVER CHAOS class queries.
|
2016-01-19 09:34:58 +00:00
|
|
|
'';
|
|
|
|
};
|
2014-06-12 10:20:43 +01:00
|
|
|
|
2016-01-19 09:34:58 +00:00
|
|
|
identity = mkOption {
|
|
|
|
type = types.str;
|
|
|
|
default = "unidentified server";
|
|
|
|
description = ''
|
|
|
|
Identify the server (CH TXT ID.SERVER entry).
|
|
|
|
'';
|
|
|
|
};
|
2014-06-12 10:20:43 +01:00
|
|
|
|
2016-01-19 09:34:58 +00:00
|
|
|
interfaces = mkOption {
|
|
|
|
type = types.listOf types.str;
|
|
|
|
default = [ "127.0.0.0" "::1" ];
|
|
|
|
description = ''
|
|
|
|
What addresses the server should listen to.
|
|
|
|
'';
|
|
|
|
};
|
2014-06-12 10:20:43 +01:00
|
|
|
|
2016-09-26 23:10:39 +01:00
|
|
|
ipFreebind = mkOption {
|
|
|
|
type = types.bool;
|
|
|
|
default = false;
|
|
|
|
description = ''
|
|
|
|
Whether to bind to nonlocal addresses and interfaces that are down.
|
|
|
|
Similar to ip-transparent.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2016-01-19 09:34:58 +00:00
|
|
|
ipTransparent = mkOption {
|
|
|
|
type = types.bool;
|
|
|
|
default = false;
|
|
|
|
description = ''
|
|
|
|
Allow binding to non local addresses.
|
|
|
|
'';
|
|
|
|
};
|
2014-06-12 10:20:43 +01:00
|
|
|
|
2016-01-19 09:34:58 +00:00
|
|
|
ipv4 = mkOption {
|
|
|
|
type = types.bool;
|
|
|
|
default = true;
|
|
|
|
description = ''
|
2016-03-06 23:54:38 +00:00
|
|
|
Whether to listen on IPv4 connections.
|
2016-01-19 09:34:58 +00:00
|
|
|
'';
|
|
|
|
};
|
2014-06-12 10:20:43 +01:00
|
|
|
|
2016-01-19 09:34:58 +00:00
|
|
|
ipv4EDNSSize = mkOption {
|
|
|
|
type = types.int;
|
|
|
|
default = 4096;
|
|
|
|
description = ''
|
|
|
|
Preferred EDNS buffer size for IPv4.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
ipv6 = mkOption {
|
|
|
|
type = types.bool;
|
|
|
|
default = true;
|
|
|
|
description = ''
|
2016-03-06 23:54:38 +00:00
|
|
|
Whether to listen on IPv6 connections.
|
2016-01-19 09:34:58 +00:00
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
ipv6EDNSSize = mkOption {
|
|
|
|
type = types.int;
|
|
|
|
default = 4096;
|
|
|
|
description = ''
|
|
|
|
Preferred EDNS buffer size for IPv6.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2016-01-19 10:03:57 +00:00
|
|
|
logTimeAscii = mkOption {
|
|
|
|
type = types.bool;
|
|
|
|
default = true;
|
|
|
|
description = ''
|
|
|
|
Log time in ascii, if false then in unix epoch seconds.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2016-01-19 09:34:58 +00:00
|
|
|
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.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2016-01-19 10:03:57 +00:00
|
|
|
reuseport = mkOption {
|
|
|
|
type = types.bool;
|
|
|
|
default = pkgs.stdenv.isLinux;
|
|
|
|
description = ''
|
2016-03-06 23:54:38 +00:00
|
|
|
Whether to enable SO_REUSEPORT on all used sockets. This lets multiple
|
2016-01-19 10:03:57 +00:00
|
|
|
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
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2016-01-19 09:34:58 +00:00
|
|
|
rootServer = mkOption {
|
|
|
|
type = types.bool;
|
|
|
|
default = false;
|
|
|
|
description = ''
|
2016-03-06 23:54:38 +00:00
|
|
|
Whether this server will be a root server (a DNS root server, you
|
2016-01-19 09:34:58 +00:00
|
|
|
usually don't want that).
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2016-01-19 10:03:57 +00:00
|
|
|
roundRobin = mkEnableOption "round robin rotation of records";
|
|
|
|
|
2016-01-19 09:34:58 +00:00
|
|
|
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.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2016-01-19 10:03:57 +00:00
|
|
|
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.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2016-01-19 09:34:58 +00:00
|
|
|
xfrdReloadTimeout = mkOption {
|
|
|
|
type = types.int;
|
|
|
|
default = 1;
|
|
|
|
description = ''
|
|
|
|
Number of seconds between reloads triggered by xfrd.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
zonefilesCheck = mkOption {
|
|
|
|
type = types.bool;
|
|
|
|
default = true;
|
|
|
|
description = ''
|
2016-03-06 23:54:38 +00:00
|
|
|
Whether to check mtime of all zone files on start and sighup.
|
2016-01-19 09:34:58 +00:00
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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 = {};
|
2016-01-19 10:10:09 +00:00
|
|
|
example = literalExample ''
|
|
|
|
{ "tsig.example.org" = {
|
|
|
|
algorithm = "hmac-md5";
|
|
|
|
keyFile = "/path/to/my/key";
|
|
|
|
};
|
2016-02-29 00:47:12 +00:00
|
|
|
}
|
2016-01-19 10:10:09 +00:00
|
|
|
'';
|
2016-01-19 09:34:58 +00:00
|
|
|
description = ''
|
|
|
|
Define your TSIG keys here.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
ratelimit = {
|
2014-06-12 10:20:43 +01:00
|
|
|
|
2016-01-19 09:34:58 +00:00
|
|
|
enable = mkEnableOption "ratelimit capabilities";
|
|
|
|
|
|
|
|
ipv4PrefixLength = mkOption {
|
|
|
|
type = types.nullOr types.int;
|
|
|
|
default = null;
|
2014-06-12 10:20:43 +01:00
|
|
|
description = ''
|
2016-01-19 09:34:58 +00:00
|
|
|
IPv4 prefix length. Addresses are grouped by netblock.
|
2014-06-12 10:20:43 +01:00
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2016-01-19 09:34:58 +00:00
|
|
|
ipv6PrefixLength = mkOption {
|
|
|
|
type = types.nullOr types.int;
|
|
|
|
default = null;
|
2014-06-12 10:20:43 +01:00
|
|
|
description = ''
|
2016-01-19 09:34:58 +00:00
|
|
|
IPv6 prefix length. Addresses are grouped by netblock.
|
2014-06-12 10:20:43 +01:00
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2016-01-19 09:34:58 +00:00
|
|
|
ratelimit = mkOption {
|
|
|
|
type = types.int;
|
|
|
|
default = 200;
|
2014-06-12 10:20:43 +01:00
|
|
|
description = ''
|
2016-01-19 09:34:58 +00:00
|
|
|
Max qps allowed from any query source.
|
|
|
|
0 means unlimited. With an verbosity of 2 blocked and
|
|
|
|
unblocked subnets will be logged.
|
2014-06-12 10:20:43 +01:00
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2016-01-19 09:34:58 +00:00
|
|
|
slip = mkOption {
|
|
|
|
type = types.nullOr types.int;
|
|
|
|
default = null;
|
2014-06-12 10:20:43 +01:00
|
|
|
description = ''
|
2016-01-19 09:34:58 +00:00
|
|
|
Number of packets that get discarded before replying a SLIP response.
|
|
|
|
0 disables SLIP responses. 1 will make every response a SLIP response.
|
2014-06-12 10:20:43 +01:00
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2016-01-19 09:34:58 +00:00
|
|
|
size = mkOption {
|
|
|
|
type = types.int;
|
|
|
|
default = 1000000;
|
2014-06-12 10:20:43 +01:00
|
|
|
description = ''
|
2016-01-19 09:34:58 +00:00
|
|
|
Size of the hashtable. More buckets use more memory but lower
|
|
|
|
the chance of hash hash collisions.
|
2014-06-12 10:20:43 +01:00
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2016-01-19 09:34:58 +00:00
|
|
|
whitelistRatelimit = mkOption {
|
|
|
|
type = types.int;
|
|
|
|
default = 2000;
|
2014-06-12 10:20:43 +01:00
|
|
|
description = ''
|
2016-01-19 09:34:58 +00:00
|
|
|
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.
|
2014-06-12 10:20:43 +01:00
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2016-01-19 09:34:58 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
remoteControl = {
|
|
|
|
|
|
|
|
enable = mkEnableOption "remote control via nsd-control";
|
|
|
|
|
|
|
|
controlCertFile = mkOption {
|
|
|
|
type = types.path;
|
|
|
|
default = "/etc/nsd/nsd_control.pem";
|
2014-06-12 10:20:43 +01:00
|
|
|
description = ''
|
2016-01-19 09:34:58 +00:00
|
|
|
Path to the client certificate signed with the server certificate.
|
|
|
|
This file is used by nsd-control and generated by nsd-control-setup.
|
2014-06-12 10:20:43 +01:00
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2016-01-19 09:34:58 +00:00
|
|
|
controlKeyFile = mkOption {
|
|
|
|
type = types.path;
|
|
|
|
default = "/etc/nsd/nsd_control.key";
|
2014-06-12 10:20:43 +01:00
|
|
|
description = ''
|
2016-01-19 09:34:58 +00:00
|
|
|
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.
|
2014-06-12 10:20:43 +01:00
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2016-01-19 09:34:58 +00:00
|
|
|
interfaces = mkOption {
|
|
|
|
type = types.listOf types.str;
|
|
|
|
default = [ "127.0.0.1" "::1" ];
|
2014-06-12 10:20:43 +01:00
|
|
|
description = ''
|
2016-01-19 09:34:58 +00:00
|
|
|
Which interfaces NSD should bind to for remote control.
|
2014-06-12 10:20:43 +01:00
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2016-01-19 09:34:58 +00:00
|
|
|
port = mkOption {
|
|
|
|
type = types.int;
|
|
|
|
default = 8952;
|
2014-06-12 10:20:43 +01:00
|
|
|
description = ''
|
2016-01-19 09:34:58 +00:00
|
|
|
Port number for remote control operations (uses TLS over TCP).
|
2014-06-12 10:20:43 +01:00
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2016-01-19 09:34:58 +00:00
|
|
|
serverCertFile = mkOption {
|
|
|
|
type = types.path;
|
|
|
|
default = "/etc/nsd/nsd_server.pem";
|
2014-06-12 10:20:43 +01:00
|
|
|
description = ''
|
2016-01-19 09:34:58 +00:00
|
|
|
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.
|
2014-06-12 10:20:43 +01:00
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2016-01-19 09:34:58 +00:00
|
|
|
serverKeyFile = mkOption {
|
|
|
|
type = types.path;
|
|
|
|
default = "/etc/nsd/nsd_server.key";
|
2014-06-12 10:20:43 +01:00
|
|
|
description = ''
|
2016-01-19 09:34:58 +00:00
|
|
|
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.
|
2014-06-12 10:20:43 +01:00
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2016-01-19 09:34:58 +00:00
|
|
|
};
|
2014-06-12 10:20:43 +01:00
|
|
|
|
|
|
|
|
2016-01-19 09:34:58 +00:00
|
|
|
zones = mkOption {
|
|
|
|
type = types.attrsOf zoneOptions;
|
|
|
|
default = {};
|
2016-01-19 10:10:09 +00:00
|
|
|
example = literalExample ''
|
|
|
|
{ "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. (
|
|
|
|
...
|
|
|
|
''';
|
|
|
|
};
|
2014-06-12 10:20:43 +01:00
|
|
|
};
|
2014-07-23 16:04:21 +01:00
|
|
|
};
|
2014-06-12 10:20:43 +01:00
|
|
|
|
2016-01-19 10:10:09 +00:00
|
|
|
"example.net." = {
|
|
|
|
provideXFR = [ "10.3.2.1 NOKEY" ];
|
|
|
|
data = '''
|
|
|
|
...
|
|
|
|
''';
|
|
|
|
};
|
2016-02-29 00:47:12 +00:00
|
|
|
}
|
2016-01-19 10:10:09 +00:00
|
|
|
'';
|
2016-01-19 09:34:58 +00:00
|
|
|
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.
|
|
|
|
'';
|
2014-06-12 10:20:43 +01:00
|
|
|
};
|
2016-01-19 09:34:58 +00:00
|
|
|
|
2014-06-12 10:20:43 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
config = mkIf cfg.enable {
|
|
|
|
|
2017-12-28 13:29:52 +00:00
|
|
|
environment.systemPackages = [ nsdPkg ];
|
|
|
|
|
2014-06-12 10:20:43 +01:00
|
|
|
users.extraGroups = singleton {
|
2014-07-23 16:04:21 +01:00
|
|
|
name = username;
|
2016-01-19 09:34:58 +00:00
|
|
|
gid = config.ids.gids.nsd;
|
2014-06-12 10:20:43 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
users.extraUsers = singleton {
|
2016-01-19 09:34:58 +00:00
|
|
|
name = username;
|
2014-07-23 16:04:21 +01:00
|
|
|
description = "NSD service user";
|
2016-01-19 09:34:58 +00:00
|
|
|
home = stateDir;
|
2014-07-23 16:04:21 +01:00
|
|
|
createHome = true;
|
2016-01-19 09:34:58 +00:00
|
|
|
uid = config.ids.uids.nsd;
|
|
|
|
group = username;
|
2014-06-12 10:20:43 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
systemd.services.nsd = {
|
|
|
|
description = "NSD authoritative only domain name service";
|
2016-01-19 20:53:16 +00:00
|
|
|
|
|
|
|
after = [ "keys.target" "network.target" ];
|
2016-01-19 09:34:58 +00:00
|
|
|
wantedBy = [ "multi-user.target" ];
|
2016-01-19 20:53:16 +00:00
|
|
|
wants = [ "keys.target" ];
|
2014-06-12 10:20:43 +01:00
|
|
|
|
|
|
|
serviceConfig = {
|
2016-01-19 20:53:16 +00:00
|
|
|
ExecStart = "${nsdPkg}/sbin/nsd -d -c ${nsdEnv}/nsd.conf";
|
2017-06-18 13:31:34 +01:00
|
|
|
StandardError = "null";
|
2016-01-19 09:34:58 +00:00
|
|
|
PIDFile = pidFile;
|
|
|
|
Restart = "always";
|
2016-01-19 20:53:16 +00:00
|
|
|
RestartSec = "4s";
|
|
|
|
StartLimitBurst = 4;
|
|
|
|
StartLimitInterval = "5min";
|
2014-06-12 10:20:43 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
preStart = ''
|
2016-01-19 20:53:16 +00:00
|
|
|
rm -Rf "${stateDir}/private/"
|
|
|
|
rm -Rf "${stateDir}/tmp/"
|
2014-06-12 10:20:43 +01:00
|
|
|
|
2016-01-19 20:53:16 +00:00
|
|
|
mkdir -m 0700 -p "${stateDir}/private"
|
|
|
|
mkdir -m 0700 -p "${stateDir}/tmp"
|
|
|
|
mkdir -m 0700 -p "${stateDir}/var"
|
2014-06-12 10:20:43 +01:00
|
|
|
|
2016-01-19 20:53:16 +00:00
|
|
|
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
|
2014-06-12 10:20:43 +01:00
|
|
|
|
2016-01-19 20:53:16 +00:00
|
|
|
chown ${username}:${username} -R "${stateDir}/private"
|
|
|
|
chown ${username}:${username} -R "${stateDir}/tmp"
|
|
|
|
chown ${username}:${username} -R "${stateDir}/var"
|
2014-06-12 10:20:43 +01:00
|
|
|
|
2016-01-19 20:53:16 +00:00
|
|
|
rm -rf "${stateDir}/zones"
|
|
|
|
cp -rL "${nsdEnv}/zones" "${stateDir}/zones"
|
2014-06-12 10:20:43 +01:00
|
|
|
|
|
|
|
${copyKeys}
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
};
|
2017-12-28 13:29:52 +00:00
|
|
|
|
|
|
|
meta.maintainers = with lib.maintainers; [ hrdinka ];
|
2014-06-12 10:20:43 +01:00
|
|
|
}
|