{ 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; }; }; }; }