generated from meterriblecrew/flake-template
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