339 lines
10 KiB
Nix
339 lines
10 KiB
Nix
|
{ config, lib, pkgs, ... }@toplevel:
|
||
|
|
||
|
with lib;
|
||
|
let
|
||
|
hostname = config.instance.hostname;
|
||
|
|
||
|
cfg = config.fudo.services.authoritative-dns;
|
||
|
|
||
|
hostSecrets = config.fudo.secrets.host-secrets."${hostname}";
|
||
|
|
||
|
domainName = config.instance.local-domain;
|
||
|
# domain = config.fudo.domains."${domainName}";
|
||
|
# primaryNameserver = domain.primaryNameserver;
|
||
|
# isPrimaryNameserver = primary-nameserver == hostname;
|
||
|
|
||
|
zoneKeySecret = zone: "${zone}-ksk";
|
||
|
|
||
|
nameserverOpts = { name, ... }: {
|
||
|
options = with types; {
|
||
|
hostname = mkOption {
|
||
|
type = str;
|
||
|
description = "Hostname of the external nameserver.";
|
||
|
default = name;
|
||
|
};
|
||
|
|
||
|
ipv4-address = mkOption {
|
||
|
type = nullOr str;
|
||
|
description = "Host ipv4 address of the external nameserver.";
|
||
|
default = null;
|
||
|
};
|
||
|
|
||
|
ipv6-address = mkOption {
|
||
|
type = nullOr str;
|
||
|
description = "Host ipv6 address of the external nameserver.";
|
||
|
default = null;
|
||
|
};
|
||
|
|
||
|
authoritative-hostname = mkOption {
|
||
|
type = nullOr str;
|
||
|
description = "Authoritative hostname of this nameserver.";
|
||
|
default = null;
|
||
|
};
|
||
|
|
||
|
description = mkOption {
|
||
|
type = str;
|
||
|
description = "Description of the external nameserver.";
|
||
|
};
|
||
|
};
|
||
|
};
|
||
|
|
||
|
networkHostOpts = {
|
||
|
options = with types; {
|
||
|
hostname = mkOption {
|
||
|
type = str;
|
||
|
description = "Hostname.";
|
||
|
};
|
||
|
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 = [ ];
|
||
|
};
|
||
|
};
|
||
|
};
|
||
|
|
||
|
zoneOpts = { name, ... }:
|
||
|
let zoneName = name;
|
||
|
in {
|
||
|
options = with types; {
|
||
|
enable = mkOption {
|
||
|
type = bool;
|
||
|
description = "Enable ${zone-name} zone on the local nameserver.";
|
||
|
default = true;
|
||
|
};
|
||
|
|
||
|
default-host = mkOption {
|
||
|
type = nullOr (submodule networkHostOpts);
|
||
|
description =
|
||
|
"Host which will respond to requests for the base domain.";
|
||
|
default = null;
|
||
|
};
|
||
|
|
||
|
external-nameservers = mkOption {
|
||
|
type = listOf (submodule nameserverOpts);
|
||
|
description = "List of external nameserver clauses.";
|
||
|
default = [ ];
|
||
|
};
|
||
|
|
||
|
domain = mkOption {
|
||
|
type = str;
|
||
|
description = "Domain which this zone serves.";
|
||
|
default = zoneName;
|
||
|
};
|
||
|
|
||
|
ksk = mkOption {
|
||
|
type = nullOr (submodule {
|
||
|
options = {
|
||
|
private-key = mkOption {
|
||
|
type = path;
|
||
|
description = "KSK private key.";
|
||
|
};
|
||
|
public-key = mkOption {
|
||
|
type = path;
|
||
|
description = "KSK public key.";
|
||
|
};
|
||
|
ds = mkOption {
|
||
|
type = path;
|
||
|
description = "KSK ds record.";
|
||
|
};
|
||
|
};
|
||
|
});
|
||
|
description =
|
||
|
"Location of the zone-signing private & public keys and DS record.";
|
||
|
default =
|
||
|
toplevel.config.fudo.secrets.files.dns.key-signing-keys."${zoneName}";
|
||
|
};
|
||
|
};
|
||
|
};
|
||
|
|
||
|
in {
|
||
|
options.fudo.services.authoritative-dns = with types; {
|
||
|
enable = mkEnableOption "Enable Authoritative DNS server.";
|
||
|
|
||
|
zones = mkOption {
|
||
|
type = attrsOf (submodule zoneOpts);
|
||
|
description = "Map of served zone to extra zone details.";
|
||
|
default = { };
|
||
|
};
|
||
|
|
||
|
nameservers = {
|
||
|
primary = mkOption {
|
||
|
type = str;
|
||
|
description = "Hostname of the primary nameserver.";
|
||
|
};
|
||
|
|
||
|
secondary = mkOption {
|
||
|
type = listOf str;
|
||
|
description = "List of internal secondary nameservers.";
|
||
|
default = [ ];
|
||
|
};
|
||
|
};
|
||
|
|
||
|
state-directory = mkOption {
|
||
|
type = str;
|
||
|
description =
|
||
|
"Directory at which to store DNS state data, including keys.";
|
||
|
};
|
||
|
};
|
||
|
|
||
|
config = {
|
||
|
fudo = {
|
||
|
secrets.host-secrets."${hostname}" = mkIf cfg.enable (mapAttrs'
|
||
|
(zone: zoneCfg:
|
||
|
nameValuePair (zoneKeySecret zone) {
|
||
|
source-file = zoneCfg.ksk.private-key;
|
||
|
target-file = "/run/nsd/${baseNameOf zoneCfg.ksk.private-key}";
|
||
|
user = config.fudo.nsd.user;
|
||
|
}) (filterAttrs (_: zoneCfg: zoneCfg.ksk != null) cfg.zones));
|
||
|
|
||
|
zones = mapAttrs (zone-name: zoneCfg:
|
||
|
let
|
||
|
domainName = zoneCfg.domain;
|
||
|
domain = config.fudo.domains."${domainName}";
|
||
|
|
||
|
makeSrvRecord = port: host: { inherit port host; };
|
||
|
|
||
|
# servedDomain = domain.primary-nameserver != null;
|
||
|
|
||
|
# primaryNameserver = domain.primary-nameserver;
|
||
|
|
||
|
isPrimaryNameserver = hostname == cfg.nameservers.primary;
|
||
|
|
||
|
internalNameserverHostnames = [ cfg.nameservers.primary ]
|
||
|
++ cfg.nameservers.secondary;
|
||
|
|
||
|
getNsDeets = hostname:
|
||
|
let hostDomain = config.fudo.hosts."${hostname}".domain;
|
||
|
in {
|
||
|
ipv4-address = pkgs.lib.network.host-ipv4 config hostname;
|
||
|
ipv6-address = pkgs.lib.network.host-ipv6 config hostname;
|
||
|
description =
|
||
|
"${domainName} nameserver ${hostname}.${hostDomain}.";
|
||
|
};
|
||
|
|
||
|
nameserverDeets = let
|
||
|
internalNameservers = map getNsDeets internalNameserverHostnames;
|
||
|
in internalNameservers ++ zoneCfg.external-nameservers;
|
||
|
|
||
|
hasAuthHostname = nsHost: nsOpts:
|
||
|
(hasAttr "authoritative-hostname" nsOpts)
|
||
|
&& (nsOpts.authoritative-hostname != null);
|
||
|
|
||
|
allNameservers = listToAttrs
|
||
|
(imap1 (i: nsOpts: nameValuePair "ns${toString i}" nsOpts)
|
||
|
nameserverDeets);
|
||
|
|
||
|
nameserverAliases =
|
||
|
mapAttrs (hostname: opts: "${opts.authoritative-hostname}.")
|
||
|
(filterAttrs hasAuthHostname allNameservers);
|
||
|
|
||
|
nameserverHosts = mapAttrs (hostname: opts: {
|
||
|
inherit (opts) ipv4-address ipv6-address description;
|
||
|
}) (filterAttrs (hostname: opts: !hasAuthHostname hostname opts)
|
||
|
allNameservers);
|
||
|
|
||
|
dnsSrvRecords = let
|
||
|
nameserverSrvRecords = mapAttrsToList (hostname: hostOpts:
|
||
|
let
|
||
|
targetHost = if (hasAuthHostname hostname hostOpts) then
|
||
|
"${hostOpts.authoritative-hostname}"
|
||
|
else
|
||
|
"${hostname}.${domainName}";
|
||
|
in makeSrvRecord 53 targetHost) allNameservers;
|
||
|
in {
|
||
|
tcp.domain = nameserverSrvRecords;
|
||
|
udp.domain = nameserverSrvRecords;
|
||
|
};
|
||
|
|
||
|
mailSrvRecords = mkIf (!isNull domain.primary-mailserver) (let
|
||
|
mailDomainName =
|
||
|
config.fudo.hosts."${domain.primary-mailserver}".domain;
|
||
|
in {
|
||
|
tcp = {
|
||
|
smtp = [{
|
||
|
host = "smtp.${mailDomainName}";
|
||
|
port = 25;
|
||
|
}];
|
||
|
imap = [{
|
||
|
host = "imap.${mailDomainName}";
|
||
|
port = 143;
|
||
|
}];
|
||
|
imaps = [{
|
||
|
host = "imap.${mailDomainName}";
|
||
|
port = 993;
|
||
|
}];
|
||
|
submission = [{
|
||
|
host = "smtp.${mailDomainName}";
|
||
|
port = 587;
|
||
|
}];
|
||
|
submissions = [{
|
||
|
host = "smtp.${mailDomainName}";
|
||
|
port = 465;
|
||
|
}];
|
||
|
};
|
||
|
udp = {
|
||
|
smtp = [{
|
||
|
host = "smtp.${mailDomainName}";
|
||
|
port = 25;
|
||
|
}];
|
||
|
submission = [{
|
||
|
host = "smtp.${mailDomainName}";
|
||
|
port = 587;
|
||
|
}];
|
||
|
};
|
||
|
});
|
||
|
|
||
|
in {
|
||
|
gssapi-realm = mkIf (!isNull domain.gssapi-realm) domain.gssapi-realm;
|
||
|
|
||
|
hosts = nameserverHosts;
|
||
|
# Don't add mailservers: remember SSL!
|
||
|
|
||
|
aliases = nameserverAliases;
|
||
|
|
||
|
mx = optional (!isNull domain.primary-mailserver) (let
|
||
|
mailserverDomain =
|
||
|
config.fudo.hosts."${domain.primary-mailserver}".domain;
|
||
|
in "smtp.${mailserverDomain}");
|
||
|
|
||
|
dmarc-report-address = mkIf (!isNull domain.primary-mailserver)
|
||
|
"dmarc-report@${domainName}";
|
||
|
|
||
|
nameservers = let
|
||
|
directExternal = attrValues nameserverAliases;
|
||
|
internal = map (hostname: "${hostname}.${domainName}.")
|
||
|
(attrNames nameserverHosts);
|
||
|
in internal ++ directExternal;
|
||
|
|
||
|
srv-records = dnsSrvRecords // mailSrvRecords;
|
||
|
|
||
|
verbatim-dns-records = mkIf (zoneCfg.ksk != null) [
|
||
|
(readFile zoneCfg.ksk.public-key)
|
||
|
(readFile zoneCfg.ksk.ds)
|
||
|
];
|
||
|
}) cfg.zones;
|
||
|
};
|
||
|
|
||
|
services = {
|
||
|
authoritative-dns = {
|
||
|
enable = cfg.enable;
|
||
|
|
||
|
identity = "${hostname}.${domainName}";
|
||
|
|
||
|
listen-ips =
|
||
|
optionals cfg.enable (pkgs.lib.network.host-ips config hostname);
|
||
|
|
||
|
state-directory = cfg.state-directory;
|
||
|
|
||
|
timestamp = toString config.instance.build-timestamp;
|
||
|
|
||
|
domains = mapAttrs' (zoneName: zoneCfg:
|
||
|
nameValuePair zoneCfg.domain {
|
||
|
ksk.key-file = mkIf (hasAttr (zoneKeySecret zoneName) hostSecrets)
|
||
|
hostSecrets."${zoneKeySecret zoneName}".target-file;
|
||
|
zone = let baseZone = config.fudo.zones."${zoneName}";
|
||
|
in baseZone // {
|
||
|
# FIXME: what's up?
|
||
|
# default-host = baseZone.hosts."${zoneCfg.default-host}";
|
||
|
};
|
||
|
}) cfg.zones;
|
||
|
};
|
||
|
};
|
||
|
};
|
||
|
}
|