{ inputs, node, dnsNix, config, lib, pkgs, ... }: # # this module requires lix' experimental `pipe-operator` feature! # with lib; let cfg = config.dns; dnsServerAddresses = isPrimary: lib.flatten ( inputs.self.nixosConfigurations |> lib.filterAttrs (hostName: nodeCfg: nodeCfg.config.dns.enable && nodeCfg.config.dns.primary == isPrimary) |> lib.mapAttrsToList ( nodeName: nodeCfg: [ (lib.mkIf (inputs.self.nodes."${nodeName}".ip6Address != "") inputs.self.nodes."${nodeName}".ip6Address) (lib.mkIf (inputs.self.nodes."${nodeName}".ip4Address != "") inputs.self.nodes."${nodeName}".ip4Address) ] ) ); dnsServerSecondaries = ( inputs.self.nixosConfigurations |> lib.filterAttrs (hostName: nodeCfg: nodeCfg.config.dns.enable && !nodeCfg.config.dns.primary) |> lib.mapAttrs( nodeName: nodeCfg: { address = [ (lib.mkIf (inputs.self.nodes."${nodeName}".ip6Address != "") inputs.self.nodes."${nodeName}".ip6Address) (lib.mkIf (inputs.self.nodes."${nodeName}".ip4Address != "") inputs.self.nodes."${nodeName}".ip4Address) ]; } ) ); in { options.dns = { enable = mkEnableOption "nix-powered DNS"; primary = lib.mkOption { type = lib.types.bool; default = true; }; zonesDir = lib.mkOption { type = lib.types.str; default = "nixZones"; }; dataDir = lib.mkOption { type = lib.types.str; default = "/var/lib/knot"; }; keyFiles = lib.mkOption { type = types.listOf types.path; default = []; }; # contains dns entries defined on the local host zones = mkOption { type = lib.types.attrsOf dnsNix.types.subzone; default = {}; }; # contains dns entries defined on the local host and on remote hosts, merged together allZones = mkOption { type = lib.types.attrsOf dnsNix.types.zone; default = {}; }; extraZones = mkOption { type = (pkgs.formats.yaml { }).type; default = {}; }; extraACL = mkOption { type = (pkgs.formats.yaml { }).type; default = {}; }; }; config = mkIf cfg.enable (let zoneFiles = ( cfg.allZones |> lib.mapAttrs' (name: zone: { name = name; value = pkgs.writeTextFile { name = "${name}.zone"; text = dnsNix.types.zoneToString name (dnsNix.evalZone name zone); }; }) ); in { networking.firewall.allowedTCPPorts = [ 53 ]; networking.firewall.allowedUDPPorts = [ 53 ]; # serve records defined in all host configs dns.allZones = mkMerge ( inputs.self.nixosConfigurations |> mapAttrsToList ( name: host: host.config.dns.zones ) ); environment.etc = lib.mkIf cfg.primary ( zoneFiles |> lib.mapAttrs' (name: file: { name = "${cfg.zonesDir}/${name}.zone"; value.source = file; }) ); services.knot = let primaryAddresses = dnsServerAddresses true; secondaryAddresses = dnsServerAddresses false; secondaries = dnsServerSecondaries; in { enable = true; keyFiles = lib.mkIf (cfg.keyFiles != []) cfg.keyFiles; settings = { log.syslog.any = "info"; server.listen = [ (lib.mkIf (node.ip6Address != "") "${node.ip6Address}@53") (lib.mkIf (node.ip4Address != "") "${node.ip4Address}@53") ]; mod-rrl.default.rate-limit = 200; mod-rrl.default.slip = 2; remote = { primary.address = primaryAddresses; } // secondaries; acl = { allowTransfer.address = secondaryAddresses; allowTransfer.action = "transfer"; allowNotify.address = primaryAddresses; allowNotify.action = "notify"; } // cfg.extraACL; template = let notify = { acl = "allowTransfer"; notify = builtins.attrNames secondaries; }; catalog = { catalog-role = "member"; catalog-zone = "catalog."; }; in { default = { semantic-checks = true; global-module = "mod-rrl/default"; }; notifyZone = notify; extraZone = notify // catalog; primaryZone = notify // catalog // { storage = "${cfg.dataDir}/nixZones"; journal-content = "all"; zonefile-sync = -1; zonefile-load = "difference-no-serial"; }; secondaryZone = { master = "primary"; acl = "allowNotify"; journal-content = "all"; zonefile-sync = -1; zonefile-load = "none"; }; }; zone = if !cfg.primary then { "catalog.".catalog-role = "interpret"; "catalog.".catalog-template = "secondaryZone"; "catalog.".template = "secondaryZone"; } else { "catalog.".catalog-role = "generate"; "catalog.".template = "notifyZone"; } // (lib.mapAttrs (name: zone: { template = "primaryZone"; }) cfg.allZones) // (lib.mapAttrs (name: zone: zone // { template = "extraZone"; acl = lib.mkIf (builtins.hasAttr "acl" zone) (lib.flatten [ [ "allowTransfer" ] zone.acl ]); }) cfg.extraZones); }; }; systemd.tmpfiles.settings = { knotDataDir."${cfg.dataDir}".d = { group = "knot"; user = "knot"; mode = "770"; age = "-"; }; knotZones."${cfg.dataDir}/nixZones".d = lib.mkIf cfg.primary { group = "knot"; user = "knot"; mode = "770"; age = "-"; }; }; systemd.services.knot = lib.mkIf cfg.primary { reloadTriggers = ( zoneFiles |> lib.mapAttrsToList (name: zone: zone) ) ++ ( cfg.extraZones |> lib.mapAttrsToList (name: zone: (if (builtins.hasAttr "storage" zone) then "${zone.storage}/${zone.file}" else "${zone.file}")) ); preStart = '' set -euo pipefail cp --dereference /etc/${cfg.zonesDir}/* ${cfg.dataDir}/nixZones chmod -R 770 ${cfg.dataDir}/nixZones ''; serviceConfig.ExecReload = lib.mkForce (pkgs.writeShellScript "knot-reload" '' set -eou pipefail cp --dereference /etc/${cfg.zonesDir}/* ${cfg.dataDir}/nixZones chmod -R 770 ${cfg.dataDir}/nixZones ${config.services.knot.package}/bin/knotc reload ''); }; }); }