1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 cfg = config.services.kresd;
9
10 # Convert systemd-style address specification to kresd config line(s).
11 # On Nix level we don't attempt to precisely validate the address specifications.
12 # The optional IPv6 scope spec comes *after* port, perhaps surprisingly.
13 mkListen =
14 kind: addr:
15 let
16 al_v4 = builtins.match "([0-9.]+):([0-9]+)($)" addr;
17 al_v6 = builtins.match "\\[(.+)]:([0-9]+)(%.*|$)" addr;
18 al_portOnly = builtins.match "(^)([0-9]+)" addr;
19 al =
20 lib.findFirst (a: a != null) (throw "services.kresd.*: incorrect address specification '${addr}'")
21 [
22 al_v4
23 al_v6
24 al_portOnly
25 ];
26 port = lib.elemAt al 1;
27 addrSpec =
28 if al_portOnly == null then "'${lib.head al}${lib.elemAt al 2}'" else "{'::', '0.0.0.0'}";
29 in
30 # freebind is set for compatibility with earlier kresd services;
31 # it could be configurable, for example.
32 ''
33 net.listen(${addrSpec}, ${port}, { kind = '${kind}', freebind = true })
34 '';
35
36 configFile = pkgs.writeText "kresd.conf" (
37 ""
38 + lib.concatMapStrings (mkListen "dns") cfg.listenPlain
39 + lib.concatMapStrings (mkListen "tls") cfg.listenTLS
40 + lib.concatMapStrings (mkListen "doh2") cfg.listenDoH
41 + cfg.extraConfig
42 );
43in
44{
45 meta.maintainers = [
46 lib.maintainers.vcunat # upstream developer
47 ];
48
49 imports = [
50 (lib.mkChangedOptionModule [ "services" "kresd" "interfaces" ] [ "services" "kresd" "listenPlain" ]
51 (
52 config:
53 let
54 value = lib.getAttrFromPath [ "services" "kresd" "interfaces" ] config;
55 in
56 map (iface: if lib.elem ":" (lib.stringToCharacters iface) then "[${iface}]:53" else "${iface}:53") # Syntax depends on being IPv6 or IPv4.
57 value
58 )
59 )
60 (lib.mkRemovedOptionModule [ "services" "kresd" "cacheDir" ] "Please use (bind-)mounting instead.")
61 ];
62
63 ###### interface
64 options.services.kresd = {
65 enable = lib.mkOption {
66 type = lib.types.bool;
67 default = false;
68 description = ''
69 Whether to enable knot-resolver domain name server.
70 DNSSEC validation is turned on by default.
71 You can run `kresd-cli 1` and give commands interactively to kresd@1.service.
72 '';
73 };
74 package = lib.mkPackageOption pkgs "knot-resolver" {
75 example = "knot-resolver.override { extraFeatures = true; }";
76 };
77 extraConfig = lib.mkOption {
78 type = lib.types.lines;
79 default = "";
80 description = ''
81 Extra lines to be added verbatim to the generated configuration file.
82 See upstream documentation <https://www.knot-resolver.cz/documentation/stable/config-overview.html> for more details.
83 '';
84 };
85 listenPlain = lib.mkOption {
86 type = with lib.types; listOf str;
87 default = [
88 "[::1]:53"
89 "127.0.0.1:53"
90 ];
91 example = [ "53" ];
92 description = ''
93 What addresses and ports the server should listen on.
94 For detailed syntax see ListenStream in {manpage}`systemd.socket(5)`.
95 '';
96 };
97 listenTLS = lib.mkOption {
98 type = with lib.types; listOf str;
99 default = [ ];
100 example = [
101 "198.51.100.1:853"
102 "[2001:db8::1]:853"
103 "853"
104 ];
105 description = ''
106 Addresses and ports on which kresd should provide DNS over TLS (see RFC 7858).
107 For detailed syntax see ListenStream in {manpage}`systemd.socket(5)`.
108 '';
109 };
110 listenDoH = lib.mkOption {
111 type = with lib.types; listOf str;
112 default = [ ];
113 example = [
114 "198.51.100.1:443"
115 "[2001:db8::1]:443"
116 "443"
117 ];
118 description = ''
119 Addresses and ports on which kresd should provide DNS over HTTPS/2 (see RFC 8484).
120 For detailed syntax see ListenStream in {manpage}`systemd.socket(5)`.
121 '';
122 };
123 instances = lib.mkOption {
124 type = lib.types.ints.unsigned;
125 default = 1;
126 description = ''
127 The number of instances to start. They will be called kresd@{1,2,...}.service.
128 Knot Resolver uses no threads, so this is the way to scale.
129 You can dynamically start/stop them at will, so this is just system default.
130 '';
131 };
132 # TODO: perhaps options for more common stuff like cache size or forwarding
133 };
134
135 ###### implementation
136 config = lib.mkIf cfg.enable {
137 environment = {
138 etc."knot-resolver/kresd.conf".source = configFile; # not required
139 systemPackages = [
140 (pkgs.writeShellScriptBin "kresd-cli" ''
141 if [[ ''${1:-} == -h || ''${1:-} == --help ]]; then
142 echo "Usage: $0 [X]"
143 echo
144 echo " X is number of the control socket and corresponds to the number of the template unit."
145 exit
146 fi
147
148 exec=exec
149 if [[ "$USER" != knot-resolver ]]; then
150 exec='exec /run/wrappers/bin/sudo -u knot-resolver'
151 fi
152 $exec ${lib.getExe pkgs.socat} - /run/knot-resolver/control/''${1:-1}
153 '')
154 ];
155 };
156
157 networking.resolvconf.useLocalResolver = lib.mkDefault true;
158
159 users.users.knot-resolver = {
160 isSystemUser = true;
161 group = "knot-resolver";
162 description = "Knot-resolver daemon user";
163 };
164 users.groups.knot-resolver.gid = null;
165
166 systemd.packages = [ cfg.package ]; # the units are patched inside the package a bit
167
168 systemd.targets.kresd = {
169 # configure units started by default
170 wantedBy = [ "multi-user.target" ];
171 wants = [
172 "kres-cache-gc.service"
173 ] ++ map (i: "kresd@${toString i}.service") (lib.range 1 cfg.instances);
174 };
175 systemd.services."kresd@".serviceConfig = {
176 ExecStart =
177 "${cfg.package}/bin/kresd --noninteractive "
178 + "-c ${cfg.package}/lib/knot-resolver/distro-preconfig.lua -c ${configFile}";
179 # Ensure /run/knot-resolver exists
180 RuntimeDirectory = "knot-resolver";
181 RuntimeDirectoryMode = "0770";
182 # Ensure /var/lib/knot-resolver exists
183 StateDirectory = "knot-resolver";
184 StateDirectoryMode = "0770";
185 # Ensure /var/cache/knot-resolver exists
186 CacheDirectory = "knot-resolver";
187 CacheDirectoryMode = "0770";
188 };
189 # We don't mind running stop phase from wrong version. It seems less racy.
190 systemd.services."kresd@".stopIfChanged = false;
191 };
192}