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