1{ config, lib, pkgs, ... }:
2with lib;
3
4let
5 cfg = config.services.dnscrypt-proxy;
6
7 stateDirectory = "/var/lib/dnscrypt-proxy";
8
9 # The minisign public key used to sign the upstream resolver list.
10 # This is somewhat more flexible than preloading the key as an
11 # embedded string.
12 upstreamResolverListPubKey = pkgs.fetchurl {
13 url = https://raw.githubusercontent.com/jedisct1/dnscrypt-proxy/master/minisign.pub;
14 sha256 = "18lnp8qr6ghfc2sd46nn1rhcpr324fqlvgsp4zaigw396cd7vnnh";
15 };
16
17 # Internal flag indicating whether the upstream resolver list is used.
18 useUpstreamResolverList = cfg.customResolver == null;
19
20 # The final local address.
21 localAddress = "${cfg.localAddress}:${toString cfg.localPort}";
22
23 # The final resolvers list path.
24 resolverList = "${stateDirectory}/dnscrypt-resolvers.csv";
25
26 # Build daemon command line
27
28 resolverArgs =
29 if (cfg.customResolver == null)
30 then
31 [ "-L ${resolverList}"
32 "-R ${cfg.resolverName}"
33 ]
34 else with cfg.customResolver;
35 [ "-N ${name}"
36 "-k ${key}"
37 "-r ${address}:${toString port}"
38 ];
39
40 daemonArgs =
41 [ "-a ${localAddress}" ]
42 ++ resolverArgs
43 ++ cfg.extraArgs;
44in
45
46{
47 meta = {
48 maintainers = with maintainers; [ joachifm ];
49 doc = ./dnscrypt-proxy.xml;
50 };
51
52 options = {
53 # Before adding another option, consider whether it could
54 # equally well be passed via extraArgs.
55
56 services.dnscrypt-proxy = {
57 enable = mkOption {
58 default = false;
59 type = types.bool;
60 description = "Whether to enable the DNSCrypt client proxy";
61 };
62
63 localAddress = mkOption {
64 default = "127.0.0.1";
65 type = types.str;
66 description = ''
67 Listen for DNS queries to relay on this address. The only reason to
68 change this from its default value is to proxy queries on behalf
69 of other machines (typically on the local network).
70 '';
71 };
72
73 localPort = mkOption {
74 default = 53;
75 type = types.int;
76 description = ''
77 Listen for DNS queries to relay on this port. The default value
78 assumes that the DNSCrypt proxy should relay DNS queries directly.
79 When running as a forwarder for another DNS client, set this option
80 to a different value; otherwise leave the default.
81 '';
82 };
83
84 resolverName = mkOption {
85 default = "random";
86 example = "dnscrypt.eu-nl";
87 type = types.nullOr types.str;
88 description = ''
89 The name of the DNSCrypt resolver to use, taken from
90 <filename>${resolverList}</filename>. The default is to
91 pick a random non-logging resolver that supports DNSSEC.
92 '';
93 };
94
95 customResolver = mkOption {
96 default = null;
97 description = ''
98 Use an unlisted resolver (e.g., a private DNSCrypt provider). For
99 advanced users only. If specified, this option takes precedence.
100 '';
101 type = types.nullOr (types.submodule ({ ... }: { options = {
102 address = mkOption {
103 type = types.str;
104 description = "IP address";
105 example = "208.67.220.220";
106 };
107
108 port = mkOption {
109 type = types.int;
110 description = "Port";
111 default = 443;
112 };
113
114 name = mkOption {
115 type = types.str;
116 description = "Fully qualified domain name";
117 example = "2.dnscrypt-cert.example.com";
118 };
119
120 key = mkOption {
121 type = types.str;
122 description = "Public key";
123 example = "B735:1140:206F:225D:3E2B:D822:D7FD:691E:A1C3:3CC8:D666:8D0C:BE04:BFAB:CA43:FB79";
124 };
125 }; }));
126 };
127
128 extraArgs = mkOption {
129 default = [];
130 type = types.listOf types.str;
131 description = ''
132 Additional command-line arguments passed verbatim to the daemon.
133 See <citerefentry><refentrytitle>dnscrypt-proxy</refentrytitle>
134 <manvolnum>8</manvolnum></citerefentry> for details.
135 '';
136 example = [ "-X libdcplugin_example_cache.so,--min-ttl=60" ];
137 };
138 };
139 };
140
141 config = mkIf cfg.enable (mkMerge [{
142 assertions = [
143 { assertion = (cfg.customResolver != null) || (cfg.resolverName != null);
144 message = "please configure upstream DNSCrypt resolver";
145 }
146 ];
147
148 users.users.dnscrypt-proxy = {
149 description = "dnscrypt-proxy daemon user";
150 isSystemUser = true;
151 group = "dnscrypt-proxy";
152 };
153 users.groups.dnscrypt-proxy = {};
154
155 systemd.sockets.dnscrypt-proxy = {
156 description = "dnscrypt-proxy listening socket";
157 documentation = [ "man:dnscrypt-proxy(8)" ];
158
159 wantedBy = [ "sockets.target" ];
160
161 socketConfig = {
162 ListenStream = localAddress;
163 ListenDatagram = localAddress;
164 };
165 };
166
167 systemd.services.dnscrypt-proxy = {
168 description = "dnscrypt-proxy daemon";
169 documentation = [ "man:dnscrypt-proxy(8)" ];
170
171 before = [ "nss-lookup.target" ];
172 after = [ "network.target" ];
173 requires = [ "dnscrypt-proxy.socket "];
174
175 serviceConfig = {
176 NonBlocking = "true";
177 ExecStart = "${pkgs.dnscrypt-proxy}/bin/dnscrypt-proxy ${toString daemonArgs}";
178 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
179
180 User = "dnscrypt-proxy";
181
182 PrivateTmp = true;
183 PrivateDevices = true;
184 ProtectHome = true;
185 };
186 };
187 }
188
189 (mkIf config.security.apparmor.enable {
190 systemd.services.dnscrypt-proxy.after = [ "apparmor.service" ];
191
192 security.apparmor.profiles = singleton (pkgs.writeText "apparmor-dnscrypt-proxy" ''
193 ${pkgs.dnscrypt-proxy}/bin/dnscrypt-proxy {
194 /dev/null rw,
195 /dev/urandom r,
196
197 /etc/passwd r,
198 /etc/group r,
199 ${config.environment.etc."nsswitch.conf".source} r,
200
201 ${getLib pkgs.glibc}/lib/*.so mr,
202 ${pkgs.tzdata}/share/zoneinfo/** r,
203
204 network inet stream,
205 network inet6 stream,
206 network inet dgram,
207 network inet6 dgram,
208
209 ${getLib pkgs.dnscrypt-proxy}/lib/dnscrypt-proxy/libdcplugin*.so mr,
210
211 ${getLib pkgs.gcc.cc}/lib/libssp.so.* mr,
212 ${getLib pkgs.libsodium}/lib/libsodium.so.* mr,
213 ${getLib pkgs.systemd}/lib/libsystemd.so.* mr,
214 ${getLib pkgs.xz}/lib/liblzma.so.* mr,
215 ${getLib pkgs.libgcrypt}/lib/libgcrypt.so.* mr,
216 ${getLib pkgs.libgpgerror}/lib/libgpg-error.so.* mr,
217 ${getLib pkgs.libcap}/lib/libcap.so.* mr,
218 ${getLib pkgs.lz4}/lib/liblz4.so.* mr,
219 ${getLib pkgs.attr}/lib/libattr.so.* mr, # */
220
221 ${resolverList} r,
222
223 /run/systemd/notify rw,
224 }
225 '');
226 })
227
228 (mkIf useUpstreamResolverList {
229 systemd.services.init-dnscrypt-proxy-statedir = {
230 description = "Initialize dnscrypt-proxy state directory";
231
232 wantedBy = [ "dnscrypt-proxy.service" ];
233 before = [ "dnscrypt-proxy.service" ];
234
235 script = ''
236 mkdir -pv ${stateDirectory}
237 chown -c dnscrypt-proxy:dnscrypt-proxy ${stateDirectory}
238 cp -uv \
239 ${pkgs.dnscrypt-proxy}/share/dnscrypt-proxy/dnscrypt-resolvers.csv \
240 ${stateDirectory}
241 '';
242
243 serviceConfig = {
244 Type = "oneshot";
245 RemainAfterExit = true;
246 };
247 };
248
249 systemd.services.update-dnscrypt-resolvers = {
250 description = "Update list of DNSCrypt resolvers";
251
252 requires = [ "init-dnscrypt-proxy-statedir.service" ];
253 after = [ "init-dnscrypt-proxy-statedir.service" ];
254
255 path = with pkgs; [ curl diffutils dnscrypt-proxy minisign ];
256 script = ''
257 cd ${stateDirectory}
258 domain=raw.githubusercontent.com
259 get="curl -fSs --resolve $domain:443:$(hostip -r 8.8.8.8 $domain | head -1)"
260 $get -o dnscrypt-resolvers.csv.tmp \
261 https://$domain/jedisct1/dnscrypt-proxy/master/dnscrypt-resolvers.csv
262 $get -o dnscrypt-resolvers.csv.minisig.tmp \
263 https://$domain/jedisct1/dnscrypt-proxy/master/dnscrypt-resolvers.csv.minisig
264 mv dnscrypt-resolvers.csv.minisig{.tmp,}
265 if ! minisign -q -V -p ${upstreamResolverListPubKey} \
266 -m dnscrypt-resolvers.csv.tmp -x dnscrypt-resolvers.csv.minisig ; then
267 echo "failed to verify resolver list!" >&2
268 exit 1
269 fi
270 [[ -f dnscrypt-resolvers.csv ]] && mv dnscrypt-resolvers.csv{,.old}
271 mv dnscrypt-resolvers.csv{.tmp,}
272 if cmp dnscrypt-resolvers.csv{,.old} ; then
273 echo "no change"
274 else
275 echo "resolver list updated"
276 fi
277 '';
278
279 serviceConfig = {
280 PrivateTmp = true;
281 PrivateDevices = true;
282 ProtectHome = true;
283 ProtectSystem = "strict";
284 ReadWritePaths = "${dirOf stateDirectory} ${stateDirectory}";
285 SystemCallFilter = "~@mount";
286 };
287 };
288
289 systemd.timers.update-dnscrypt-resolvers = {
290 wantedBy = [ "timers.target" ];
291 timerConfig = {
292 OnBootSec = "5min";
293 OnUnitActiveSec = "6h";
294 };
295 };
296 })
297 ]);
298
299 imports = [
300 (mkRenamedOptionModule [ "services" "dnscrypt-proxy" "port" ] [ "services" "dnscrypt-proxy" "localPort" ])
301
302 (mkChangedOptionModule
303 [ "services" "dnscrypt-proxy" "tcpOnly" ]
304 [ "services" "dnscrypt-proxy" "extraArgs" ]
305 (config:
306 let val = getAttrFromPath [ "services" "dnscrypt-proxy" "tcpOnly" ] config; in
307 optional val "-T"))
308
309 (mkChangedOptionModule
310 [ "services" "dnscrypt-proxy" "ephemeralKeys" ]
311 [ "services" "dnscrypt-proxy" "extraArgs" ]
312 (config:
313 let val = getAttrFromPath [ "services" "dnscrypt-proxy" "ephemeralKeys" ] config; in
314 optional val "-E"))
315
316 (mkRemovedOptionModule [ "services" "dnscrypt-proxy" "resolverList" ] ''
317 The current resolver listing from upstream is always used
318 unless a custom resolver is specified.
319 '')
320 ];
321}