diff --git a/nixos/modules/services/networking/knot.nix b/nixos/modules/services/networking/knot.nix index 94c32586736a..6488a159b3b7 100644 --- a/nixos/modules/services/networking/knot.nix +++ b/nixos/modules/services/networking/knot.nix @@ -1,8 +1,36 @@ -{ config, lib, pkgs, ... }: +{ config, lib, pkgs, utils, ... }: -with lib; let + inherit (lib) + attrNames + concatMapStrings + concatMapStringsSep + concatStrings + concatStringsSep + elem + filter + flip + hasAttr + hasPrefix + isAttrs + isBool + isDerivation + isList + mapAttrsToList + mkChangedOptionModule + mkEnableOption + mkIf + mkOption + mkPackageOption + optionals + types + ; + + inherit (utils) + escapeSystemdExecArgs + ; + cfg = config.services.knot; yamlConfig = let @@ -113,8 +141,7 @@ let mkConfigFile = configString: pkgs.writeTextFile { name = "knot.conf"; text = (concatMapStringsSep "\n" (file: "include: ${file}") cfg.keyFiles) + "\n" + configString; - # TODO: maybe we could do some checks even when private keys complicate this? - checkPhase = lib.optionalString (cfg.keyFiles == []) '' + checkPhase = lib.optionalString cfg.checkConfig '' ${cfg.package}/bin/knotc --config=$out conf-check ''; }; @@ -142,12 +169,45 @@ let in { options = { services.knot = { - enable = mkEnableOption (lib.mdDoc "Knot authoritative-only DNS server"); + enable = mkEnableOption "Knot authoritative-only DNS server"; + + enableXDP = mkOption { + type = types.bool; + default = lib.hasAttrByPath [ "xdp" "listen" ] cfg.settings; + defaultText = '' + Enabled when the `xdp.listen` setting is configured through `settings`. + ''; + example = true; + description = '' + Extends the systemd unit with permissions to allow for the use of + the eXpress Data Path (XDP). + + ::: {.note} + Make sure to read up on functional [limitations](https://www.knot-dns.cz/docs/latest/singlehtml/index.html#mode-xdp-limitations) + when running in XDP mode. + ::: + ''; + }; + + checkConfig = mkOption { + type = types.bool; + # TODO: maybe we could do some checks even when private keys complicate this? + # conf-check fails hard on missing IPs/devices with XDP + default = cfg.keyFiles == [] && !cfg.enableXDP; + defaultText = '' + Disabled when the config uses `keyFiles` or `enableXDP`. + ''; + example = false; + description = '' + Toggles the configuration test at build time. It runs in a + sandbox, and therefore cannot be used in all scenarios. + ''; + }; extraArgs = mkOption { type = types.listOf types.str; default = []; - description = lib.mdDoc '' + description = '' List of additional command line parameters for knotd ''; }; @@ -155,7 +215,7 @@ in { keyFiles = mkOption { type = types.listOf types.path; default = []; - description = lib.mdDoc '' + description = '' A list of files containing additional configuration to be included using the include directive. This option allows to include configuration like TSIG keys without @@ -168,7 +228,7 @@ in { settings = mkOption { type = types.attrs; default = {}; - description = lib.mdDoc '' + description = '' Extra configuration as nix values. ''; }; @@ -176,7 +236,7 @@ in { settingsFile = mkOption { type = types.nullOr types.path; default = null; - description = lib.mdDoc '' + description = '' As alternative to ``settings``, you can provide whole configuration directly in the almost-YAML format of Knot DNS. You might want to utilize ``pkgs.writeText "knot.conf" "longConfigString"`` for this. @@ -210,19 +270,35 @@ in { wants = [ "network.target" ]; after = ["network.target" ]; - serviceConfig = { + serviceConfig = let + # https://www.knot-dns.cz/docs/3.3/singlehtml/index.html#pre-requisites + xdpCapabilities = lib.optionals (cfg.enableXDP) [ + "CAP_NET_ADMIN" + "CAP_NET_RAW" + "CAP_SYS_ADMIN" + "CAP_IPC_LOCK" + ] ++ lib.optionals (lib.versionOlder config.boot.kernelPackages.kernel.version "5.11") [ + "CAP_SYS_RESOURCE" + ]; + in { Type = "notify"; - ExecStart = "${cfg.package}/bin/knotd --config=${configFile} --socket=${socketFile} ${concatStringsSep " " cfg.extraArgs}"; - ExecReload = "${knot-cli-wrappers}/bin/knotc reload"; + ExecStart = escapeSystemdExecArgs ([ + (lib.getExe cfg.package) + "--config=${configFile}" + "--socket=${socketFile}" + ] ++ cfg.extraArgs); + ExecReload = escapeSystemdExecArgs [ + "${knot-cli-wrappers}/bin/knotc" "reload" + ]; User = "knot"; Group = "knot"; AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" - ]; + ] ++ xdpCapabilities; CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" - ]; + ] ++ xdpCapabilities; DeviceAllow = ""; DevicePolicy = "closed"; LockPersonality = true; @@ -247,6 +323,9 @@ in { "AF_INET" "AF_INET6" "AF_UNIX" + ] ++ optionals (cfg.enableXDP) [ + "AF_NETLINK" + "AF_XDP" ]; RestrictNamespaces = true; RestrictRealtime =true; @@ -258,6 +337,8 @@ in { SystemCallFilter = [ "@system-service" "~@privileged" + ] ++ optionals (cfg.enableXDP) [ + "bpf" ]; UMask = "0077"; }; diff --git a/nixos/tests/knot.nix b/nixos/tests/knot.nix index 44efd93b6fa9..c5af8bf1edcc 100644 --- a/nixos/tests/knot.nix +++ b/nixos/tests/knot.nix @@ -114,13 +114,16 @@ in { services.knot.extraArgs = [ "-v" ]; services.knot.settings = { server = { - listen = [ - "0.0.0.0@53" - "::@53" - ]; automatic-acl = true; }; + xdp = { + listen = [ + "eth1" + ]; + tcp = true; + }; + remote.primary = { address = "192.168.0.1@53"; key = "xfr_key"; @@ -140,7 +143,7 @@ in { "sub.example.com".file = "sub.example.com.zone"; }; - log.syslog.any = "info"; + log.syslog.any = "debug"; }; }; client = { lib, nodes, ... }: {