From d1d8dd3e55ee84f188bc543bf262f29da65dde3e Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Tue, 6 Feb 2024 13:25:54 +0100 Subject: [PATCH 1/3] nixos/knot: add support for XDP setups The Express Data Path (XDP) is a way to circumvent the traditional Linux networking stack and instead run an eBPF program on your NIC, that makes the decision to provide Knot with certain packets. This is way faster and more scalable but comes at the cost of reduced introspection. Unfortunately the `knotc conf-check` command fails hard with missing interfaces or IP addresses configured in `xdp.listen`, so we disable it for now, once the `xdp` config section is set. We also promote the config check condition to a proper option, so our conditions become public documentation, and we allow users to deal with corner cases, that we have not thought of yet. We follow the pre-requisites documented in the Knot 3.3 manual, and set up the required capabilities and allow the AF_XDP address family. But on top of that, due to our strict hardening, we found two more requirements, that were communicated upstream while debugging this. - There is a requirement on AF_NETLINK, likely to query for and configure the relevant network interface - Running eBPF programs requires access to the `bpf` syscall, which we deny through the `~@privileged` configuration. In summary We now conditionally loosen the hardening of the unit once we detect that an XDP configuration is wanted. And since we cannot introspect arbitrary files from the `settingsFiles` option, we expose XDP support through the `enableXDP` toggle option on the module. --- nixos/modules/services/networking/knot.nix | 57 ++++++++++++++++++++-- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/nixos/modules/services/networking/knot.nix b/nixos/modules/services/networking/knot.nix index 94c32586736a..1646e4c491b0 100644 --- a/nixos/modules/services/networking/knot.nix +++ b/nixos/modules/services/networking/knot.nix @@ -113,8 +113,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 ''; }; @@ -144,6 +143,39 @@ in { services.knot = { enable = mkEnableOption (lib.mdDoc "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 = []; @@ -210,7 +242,17 @@ 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"; @@ -219,10 +261,10 @@ in { AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" - ]; + ] ++ xdpCapabilities; CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" - ]; + ] ++ xdpCapabilities; DeviceAllow = ""; DevicePolicy = "closed"; LockPersonality = true; @@ -247,6 +289,9 @@ in { "AF_INET" "AF_INET6" "AF_UNIX" + ] ++ lib.optionals (cfg.enableXDP) [ + "AF_NETLINK" + "AF_XDP" ]; RestrictNamespaces = true; RestrictRealtime =true; @@ -258,6 +303,8 @@ in { SystemCallFilter = [ "@system-service" "~@privileged" + ] ++ optionals (cfg.enableXDP) [ + "bpf" ]; UMask = "0077"; }; From 08a775839dc3cf82fb32744d027ae0dbe3391c1c Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Tue, 6 Feb 2024 13:34:06 +0100 Subject: [PATCH 2/3] nixos/tests/knot: test the XDP interface We reconfigure the secondary nameserver VM to do all the same things that it did before, but now over the XDP interface. --- nixos/tests/knot.nix | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) 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, ... }: { From ec89463a611237052d32fe6da418eb805e11bbeb Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Sun, 11 Feb 2024 17:07:12 +0100 Subject: [PATCH 3/3] nixos/knot: refactor - Stop using `with lib` - Drop `lib.mdDoc` - Use `escaepSystemdExecArgs` for escaping --- nixos/modules/services/networking/knot.nix | 54 ++++++++++++++++++---- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/nixos/modules/services/networking/knot.nix b/nixos/modules/services/networking/knot.nix index 1646e4c491b0..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 @@ -141,7 +169,7 @@ 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; @@ -179,7 +207,7 @@ in { extraArgs = mkOption { type = types.listOf types.str; default = []; - description = lib.mdDoc '' + description = '' List of additional command line parameters for knotd ''; }; @@ -187,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 @@ -200,7 +228,7 @@ in { settings = mkOption { type = types.attrs; default = {}; - description = lib.mdDoc '' + description = '' Extra configuration as nix values. ''; }; @@ -208,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. @@ -254,8 +282,14 @@ in { ]; 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"; @@ -289,7 +323,7 @@ in { "AF_INET" "AF_INET6" "AF_UNIX" - ] ++ lib.optionals (cfg.enableXDP) [ + ] ++ optionals (cfg.enableXDP) [ "AF_NETLINK" "AF_XDP" ];