{ config, lib, pkgs, ... }@toplevel: with lib; let hostname = config.instance.hostname; cfg = config.fudo.services.authoritative-dns; inherit (pkgs.lib) getHostIps getSiteGatewayV4 getSiteV4PrefixLength getSiteV6PrefixLength; hostSecrets = config.fudo.secrets.host-secrets."${hostname}"; domainName = config.instance.local-domain; primaryZone = config.fudo.domains."${domainName}".zone; siteName = config.instance.local-site; zoneKeySecret = zone: "${zone}-ksk"; containerModule = { pkgs, config, ... }: { config = mkIf (cfg.enable && !isNull cfg.container) { containers.nameserver = let securedZones = filterAttrs (_: zoneOpts: !isNull zoneOpts.ksk) cfg.zones; in { autoStart = true; additionalCapabilities = [ "CAP_NET_ADMIN" ]; macvlans = [ cfg.container.interface ]; bindMounts = { "/var/lib/nsd" = { hostPath = cfg.state-directory; isReadOnly = false; }; } // (mapAttrs' (zoneName: _: nameValuePair "/run/nsd/keys/${zoneName}" { hostPath = dirOf hostSecrets."${zoneKeySecret zoneName}".target-file; }) securedZones); config = let nameserverHost = cfg.container.hostname; nameserverDeets = config.fudo.zones."${primaryZone}".hosts."${nameserverHost}"; in { imports = [ pkgs.moduleRegistry.authoritativeDns ]; nixpkgs.pkgs = pkgs; networking = { enableIPv6 = false; defaultGateway = { address = getSiteGatewayV4 siteName; interface = "mv-${cfg.container.interface}"; }; firewall = { enable = true; allowedTCPPorts = [ 53 ]; allowedUDPPorts = [ 53 ]; }; interfaces."mv-${cfg.container.interface}" = { ipv4.addresses = optional (nameserverDeets.ipv4-address != null) { address = nameserverDeets.ipv4-address; prefixLength = getSiteV4PrefixLength siteName; }; ipv6.addresses = optional (nameserverDeets.ipv6-address != null) { address = nameserverDeets.ipv6-address; prefixLength = getSiteV6PrefixLength siteName; }; }; }; services.authoritative-dns = { enable = true; identity = "${nameserverHost}.${primaryZone}"; listen-ips = getHostIps nameserverHost; state-directory = "/var/lib/nsd"; timestamp = toString config.instance.build-timestamp; ip-host-map = cfg.ip-host-map; domains = mapAttrs' (zoneName: zoneCfg: nameValuePair zoneCfg.domain { ksk.key-file = "/run/nsd/keys/${zoneName}/${ baseNameOf 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; }; }; }; }; }; hostModule = { config, ... }: { config.services.authoritative-dns = mkIf (cfg.enable && isNull cfg.container) { enable = true; identity = "${hostname}.${primaryZone}"; listen-ips = getHostIps 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; }; }; 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 pkgs.lib.fudo-types.networkHost); 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."; container = mkOption { type = nullOr (submodule { options = { interface = mkOption { type = str; description = "Interface on which to listen for DNS traffic."; }; hostname = mkOption { type = str; description = '' Hostname (in the zone) of the container nameserver. The associated IP(s) will be assigned to the container, and must be accessible. ''; }; }; }); default = null; }; 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 pkgs.lib.fudo-types.networkHost); description = "List of external secondary nameserver attributes."; default = [ ]; }; }; state-directory = mkOption { type = str; description = "Directory at which to store DNS state data, including keys."; }; }; imports = [ hostModule containerModule ]; config = mkIf cfg.enable { fileSystems."/var/lib/nsd" = mkIf (isNull cfg.container) { device = cfg.state-directory; options = [ "bind" ]; }; fudo = { secrets.host-secrets."${hostname}" = concatMapAttrs (zone: zoneCfg: { "${zoneKeySecret zone}" = { source-file = zoneCfg.ksk.private-key; target-file = "/run/nsd/${zone}/${baseNameOf zoneCfg.ksk.private-key}"; }; "${zone}-ds" = { source-file = zoneCfg.ksk.ds; target-file = "/run/nsd/${zone}/${baseNameOf zoneCfg.ksk.ds}"; }; "${zone}-pubkey" = { source-file = zoneCfg.ksk.public-key; target-file = "/run/nsd/${zone}/${baseNameOf zoneCfg.ksk.public-key}"; }; }) (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; }; }; }