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