{ 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; zoneKeySecret = zone: "${zone}-ksk"; 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; }; domain = mkOption { type = str; description = "Domain which this zone serves."; default = zoneName; }; reverse-zones = mkOption { type = listOf str; description = "List of networks for which to generate reverse zones."; default = [ ]; }; mail = { smtp-servers = mkOption { type = listOf str; description = "List of SMTP server for this zone."; default = [ ]; }; imap-servers = mkOption { type = listOf str; description = "List of IMAP server for this zone."; default = [ ]; }; }; 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."; enable-notifications = mkEnableOption "Enable notifications to secondary servers."; zones = mkOption { type = attrsOf (submodule zoneOpts); description = "Map of served zone to extra zone details."; default = { }; }; ip-host-map = mkOption { type = attrsOf str; description = "Map of ip address to authoritative hostname, for reverse zones."; default = { }; }; nameservers = { primary = mkOption { type = str; description = "Hostname of the primary nameserver."; }; secondary = mkOption { type = listOf str; description = "List of internal secondary nameservers."; default = [ ]; }; external = mkOption { type = listOf (submodule networkHostOpts); description = "List of external secondary nameserver attributes."; 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; }; 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 = (map getNsDeets internalNameserverHostnames) ++ cfg.nameservers.external; allNameservers = listToAttrs (imap1 (i: nsOpts: nameValuePair "ns${toString i}" nsOpts) nameserverDeets); 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 = let mkRecords = port: servers: map (host: { inherit host port; }) servers; in { tcp = { smtp = mkRecords 25 zoneCfg.mail.smtp-servers; submission = mkRecords 587 zoneCfg.mail.smtp-servers; submissions = mkRecords 465 zoneCfg.mail.smtp-servers; imap = mkRecords 143 zoneCfg.mail.imap-servers; imaps = mkRecords 993 zoneCfg.mail.imap-servers; }; udp = { smtp = mkRecords 25 zoneCfg.mail.smtp-servers; submission = mkRecords 587 zoneCfg.mail.smtp-servers; }; }; in { gssapi-realm = mkIf (!isNull domain.gssapi-realm) domain.gssapi-realm; hosts = allNameservers; mx = zoneCfg.mail.smtp-servers; dmarc-report-address = "dmarc-report@${domainName}"; default-host = zoneCfg.default-host; nameservers = map (hostname: "${hostname}.${domainName}.") (attrNames allNameservers); 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; ip-host-map = cfg.ip-host-map; domains = mapAttrs' (zoneName: zoneCfg: nameValuePair zoneCfg.domain { ksk.key-file = mkIf (hasAttr (zoneKeySecret zoneName) hostSecrets) hostSecrets."${zoneKeySecret zoneName}".target-file; reverse-zones = zoneCfg.reverse-zones; notify = mkIf cfg.enable-notifications { ipv4 = concatMap (ns: optional (ns.ipv4-address != null) ns.ipv4-address) cfg.nameservers.external; ipv6 = concatMap (ns: optional (ns.ipv6-address != null) ns.ipv6-address) cfg.nameservers.external; }; zone = config.fudo.zones."${zoneName}"; }) cfg.zones; }; }; }; }