Various updates.

This commit is contained in:
niten 2022-08-22 11:30:04 -07:00
parent 070a5e1831
commit b9def9eba9
4 changed files with 266 additions and 250 deletions

View File

@ -1,4 +1,4 @@
{ config, lib, pkgs, ... } @ toplevel: { config, lib, pkgs, ... }@toplevel:
with lib; with lib;
let let
@ -9,12 +9,10 @@ let
localhost-ips = let localhost-ips = let
addr-only = addrinfo: addrinfo.address; addr-only = addrinfo: addrinfo.address;
interface = config.networking.interfaces.lo; interface = config.networking.interfaces.lo;
in in (map addr-only interface.ipv4.addresses)
(map addr-only interface.ipv4.addresses) ++ ++ (map addr-only interface.ipv6.addresses);
(map addr-only interface.ipv6.addresses);
host-ips = host-ips = (pkgs.lib.fudo.network.host-ips hostname) ++ localhost-ips;
(pkgs.lib.fudo.network.host-ips hostname) ++ localhost-ips;
state-directory = toplevel.config.fudo.auth.kdc.state-directory; state-directory = toplevel.config.fudo.auth.kdc.state-directory;
@ -24,8 +22,7 @@ let
master-server = cfg.master-config != null; master-server = cfg.master-config != null;
slave-server = cfg.slave-config != null; slave-server = cfg.slave-config != null;
get-fqdn = hostname: get-fqdn = hostname: "${hostname}.${config.fudo.hosts.${hostname}.domain}";
"${hostname}.${config.fudo.hosts.${hostname}.domain}";
kdc-conf = generate-kdc-conf { kdc-conf = generate-kdc-conf {
realm = cfg.realm; realm = cfg.realm;
@ -34,102 +31,103 @@ let
acl-data = if master-server then cfg.master-config.acl else null; acl-data = if master-server then cfg.master-config.acl else null;
}; };
initialize-db = initialize-db = { realm, user, group, kdc-conf, key-file, db-name
{ realm, user, group, kdc-conf, key-file, db-name, max-lifetime, max-renewal, , max-lifetime, max-renewal, primary-keytab, kadmin-keytab, kpasswd-keytab
primary-keytab, kadmin-keytab, kpasswd-keytab, ipropd-keytab, local-hostname }: let , ipropd-keytab, local-hostname }:
let
kadmin-cmd = "kadmin -l -c ${kdc-conf} --"; kadmin-cmd = "kadmin -l -c ${kdc-conf} --";
get-domain-hosts = domain: let get-domain-hosts = domain:
let
host-in-subdomain = host: hostOpts: host-in-subdomain = host: hostOpts:
(builtins.match "(.+[.])?${domain}$" hostOpts.domain) != null; (builtins.match "(.+[.])?${domain}$" hostOpts.domain) != null;
in attrNames (filterAttrs host-in-subdomain config.fudo.hosts); in attrNames (filterAttrs host-in-subdomain config.fudo.hosts);
get-host-principals = realm: hostname: let get-host-principals = realm: hostname:
host = config.fudo.hosts.${hostname}; let host = config.fudo.hosts.${hostname};
in map (service: "${service}/${hostname}.${host.domain}@${realm}") in map (service: "${service}/${hostname}.${host.domain}@${realm}")
host.kerberos-services; host.kerberos-services;
add-principal-str = principal: add-principal-str = principal:
"${kadmin-cmd} add --random-key --use-defaults ${principal}"; "${kadmin-cmd} add --random-key --use-defaults ${principal}";
test-existence = principal: test-existence = principal: "[[ $( ${kadmin-cmd} get ${principal} ) ]]";
"[[ $( ${kadmin-cmd} get ${principal} ) ]]";
exists-or-add = principal: '' exists-or-add = principal: ''
if ${test-existence principal}; then if ${test-existence principal}; then
echo "skipping ${principal}, already exists" echo "skipping ${principal}, already exists"
else else
${add-principal-str principal} ${add-principal-str principal}
fi fi
''; '';
ensure-host-principals = realm: ensure-host-principals = realm:
concatStringsSep "\n" concatStringsSep "\n" (map exists-or-add
(map exists-or-add (concatMap (get-host-principals realm)
(concatMap (get-host-principals realm) (get-domain-hosts (toLower realm))));
(get-domain-hosts (toLower realm))));
slave-hostnames = map get-fqdn cfg.master-config.slave-hosts; slave-hostnames = map get-fqdn cfg.master-config.slave-hosts;
ensure-iprop-principals = concatStringsSep "\n" ensure-iprop-principals = concatStringsSep "\n"
(map (host: exists-or-add "iprop/${host}@${realm}") (map (host: exists-or-add "iprop/${host}@${realm}") [ local-hostname ]
[ local-hostname ] ++ slave-hostnames); ++ slave-hostnames);
copy-slave-principals-file = let copy-slave-principals-file = let
slave-principals = map slave-principals =
(host: "iprop/${hostname}@${cfg.realm}") map (host: "iprop/${hostname}@${cfg.realm}") slave-hostnames;
slave-hostnames; slave-principals-file = pkgs.writeText "heimdal-slave-principals"
slave-principals-file = pkgs.writeText "heimdal-slave-principals" (concatStringsSep "\n" slave-principals);
(concatStringsSep "\n" slave-principals); in optionalString (slave-principals-file != null) ''
in optionalString (slave-principals-file != null) '' cp ${slave-principals-file} ${state-directory}/slaves
cp ${slave-principals-file} ${state-directory}/slaves # Since it's copied from /nix/store, this is by default read-only,
# Since it's copied from /nix/store, this is by default read-only, # which causes updates to fail.
# which causes updates to fail. chmod u+w ${state-directory}/slaves
chmod u+w ${state-directory}/slaves '';
'';
in pkgs.writeShellScript "initialize-kdc-db.sh" '' in pkgs.writeShellScript "initialize-kdc-db.sh" ''
TMP=$(mktemp -d -t kdc-XXXXXXXX) TMP=$(mktemp -d -t kdc-XXXXXXXX)
if [ ! -e ${database-file} ]; then if [ ! -e ${database-file} ]; then
## CHANGING HOW THIS WORKS ## CHANGING HOW THIS WORKS
## Now we expect the key to be provided ## Now we expect the key to be provided
# kstash --key-file=${key-file} --random-key # kstash --key-file=${key-file} --random-key
${kadmin-cmd} init --realm-max-ticket-life="${max-lifetime}" --realm-max-renewable-life="${max-renewal}" ${realm} ${kadmin-cmd} init --realm-max-ticket-life="${max-lifetime}" --realm-max-renewable-life="${max-renewal}" ${realm}
fi fi
${ensure-host-principals realm} ${ensure-host-principals realm}
${ensure-iprop-principals} ${ensure-iprop-principals}
echo "*** BEGIN EXTRACTING KEYTABS" echo "*** BEGIN EXTRACTING KEYTABS"
echo "*** You can probably ignore the 'principal does not exist' errors that follow," echo "*** You can probably ignore the 'principal does not exist' errors that follow,"
echo "*** they're just testing for principal existence before creating those that" echo "*** they're just testing for principal existence before creating those that"
echo "*** don't already exist" echo "*** don't already exist"
${kadmin-cmd} ext_keytab --keytab=$TMP/primary.keytab */${local-hostname}@${realm} ${kadmin-cmd} ext_keytab --keytab=$TMP/primary.keytab */${local-hostname}@${realm}
mv $TMP/primary.keytab ${primary-keytab} mv $TMP/primary.keytab ${primary-keytab}
${kadmin-cmd} ext_keytab --keytab=$TMP/kadmin.keytab kadmin/admin@${realm} ${kadmin-cmd} ext_keytab --keytab=$TMP/kadmin.keytab kadmin/admin@${realm}
mv $TMP/kadmin.keytab ${kadmin-keytab} mv $TMP/kadmin.keytab ${kadmin-keytab}
${kadmin-cmd} ext_keytab --keytab=$TMP/kpasswd.keytab kadmin/changepw@${realm} ${kadmin-cmd} ext_keytab --keytab=$TMP/kpasswd.keytab kadmin/changepw@${realm}
mv $TMP/kpasswd.keytab ${kpasswd-keytab} mv $TMP/kpasswd.keytab ${kpasswd-keytab}
${kadmin-cmd} ext_keytab --keytab=$TMP/ipropd.keytab iprop/${local-hostname}@${realm} ${kadmin-cmd} ext_keytab --keytab=$TMP/ipropd.keytab iprop/${local-hostname}@${realm}
mv $TMP/ipropd.keytab ${ipropd-keytab} mv $TMP/ipropd.keytab ${ipropd-keytab}
echo "*** END EXTRACTING KEYTABS" echo "*** END EXTRACTING KEYTABS"
${copy-slave-principals-file} ${copy-slave-principals-file}
''; '';
generate-kdc-conf = { realm, db-file, key-file, acl-data }: generate-kdc-conf = { realm, db-file, key-file, acl-data }:
pkgs.writeText "kdc.conf" '' pkgs.writeText "kdc.conf" ''
[kdc] [kdc]
database = { database = {
dbname = sqlite:${db-file} dbname = sqlite:${db-file}
realm = ${realm} realm = ${realm}
mkey_file = ${key-file} mkey_file = ${key-file}
${optionalString (acl-data != null) ${
"acl_file = ${generate-acl-file acl-data}"} optionalString (acl-data != null)
"acl_file = ${generate-acl-file acl-data}"
}
log_file = ${iprop-log} log_file = ${iprop-log}
} }
@ -171,18 +169,17 @@ let
}; };
}; };
generate-acl-file = acl-entries: let generate-acl-file = acl-entries:
perms-to-permstring = perms: concatStringsSep "," perms; let perms-to-permstring = perms: concatStringsSep "," perms;
in in pkgs.writeText "kdc.acl" (concatStringsSep "\n" (mapAttrsToList
pkgs.writeText "kdc.acl" (concatStringsSep "\n" (mapAttrsToList
(principal: opts: (principal: opts:
"${principal} ${perms-to-permstring opts.perms}${ "${principal} ${perms-to-permstring opts.perms}${
optionalString (opts.target != null) " ${opts.target}" }") optionalString (opts.target != null) " ${opts.target}"
acl-entries)); }") acl-entries));
kadmin-local = kdc-conf: kadmin-local = kdc-conf:
pkgs.writeShellScriptBin "kadmin.local" '' pkgs.writeShellScriptBin "kadmin.local" ''
${pkgs.heimdalFull}/bin/kadmin -l -c ${kdc-conf} $@ ${pkgs.heimdal}/bin/kadmin -l -c ${kdc-conf} $@
''; '';
masterOpts = { ... }: { masterOpts = { ... }: {
@ -329,8 +326,7 @@ in {
} }
{ {
assertion = !(master-server && slave-server); assertion = !(master-server && slave-server);
message = message = "Only one of master-config and slave-config may be provided.";
"Only one of master-config and slave-config may be provided.";
} }
]; ];
@ -363,7 +359,7 @@ in {
}; };
environment = { environment = {
systemPackages = [ pkgs.heimdalFull (kadmin-local kdc-conf) ]; systemPackages = [ pkgs.heimdal (kadmin-local kdc-conf) ];
## This shouldn't be necessary...every host gets a krb5.keytab ## This shouldn't be necessary...every host gets a krb5.keytab
# etc = { # etc = {
@ -376,9 +372,8 @@ in {
# }; # };
}; };
systemd.tmpfiles.rules = [ systemd.tmpfiles.rules =
"d ${state-directory} 0740 ${cfg.user} ${cfg.group} - -" [ "d ${state-directory} 0740 ${cfg.user} ${cfg.group} - -" ];
];
fudo.system = { fudo.system = {
services = if master-server then { services = if master-server then {
@ -391,7 +386,8 @@ in {
after = [ "network.target" ]; after = [ "network.target" ];
description = description =
"Heimdal Kerberos Key Distribution Center (ticket server)."; "Heimdal Kerberos Key Distribution Center (ticket server).";
execStart = "${pkgs.heimdalFull}/libexec/heimdal/kdc -c ${kdc-conf} --ports=88 ${listen-addrs}"; execStart =
"${pkgs.heimdal}/libexec/heimdal/kdc -c ${kdc-conf} --ports=88 ${listen-addrs}";
user = cfg.user; user = cfg.user;
group = cfg.group; group = cfg.group;
workingDirectory = state-directory; workingDirectory = state-directory;
@ -426,26 +422,29 @@ in {
execStart = "${init-cmd}"; execStart = "${init-cmd}";
user = cfg.user; user = cfg.user;
group = cfg.group; group = cfg.group;
path = with pkgs; [ heimdalFull ]; path = with pkgs; [ heimdal ];
protectSystem = "full"; protectSystem = "full";
addressFamilies = [ "AF_INET" "AF_INET6" ]; addressFamilies = [ "AF_INET" "AF_INET6" ];
workingDirectory = state-directory; workingDirectory = state-directory;
environment = { KRB5_CONFIG = "/etc/krb5.conf"; }; environment = { KRB5_CONFIG = "/etc/krb5.conf"; };
}; };
heimdal-ipropd-master = mkIf (length cfg.master-config.slave-hosts > 0) { heimdal-ipropd-master =
requires = [ "heimdal-kdc.service" ]; mkIf (length cfg.master-config.slave-hosts > 0) {
wantedBy = [ "multi-user.target" ]; requires = [ "heimdal-kdc.service" ];
description = "Propagate changes to the master KDC DB to all slaves."; wantedBy = [ "multi-user.target" ];
path = with pkgs; [ heimdalFull ]; description =
execStart = "${pkgs.heimdalFull}/libexec/heimdal/ipropd-master -c ${kdc-conf} -k ${cfg.master.ipropd-keytab}"; "Propagate changes to the master KDC DB to all slaves.";
user = cfg.user; path = with pkgs; [ heimdal ];
group = cfg.group; execStart =
workingDirectory = state-directory; "${pkgs.heimdal}/libexec/heimdal/ipropd-master -c ${kdc-conf} -k ${cfg.master.ipropd-keytab}";
privateNetwork = false; user = cfg.user;
addressFamilies = [ "AF_INET" "AF_INET6" ]; group = cfg.group;
environment = { KRB5_CONFIG = "/etc/krb5.conf"; }; workingDirectory = state-directory;
}; privateNetwork = false;
addressFamilies = [ "AF_INET" "AF_INET6" ];
environment = { KRB5_CONFIG = "/etc/krb5.conf"; };
};
} else { } else {
@ -453,7 +452,7 @@ in {
listen-addrs = concatStringsSep " " listen-addrs = concatStringsSep " "
(map (addr: "--addresses=${addr}") cfg.bind-addresses); (map (addr: "--addresses=${addr}") cfg.bind-addresses);
command = command =
"${pkgs.heimdalFull}/libexec/heimdal/kdc -c ${kdc-conf} --ports=88 ${listen-addrs}"; "${pkgs.heimdal}/libexec/heimdal/kdc -c ${kdc-conf} --ports=88 ${listen-addrs}";
in { in {
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
after = [ "network.target" ]; after = [ "network.target" ];
@ -471,10 +470,11 @@ in {
heimdal-ipropd-slave = { heimdal-ipropd-slave = {
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
description = "Receive changes propagated from the KDC master server."; description =
path = with pkgs; [ heimdalFull ]; "Receive changes propagated from the KDC master server.";
path = with pkgs; [ heimdal ];
execStart = concatStringsSep " " [ execStart = concatStringsSep " " [
"${pkgs.heimdalFull}/libexec/heimdal/ipropd-slave" "${pkgs.heimdal}/libexec/heimdal/ipropd-slave"
"--config-file=${kdc-conf}" "--config-file=${kdc-conf}"
"--keytab=${cfg.slave-config.ipropd-keytab}" "--keytab=${cfg.slave-config.ipropd-keytab}"
"--realm=${cfg.realm}" "--realm=${cfg.realm}"
@ -501,7 +501,7 @@ in {
{ {
name = "kerberos-adm"; name = "kerberos-adm";
user = cfg.user; user = cfg.user;
server = "${pkgs.heimdalFull}/libexec/heimdal/kadmind"; server = "${pkgs.heimdal}/libexec/heimdal/kadmind";
protocol = "tcp"; protocol = "tcp";
serverArgs = serverArgs =
"--config-file=${kdc-conf} --keytab=${cfg.master-config.kadmin-keytab}"; "--config-file=${kdc-conf} --keytab=${cfg.master-config.kadmin-keytab}";
@ -509,7 +509,7 @@ in {
{ {
name = "kpasswd"; name = "kpasswd";
user = cfg.user; user = cfg.user;
server = "${pkgs.heimdalFull}/libexec/heimdal/kpasswdd"; server = "${pkgs.heimdal}/libexec/heimdal/kpasswdd";
protocol = "udp"; protocol = "udp";
serverArgs = serverArgs =
"--config-file=${kdc-conf} --keytab=${cfg.master-config.kpasswdd-keytab}"; "--config-file=${kdc-conf} --keytab=${cfg.master-config.kpasswdd-keytab}";
@ -519,12 +519,10 @@ in {
networking = { networking = {
firewall = { firewall = {
allowedTCPPorts = [ 88 ] ++ allowedTCPPorts = [ 88 ] ++ (optionals master-server [ 749 ])
(optionals master-server [ 749 ]) ++ ++ (optionals slave-server [ 2121 ]);
(optionals slave-server [ 2121 ]); allowedUDPPorts = [ 88 ] ++ (optionals master-server [ 464 ])
allowedUDPPorts = [ 88 ] ++ ++ (optionals slave-server [ 2121 ]);
(optionals master-server [ 464 ]) ++
(optionals slave-server [ 2121 ]);
}; };
}; };
}; };

View File

@ -1,8 +1,7 @@
{ config, lib, pkgs, ... }: { config, lib, pkgs, ... }:
with lib; with lib;
let let cfg = config.fudo.minecraft-server;
cfg = config.fudo.minecraft-server;
in { in {
options.fudo.minecraft-server = { options.fudo.minecraft-server = {
@ -11,7 +10,7 @@ in {
package = mkOption { package = mkOption {
type = types.package; type = types.package;
description = "Minecraft package to use."; description = "Minecraft package to use.";
default = pkgs.minecraft-server_1_15_1; default = pkgs.minecraft-current;
}; };
data-dir = mkOption { data-dir = mkOption {
@ -27,10 +26,11 @@ in {
motd = mkOption { motd = mkOption {
type = types.str; type = types.str;
description = "Welcome message for newcomers."; description = "Welcome message for newcomers.";
default = "Welcome to Minecraft! Have fun building...";
}; };
game-mode = mkOption { game-mode = mkOption {
type = types.enum ["survival" "creative" "adventure" "spectator"]; type = types.enum [ "survival" "creative" "adventure" "spectator" ];
description = "Game mode of the server."; description = "Game mode of the server.";
default = "survival"; default = "survival";
}; };
@ -40,12 +40,15 @@ in {
description = "Difficulty level, where 0 is peaceful and 3 is hard."; description = "Difficulty level, where 0 is peaceful and 3 is hard.";
default = 2; default = 2;
}; };
allow-cheats = mkOption {
type = types.bool;
default = false;
};
}; };
config = mkIf cfg.enable { config = mkIf cfg.enable {
environment.systemPackages = [ environment.systemPackages = [ cfg.package ];
cfg.package
];
services.minecraft-server = { services.minecraft-server = {
enable = true; enable = true;
@ -58,6 +61,7 @@ in {
motd = cfg.motd; motd = cfg.motd;
difficulty = cfg.difficulty; difficulty = cfg.difficulty;
gamemode = cfg.game-mode; gamemode = cfg.game-mode;
allow-cheats = true;
}; };
}; };
}; };

View File

@ -1,4 +1,4 @@
### NOTE: # ## NOTE:
## This is a copy of the upstream version, which allows for overriding the state directory ## This is a copy of the upstream version, which allows for overriding the state directory
{ config, pkgs, lib, ... }: { config, pkgs, lib, ... }:
@ -18,25 +18,26 @@ let
ipv6 = cfg.ipv6; ipv6 = cfg.ipv6;
ratelimit = cfg.ratelimit.enable; ratelimit = cfg.ratelimit.enable;
rootServer = cfg.rootServer; 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;
}; };
mkZoneFileName = name: if name == "." then "root" else name; mkZoneFileName = name: if name == "." then "root" else name;
# replaces include: directives for keys with fake keys for nsd-checkconf # replaces include: directives for keys with fake keys for nsd-checkconf
injectFakeKeys = keys: concatStrings injectFakeKeys = keys:
(mapAttrsToList concatStrings (mapAttrsToList (keyName: keyOptions: ''
(keyName: keyOptions: '' fakeKey="$(${pkgs.bind}/bin/tsig-keygen -a ${
fakeKey="$(${pkgs.bind}/bin/tsig-keygen -a ${escapeShellArgs [ keyOptions.algorithm keyName ]} | grep -oP "\s*secret \"\K.*(?=\";)")" escapeShellArgs [ keyOptions.algorithm keyName ]
sed "s@^\s*include:\s*\"${stateDir}/private/${keyName}\"\$@secret: $fakeKey@" -i $out/nsd.conf } | grep -oP "\s*secret \"\K.*(?=\";)")"
'') sed "s@^\s*include:\s*\"${stateDir}/private/${keyName}\"\$@secret: $fakeKey@" -i $out/nsd.conf
keys); '') keys);
nsdEnv = pkgs.buildEnv { nsdEnv = pkgs.buildEnv {
name = "nsd-env"; name = "nsd-env";
paths = [ configFile ] paths = [ configFile ]
++ mapAttrsToList (name: zone: writeZoneData name zone.data) zoneConfigs; ++ mapAttrsToList (name: zone: writeZoneData name zone.data) zoneConfigs;
postBuild = '' postBuild = ''
echo "checking zone files" echo "checking zone files"
@ -64,12 +65,12 @@ let
''; '';
}; };
writeZoneData = name: text: pkgs.writeTextFile { writeZoneData = name: text:
name = "nsd-zone-${mkZoneFileName name}"; pkgs.writeTextFile {
inherit text; name = "nsd-zone-${mkZoneFileName name}";
destination = "/zones/${mkZoneFileName name}"; inherit text;
}; destination = "/zones/${mkZoneFileName name}";
};
# options are ordered alphanumerically by the nixos option name # options are ordered alphanumerically by the nixos option name
configFile = pkgs.writeTextDir "nsd.conf" '' configFile = pkgs.writeTextDir "nsd.conf" ''
@ -86,19 +87,19 @@ let
zonelistfile: "${stateDir}/var/zone.list" zonelistfile: "${stateDir}/var/zone.list"
# interfaces # interfaces
${forEach " ip-address: " cfg.interfaces} ${forEach " ip-address: " cfg.interfaces}
ip-freebind: ${yesOrNo cfg.ipFreebind} ip-freebind: ${yesOrNo cfg.ipFreebind}
hide-version: ${yesOrNo cfg.hideVersion} hide-version: ${yesOrNo cfg.hideVersion}
identity: "${cfg.identity}" identity: "${cfg.identity}"
ip-transparent: ${yesOrNo cfg.ipTransparent} ip-transparent: ${yesOrNo cfg.ipTransparent}
do-ip4: ${yesOrNo cfg.ipv4} do-ip4: ${yesOrNo cfg.ipv4}
ipv4-edns-size: ${toString cfg.ipv4EDNSSize} ipv4-edns-size: ${toString cfg.ipv4EDNSSize}
do-ip6: ${yesOrNo cfg.ipv6} do-ip6: ${yesOrNo cfg.ipv6}
ipv6-edns-size: ${toString cfg.ipv6EDNSSize} ipv6-edns-size: ${toString cfg.ipv6EDNSSize}
log-time-ascii: ${yesOrNo cfg.logTimeAscii} log-time-ascii: ${yesOrNo cfg.logTimeAscii}
${maybeString "nsid: " cfg.nsid} ${maybeString "nsid: " cfg.nsid}
port: ${toString cfg.port} port: ${toString cfg.port}
reuseport: ${yesOrNo cfg.reuseport} reuseport: ${yesOrNo cfg.reuseport}
round-robin: ${yesOrNo cfg.roundRobin} round-robin: ${yesOrNo cfg.roundRobin}
server-count: ${toString cfg.serverCount} server-count: ${toString cfg.serverCount}
${maybeToString "statistics: " cfg.statistics} ${maybeToString "statistics: " cfg.statistics}
tcp-count: ${toString cfg.tcpCount} tcp-count: ${toString cfg.tcpCount}
@ -107,16 +108,16 @@ let
verbosity: ${toString cfg.verbosity} verbosity: ${toString cfg.verbosity}
${maybeString "version: " cfg.version} ${maybeString "version: " cfg.version}
xfrd-reload-timeout: ${toString cfg.xfrdReloadTimeout} xfrd-reload-timeout: ${toString cfg.xfrdReloadTimeout}
zonefiles-check: ${yesOrNo cfg.zonefilesCheck} zonefiles-check: ${yesOrNo cfg.zonefilesCheck}
${maybeString "rrl-ipv4-prefix-length: " cfg.ratelimit.ipv4PrefixLength} ${maybeString "rrl-ipv4-prefix-length: " cfg.ratelimit.ipv4PrefixLength}
${maybeString "rrl-ipv6-prefix-length: " cfg.ratelimit.ipv6PrefixLength} ${maybeString "rrl-ipv6-prefix-length: " cfg.ratelimit.ipv6PrefixLength}
rrl-ratelimit: ${toString cfg.ratelimit.ratelimit} rrl-ratelimit: ${toString cfg.ratelimit.ratelimit}
${maybeString "rrl-slip: " cfg.ratelimit.slip} ${maybeString "rrl-slip: " cfg.ratelimit.slip}
rrl-size: ${toString cfg.ratelimit.size} rrl-size: ${toString cfg.ratelimit.size}
rrl-whitelist-ratelimit: ${toString cfg.ratelimit.whitelistRatelimit} rrl-whitelist-ratelimit: ${toString cfg.ratelimit.whitelistRatelimit}
${keyConfigFile} ${keyConfigFile}
remote-control: remote-control:
control-enable: ${yesOrNo cfg.remoteControl.enable} control-enable: ${yesOrNo cfg.remoteControl.enable}
control-key-file: "${cfg.remoteControl.controlKeyFile}" control-key-file: "${cfg.remoteControl.controlKeyFile}"
control-cert-file: "${cfg.remoteControl.controlCertFile}" control-cert-file: "${cfg.remoteControl.controlCertFile}"
${forEach " control-interface: " cfg.remoteControl.interfaces} ${forEach " control-interface: " cfg.remoteControl.interfaces}
@ -129,10 +130,10 @@ let
yesOrNo = b: if b then "yes" else "no"; yesOrNo = b: if b then "yes" else "no";
maybeString = prefix: x: if x == null then "" else ''${prefix} "${x}"''; maybeString = prefix: x: if x == null then "" else ''${prefix} "${x}"'';
maybeToString = prefix: x: if x == null then "" else ''${prefix} ${toString x}''; maybeToString = prefix: x:
if x == null then "" else "${prefix} ${toString x}";
forEach = pre: l: concatMapStrings (x: pre + x + "\n") l; forEach = pre: l: concatMapStrings (x: pre + x + "\n") l;
keyConfigFile = concatStrings (mapAttrsToList (keyName: keyOptions: '' keyConfigFile = concatStrings (mapAttrsToList (keyName: keyOptions: ''
key: key:
name: "${keyName}" name: "${keyName}"
@ -148,53 +149,44 @@ let
chmod 0400 "$dest" chmod 0400 "$dest"
'') cfg.keys); '') cfg.keys);
# options are ordered alphanumerically by the nixos option name # options are ordered alphanumerically by the nixos option name
zoneConfigFile = name: zone: '' zoneConfigFile = name: zone: ''
zone: zone:
name: "${name}" name: "${name}"
zonefile: "${stateDir}/zones/${mkZoneFileName name}" zonefile: "${stateDir}/zones/${mkZoneFileName name}"
${maybeString "outgoing-interface: " zone.outgoingInterface} ${maybeString "outgoing-interface: " zone.outgoingInterface}
${forEach " rrl-whitelist: " zone.rrlWhitelist} ${forEach " rrl-whitelist: " zone.rrlWhitelist}
${maybeString "zonestats: " zone.zoneStats} ${maybeString "zonestats: " zone.zoneStats}
${maybeToString "max-refresh-time: " zone.maxRefreshSecs} ${maybeToString "max-refresh-time: " zone.maxRefreshSecs}
${maybeToString "min-refresh-time: " zone.minRefreshSecs} ${maybeToString "min-refresh-time: " zone.minRefreshSecs}
${maybeToString "max-retry-time: " zone.maxRetrySecs} ${maybeToString "max-retry-time: " zone.maxRetrySecs}
${maybeToString "min-retry-time: " zone.minRetrySecs} ${maybeToString "min-retry-time: " zone.minRetrySecs}
allow-axfr-fallback: ${yesOrNo zone.allowAXFRFallback} allow-axfr-fallback: ${yesOrNo zone.allowAXFRFallback}
${forEach " allow-notify: " zone.allowNotify} ${forEach " allow-notify: " zone.allowNotify}
${forEach " request-xfr: " zone.requestXFR} ${forEach " request-xfr: " zone.requestXFR}
${forEach " notify: " zone.notify} ${forEach " notify: " zone.notify}
notify-retry: ${toString zone.notifyRetry} notify-retry: ${toString zone.notifyRetry}
${forEach " provide-xfr: " zone.provideXFR} ${forEach " provide-xfr: " zone.provideXFR}
''; '';
zoneConfigs = zoneConfigs' {} "" { children = cfg.zones; }; zoneConfigs = zoneConfigs' { } "" { children = cfg.zones; };
zoneConfigs' = parent: name: zone: zoneConfigs' = parent: name: zone:
if !(zone ? children) || zone.children == null || zone.children == { } if !(zone ? children) || zone.children == null || zone.children == { }
# leaf -> actual zone # leaf -> actual zone
then listToAttrs [ (nameValuePair name (parent // zone)) ] then
listToAttrs [
(nameValuePair name (parent // zone))
]
# fork -> pattern # fork -> pattern
else zipAttrsWith (name: head) ( else
mapAttrsToList (name: child: zoneConfigs' (parent // zone // { children = {}; }) name child) zipAttrsWith (name: head) (mapAttrsToList (name: child:
zone.children 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; }; };
# options are ordered alphanumerically # options are ordered alphanumerically
zoneOptionsRaw = types.submodule { zoneOptions = types.submodule {
options = { options = {
allowAXFRFallback = mkOption { allowAXFRFallback = mkOption {
@ -209,9 +201,11 @@ let
allowNotify = mkOption { allowNotify = mkOption {
type = types.listOf types.str; type = types.listOf types.str;
default = [ ]; default = [ ];
example = [ "192.0.2.0/24 NOKEY" "10.0.0.1-10.0.0.5 my_tsig_key_name" example = [
"10.0.3.4&255.255.0.0 BLOCKED" "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 = '' description = ''
Listed primary servers are allowed to notify this secondary server. Listed primary servers are allowed to notify this secondary server.
<screen><![CDATA[ <screen><![CDATA[
@ -231,7 +225,14 @@ let
}; };
children = mkOption { children = mkOption {
default = {}; # TODO: This relies on the fact that `types.anything` doesn't set any
# values of its own to any defaults, because in the above zoneConfigs',
# values from children override ones from parents, but only if the
# attributes are defined. Because of this, we can't replace the element
# type here with `zoneConfigs`, since that would set all the attributes
# to default values, breaking the parent inheriting function.
type = types.attrsOf types.anything;
default = { };
description = '' description = ''
Children zones inherit all options of their parents. Attributes Children zones inherit all options of their parents. Attributes
defined in a child will overwrite the ones of its parent. Only defined in a child will overwrite the ones of its parent. Only
@ -245,7 +246,6 @@ let
data = mkOption { data = mkOption {
type = types.lines; type = types.lines;
default = ""; default = "";
example = "";
description = '' description = ''
The actual zone data. This is the content of your zone file. 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. Use imports or pkgs.lib.readFile if you don't want this data in your config file.
@ -274,20 +274,22 @@ let
}; };
zsk = mkOption { zsk = mkOption {
type = keyPolicy; type = keyPolicy;
default = { keySize = 2048; default = {
prePublish = "1w"; keySize = 2048;
postPublish = "1w"; prePublish = "1w";
rollPeriod = "1mo"; postPublish = "1w";
}; rollPeriod = "1mo";
};
description = "Key policy for zone signing keys"; description = "Key policy for zone signing keys";
}; };
ksk = mkOption { ksk = mkOption {
type = keyPolicy; type = keyPolicy;
default = { keySize = 4096; default = {
prePublish = "1mo"; keySize = 4096;
postPublish = "1mo"; prePublish = "1mo";
rollPeriod = "0"; postPublish = "1mo";
}; rollPeriod = "0";
};
description = "Key policy for key signing keys"; description = "Key policy for key signing keys";
}; };
}; };
@ -329,10 +331,9 @@ let
''; '';
}; };
notify = mkOption { notify = mkOption {
type = types.listOf types.str; type = types.listOf types.str;
default = []; default = [ ];
example = [ "10.0.0.1@3721 my_key" "::5 NOKEY" ]; example = [ "10.0.0.1@3721 my_key" "::5 NOKEY" ];
description = '' description = ''
This primary server will notify all given secondary servers about This primary server will notify all given secondary servers about
@ -369,7 +370,7 @@ let
provideXFR = mkOption { provideXFR = mkOption {
type = types.listOf types.str; type = types.listOf types.str;
default = []; default = [ ];
example = [ "192.0.2.0/24 NOKEY" "192.0.2.0/24 my_tsig_key_name" ]; example = [ "192.0.2.0/24 NOKEY" "192.0.2.0/24 my_tsig_key_name" ];
description = '' description = ''
Allow these IPs and TSIG to transfer zones, addr TSIG|NOKEY|BLOCKED Allow these IPs and TSIG to transfer zones, addr TSIG|NOKEY|BLOCKED
@ -379,16 +380,27 @@ let
requestXFR = mkOption { requestXFR = mkOption {
type = types.listOf types.str; type = types.listOf types.str;
default = []; default = [ ];
example = [];
description = '' description = ''
Format: <code>[AXFR|UDP] &lt;ip-address&gt; &lt;key-name | NOKEY&gt;</code> Format: <code>[AXFR|UDP] &lt;ip-address&gt; &lt;key-name | NOKEY&gt;</code>
''; '';
}; };
rrlWhitelist = mkOption { rrlWhitelist = mkOption {
type = with types; listOf (enum [ "nxdomain" "error" "referral" "any" "rrsig" "wildcard" "nodata" "dnskey" "positive" "all" ]); type = with types;
default = []; listOf (enum [
"nxdomain"
"error"
"referral"
"any"
"rrsig"
"wildcard"
"nodata"
"dnskey"
"positive"
"all"
]);
default = [ ];
description = '' description = ''
Whitelists the given rrl-types. Whitelists the given rrl-types.
''; '';
@ -430,9 +442,10 @@ let
}; };
}; };
dnssecZones = (filterAttrs (n: v: if v ? dnssec then v.dnssec else false) zoneConfigs); dnssecZones =
(filterAttrs (n: v: if v ? dnssec then v.dnssec else false) zoneConfigs);
dnssec = dnssecZones != {}; dnssec = dnssecZones != { };
dnssecTools = pkgs.bind.override { enablePython = true; }; dnssecTools = pkgs.bind.override { enablePython = true; };
@ -443,32 +456,40 @@ let
${concatStrings (mapAttrsToList signZone dnssecZones)} ${concatStrings (mapAttrsToList signZone dnssecZones)}
''; '';
signZone = name: zone: '' signZone = name: zone: ''
${dnssecTools}/bin/dnssec-keymgr -g ${dnssecTools}/bin/dnssec-keygen -s ${dnssecTools}/bin/dnssec-settime -K ${stateDir}/dnssec -c ${policyFile name zone.dnssecPolicy} ${name} ${dnssecTools}/bin/dnssec-keymgr -g ${dnssecTools}/bin/dnssec-keygen -s ${dnssecTools}/bin/dnssec-settime -K ${stateDir}/dnssec -c ${
policyFile name zone.dnssecPolicy
} ${name}
${dnssecTools}/bin/dnssec-signzone -S -K ${stateDir}/dnssec -o ${name} -O full -N date ${stateDir}/zones/${name} ${dnssecTools}/bin/dnssec-signzone -S -K ${stateDir}/dnssec -o ${name} -O full -N date ${stateDir}/zones/${name}
${nsdPkg}/sbin/nsd-checkzone ${name} ${stateDir}/zones/${name}.signed && mv -v ${stateDir}/zones/${name}.signed ${stateDir}/zones/${name} ${nsdPkg}/sbin/nsd-checkzone ${name} ${stateDir}/zones/${name}.signed && mv -v ${stateDir}/zones/${name}.signed ${stateDir}/zones/${name}
''; '';
policyFile = name: policy: pkgs.writeText "${name}.policy" '' policyFile = name: policy:
zone ${name} { pkgs.writeText "${name}.policy" ''
algorithm ${policy.algorithm}; zone ${name} {
key-size zsk ${toString policy.zsk.keySize}; algorithm ${policy.algorithm};
key-size ksk ${toString policy.ksk.keySize}; key-size zsk ${toString policy.zsk.keySize};
keyttl ${policy.keyttl}; key-size ksk ${toString policy.ksk.keySize};
pre-publish zsk ${policy.zsk.prePublish}; keyttl ${policy.keyttl};
pre-publish ksk ${policy.ksk.prePublish}; pre-publish zsk ${policy.zsk.prePublish};
post-publish zsk ${policy.zsk.postPublish}; pre-publish ksk ${policy.ksk.prePublish};
post-publish ksk ${policy.ksk.postPublish}; post-publish zsk ${policy.zsk.postPublish};
roll-period zsk ${policy.zsk.rollPeriod}; post-publish ksk ${policy.ksk.postPublish};
roll-period ksk ${policy.ksk.rollPeriod}; roll-period zsk ${policy.zsk.rollPeriod};
coverage ${policy.coverage}; roll-period ksk ${policy.ksk.rollPeriod};
}; coverage ${policy.coverage};
''; };
in '';
{ in {
# options are ordered alphanumerically # options are ordered alphanumerically
options.fudo.nsd = { options.fudo.nsd = {
enable = mkEnableOption "NSD authoritative DNS server"; enable = mkEnableOption "NSD authoritative DNS server";
stateDir = mkOption {
type = types.str;
description = "Directory at which to store NSD state data.";
default = "/var/lib/nsd";
};
bind8Stats = mkEnableOption "BIND8 like statistics"; bind8Stats = mkEnableOption "BIND8 like statistics";
dnssecInterval = mkOption { dnssecInterval = mkOption {
@ -587,6 +608,7 @@ in
reuseport = mkOption { reuseport = mkOption {
type = types.bool; type = types.bool;
default = pkgs.stdenv.isLinux; default = pkgs.stdenv.isLinux;
defaultText = literalExpression "pkgs.stdenv.isLinux";
description = '' description = ''
Whether to enable SO_REUSEPORT on all used sockets. This lets multiple Whether to enable SO_REUSEPORT on all used sockets. This lets multiple
processes bind to the same port. This speeds up operation especially processes bind to the same port. This speeds up operation especially
@ -614,13 +636,6 @@ in
''; '';
}; };
stateDir = mkOption {
type = types.str;
description = "Directory at which to store NSD state data.";
default = "/var/lib/nsd";
};
statistics = mkOption { statistics = mkOption {
type = types.nullOr types.int; type = types.nullOr types.int;
default = null; default = null;
@ -689,7 +704,6 @@ in
''; '';
}; };
keys = mkOption { keys = mkOption {
type = types.attrsOf (types.submodule { type = types.attrsOf (types.submodule {
options = { options = {
@ -714,8 +728,8 @@ in
}; };
}); });
default = {}; default = { };
example = literalExample '' example = literalExpression ''
{ "tsig.example.org" = { { "tsig.example.org" = {
algorithm = "hmac-md5"; algorithm = "hmac-md5";
keyFile = "/path/to/my/key"; keyFile = "/path/to/my/key";
@ -727,7 +741,6 @@ in
''; '';
}; };
ratelimit = { ratelimit = {
enable = mkEnableOption "ratelimit capabilities"; enable = mkEnableOption "ratelimit capabilities";
@ -788,7 +801,6 @@ in
}; };
remoteControl = { remoteControl = {
enable = mkEnableOption "remote control via nsd-control"; enable = mkEnableOption "remote control via nsd-control";
@ -849,8 +861,8 @@ in
zones = mkOption { zones = mkOption {
type = types.attrsOf zoneOptions; type = types.attrsOf zoneOptions;
default = {}; default = { };
example = literalExample '' example = literalExpression ''
{ "serverGroup1" = { { "serverGroup1" = {
provideXFR = [ "10.1.2.3 NOKEY" ]; provideXFR = [ "10.1.2.3 NOKEY" ];
children = { children = {
@ -861,7 +873,7 @@ in
@ IN SOA a.ns.example.com. admin.example.com. ( @ IN SOA a.ns.example.com. admin.example.com. (
... ...
'''; ''';
}; };
"example.org." = { "example.org." = {
data = ''' data = '''
$ORIGIN example.org. $ORIGIN example.org.
@ -869,16 +881,16 @@ in
@ IN SOA a.ns.example.com. admin.example.com. ( @ IN SOA a.ns.example.com. admin.example.com. (
... ...
'''; ''';
};
};
}; };
};
};
"example.net." = { "example.net." = {
provideXFR = [ "10.3.2.1 NOKEY" ]; provideXFR = [ "10.3.2.1 NOKEY" ];
data = ''' data = '''
... ...
'''; ''';
}; };
} }
''; '';
description = '' description = ''
Define your zones here. Zones can cascade other zones and therefore Define your zones here. Zones can cascade other zones and therefore
@ -896,7 +908,7 @@ in
assertions = singleton { assertions = singleton {
assertion = zoneConfigs ? "." -> cfg.rootServer; assertion = zoneConfigs ? "." -> cfg.rootServer;
message = "You have a root zone configured. If this is really what you " message = "You have a root zone configured. If this is really what you "
+ "want, please enable 'services.nsd.rootServer'."; + "want, please enable 'services.nsd.rootServer'.";
}; };
environment = { environment = {
@ -909,7 +921,7 @@ in
users.users.${username} = { users.users.${username} = {
description = "NSD service user"; description = "NSD service user";
home = stateDir; home = stateDir;
createHome = true; createHome = true;
uid = config.ids.uids.nsd; uid = config.ids.uids.nsd;
group = username; group = username;
}; };
@ -921,7 +933,7 @@ in
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
startLimitBurst = 4; startLimitBurst = 4;
startLimitIntervalSec = 5 * 60; # 5 mins startLimitIntervalSec = 5 * 60; # 5 mins
serviceConfig = { serviceConfig = {
ExecStart = "${nsdPkg}/sbin/nsd -d -c ${nsdEnv}/nsd.conf"; ExecStart = "${nsdPkg}/sbin/nsd -d -c ${nsdEnv}/nsd.conf";
StandardError = "null"; StandardError = "null";
@ -975,4 +987,6 @@ in
}; };
}; };
meta.maintainers = with lib.maintainers; [ hrdinka ];
} }

View File

@ -32,7 +32,6 @@ let
pdns-config-dir = pkgs.writeTextDir "pdns.conf" '' pdns-config-dir = pkgs.writeTextDir "pdns.conf" ''
local-address=${concatStringsSep ", " cfg.listen-v4-addresses} local-address=${concatStringsSep ", " cfg.listen-v4-addresses}
local-ipv6=${concatStringsSep ", " cfg.listen-v6-addresses}
local-port=${toString cfg.port} local-port=${toString cfg.port}
launch= launch=
include-dir=${runtime-dir} include-dir=${runtime-dir}
@ -305,13 +304,14 @@ in {
psql -f ${pkgs.powerdns}/share/doc/pdns/schema.pgsql.sql psql -f ${pkgs.powerdns}/share/doc/pdns/schema.pgsql.sql
fi fi
''; '';
# Wait until posgresql is available before starting ## Doesn't seem to use env vars, and so fails if remote
ExecStartPre = ## Wait until posgresql is available before starting
pkgs.writeShellScript "ensure-postgresql-running.sh" '' # ExecStartPre =
while [ ! "$( psql -tAc "SELECT 1" )" ]; do # pkgs.writeShellScript "ensure-postgresql-running.sh" ''
${pkgs.coreutils}/bin/sleep 3 # while [ ! "$( psql -tAc "SELECT 1" )" ]; do
done # ${pkgs.coreutils}/bin/sleep 3
''; # done
# '';
}; };
}; };