Initial commit
This commit is contained in:
commit
382a7ac29c
|
@ -0,0 +1,84 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
let
|
||||
cfg = config.services.authoritative-dns;
|
||||
|
||||
zoneOpts = import ./zone-definition.nix { inherit lib; };
|
||||
|
||||
domainOpts = { name, ... }: {
|
||||
options = with types; {
|
||||
domain = mkOption {
|
||||
type = str;
|
||||
description = "Domain name.";
|
||||
default = name;
|
||||
};
|
||||
|
||||
ksk = {
|
||||
key-file = mkOption {
|
||||
type = nullOr str;
|
||||
description =
|
||||
"Key-signing key for this zone. DNSSEC disabled when null.";
|
||||
default = null;
|
||||
};
|
||||
};
|
||||
|
||||
zone = mkOption {
|
||||
type = submodule zoneOpts;
|
||||
description = "Definition of network zone to be served.";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
in {
|
||||
options.services.authoritative-dns = with types; {
|
||||
enable = mkEnableOption "Enable authoritative DNS service.";
|
||||
|
||||
identity = mkOPtion {
|
||||
type = str;
|
||||
description = "The identity (CH TXT ID.SERVER) of this host.";
|
||||
};
|
||||
|
||||
domains = mkOption {
|
||||
type = attrsOf (submodule domainOpts);
|
||||
default = { };
|
||||
description = "A map of domain to domain options.";
|
||||
};
|
||||
|
||||
listen-ips = mkOption {
|
||||
type = listOf str;
|
||||
description =
|
||||
"List of IP addresses on which to listen. If empty, listen on all addresses.";
|
||||
default = [ ];
|
||||
};
|
||||
|
||||
state-directory = mkOption {
|
||||
type = str;
|
||||
description =
|
||||
"Path on which to store nameserver state, including DNSSEC keys.";
|
||||
};
|
||||
|
||||
timestamp = mkOption {
|
||||
type = str;
|
||||
description = "Timestamp to attach to zone record.";
|
||||
};
|
||||
};
|
||||
|
||||
imports = [ ./nsd.nix ];
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
services.fudo-nsd = {
|
||||
enable = true;
|
||||
identity = cfg.identity;
|
||||
interfaces = cfg.listen-ips;
|
||||
stateDirectory = cfg.state-directory;
|
||||
zones = mkAttrs' (dom: domCfg:
|
||||
let zoneCfg = domCfg.zone;
|
||||
in nameValuePair "${dom}." {
|
||||
dnssec = domCfg.ksk.key-file != null;
|
||||
ksk.keyFile = domCfg.ksk.key-file;
|
||||
data = zoneToZonefile cfg.timestamp dom domCfg.zone-definition;
|
||||
}) cfg.domains;
|
||||
};
|
||||
};
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
description = "Authoritative DNS Server";
|
||||
|
||||
inputs = { nixpkgs.url = "nixpkgs/nixos-23.05"; };
|
||||
|
||||
outputs = { self, nixpkgs, ... }: {
|
||||
nixosModules = rec {
|
||||
default = authoritativeDns;
|
||||
authoritativeDns = { ... }: { imports = [ ./authoritative-dns.nix ]; };
|
||||
};
|
||||
};
|
||||
}
|
|
@ -0,0 +1,186 @@
|
|||
{ lib, ... }:
|
||||
|
||||
with lib;
|
||||
let
|
||||
|
||||
networkHostOpts = {
|
||||
options = with types; {
|
||||
hostname = mkOption {
|
||||
type = str;
|
||||
description = "Hostname.";
|
||||
default = name;
|
||||
};
|
||||
ipv4-address = mkOption {
|
||||
type = nullOr str;
|
||||
description = "The V4 IP of a given host, if any.";
|
||||
default = null;
|
||||
};
|
||||
|
||||
ipv6-address = mkOption {
|
||||
type = nullOr str;
|
||||
description = "The V6 IP of a given host, if any.";
|
||||
default = null;
|
||||
};
|
||||
|
||||
mac-address = mkOption {
|
||||
type = nullOr str;
|
||||
description =
|
||||
"The MAC address of a given host, if desired for IP reservation.";
|
||||
default = null;
|
||||
};
|
||||
|
||||
description = mkOption {
|
||||
type = nullOr str;
|
||||
description = "Description of the host.";
|
||||
default = null;
|
||||
};
|
||||
|
||||
sshfp-records = mkOption {
|
||||
type = listOf str;
|
||||
description = "List of SSHFP records for this host.";
|
||||
default = [ ];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
srvRecordEntry = {
|
||||
options = {
|
||||
host = mkOption {
|
||||
type = str;
|
||||
description = "Host providing service.";
|
||||
example = "my-host.domain.com";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = port;
|
||||
description = "Port for service on this host.";
|
||||
example = 55;
|
||||
};
|
||||
|
||||
priority = mkOption {
|
||||
type = int;
|
||||
description = "Priority to give this record.";
|
||||
default = 0;
|
||||
};
|
||||
|
||||
weight = mkOption {
|
||||
type = int;
|
||||
description =
|
||||
"Weight to give this record, among records of equivalent priority.";
|
||||
default = 5;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
zoneOpts = {
|
||||
options = with types; {
|
||||
hosts = attrsOf (submodule networkHostOpts);
|
||||
description = "Hosts on the local network, with relevant settings.";
|
||||
default = { };
|
||||
};
|
||||
|
||||
nameservers = mkOption {
|
||||
type = listOf str;
|
||||
description = "List of zone nameservers.";
|
||||
example = [ "ns1.domain.com." "10.0.0.1" ];
|
||||
default = [ ];
|
||||
};
|
||||
|
||||
srv-records = mkOption {
|
||||
type = attrsOf (attrsOf (listOf (submodule srvRecordEntry)));
|
||||
description = "SRV records for this zone.";
|
||||
example = {
|
||||
tcp = {
|
||||
xmpp = [{
|
||||
host = "my-host.com";
|
||||
port = 55;
|
||||
}];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
meric-records = mkOption {
|
||||
type = attrsOf (listOf (submodule srvRecordEntry));
|
||||
description = "Map of metric types to a list of SRV host records.";
|
||||
example = {
|
||||
node = [{
|
||||
host = "my-host.my-domain.com";
|
||||
port = 443;
|
||||
}];
|
||||
postfix = [{
|
||||
host = "my-mailserver.my-domain.com";
|
||||
port = 443;
|
||||
}];
|
||||
};
|
||||
default = { };
|
||||
};
|
||||
|
||||
aliases = mkOption {
|
||||
type = attrsOf str;
|
||||
description =
|
||||
"A mapping of host-alias -> hostname to add to the doamin record.";
|
||||
default = { };
|
||||
example = {
|
||||
my-alias = "some-host";
|
||||
external-alias = "host-outside.domain.com.";
|
||||
};
|
||||
};
|
||||
|
||||
verbatim-dns-records = mkOption {
|
||||
type = listOf str;
|
||||
description = "Records to be inserted verbatim into the DNS zone.";
|
||||
default = [ ];
|
||||
example = [ "some-host IN CNAME target-host" ];
|
||||
};
|
||||
|
||||
dmark-report-address = mkOption {
|
||||
type = nullOr str;
|
||||
description = "Email address to recieve DMARC reports, if any.";
|
||||
example = "admin-user@domain.com";
|
||||
default = null;
|
||||
};
|
||||
|
||||
default-host = mkOption {
|
||||
type = nullOr (submodule networkHostOpts);
|
||||
description =
|
||||
"Network properties of the default host for this domain, if any.";
|
||||
default = null;
|
||||
};
|
||||
|
||||
mx = mkOption {
|
||||
type = listOf str;
|
||||
description = "A list of mail servers which serve this domain.";
|
||||
default = [ ];
|
||||
};
|
||||
|
||||
gssapi-realm = mkOption {
|
||||
type = nullOr str;
|
||||
description = "Kerberos GSSAPI realm of the zone.";
|
||||
default = null;
|
||||
};
|
||||
|
||||
default-ttl = mkOption {
|
||||
type = str;
|
||||
description = "Default time-to-live for this zone.";
|
||||
default = "3h";
|
||||
};
|
||||
|
||||
host-record-ttl = mkOption {
|
||||
type = str;
|
||||
description = "Default time-to-live for hosts in this zone";
|
||||
default = "1h";
|
||||
};
|
||||
|
||||
description = mkOption {
|
||||
type = str;
|
||||
description = "Description of this zone.";
|
||||
};
|
||||
|
||||
subdomains = mkOption {
|
||||
type = attrsOf (submodule zoneOpts);
|
||||
description = "Subdomains of the current zone.";
|
||||
default = { };
|
||||
};
|
||||
};
|
||||
|
||||
in zoneOpts
|
|
@ -0,0 +1,158 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
{ timestamp, domain, zone, ... }:
|
||||
|
||||
let
|
||||
removeBlankLines = str:
|
||||
concatStringsSep "\n\n" (filter isString (split ''
|
||||
|
||||
|
||||
|
||||
+'' str));
|
||||
|
||||
joinLines = concatStringsSep "\n";
|
||||
|
||||
nSpaces = n: concatStrings (genList (_: " ") n);
|
||||
|
||||
padToLength = strlen: str:
|
||||
let spaces = nSpaces (strlen - (stringLength str));
|
||||
in str + spaces;
|
||||
|
||||
maxInt = foldr (a: b: if (a < b) then b else a) 0;
|
||||
|
||||
recordMatcher = match "^([^;].*) IN ([A-Z][A-Z0-9]*) (.+)$";
|
||||
|
||||
isRecord = str: (recordMatcher str) != null;
|
||||
|
||||
makeZoneFormatter = zonedata:
|
||||
let
|
||||
lines = splitString "\n" zonedata;
|
||||
records = filter isRecord lines;
|
||||
split-records = map recordMatcher records;
|
||||
indexStrlen = i: record: stringLength (elemAt record i);
|
||||
recordIndexMaxlen = i: maxInt (map (indexStrlen i) splitRecords);
|
||||
in recordFormatter (recordIndexMaxlen 0) (recordIndexMaxlen 1);
|
||||
|
||||
recordFormatter = nameMax: typeMax:
|
||||
let
|
||||
namePadder = padToLength nameMax;
|
||||
typePadder = padToLength typeMax;
|
||||
in recordLine:
|
||||
let recordParts = recordMatcher recordLine;
|
||||
in if (recordParts == null) then
|
||||
recordLine
|
||||
else
|
||||
(let
|
||||
name = elemAt recordParts 0;
|
||||
type = elemAt recordParts 1;
|
||||
data = elemAt recordParts 2;
|
||||
in "${namePadder name} IN ${typePadder type} ${data}");
|
||||
|
||||
formatZone = zonedata:
|
||||
let
|
||||
formatter = makeZoneFormatter zonedata;
|
||||
lines = splitString "\n" zonedata;
|
||||
in concatStringsSep "\n" (map formatter lines);
|
||||
|
||||
hostToFqdn = host:
|
||||
if isNotNull (match ".+.$" host) then
|
||||
host
|
||||
else if isNotNull (match ".+..+$" host) then
|
||||
"${host}."
|
||||
else if (hasAttr host zone.hosts) then
|
||||
"${host}.${domain}."
|
||||
else
|
||||
abort "unrecognized hostname: ${host}";
|
||||
|
||||
makeSrvRecords = protocol: service: records:
|
||||
joinLines (map (record:
|
||||
"_${service}._${protocol} IN SRV ${toString record.priority} ${
|
||||
toString record.weight
|
||||
} ${toString record.port} ${hostToFqdn record.host}") records);
|
||||
|
||||
makeSrvProtocolRecords = protocol: serviceRecords:
|
||||
joinLines (mapAttrsToList (makeSrvRecords protocol) serviceRecords);
|
||||
|
||||
makeMetricRecords = metricType: makeSrvRecords "tcp" metricType;
|
||||
|
||||
makeHostRecords = hostname: hostData:
|
||||
let
|
||||
sshfpRecords =
|
||||
map (sshfp: "${hostname} IN SSHFP ${sshfp}") hostData.sshfp-records;
|
||||
aRecord = optional (hostData.ipv4-address != null)
|
||||
"${hostname} IN A ${hostData.ipv4-address}";
|
||||
aaaaRecord = optional (hostData.ipv6-address != null)
|
||||
"${hostname} IN AAAA ${hostData.ipv6-address}";
|
||||
descriptionRecord = optional (hostData.description != null)
|
||||
''${hostname} IN TXT "${hostData.description}"'';
|
||||
in joinLines (aRecord ++ aaaarecord ++ sshfpRecords ++ descriptionRecord);
|
||||
|
||||
cnameRecord = alias: host: "${alias} IN CNAME ${host}";
|
||||
|
||||
dmarkRecord = dmarcEmail:
|
||||
optionalString (dmarcEmail != null) ''
|
||||
_dmarc IN TXT "v=DMARC1;p=quarantine;sp=quarantine;rua=mailto:${dmarc-email};"'';
|
||||
|
||||
mxRecords = map (mx: "@ IN MX 10 ${mx}.");
|
||||
|
||||
nsRecords = map (ns-host: "@ IN NS ${ns-host}");
|
||||
|
||||
flatmapAttrsToList = f: attrs:
|
||||
foldr (a: b: a ++ b) [ ] (mapAttrsToList f attrs);
|
||||
|
||||
domainRecords = domain: zone:
|
||||
let
|
||||
defaultHostRecords = optionals (cfg.default-host != null)
|
||||
(makeHostRecords "@" cfg.default-host);
|
||||
|
||||
kerberosRecord = optionalString (zone.gssapi-realm != null)
|
||||
''_kerberos IN TXT "${zone.gssapi-realm}"'';
|
||||
|
||||
subdomainRecords = joinLines (mapAttrsToList
|
||||
(subdom: subdomCfg: domainRecords "${subdom}.${domain}" subdomCfg))
|
||||
zone.subdomains;
|
||||
|
||||
in ''
|
||||
$ORIGIN ${domain};
|
||||
$TTL ${zone.default-ttl}
|
||||
|
||||
${joinLines defaultHostRecords}
|
||||
|
||||
${joinLines (mxRecords zone.mx)}
|
||||
|
||||
${dmarcRecord zone.dmarc-report-address}
|
||||
|
||||
${kerberosRecord}
|
||||
|
||||
${joinLines (nsRecords zone.nameservers)}
|
||||
|
||||
${joinLines (mapAttrsToList makeSrvProtocolRecords zone.srv-records)}
|
||||
|
||||
${joinLines (mapAttrsToList makeMetricRecords zone.metric-records)}
|
||||
|
||||
$TTL ${zone.host-record-ttl}
|
||||
|
||||
${joinLines (mapAttrsToList hostRecords zone.hosts)}
|
||||
|
||||
${joinLines (mapAttrsToList cnameRecord zone.aliases)}
|
||||
|
||||
${joinLines zone.verbatim-dns-records}
|
||||
|
||||
${subdomainRecords}
|
||||
'';
|
||||
|
||||
in removeBlankLinkes (formatZone ''
|
||||
$ORIGIN ${domain}.
|
||||
$TTL ${zone.default-ttl}
|
||||
|
||||
@ IN SOA ns1.${domain}. hostmaster.${domain}. (
|
||||
${toString timestamp}
|
||||
30m
|
||||
2m
|
||||
3w
|
||||
5m)
|
||||
|
||||
${domainRecords domain zone}
|
||||
'')
|
Loading…
Reference in New Issue