2024-02-29 20:09:43 +00:00
|
|
|
{ config, lib, pkgs, utils, ... }:
|
2020-04-24 23:36:52 +00:00
|
|
|
|
|
|
|
|
|
|
|
let
|
2024-02-29 20:09:43 +00:00
|
|
|
inherit (lib)
|
|
|
|
attrNames
|
|
|
|
concatMapStrings
|
|
|
|
concatMapStringsSep
|
|
|
|
concatStrings
|
|
|
|
concatStringsSep
|
|
|
|
elem
|
|
|
|
filter
|
|
|
|
flip
|
|
|
|
hasAttr
|
|
|
|
hasPrefix
|
|
|
|
isAttrs
|
|
|
|
isBool
|
|
|
|
isDerivation
|
|
|
|
isList
|
|
|
|
mapAttrsToList
|
|
|
|
mkChangedOptionModule
|
|
|
|
mkEnableOption
|
|
|
|
mkIf
|
|
|
|
mkOption
|
|
|
|
mkPackageOption
|
|
|
|
optionals
|
|
|
|
types
|
|
|
|
;
|
|
|
|
|
|
|
|
inherit (utils)
|
|
|
|
escapeSystemdExecArgs
|
|
|
|
;
|
|
|
|
|
2020-04-24 23:36:52 +00:00
|
|
|
cfg = config.services.knot;
|
|
|
|
|
2023-10-09 19:29:22 +00:00
|
|
|
yamlConfig = let
|
|
|
|
result = assert secsCheck; nix2yaml cfg.settings;
|
|
|
|
|
|
|
|
secAllow = n: hasPrefix "mod-" n || elem n [
|
|
|
|
"module"
|
|
|
|
"server" "xdp" "control"
|
|
|
|
"log"
|
|
|
|
"statistics" "database"
|
|
|
|
"keystore" "key" "remote" "remotes" "acl" "submission" "policy"
|
|
|
|
"template"
|
|
|
|
"zone"
|
|
|
|
"include"
|
|
|
|
];
|
|
|
|
secsCheck = let
|
|
|
|
secsBad = filter (n: !secAllow n) (attrNames cfg.settings);
|
|
|
|
in if secsBad == [] then true else throw
|
|
|
|
("services.knot.settings contains unknown sections: " + toString secsBad);
|
|
|
|
|
|
|
|
nix2yaml = nix_def: concatStrings (
|
|
|
|
# We output the config section in the upstream-mandated order.
|
|
|
|
# Ordering is important due to forward-references not being allowed.
|
|
|
|
# See definition of conf_export and 'const yp_item_t conf_schema'
|
|
|
|
# upstream for reference. Last updated for 3.3.
|
|
|
|
# When changing the set of sections, also update secAllow above.
|
|
|
|
[ (sec_list_fa "id" nix_def "module") ]
|
|
|
|
++ map (sec_plain nix_def)
|
|
|
|
[ "server" "xdp" "control" ]
|
|
|
|
++ [ (sec_list_fa "target" nix_def "log") ]
|
|
|
|
++ map (sec_plain nix_def)
|
|
|
|
[ "statistics" "database" ]
|
|
|
|
++ map (sec_list_fa "id" nix_def)
|
|
|
|
[ "keystore" "key" "remote" "remotes" "acl" "submission" "policy" ]
|
|
|
|
|
|
|
|
# Export module sections before the template section.
|
|
|
|
++ map (sec_list_fa "id" nix_def) (filter (hasPrefix "mod-") (attrNames nix_def))
|
|
|
|
|
|
|
|
++ [ (sec_list_fa "id" nix_def "template") ]
|
|
|
|
++ [ (sec_list_fa "domain" nix_def "zone") ]
|
|
|
|
++ [ (sec_plain nix_def "include") ]
|
2024-01-25 14:12:00 +00:00
|
|
|
++ [ (sec_plain nix_def "clear") ]
|
2023-10-09 19:29:22 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
# A plain section contains directly attributes (we don't really check that ATM).
|
|
|
|
sec_plain = nix_def: sec_name: if !hasAttr sec_name nix_def then "" else
|
|
|
|
n2y "" { ${sec_name} = nix_def.${sec_name}; };
|
|
|
|
|
|
|
|
# This section contains a list of attribute sets. In each of the sets
|
|
|
|
# there's an attribute (`fa_name`, typically "id") that must exist and come first.
|
|
|
|
# Alternatively we support using attribute sets instead of lists; example diff:
|
|
|
|
# -template = [ { id = "default"; /* other attributes */ } { id = "foo"; } ]
|
|
|
|
# +template = { default = { /* those attributes */ }; foo = { }; }
|
|
|
|
sec_list_fa = fa_name: nix_def: sec_name: if !hasAttr sec_name nix_def then "" else
|
|
|
|
let
|
|
|
|
elem2yaml = fa_val: other_attrs:
|
|
|
|
" - " + n2y "" { ${fa_name} = fa_val; }
|
|
|
|
+ " " + n2y " " other_attrs
|
|
|
|
+ "\n";
|
|
|
|
sec = nix_def.${sec_name};
|
|
|
|
in
|
|
|
|
sec_name + ":\n" +
|
|
|
|
(if isList sec
|
|
|
|
then flip concatMapStrings sec
|
|
|
|
(elem: elem2yaml elem.${fa_name} (removeAttrs elem [ fa_name ]))
|
|
|
|
else concatStrings (mapAttrsToList elem2yaml sec)
|
|
|
|
);
|
|
|
|
|
|
|
|
# This convertor doesn't care about ordering of attributes.
|
|
|
|
# TODO: it could probably be simplified even more, now that it's not
|
|
|
|
# to be used directly, but we might want some other tweaks, too.
|
|
|
|
n2y = indent: val:
|
|
|
|
if doRecurse val then concatStringsSep "\n${indent}"
|
|
|
|
(mapAttrsToList
|
|
|
|
# This is a bit wacky - set directly under a set would start on bad indent,
|
|
|
|
# so we start those on a new line, but not other types of attribute values.
|
|
|
|
(aname: aval: "${aname}:${if doRecurse aval then "\n${indent} " else " "}"
|
|
|
|
+ n2y (indent + " ") aval)
|
|
|
|
val
|
|
|
|
)
|
|
|
|
+ "\n"
|
|
|
|
else
|
|
|
|
/*
|
|
|
|
if isList val && stringLength indent < 4 then concatMapStrings
|
|
|
|
(elem: "\n${indent}- " + n2y (indent + " ") elem)
|
|
|
|
val
|
|
|
|
else
|
|
|
|
*/
|
|
|
|
if isList val /* and long indent */ then
|
|
|
|
"[ " + concatMapStringsSep ", " quoteString val + " ]" else
|
|
|
|
if isBool val then (if val then "on" else "off") else
|
|
|
|
quoteString val;
|
|
|
|
|
|
|
|
# We don't want paths like ./my-zone.txt be converted to plain strings.
|
|
|
|
quoteString = s: ''"${if builtins.typeOf s == "path" then s else toString s}"'';
|
|
|
|
# We don't want to walk the insides of derivation attributes.
|
|
|
|
doRecurse = val: isAttrs val && !isDerivation val;
|
|
|
|
|
|
|
|
in result;
|
|
|
|
|
|
|
|
configFile = if cfg.settingsFile != null then
|
|
|
|
# Note: with extraConfig, the 23.05 compat code did include keyFiles from settingsFile.
|
|
|
|
assert cfg.settings == {} && (cfg.keyFiles == [] || cfg.extraConfig != null);
|
|
|
|
cfg.settingsFile
|
|
|
|
else
|
|
|
|
mkConfigFile yamlConfig;
|
|
|
|
|
|
|
|
mkConfigFile = configString: pkgs.writeTextFile {
|
2020-04-24 23:36:52 +00:00
|
|
|
name = "knot.conf";
|
2023-10-09 19:29:22 +00:00
|
|
|
text = (concatMapStringsSep "\n" (file: "include: ${file}") cfg.keyFiles) + "\n" + configString;
|
2024-02-29 20:09:43 +00:00
|
|
|
checkPhase = lib.optionalString cfg.checkConfig ''
|
2020-04-24 23:36:52 +00:00
|
|
|
${cfg.package}/bin/knotc --config=$out conf-check
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
socketFile = "/run/knot/knot.sock";
|
|
|
|
|
|
|
|
knot-cli-wrappers = pkgs.stdenv.mkDerivation {
|
|
|
|
name = "knot-cli-wrappers";
|
2022-10-21 18:38:19 +00:00
|
|
|
nativeBuildInputs = [ pkgs.makeWrapper ];
|
2020-04-24 23:36:52 +00:00
|
|
|
buildCommand = ''
|
|
|
|
mkdir -p $out/bin
|
|
|
|
makeWrapper ${cfg.package}/bin/knotc "$out/bin/knotc" \
|
|
|
|
--add-flags "--config=${configFile}" \
|
|
|
|
--add-flags "--socket=${socketFile}"
|
|
|
|
makeWrapper ${cfg.package}/bin/keymgr "$out/bin/keymgr" \
|
|
|
|
--add-flags "--config=${configFile}"
|
|
|
|
for executable in kdig khost kjournalprint knsec3hash knsupdate kzonecheck
|
|
|
|
do
|
|
|
|
ln -s "${cfg.package}/bin/$executable" "$out/bin/$executable"
|
|
|
|
done
|
|
|
|
mkdir -p "$out/share"
|
|
|
|
ln -s '${cfg.package}/share/man' "$out/share/"
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
in {
|
|
|
|
options = {
|
|
|
|
services.knot = {
|
2024-02-29 20:09:43 +00:00
|
|
|
enable = mkEnableOption "Knot authoritative-only DNS server";
|
|
|
|
|
|
|
|
enableXDP = mkOption {
|
|
|
|
type = types.bool;
|
|
|
|
default = lib.hasAttrByPath [ "xdp" "listen" ] cfg.settings;
|
|
|
|
defaultText = ''
|
|
|
|
Enabled when the `xdp.listen` setting is configured through `settings`.
|
|
|
|
'';
|
|
|
|
example = true;
|
|
|
|
description = ''
|
|
|
|
Extends the systemd unit with permissions to allow for the use of
|
|
|
|
the eXpress Data Path (XDP).
|
|
|
|
|
|
|
|
::: {.note}
|
|
|
|
Make sure to read up on functional [limitations](https://www.knot-dns.cz/docs/latest/singlehtml/index.html#mode-xdp-limitations)
|
|
|
|
when running in XDP mode.
|
|
|
|
:::
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
checkConfig = mkOption {
|
|
|
|
type = types.bool;
|
|
|
|
# TODO: maybe we could do some checks even when private keys complicate this?
|
|
|
|
# conf-check fails hard on missing IPs/devices with XDP
|
|
|
|
default = cfg.keyFiles == [] && !cfg.enableXDP;
|
|
|
|
defaultText = ''
|
|
|
|
Disabled when the config uses `keyFiles` or `enableXDP`.
|
|
|
|
'';
|
|
|
|
example = false;
|
|
|
|
description = ''
|
|
|
|
Toggles the configuration test at build time. It runs in a
|
|
|
|
sandbox, and therefore cannot be used in all scenarios.
|
|
|
|
'';
|
|
|
|
};
|
2020-04-24 23:36:52 +00:00
|
|
|
|
|
|
|
extraArgs = mkOption {
|
|
|
|
type = types.listOf types.str;
|
|
|
|
default = [];
|
2024-02-29 20:09:43 +00:00
|
|
|
description = ''
|
2022-12-28 21:21:41 +00:00
|
|
|
List of additional command line parameters for knotd
|
2020-04-24 23:36:52 +00:00
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
keyFiles = mkOption {
|
|
|
|
type = types.listOf types.path;
|
|
|
|
default = [];
|
2024-02-29 20:09:43 +00:00
|
|
|
description = ''
|
2020-04-24 23:36:52 +00:00
|
|
|
A list of files containing additional configuration
|
|
|
|
to be included using the include directive. This option
|
|
|
|
allows to include configuration like TSIG keys without
|
|
|
|
exposing them to the nix store readable to any process.
|
|
|
|
Note that using this option will also disable configuration
|
|
|
|
checks at build time.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2023-10-09 19:29:22 +00:00
|
|
|
settings = mkOption {
|
2024-05-15 15:35:15 +00:00
|
|
|
type = (pkgs.formats.yaml {}).type;
|
2023-10-09 19:29:22 +00:00
|
|
|
default = {};
|
2024-02-29 20:09:43 +00:00
|
|
|
description = ''
|
2023-10-09 19:29:22 +00:00
|
|
|
Extra configuration as nix values.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
settingsFile = mkOption {
|
|
|
|
type = types.nullOr types.path;
|
|
|
|
default = null;
|
2024-02-29 20:09:43 +00:00
|
|
|
description = ''
|
2023-10-09 19:29:22 +00:00
|
|
|
As alternative to ``settings``, you can provide whole configuration
|
|
|
|
directly in the almost-YAML format of Knot DNS.
|
|
|
|
You might want to utilize ``pkgs.writeText "knot.conf" "longConfigString"`` for this.
|
2020-04-24 23:36:52 +00:00
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2024-01-02 11:29:13 +00:00
|
|
|
package = mkPackageOption pkgs "knot-dns" { };
|
2020-04-24 23:36:52 +00:00
|
|
|
};
|
|
|
|
};
|
2023-10-09 19:29:22 +00:00
|
|
|
imports = [
|
|
|
|
# Compatibility with NixOS 23.05.
|
|
|
|
(mkChangedOptionModule [ "services" "knot" "extraConfig" ] [ "services" "knot" "settingsFile" ]
|
|
|
|
(config: mkConfigFile config.services.knot.extraConfig)
|
|
|
|
)
|
|
|
|
];
|
2020-04-24 23:36:52 +00:00
|
|
|
|
|
|
|
config = mkIf config.services.knot.enable {
|
2021-12-06 16:07:01 +00:00
|
|
|
users.groups.knot = {};
|
2020-04-24 23:36:52 +00:00
|
|
|
users.users.knot = {
|
|
|
|
isSystemUser = true;
|
|
|
|
group = "knot";
|
|
|
|
description = "Knot daemon user";
|
|
|
|
};
|
|
|
|
|
2023-10-09 19:29:22 +00:00
|
|
|
environment.etc."knot/knot.conf".source = configFile; # just for user's convenience
|
|
|
|
|
2020-04-24 23:36:52 +00:00
|
|
|
systemd.services.knot = {
|
|
|
|
unitConfig.Documentation = "man:knotd(8) man:knot.conf(5) man:knotc(8) https://www.knot-dns.cz/docs/${cfg.package.version}/html/";
|
|
|
|
description = cfg.package.meta.description;
|
|
|
|
wantedBy = [ "multi-user.target" ];
|
|
|
|
wants = [ "network.target" ];
|
|
|
|
after = ["network.target" ];
|
|
|
|
|
2024-02-29 20:09:43 +00:00
|
|
|
serviceConfig = let
|
|
|
|
# https://www.knot-dns.cz/docs/3.3/singlehtml/index.html#pre-requisites
|
|
|
|
xdpCapabilities = lib.optionals (cfg.enableXDP) [
|
|
|
|
"CAP_NET_ADMIN"
|
|
|
|
"CAP_NET_RAW"
|
|
|
|
"CAP_SYS_ADMIN"
|
|
|
|
"CAP_IPC_LOCK"
|
|
|
|
] ++ lib.optionals (lib.versionOlder config.boot.kernelPackages.kernel.version "5.11") [
|
|
|
|
"CAP_SYS_RESOURCE"
|
|
|
|
];
|
|
|
|
in {
|
2020-04-24 23:36:52 +00:00
|
|
|
Type = "notify";
|
2024-02-29 20:09:43 +00:00
|
|
|
ExecStart = escapeSystemdExecArgs ([
|
|
|
|
(lib.getExe cfg.package)
|
|
|
|
"--config=${configFile}"
|
|
|
|
"--socket=${socketFile}"
|
|
|
|
] ++ cfg.extraArgs);
|
|
|
|
ExecReload = escapeSystemdExecArgs [
|
|
|
|
"${knot-cli-wrappers}/bin/knotc" "reload"
|
|
|
|
];
|
2020-04-24 23:36:52 +00:00
|
|
|
User = "knot";
|
2021-12-06 16:07:01 +00:00
|
|
|
Group = "knot";
|
|
|
|
|
|
|
|
AmbientCapabilities = [
|
|
|
|
"CAP_NET_BIND_SERVICE"
|
2024-02-29 20:09:43 +00:00
|
|
|
] ++ xdpCapabilities;
|
2021-12-06 16:07:01 +00:00
|
|
|
CapabilityBoundingSet = [
|
|
|
|
"CAP_NET_BIND_SERVICE"
|
2024-02-29 20:09:43 +00:00
|
|
|
] ++ xdpCapabilities;
|
2021-12-06 16:07:01 +00:00
|
|
|
DeviceAllow = "";
|
|
|
|
DevicePolicy = "closed";
|
|
|
|
LockPersonality = true;
|
|
|
|
MemoryDenyWriteExecute = true;
|
|
|
|
NoNewPrivileges = true;
|
|
|
|
PrivateDevices = true;
|
|
|
|
PrivateTmp = true;
|
|
|
|
PrivateUsers = false; # breaks capability passing
|
|
|
|
ProcSubset = "pid";
|
|
|
|
ProtectClock = true;
|
|
|
|
ProtectControlGroups = true;
|
|
|
|
ProtectHome = true;
|
|
|
|
ProtectHostname = true;
|
|
|
|
ProtectKernelLogs = true;
|
|
|
|
ProtectKernelModules = true;
|
|
|
|
ProtectKernelTunables = true;
|
|
|
|
ProtectProc = "invisible";
|
|
|
|
ProtectSystem = "strict";
|
|
|
|
RemoveIPC = true;
|
|
|
|
Restart = "on-abort";
|
|
|
|
RestrictAddressFamilies = [
|
|
|
|
"AF_INET"
|
|
|
|
"AF_INET6"
|
|
|
|
"AF_UNIX"
|
2024-02-29 20:09:43 +00:00
|
|
|
] ++ optionals (cfg.enableXDP) [
|
|
|
|
"AF_NETLINK"
|
|
|
|
"AF_XDP"
|
2021-12-06 16:07:01 +00:00
|
|
|
];
|
|
|
|
RestrictNamespaces = true;
|
|
|
|
RestrictRealtime =true;
|
|
|
|
RestrictSUIDSGID = true;
|
2020-04-24 23:36:52 +00:00
|
|
|
RuntimeDirectory = "knot";
|
|
|
|
StateDirectory = "knot";
|
|
|
|
StateDirectoryMode = "0700";
|
|
|
|
SystemCallArchitectures = "native";
|
2021-12-06 16:07:01 +00:00
|
|
|
SystemCallFilter = [
|
|
|
|
"@system-service"
|
|
|
|
"~@privileged"
|
2024-02-29 20:09:43 +00:00
|
|
|
] ++ optionals (cfg.enableXDP) [
|
|
|
|
"bpf"
|
2021-12-06 16:07:01 +00:00
|
|
|
];
|
|
|
|
UMask = "0077";
|
2020-04-24 23:36:52 +00:00
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
environment.systemPackages = [ knot-cli-wrappers ];
|
|
|
|
};
|
|
|
|
}
|