at 24.11-pre 12 kB view raw
1{ config, lib, pkgs, utils, ... }: 2 3 4let 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 ; 29 30 inherit (utils) 31 escapeSystemdExecArgs 32 ; 33 34 cfg = config.services.knot; 35 36 yamlConfig = let 37 result = assert secsCheck; nix2yaml cfg.settings; 38 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); 53 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" ] 68 69 # Export module sections before the template section. 70 ++ map (sec_list_fa "id" nix_def) (filter (hasPrefix "mod-") (attrNames nix_def)) 71 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 ); 77 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}; }; 81 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 ); 101 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; 126 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; 131 132 in result; 133 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; 140 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 }; 148 149 socketFile = "/run/knot/knot.sock"; 150 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 }; 169in { 170 options = { 171 services.knot = { 172 enable = mkEnableOption "Knot authoritative-only DNS server"; 173 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). 184 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 }; 191 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 }; 206 207 extraArgs = mkOption { 208 type = types.listOf types.str; 209 default = []; 210 description = '' 211 List of additional command line parameters for knotd 212 ''; 213 }; 214 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 }; 227 228 settings = mkOption { 229 type = (pkgs.formats.yaml {}).type; 230 default = {}; 231 description = '' 232 Extra configuration as nix values. 233 ''; 234 }; 235 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 }; 245 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 ]; 255 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 }; 263 264 environment.etc."knot/knot.conf".source = configFile; # just for user's convenience 265 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" ]; 272 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"; 295 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 }; 346 347 environment.systemPackages = [ knot-cli-wrappers ]; 348 }; 349}