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