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