generated from meterriblecrew/flake-template
Add tinydns module
parent
bf3dec87ae
commit
0b1f1bf777
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1606600693,
|
||||
"narHash": "sha256-Xr03i0LlCD7PoBOIUNNFJss2uITrGoLF3LiOIW6seC0=",
|
||||
"path": "/nix/store/60394i1ywi6jahim7fq0zkfxxn3f1pxr-source",
|
||||
"rev": "29e9c10750e2b35a0e47db55f36c685ef9219f4e",
|
||||
"type": "path"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
description = "NixOS modules too specialized to be integrated into the official nixpkgs tree";
|
||||
|
||||
outputs = { self, nixpkgs }: {
|
||||
|
||||
lib = nixpkgs.lib.extend (self: super: let
|
||||
callLibs = file: import file { lib = self; };
|
||||
in {
|
||||
tinydns = callLibs ./tinydns/lib.nix;
|
||||
});
|
||||
|
||||
nixosModules = {
|
||||
tinydns = import ./tinydns;
|
||||
};
|
||||
};
|
||||
}
|
|
@ -0,0 +1,282 @@
|
|||
{ 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;
|
||||
};
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
{ lib }:
|
||||
let
|
||||
inherit (lib) concatStrings concatMapStringsSep concatStringsSep
|
||||
getAttrFromPath getBin literalExample mapAttrsToList mkForce mkIf
|
||||
mkChangedOptionModule mkEnableOption mkOption mod optional optionalString
|
||||
remove singleton splitString types;
|
||||
inherit (builtins) attrNames genList head isFunction isString length
|
||||
removeAttrs replaceStrings stringLength substring tail;
|
||||
|
||||
_lib = rec {
|
||||
|
||||
# Convert a number base 10 to another number base k (k <= 16)
|
||||
# l determins the 0 prefixed length of the output.
|
||||
# Returns the output as a string
|
||||
klconv = k: l: num:
|
||||
let
|
||||
snum = { base = k; d = num; c = 0; result = ""; };
|
||||
replaceDigit = d: replaceStrings [ "10" "11" "12" "13" "14" "15" ]
|
||||
[ "a" "b" "c" "d" "e" "f" ] d;
|
||||
kconvDigit = { d, ... }@args:
|
||||
args // { d = args.d / args.base;
|
||||
result = replaceDigit (toString (d - ((d / args.base) * args.base))) + args.result;
|
||||
};
|
||||
kconv' = snum:
|
||||
if snum.d == 0
|
||||
then if stringLength snum.result < l
|
||||
then (concatStrings (genList (_: "0") (l - stringLength snum.result))) + snum.result
|
||||
else snum.result
|
||||
else kconv' (kconvDigit snum);
|
||||
in kconv' snum;
|
||||
|
||||
# Convert integer to octal code string of 16 or 8 bits
|
||||
octconv16 = x: let
|
||||
low = mod x 256;
|
||||
high = (mod x 65536) / 256;
|
||||
in "\\" + (substring 0 3 (klconv 8 3 high)) + "\\" + (substring 0 3 (klconv 8 3 low));
|
||||
octconv8 = x: let s =(klconv 8 3 x); in "\\" + (substring 0 3 s);
|
||||
|
||||
# Convert seconds since Epoch to TAI64 into hex-coded string external format
|
||||
epochToTAI64 = epoch: klconv 16 0 (4611686018427387904 + epoch);
|
||||
|
||||
# Quote special characters with octal replacements to suit tinydns-data
|
||||
quoteDATA = s: replaceStrings [ " " "#" ":" ]
|
||||
[ "\\040" "\\043" "\\072" ] s;
|
||||
|
||||
# Convert string into fixed-length string
|
||||
# abcd -> \004abcd
|
||||
fixedString = s: let
|
||||
prefixShort = short: (octconv8 (stringLength short)) + (quoteDATA short);
|
||||
fixedString' = s:
|
||||
if stringLength s > 255
|
||||
then (prefixShort (substring 0 255 s))+ (fixedString' (substring 255 (stringLength s) s))
|
||||
else prefixShort s;
|
||||
in
|
||||
if stringLength s <= 65280 then fixedString' s
|
||||
else abort "String payload too big, must be less than 65281 bytes";
|
||||
|
||||
# Split target at '.' into a length-prefixed string list suitable for tinydns-data
|
||||
# abcd.ef. -> \004abcd\002ef\000
|
||||
splitTarget = target:
|
||||
concatStrings (map fixedString (splitString "." target));
|
||||
|
||||
# Parse DNS records defined in option records.
|
||||
# Delivers var-args for the data* lambda type functions by supplying default
|
||||
# values (i.e. "") for missing arguments.
|
||||
parseRecord = f: l: if isFunction f
|
||||
then if (length l) > 0
|
||||
then parseRecord (f (head l)) (tail l)
|
||||
else parseRecord (f "") []
|
||||
else f;
|
||||
|
||||
# Capture the varargs parser and its varargs into a set containing all
|
||||
# arguments as a list. Run the captured parser on the args when coercing the
|
||||
# set to string.
|
||||
captureParser = parser: { args = [];
|
||||
__functor = self: arg: self // { args = self.args ++ [ arg ]; };
|
||||
__toString = self: parser self.args;
|
||||
};
|
||||
|
||||
# Make a DNS record by taking a prefabricated record and parsing the
|
||||
# additional flags ttl, timestamp and location
|
||||
dataFlags = record: ttl: ts: location:
|
||||
"${record}"
|
||||
+ ":${if ttl == null then "" else if isString ttl then ttl else toString ttl}"
|
||||
+ ":${if ts == null then "" else if isString ts then ts else epochToTAI64 ts}"
|
||||
+ ":${location}";
|
||||
|
||||
}; # _lib
|
||||
|
||||
# Each parser is expected to produce a string.
|
||||
#
|
||||
# captureParser collects the (variable amount of) arguments and the specific
|
||||
# parser. It sets up the result such that, when it is coerced to a string, the
|
||||
# captured parser is called with the captured arguments.
|
||||
#
|
||||
# parseRecord ensures that the specialized parser is called with enough
|
||||
# arguments eventually. Left-out arguments are replaced with the empty string.
|
||||
#
|
||||
# Parser-building functions are collected in lib.
|
||||
_parsers = with _lib; {
|
||||
lib = _lib;
|
||||
|
||||
# Add the flags TTL, timestamp and location to a record
|
||||
Flag = captureParser (parseRecord dataFlags);
|
||||
|
||||
# Make an AFSDB DNS record
|
||||
AFSDB = captureParser (parseRecord (cell: server: dataFlags
|
||||
(":${cell}:18:" + (octconv16 1) + (splitTarget server))));
|
||||
|
||||
# Make a SRV DNS record
|
||||
SRV = captureParser (parseRecord (service: prio: w: port: target: dataFlags
|
||||
(":${service}:33:" + (concatStrings (map octconv16 [ prio w port ])) + (splitTarget target))));
|
||||
|
||||
# Make a TXT DNS record
|
||||
TXT = captureParser (parseRecord (service: txt: dataFlags
|
||||
(":${service}:16:" + (fixedString txt))));
|
||||
|
||||
# Make a URI DNS record
|
||||
URI = captureParser (parseRecord (service: prio: weight: pl: dataFlags
|
||||
(":${service}:256:" + "${octconv16 prio}${octconv16 weight}" + (quoteDATA pl))));
|
||||
|
||||
}; # _parsers
|
||||
|
||||
# Update the documentation of services.tinydns.*.data when adding new record-building functions, here!
|
||||
in _parsers
|
Loading…
Reference in New Issue