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/dyne/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 # make man 8 dnscrypt-proxy work
149 environment.systemPackages = [ pkgs.dnscrypt-proxy ];
150
151 users.users.dnscrypt-proxy = {
152 description = "dnscrypt-proxy daemon user";
153 isSystemUser = true;
154 group = "dnscrypt-proxy";
155 };
156 users.groups.dnscrypt-proxy = {};
157
158 systemd.sockets.dnscrypt-proxy = {
159 description = "dnscrypt-proxy listening socket";
160 documentation = [ "man:dnscrypt-proxy(8)" ];
161
162 wantedBy = [ "sockets.target" ];
163
164 socketConfig = {
165 ListenStream = localAddress;
166 ListenDatagram = localAddress;
167 };
168 };
169
170 systemd.services.dnscrypt-proxy = {
171 description = "dnscrypt-proxy daemon";
172 documentation = [ "man:dnscrypt-proxy(8)" ];
173
174 before = [ "nss-lookup.target" ];
175 after = [ "network.target" ];
176 requires = [ "dnscrypt-proxy.socket "];
177
178 serviceConfig = {
179 NonBlocking = "true";
180 ExecStart = "${pkgs.dnscrypt-proxy}/bin/dnscrypt-proxy ${toString daemonArgs}";
181 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
182
183 User = "dnscrypt-proxy";
184
185 PrivateTmp = true;
186 PrivateDevices = true;
187 ProtectHome = true;
188 };
189 };
190 }
191
192 (mkIf config.security.apparmor.enable {
193 systemd.services.dnscrypt-proxy.after = [ "apparmor.service" ];
194
195 security.apparmor.profiles = singleton (pkgs.writeText "apparmor-dnscrypt-proxy" ''
196 ${pkgs.dnscrypt-proxy}/bin/dnscrypt-proxy {
197 /dev/null rw,
198 /dev/random r,
199 /dev/urandom r,
200
201 /etc/passwd r,
202 /etc/group r,
203 ${config.environment.etc."nsswitch.conf".source} r,
204
205 ${getLib pkgs.glibc}/lib/*.so mr,
206 ${pkgs.tzdata}/share/zoneinfo/** r,
207
208 network inet stream,
209 network inet6 stream,
210 network inet dgram,
211 network inet6 dgram,
212
213 ${getLib pkgs.dnscrypt-proxy}/lib/dnscrypt-proxy/libdcplugin*.so mr,
214
215 ${getLib pkgs.gcc.cc}/lib/libssp.so.* mr,
216 ${getLib pkgs.libsodium}/lib/libsodium.so.* mr,
217 ${getLib pkgs.systemd}/lib/libsystemd.so.* mr,
218 ${getLib pkgs.utillinuxMinimal.out}/lib/libmount.so.* mr,
219 ${getLib pkgs.utillinuxMinimal.out}/lib/libblkid.so.* mr,
220 ${getLib pkgs.utillinuxMinimal.out}/lib/libuuid.so.* mr,
221 ${getLib pkgs.xz}/lib/liblzma.so.* mr,
222 ${getLib pkgs.libgcrypt}/lib/libgcrypt.so.* mr,
223 ${getLib pkgs.libgpgerror}/lib/libgpg-error.so.* mr,
224 ${getLib pkgs.libcap}/lib/libcap.so.* mr,
225 ${getLib pkgs.lz4}/lib/liblz4.so.* mr,
226 ${getLib pkgs.attr}/lib/libattr.so.* mr, # */
227
228 ${resolverList} r,
229
230 /run/systemd/notify rw,
231 }
232 '');
233 })
234
235 (mkIf useUpstreamResolverList {
236 systemd.services.init-dnscrypt-proxy-statedir = {
237 description = "Initialize dnscrypt-proxy state directory";
238
239 wantedBy = [ "dnscrypt-proxy.service" ];
240 before = [ "dnscrypt-proxy.service" ];
241
242 script = ''
243 mkdir -pv ${stateDirectory}
244 chown -c dnscrypt-proxy:dnscrypt-proxy ${stateDirectory}
245 cp -uv \
246 ${pkgs.dnscrypt-proxy}/share/dnscrypt-proxy/dnscrypt-resolvers.csv \
247 ${stateDirectory}
248 '';
249
250 serviceConfig = {
251 Type = "oneshot";
252 RemainAfterExit = true;
253 };
254 };
255
256 systemd.services.update-dnscrypt-resolvers = {
257 description = "Update list of DNSCrypt resolvers";
258
259 requires = [ "init-dnscrypt-proxy-statedir.service" ];
260 after = [ "init-dnscrypt-proxy-statedir.service" ];
261
262 path = with pkgs; [ curl diffutils dnscrypt-proxy minisign ];
263 script = ''
264 cd ${stateDirectory}
265 domain=raw.githubusercontent.com
266 get="curl -fSs --resolve $domain:443:$(hostip -r 8.8.8.8 $domain | head -1)"
267 $get -o dnscrypt-resolvers.csv.tmp \
268 https://$domain/dyne/dnscrypt-proxy/master/dnscrypt-resolvers.csv
269 $get -o dnscrypt-resolvers.csv.minisig.tmp \
270 https://$domain/dyne/dnscrypt-proxy/master/dnscrypt-resolvers.csv.minisig
271 mv dnscrypt-resolvers.csv.minisig{.tmp,}
272 if ! minisign -q -V -p ${upstreamResolverListPubKey} \
273 -m dnscrypt-resolvers.csv.tmp -x dnscrypt-resolvers.csv.minisig ; then
274 echo "failed to verify resolver list!" >&2
275 exit 1
276 fi
277 [[ -f dnscrypt-resolvers.csv ]] && mv dnscrypt-resolvers.csv{,.old}
278 mv dnscrypt-resolvers.csv{.tmp,}
279 if cmp dnscrypt-resolvers.csv{,.old} ; then
280 echo "no change"
281 else
282 echo "resolver list updated"
283 fi
284 '';
285
286 serviceConfig = {
287 PrivateTmp = true;
288 PrivateDevices = true;
289 ProtectHome = true;
290 ProtectSystem = "strict";
291 ReadWritePaths = "${dirOf stateDirectory} ${stateDirectory}";
292 SystemCallFilter = "~@mount";
293 };
294 };
295
296 systemd.timers.update-dnscrypt-resolvers = {
297 wantedBy = [ "timers.target" ];
298 timerConfig = {
299 OnBootSec = "5min";
300 OnUnitActiveSec = "6h";
301 };
302 };
303 })
304 ]);
305
306 imports = [
307 (mkRenamedOptionModule [ "services" "dnscrypt-proxy" "port" ] [ "services" "dnscrypt-proxy" "localPort" ])
308
309 (mkChangedOptionModule
310 [ "services" "dnscrypt-proxy" "tcpOnly" ]
311 [ "services" "dnscrypt-proxy" "extraArgs" ]
312 (config:
313 let val = getAttrFromPath [ "services" "dnscrypt-proxy" "tcpOnly" ] config; in
314 optional val "-T"))
315
316 (mkChangedOptionModule
317 [ "services" "dnscrypt-proxy" "ephemeralKeys" ]
318 [ "services" "dnscrypt-proxy" "extraArgs" ]
319 (config:
320 let val = getAttrFromPath [ "services" "dnscrypt-proxy" "ephemeralKeys" ] config; in
321 optional val "-E"))
322
323 (mkRemovedOptionModule [ "services" "dnscrypt-proxy" "resolverList" ] ''
324 The current resolver listing from upstream is always used
325 unless a custom resolver is specified.
326 '')
327 ];
328}