vuls: init at 0.27.0
[NixPkgs.git] / nixos / modules / services / networking / knot.nix
blob145b4ad1dd3f12ef117457dd4ea5ea6f3526b159
1 { config, lib, pkgs, utils, ... }:
4 let
5   inherit (lib)
6     attrNames
7     concatMapStrings
8     concatMapStringsSep
9     concatStrings
10     concatStringsSep
11     elem
12     filter
13     flip
14     hasAttr
15     hasPrefix
16     isAttrs
17     isBool
18     isDerivation
19     isList
20     mapAttrsToList
21     mkChangedOptionModule
22     mkEnableOption
23     mkIf
24     mkOption
25     mkPackageOption
26     optionals
27     types
28   ;
30   inherit (utils)
31     escapeSystemdExecArgs
32   ;
34   cfg = config.services.knot;
36   yamlConfig = let
37     result = assert secsCheck; nix2yaml cfg.settings;
39     secAllow = n: hasPrefix "mod-" n || elem n [
40       "module"
41       "server" "xdp" "control"
42       "log"
43       "statistics" "database"
44       "keystore" "key" "remote" "remotes" "acl" "submission" "policy"
45       "template"
46       "zone"
47       "include"
48     ];
49     secsCheck = let
50       secsBad = filter (n: !secAllow n) (attrNames cfg.settings);
51     in if secsBad == [] then true else throw
52       ("services.knot.settings contains unknown sections: " + toString secsBad);
54     nix2yaml = nix_def: concatStrings (
55         # We output the config section in the upstream-mandated order.
56         # Ordering is important due to forward-references not being allowed.
57         # See definition of conf_export and 'const yp_item_t conf_schema'
58         # upstream for reference.  Last updated for 3.3.
59         # When changing the set of sections, also update secAllow above.
60         [ (sec_list_fa "id" nix_def "module") ]
61         ++ map (sec_plain nix_def)
62           [ "server" "xdp" "control" ]
63         ++ [ (sec_list_fa "target" nix_def "log") ]
64         ++ map (sec_plain nix_def)
65           [  "statistics" "database" ]
66         ++ map (sec_list_fa "id" nix_def)
67           [ "keystore" "key" "remote" "remotes" "acl" "submission" "policy" ]
69         # Export module sections before the template section.
70         ++ map (sec_list_fa "id" nix_def) (filter (hasPrefix "mod-") (attrNames nix_def))
72         ++ [ (sec_list_fa "id" nix_def "template") ]
73         ++ [ (sec_list_fa "domain" nix_def "zone") ]
74         ++ [ (sec_plain nix_def "include") ]
75         ++ [ (sec_plain nix_def "clear") ]
76       );
78     # A plain section contains directly attributes (we don't really check that ATM).
79     sec_plain = nix_def: sec_name: if !hasAttr sec_name nix_def then "" else
80       n2y "" { ${sec_name} = nix_def.${sec_name}; };
82     # This section contains a list of attribute sets.  In each of the sets
83     # there's an attribute (`fa_name`, typically "id") that must exist and come first.
84     # Alternatively we support using attribute sets instead of lists; example diff:
85     # -template = [ { id = "default"; /* other attributes */ }   { id = "foo"; } ]
86     # +template = { default = {       /* those attributes */ };  foo = { };      }
87     sec_list_fa = fa_name: nix_def: sec_name: if !hasAttr sec_name nix_def then "" else
88       let
89         elem2yaml = fa_val: other_attrs:
90           "  - " + n2y "" { ${fa_name} = fa_val; }
91           + "    " + n2y "    " other_attrs
92           + "\n";
93         sec = nix_def.${sec_name};
94       in
95         sec_name + ":\n" +
96           (if isList sec
97             then flip concatMapStrings sec
98               (elem: elem2yaml elem.${fa_name} (removeAttrs elem [ fa_name ]))
99             else concatStrings (mapAttrsToList elem2yaml sec)
100           );
102     # This convertor doesn't care about ordering of attributes.
103     # TODO: it could probably be simplified even more, now that it's not
104     # to be used directly, but we might want some other tweaks, too.
105     n2y = indent: val:
106       if doRecurse val then concatStringsSep "\n${indent}"
107         (mapAttrsToList
108           # This is a bit wacky - set directly under a set would start on bad indent,
109           # so we start those on a new line, but not other types of attribute values.
110           (aname: aval: "${aname}:${if doRecurse aval then "\n${indent}  " else " "}"
111             + n2y (indent + "  ") aval)
112           val
113         )
114         + "\n"
115         else
116       /*
117       if isList val && stringLength indent < 4 then concatMapStrings
118         (elem: "\n${indent}- " + n2y (indent + "  ") elem)
119         val
120         else
121       */
122       if isList val /* and long indent */ then
123         "[ " + concatMapStringsSep ", " quoteString val + " ]" else
124       if isBool val then (if val then "on" else "off") else
125       quoteString val;
127     # We don't want paths like ./my-zone.txt be converted to plain strings.
128     quoteString = s: ''"${if builtins.typeOf s == "path" then s else toString s}"'';
129     # We don't want to walk the insides of derivation attributes.
130     doRecurse = val: isAttrs val && !isDerivation val;
132   in result;
134   configFile = if cfg.settingsFile != null then
135     # Note: with extraConfig, the 23.05 compat code did include keyFiles from settingsFile.
136     assert cfg.settings == {} && (cfg.keyFiles == [] || cfg.extraConfig != null);
137     cfg.settingsFile
138   else
139     mkConfigFile yamlConfig;
141   mkConfigFile = configString: pkgs.writeTextFile {
142     name = "knot.conf";
143     text = (concatMapStringsSep "\n" (file: "include: ${file}") cfg.keyFiles) + "\n" + configString;
144     checkPhase = lib.optionalString cfg.checkConfig ''
145       ${cfg.package}/bin/knotc --config=$out conf-check
146     '';
147   };
149   socketFile = "/run/knot/knot.sock";
151   knot-cli-wrappers = pkgs.stdenv.mkDerivation {
152     name = "knot-cli-wrappers";
153     nativeBuildInputs = [ pkgs.makeWrapper ];
154     buildCommand = ''
155       mkdir -p $out/bin
156       makeWrapper ${cfg.package}/bin/knotc "$out/bin/knotc" \
157         --add-flags "--config=${configFile}" \
158         --add-flags "--socket=${socketFile}"
159       makeWrapper ${cfg.package}/bin/keymgr "$out/bin/keymgr" \
160         --add-flags "--config=${configFile}"
161       for executable in kdig khost kjournalprint knsec3hash knsupdate kzonecheck
162       do
163         ln -s "${cfg.package}/bin/$executable" "$out/bin/$executable"
164       done
165       mkdir -p "$out/share"
166       ln -s '${cfg.package}/share/man' "$out/share/"
167     '';
168   };
169 in {
170   options = {
171     services.knot = {
172       enable = mkEnableOption "Knot authoritative-only DNS server";
174       enableXDP = mkOption {
175         type = types.bool;
176         default = lib.hasAttrByPath [ "xdp" "listen" ] cfg.settings;
177         defaultText = ''
178           Enabled when the `xdp.listen` setting is configured through `settings`.
179         '';
180         example = true;
181         description = ''
182           Extends the systemd unit with permissions to allow for the use of
183           the eXpress Data Path (XDP).
185           ::: {.note}
186             Make sure to read up on functional [limitations](https://www.knot-dns.cz/docs/latest/singlehtml/index.html#mode-xdp-limitations)
187             when running in XDP mode.
188           :::
189         '';
190       };
192       checkConfig = mkOption {
193         type = types.bool;
194         # TODO: maybe we could do some checks even when private keys complicate this?
195         # conf-check fails hard on missing IPs/devices with XDP
196         default = cfg.keyFiles == [] && !cfg.enableXDP;
197         defaultText = ''
198           Disabled when the config uses `keyFiles` or `enableXDP`.
199         '';
200         example = false;
201         description = ''
202           Toggles the configuration test at build time. It runs in a
203           sandbox, and therefore cannot be used in all scenarios.
204         '';
205       };
207       extraArgs = mkOption {
208         type = types.listOf types.str;
209         default = [];
210         description = ''
211           List of additional command line parameters for knotd
212         '';
213       };
215       keyFiles = mkOption {
216         type = types.listOf types.path;
217         default = [];
218         description = ''
219           A list of files containing additional configuration
220           to be included using the include directive. This option
221           allows to include configuration like TSIG keys without
222           exposing them to the nix store readable to any process.
223           Note that using this option will also disable configuration
224           checks at build time.
225         '';
226       };
228       settings = mkOption {
229         type = (pkgs.formats.yaml {}).type;
230         default = {};
231         description = ''
232           Extra configuration as nix values.
233         '';
234       };
236       settingsFile = mkOption {
237         type = types.nullOr types.path;
238         default = null;
239         description = ''
240           As alternative to ``settings``, you can provide whole configuration
241           directly in the almost-YAML format of Knot DNS.
242           You might want to utilize ``pkgs.writeText "knot.conf" "longConfigString"`` for this.
243         '';
244       };
246       package = mkPackageOption pkgs "knot-dns" { };
247     };
248   };
249   imports = [
250     # Compatibility with NixOS 23.05.
251     (mkChangedOptionModule [ "services" "knot" "extraConfig" ] [ "services" "knot" "settingsFile" ]
252       (config: mkConfigFile config.services.knot.extraConfig)
253     )
254   ];
256   config = mkIf config.services.knot.enable {
257     users.groups.knot = {};
258     users.users.knot = {
259       isSystemUser = true;
260       group = "knot";
261       description = "Knot daemon user";
262     };
264     environment.etc."knot/knot.conf".source = configFile; # just for user's convenience
266     systemd.services.knot = {
267       unitConfig.Documentation = "man:knotd(8) man:knot.conf(5) man:knotc(8) https://www.knot-dns.cz/docs/${cfg.package.version}/html/";
268       description = cfg.package.meta.description;
269       wantedBy = [ "multi-user.target" ];
270       wants = [ "network.target" ];
271       after = ["network.target" ];
273       serviceConfig = let
274         # https://www.knot-dns.cz/docs/3.3/singlehtml/index.html#pre-requisites
275         xdpCapabilities = lib.optionals (cfg.enableXDP) [
276           "CAP_NET_ADMIN"
277           "CAP_NET_RAW"
278           "CAP_SYS_ADMIN"
279           "CAP_IPC_LOCK"
280         ] ++ lib.optionals (lib.versionOlder config.boot.kernelPackages.kernel.version "5.11") [
281           "CAP_SYS_RESOURCE"
282         ];
283       in {
284         Type = "notify";
285         ExecStart = escapeSystemdExecArgs ([
286           (lib.getExe cfg.package)
287           "--config=${configFile}"
288           "--socket=${socketFile}"
289         ] ++ cfg.extraArgs);
290         ExecReload = escapeSystemdExecArgs [
291           "${knot-cli-wrappers}/bin/knotc" "reload"
292         ];
293         User = "knot";
294         Group = "knot";
296         AmbientCapabilities = [
297           "CAP_NET_BIND_SERVICE"
298         ] ++ xdpCapabilities;
299         CapabilityBoundingSet = [
300           "CAP_NET_BIND_SERVICE"
301         ] ++ xdpCapabilities;
302         DeviceAllow = "";
303         DevicePolicy = "closed";
304         LockPersonality = true;
305         MemoryDenyWriteExecute = true;
306         NoNewPrivileges = true;
307         PrivateDevices = true;
308         PrivateTmp = true;
309         PrivateUsers = false; # breaks capability passing
310         ProcSubset = "pid";
311         ProtectClock = true;
312         ProtectControlGroups = true;
313         ProtectHome = true;
314         ProtectHostname = true;
315         ProtectKernelLogs = true;
316         ProtectKernelModules = true;
317         ProtectKernelTunables = true;
318         ProtectProc = "invisible";
319         ProtectSystem = "strict";
320         RemoveIPC = true;
321         Restart = "on-abort";
322         RestrictAddressFamilies = [
323           "AF_INET"
324           "AF_INET6"
325           "AF_UNIX"
326         ] ++ optionals (cfg.enableXDP) [
327           "AF_NETLINK"
328           "AF_XDP"
329         ];
330         RestrictNamespaces = true;
331         RestrictRealtime =true;
332         RestrictSUIDSGID = true;
333         RuntimeDirectory = "knot";
334         StateDirectory = "knot";
335         StateDirectoryMode = "0700";
336         SystemCallArchitectures = "native";
337         SystemCallFilter = [
338           "@system-service"
339           "~@privileged"
340         ] ++ optionals (cfg.enableXDP) [
341           "bpf"
342         ];
343         UMask = "0077";
344       };
345     };
347     environment.systemPackages = [ knot-cli-wrappers ];
348   };