at 25.11-pre 13 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 utils, 6 ... 7}: 8 9let 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 ; 34 35 inherit (utils) 36 escapeSystemdExecArgs 37 ; 38 39 cfg = config.services.knot; 40 41 yamlConfig = 42 let 43 result = 44 assert secsCheck; 45 nix2yaml cfg.settings; 46 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); 77 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 ] 106 107 # Export module sections before the template section. 108 ++ map (sec_list_fa "id" nix_def) (filter (hasPrefix "mod-") (attrNames nix_def)) 109 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 ); 115 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}; }; 120 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 ); 145 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; 178 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; 183 184 in 185 result; 186 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; 194 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 }; 204 205 socketFile = "/run/knot/knot.sock"; 206 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 }; 229in 230{ 231 options = { 232 services.knot = { 233 enable = mkEnableOption "Knot authoritative-only DNS server"; 234 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). 245 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 }; 252 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 }; 267 268 extraArgs = mkOption { 269 type = types.listOf types.str; 270 default = [ ]; 271 description = '' 272 List of additional command line parameters for knotd 273 ''; 274 }; 275 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 }; 288 289 settings = mkOption { 290 type = (pkgs.formats.yaml { }).type; 291 default = { }; 292 description = '' 293 Extra configuration as nix values. 294 ''; 295 }; 296 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 }; 306 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 ]; 316 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 }; 324 325 environment.etc."knot/knot.conf".source = configFile; # just for user's convenience 326 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" ]; 333 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"; 364 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 }; 420 421 environment.systemPackages = [ knot-cli-wrappers ]; 422 }; 423}