{ lib, config, pkgs, ... }: with lib; let cfg = config.fudo.dns; join-lines = concatStringsSep "\n"; domainOpts = { domain, ... }: { options = with types; { dnssec = mkOption { type = bool; description = "Enable DNSSEC security for this zone."; default = true; }; 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; }; network-definition = mkOption { type = submodule (import ../types/network-definition.nix); description = "Definition of network to be served by local server."; }; default-host = mkOption { type = str; description = "The host to which the domain should map by default."; }; mx = mkOption { type = listOf str; description = "The hosts which act as the domain mail exchange."; default = []; }; gssapi-realm = mkOption { type = nullOr str; description = "The GSSAPI realm of this domain."; default = null; }; }; }; networkHostOpts = import ../types/network-host.nix { inherit lib; }; hostRecords = hostname: nethost-data: let # FIXME: RP doesn't work. # generic-host-records = let # host-data = if (hasAttr hostname config.fudo.hosts) then config.fudo.hosts.${hostname} else null; # in # if (host-data == null) then [] else ( # (map (sshfp: "${hostname} IN SSHFP ${sshfp}") host-data.ssh-fingerprints) ++ (optional (host-data.rp != null) "${hostname} IN RP ${host-data.rp}") # ); sshfp-records = if (hasAttr hostname config.fudo.hosts) then (map (sshfp: "${hostname} IN SSHFP ${sshfp}") config.fudo.hosts.${hostname}.ssh-fingerprints) else []; a-record = optional (nethost-data.ipv4-address != null) "${hostname} IN A ${nethost-data.ipv4-address}"; aaaa-record = optional (nethost-data.ipv6-address != null) "${hostname} IN AAAA ${nethost-data.ipv6-address}"; description-record = optional (nethost-data.description != null) "${hostname} IN TXT \"${nethost-data.description}\""; in join-lines (a-record ++ aaaa-record ++ description-record ++ sshfp-records); 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 = domain: ns-hosts: join-lines (mapAttrsToList (host: _: "@ IN NS ${host}.${domain}.") 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 = attrsOf (submodule networkHostOpts); description = "Map of domain nameserver FQDNs to IP."; example = { "ns1.domain.com" = { ipv4-address = "1.1.1.1"; description = "my fancy dns server"; }; }; }; 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 = "A list of IPs on which to listen for DNS queries."; example = [ "1.2.3.4" ]; }; }; config = mkIf cfg.enable { networking.firewall = { allowedTCPPorts = [ 53 ]; allowedUDPPorts = [ 53 ]; }; services.nsd = { enable = true; identity = cfg.identity; interfaces = cfg.listen-ips; zones = mapAttrs' (dom: dom-cfg: let net-cfg = dom-cfg.network-definition; in nameValuePair "${dom}." { dnssec = dom-cfg.dnssec; data = '' $ORIGIN ${dom}. $TTL 12h @ IN SOA ns1.${dom}. hostmaster.${dom}. ( ${toString config.instance.build-timestamp} 30m 2m 3w 5m) ${optionalString (dom-cfg.default-host != null) "@ IN A ${dom-cfg.default-host}"} ${mxRecords dom-cfg.mx} $TTL 6h ${optionalString (dom-cfg.gssapi-realm != null) ''_kerberos IN TXT "${dom-cfg.gssapi-realm}"''} ${nsRecords dom cfg.nameservers} ${join-lines (mapAttrsToList hostRecords cfg.nameservers)} ${dmarcRecord dom-cfg.dmarc-report-address} ${join-lines (mapAttrsToList makeSrvProtocolRecords net-cfg.srv-records)} ${join-lines (mapAttrsToList hostRecords net-cfg.hosts)} ${join-lines (mapAttrsToList cnameRecord net-cfg.aliases)} ${join-lines net-cfg.verbatim-dns-records} ''; }) cfg.domains; }; }; }