{ lib, config, pkgs, ... }: with lib; let cfg = config.fudo.dns; 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. ''; default = [ ]; }; description = mkOption { type = with types; nullOr str; description = "Description of this host for a TXT record."; default = null; }; rp = mkOption { type = with types; nullOr str; description = "Responsible person."; default = null; }; }; }; 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 = mkOption { type = bool; description = "Enable DNSSEC security for this zone."; default = true; }; 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; }; }; }; hostRecords = host: data: join-lines ((map (ip: "${host} IN A ${ip}") data.ip-addresses) ++ (map (ip: "${host} IN AAAA ${ip}") data.ipv6-addresses) ++ (map (sshfp: "${host} IN SSHFP ${sshfp}") data.ssh-fingerprints) ++ (optional (data.rp != null) "${host} IN RP ${data.rp}") ++ (optional (data.description != null) "${host} IN TXT ${data.description}")); 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}"; 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 = dom: ns-hosts: join-lines (mapAttrsToList (host: _: "@ IN NS ${host}.${dom}.") ns-hosts); in { options.fudo.dns = with types; { enable = mkEnableOption "Enable master DNS services."; # FIXME: This should allow for AAAA addresses too... nameservers = mkOption { type = loaOf (submodule hostOpts); description = "Map of domain nameserver FQDNs to IP."; example = { "ns1.domain.com" = { ip-addresses = [ "1.1.1.1" ]; ipv6-addresses = [ ]; description = "my fancy dns server"; }; }; }; identity = mkOption { type = str; description = "The identity (CH TXT ID.SERVER) of this host."; }; 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 = cfg.identity; 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 dom cfg.nameservers} ${join-lines (mapAttrsToList hostRecords cfg.nameservers)} ${dmarcRecord dom-cfg.dmarc-report-address} ${join-lines (mapAttrsToList makeSrvProtocolRecords dom-cfg.srv-records)} ${join-lines (mapAttrsToList hostRecords dom-cfg.hosts)} ${join-lines (mapAttrsToList cnameRecord dom-cfg.aliases)} ${join-lines dom-cfg.extra-dns-records} ''; }) cfg.domains; }; }; }