modules/tinydns/default.nix

283 lines
11 KiB
Nix

{ config, lib, pkgs, ... }:
let
inherit (lib) any concatStrings concatStringsSep getAttrFromPath getBin id
literalExample mapAttrs' mapAttrsToList mkRemovedOptionModule mkDefault
mkEnableOption mkIf mkOption nameValuePair optional optionalString singleton
types;
inherit (builtins) attrNames;
in {
###### interface
disabledModules = [ "services/networking/tinydns.nix" ];
options = {
services.tinydns = mkOption {
description = "An attribute set of tinydns service configurations. Each Attribute must name the IP address this server will listen on.";
example = literalExample ''
services.tinydns = {
"128.1.2.3" = {
enable = true;
data = "=host.on.network.a:128.1.2.3";
};
"10.0.0.1" = {
enable = true;
data = "=host.on.network.b:10.0.0.1";
};
}
'';
default = {};
type = with types; attrsOf (submodule (
{ config, name, ... }:
{
options = {
enable = mkOption {
default = false;
type = types.bool;
description = ''
Whether to run the tinydns DNS server on the IP address given as
the submodule's name. To answer queries larger than 512 bytes,
make sure to also enable <literal>listenTCP</literal>.
'';
};
data = mkOption {
default = "";
type = types.lines;
description = ''
DNS records, one per line, in the format described in
<citerefentry><refentrytitle>tinydns-data</refentrytitle><manvolnum>8</manvolnum></citerefentry>.
Many advanced records need special encoding when appearing inside this
file. Use the functions from <literal>lib.tinydns</literal> to
generate various kinds of records. All record-building functions
accept a variable amount of arguments with unspecified arguments
filled up with the empty string. See also the example below.</para>
<para>The following record-building functions are implemented in
<literal>lib.tinydns</literal>:</para>
<para><literal>Flag</literal> has one mandatory argument; a DNS
record in the format described in
<citerefentry><refentrytitle>tinydns-data</refentrytitle><manvolnum>8</manvolnum></citerefentry>;
adds up to three additional flags to the DNS record, TTL, timestamp
from which this record becomes valid and the location it is valid in.
All other record-building functions also respect these flags as their
last three arguments in addition to the arguments that are relevant
for their particular type. TTL must be an integer. If timestamp is
given as an integer, it is taken as a UNIX timestamp, otherwise it
must be a string and is taken to be a hex-encoded TAI64 timestamp.
Location must be a string.</para>
<para><literal>AFSDB</literal> has two mandatory arguments;
the requested AFS cell and the database server.</para>
<para><literal>SRV</literal> has five mandatory arguments; the
requested service, a priority (int), a weight (int), a port (int), and
a target.</para>
<para><literal>TXT</literal> has two mandatory arguments; the
requested service and a data field in the format that the service
requires.</para>
<para><literal>URI</literal> has four mandatory arguments; the
requested service, a priority (int), a weight (int), and a target.
'';
example = literalExample ''
%ex
%in:10.0.0
.example.com:127.0.0.2:b
=server.example.com:1.2.3.4:::ex
=server.example.com:10.0.0.1:3600:400000005c0f6e84:in
+future.example.com:127.0.0.127
''${TXT [ "_kerberos.example.com" "EXAMPLE.COM" 360 ]}
''${SRV [ "_kerberos._udp.example.com" 10 100 88 "current.example.com." ]}
''${URIs [ [ "_kerberos.EXAMPLE.COM" 10 1 "krb5srv:m:udp:current.example.com" "" 1542812577 ]
[ "_kerberos.EXAMPLE.COM" 10 1 "krb5srv:m:udp:future.example.com" "" "410000005c6bd4d3" ] ]}
'';
};
listenTCP = mkOption {
type = types.bool;
default = false;
description = ''
Also answer queries via TCP. You need this to answer queries whose
responses are larger than 512 bytes and for zone transfers via AXFR
(not supported, use <literal>secondaries</literal> instead).
'';
};
rootPath = mkOption {
internal = true;
type = types.str;
default = "/run/tinydns-${name}";
description = ''
Set this to an on-disk location to permanently store the DNS record database.
'';
};
secondaries = mkOption {
type = with types; attrsOf (submodule (
{ config, name, ... }:
{
options = {
uri = mkOption {
type = types.str;
example = "tinydns@secondary.example.com";
description = ''
SSH connection uri to the secondary server. On zone updates the
new set of DNS records is transferred to this secondary server
via SSH. Make sure to add the secondary's SSH host key to
programs.ssh.knownHosts!
'';
};
sshKey = mkOption {
type = types.str;
example = "/some/secure/location";
description = ''
File location of the private SSH key to authenticate zone
updates to the secondary server.
'';
};
};
config = {};
}));
default = {};
description = ''
Set of secondary DNS servers. Updates to this server's DNS records are
sent to secondaries via SSH.
'';
};
secondary.enable = mkEnableOption "Configure this tinydns instance as a secondary server receiving its data via SSH.";
secondary.sshKey = mkOption {
type = types.str;
description = "SSH key to authenticate zone transfers to the secondary DNS server.";
default = "";
};
};
}));
};
};
###### implementation
config = mkIf (any id (mapAttrsToList (_: v: v.enable) config.services.tinydns)) {
environment.systemPackages = [ pkgs.djbdns ];
# Restrict to a single session at a time
services.openssh.extraConfig = ''
Match User tinydns
MaxSessions=1
Match All
'';
ids.uids.tinydns = 999;
ids.gids.tinydns = 999;
users.users.tinydns = {
isSystemUser = true;
uid = config.ids.uids.tinydns;
group = "tinydns";
shell = "/run/current-system/sw/bin/bash";
openssh.authorizedKeys.keys = mapAttrsToList (name: cfg: let
# Expects tinydns data on standard input
tinydnsReceive = pkgs.writeScript "tinydns-receive" ''
#!${pkgs.stdenv.shell} -e
cd ${cfg.rootPath}
(
flock -n 5 || exit 1
mv data data.backup
cat >data
if ${getBin pkgs.djbdns}/bin/tinydns-data ; then
rm data.backup
else
mv data.backup data
fi
# Do not allow zone transfers to run more often than once per second due to
# minimum DNS record time resolution.
sleep 1
) 5>data-lock
'';
in optionalString (cfg.secondary.sshKey != "") ''
restrict,command="${tinydnsReceive}" ${cfg.secondary.sshKey}
'') config.services.tinydns;
};
users.groups.tinydns = {
gid = config.ids.gids.tinydns;
};
systemd.services = (mapAttrs' (ip: cfg: nameValuePair "tinydns-${ip}" {
enable = cfg.enable;
description = "djbdns tinydns server";
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" ];
requires = [ "network-online.target" ];
environment = {
IP = ip;
ROOT = cfg.rootPath;
UID = "${toString config.users.users.tinydns.uid}";
GID = "${toString config.users.groups.tinydns.gid}";
AXFR_JOBS = "2"; # Number of concurrent zone transfers to secondary servers.
};
path = with pkgs; [ coreutils djbdns ];
serviceConfig = {
# Drops privileges and chroots to ROOT by itself
ExecStart = "${getBin pkgs.djbdns}/bin/tinydns";
LimitDATA = mkDefault "40000000";
RestartSec = 1;
Restart = "always";
};
preStart = ''
rm -rf $ROOT
mkdir -m750 -p $ROOT
chown ''${UID}:''${GID} $ROOT
cd $ROOT
${if cfg.secondary.enable then "touch data" else "ln -sf ${pkgs.writeText "tinydns-data" cfg.data} data"}
tinydns-data
'';
reload = let
tinydnsSend = pkgs.writeText "tinydns-send.mk" (concatStrings (
(singleton ''
all: ${concatStringsSep " " (attrNames cfg.secondaries)}
'') ++ (mapAttrsToList (k: v: ''
.PHONY: ${k}
${k}::
-${getBin pkgs.openssh}/bin/ssh -a -x -S none -T -i ${v.sshKey} ${v.uri} <${cfg.rootPath}/data
'') cfg.secondaries)));
in ''
cd $ROOT
${optionalString (!cfg.secondary.enable) "ln -sf ${pkgs.writeText "tinydns-data" cfg.data} data"}
tinydns-data
${optionalString (cfg.secondaries != {}) "${getBin pkgs.gnumake}/bin/make -j$AXFR_JOBS -f ${tinydnsSend}"}
'';
reloadIfChanged = true;
}) config.services.tinydns) // (mapAttrs' (ip: cfg: nameValuePair "axfrdns-${ip}" {
enable = cfg.enable;
description = "axfrdns tcp dns server";
after = [ "tinydns-${ip}.service" ];
requires = [ "tinydns-${ip}.service" ];
environment = {
IP = ip;
ROOT = cfg.rootPath;
AXFR = ""; # Do not allow any zone transfers!
UID = "${toString config.users.users.tinydns.uid}";
GID = "${toString config.users.groups.tinydns.gid}";
};
serviceConfig = {
StandardInput = "socket";
StandardError = "journal";
ExecStart = "${getBin pkgs.djbdns}/bin/axfrdns";
LimitDATA = mkDefault "15000000";
};
}) config.services.tinydns);
systemd.sockets = mapAttrs' (ip: cfg: nameValuePair "axfrdns-${ip}" {
enable = cfg.enable;
wantedBy = [ "sockets.target" ];
description = "axfrdns socket";
listenStreams = [ "${ip}:53" ];
socketConfig.Accept = true;
}) config.services.tinydns;
};
}