{ lib, config, pkgs, ... }: with lib; let cfg = config.fudo.dns; ip = import ../../lib/ip.nix { lib = lib; }; join-lines = concatStringsSep "\n"; hostOpts = { host, ...}: { options = { ip-addresses = mkOption { type = with types; listOf str; description = '' A list of IPv4 addresses assigned to this host. ''; default = []; }; ipv6-addresses = mkOption { type = with types; listOf str; description = '' A list of IPv6 addresses assigned to this host. ''; default = []; }; ssh-fingerprints = mkOption { type = with types; listOf str; description = '' A list of DNS SSHFP records for this host. ''; }; }; }; srvRecordOpts = with types; { options = { weight = mkOption { type = int; description = "Weight relative to other records."; default = 1; }; priority = mkOption { type = int; description = "Priority to give this record."; default = 0; }; port = mkOption { type = port; description = "Port to use while connecting to this service."; }; host = mkOption { type = str; description = "Host that provides this service."; example = "my-host.my-domain.com"; }; }; }; domainOpts = { domain, ... }: with types; { options = { hosts = mkOption { type = loaOf (submodule hostOpts); default = {}; description = "A map of hostname to { host_attributes }."; }; dnssec = mkEnableOption "Enable DNSSEC security for this zone."; mx = mkOption { type = listOf str; description = "A list of mail servers serving this domain."; default = []; }; srv-records = mkOption { type = attrsOf (attrsOf (listOf (submodule srvRecordOpts))); description = "Map of traffic type to srv records."; default = {}; example = { tcp = { kerberos = { port = 88; host = "auth-host.my-domain.com"; }; }; }; }; aliases = mkOption { type = loaOf str; default = {}; description = "A mapping of host-alias => hostnames to add to DNS."; example = { "music" = "host.dom.com."; "mail" = "hostname"; }; }; extra-dns-records = mkOption { type = listOf str; description = "Records to be inserted verbatim into the DNS zone."; example = ["some-host IN CNAME base-host"]; default = []; }; dmarc-report-address = mkOption { type = nullOr str; description = "The email to use to recieve DMARC reports, if any."; example = "admin-user@domain.com"; default = null; }; default-host = mkOption { type = nullOr str; description = "IP of the host which will act as the default server for this domain, if any."; default = null; }; }; }; hostARecords = host: data: join-lines ((map (ip: "${host} IN A ${ip}") data.ip-addresses) ++ (map (ip: "${host} IN AAAA ${ip}") data.ipv6-addresses)); makeSrvRecords = protocol: type: records: join-lines (map (record: "_${type}._${protocol} IN SRV ${toString record.priority} ${toString record.weight} ${toString record.port} ${toString record.host}.") records); makeSrvProtocolRecords = protocol: types: join-lines (mapAttrsToList (makeSrvRecords protocol) types); cnameRecord = alias: host: "${alias} IN CNAME ${host}"; hostSshFpRecords = host: data: join-lines (map (sshfp: "${host} IN SSHFP ${sshfp}") data.ssh-fingerprints); mxRecords = mxs: concatStringsSep "\n" (map (mx: "@ IN MX 10 ${mx}.") mxs); dmarcRecord = dmarc-email: optionalString (dmarc-email != null) ''_dmarc IN TXT "v=DMARC1;p=quarantine;sp=quarantine;rua=mailto:${dmarc-email};"''; nsRecords = ns-hosts: join-lines ((mapAttrsToList (host: _: "@ IN NS ${host}.") ns-hosts) ++ (mapAttrsToList (host: ip: "${host} IN A ${ip}") ns-hosts)); in { options.fudo.dns = with types; { enable = mkEnableOption "Enable master DNS services."; # FIXME: This should allow for AAAA addresses too... dns-hosts = mkOption { type = loaOf str; description = "Map of domain nameserver FQDNs to IP."; example = { "ns1.domain.com" = "1.1.1.1"; }; }; domains = mkOption { type = loaOf (submodule domainOpts); default = {}; description = "A map of domain to domain options."; }; listen-ips = mkOption { type = listOf str; description = "A list of IPs on which to listen for DNS queries."; example = ["1.2.3.4"]; }; }; config = mkIf cfg.enable { services.nsd = { enable = true; identity = "procul.informis.land"; interfaces = cfg.listen-ips; zones = mapAttrs' (dom: dom-cfg: nameValuePair "${dom}." { dnssec = dom-cfg.dnssec; data = '' $ORIGIN ${dom}. $TTL 12h @ IN SOA ns1.${dom}. hostmaster.${dom}. ( ${toString builtins.currentTime} 5m 2m 6w 5m) ${optionalString (dom-cfg.default-host != null) "@ IN A ${dom-cfg.default-host}"} ${mxRecords dom-cfg.mx} $TTL 6h ${nsRecords cfg.dns-hosts} ${dmarcRecord dom-cfg.dmarc-report-address} ${join-lines (mapAttrsToList makeSrvProtocolRecords dom-cfg.srv-records)} ${join-lines (mapAttrsToList hostARecords dom-cfg.hosts)} ${join-lines (mapAttrsToList hostSshFpRecords dom-cfg.hosts)} ${join-lines (mapAttrsToList cnameRecord dom-cfg.aliases)} ${join-lines dom-cfg.extra-dns-records} ''; }) cfg.domains; }; }; }