1{ config, lib, pkgs, ... }:
2with lib;
3
4let
5 apparmorEnabled = config.security.apparmor.enable;
6 dnscrypt-proxy = pkgs.dnscrypt-proxy;
7 cfg = config.services.dnscrypt-proxy;
8
9 localAddress = "${cfg.localAddress}:${toString cfg.localPort}";
10
11 daemonArgs =
12 [ "--local-address=${localAddress}"
13 (optionalString cfg.tcpOnly "--tcp-only")
14 (optionalString cfg.ephemeralKeys "-E")
15 ]
16 ++ resolverArgs;
17
18 resolverArgs = if (cfg.customResolver != null)
19 then
20 [ "--resolver-address=${cfg.customResolver.address}:${toString cfg.customResolver.port}"
21 "--provider-name=${cfg.customResolver.name}"
22 "--provider-key=${cfg.customResolver.key}"
23 ]
24 else
25 [ "--resolvers-list=${cfg.resolverList}"
26 "--resolver-name=${toString cfg.resolverName}"
27 ];
28in
29
30{
31 meta = {
32 maintainers = with maintainers; [ joachifm ];
33 doc = ./dnscrypt-proxy.xml;
34 };
35
36 options = {
37 services.dnscrypt-proxy = {
38 enable = mkEnableOption "DNSCrypt client proxy";
39
40 localAddress = mkOption {
41 default = "127.0.0.1";
42 type = types.str;
43 description = ''
44 Listen for DNS queries to relay on this address. The only reason to
45 change this from its default value is to proxy queries on behalf
46 of other machines (typically on the local network).
47 '';
48 };
49
50 localPort = mkOption {
51 default = 53;
52 type = types.int;
53 description = ''
54 Listen for DNS queries to relay on this port. The default value
55 assumes that the DNSCrypt proxy should relay DNS queries directly.
56 When running as a forwarder for another DNS client, set this option
57 to a different value; otherwise leave the default.
58 '';
59 };
60
61 resolverName = mkOption {
62 default = "dnscrypt.eu-nl";
63 type = types.nullOr types.str;
64 description = ''
65 The name of the upstream DNSCrypt resolver to use, taken from the
66 list named in the <literal>resolverList</literal> option.
67 The default resolver is located in Holland, supports DNS security
68 extensions, and claims to not keep logs.
69 '';
70 };
71
72 resolverList = mkOption {
73 description = ''
74 The list of upstream DNSCrypt resolvers. By default, we use the most
75 recent list published by upstream.
76 '';
77 example = literalExample "${pkgs.dnscrypt-proxy}/share/dnscrypt-proxy/dnscrypt-resolvers.csv";
78 default = pkgs.fetchurl {
79 url = https://raw.githubusercontent.com/jedisct1/dnscrypt-proxy/master/dnscrypt-resolvers.csv;
80 sha256 = "1i9wzw4zl052h5nyp28bwl8d66cgj0awvjhw5wgwz0warkjl1g8g";
81 };
82 defaultText = "pkgs.fetchurl { url = ...; sha256 = ...; }";
83 };
84
85 customResolver = mkOption {
86 default = null;
87 description = ''
88 Use an unlisted resolver (e.g., a private DNSCrypt provider). For
89 advanced users only. If specified, this option takes precedence.
90 '';
91 type = types.nullOr (types.submodule ({ ... }: { options = {
92 address = mkOption {
93 type = types.str;
94 description = "IP address";
95 example = "208.67.220.220";
96 };
97
98 port = mkOption {
99 type = types.int;
100 description = "Port";
101 default = 443;
102 };
103
104 name = mkOption {
105 type = types.str;
106 description = "Fully qualified domain name";
107 example = "2.dnscrypt-cert.opendns.com";
108 };
109
110 key = mkOption {
111 type = types.str;
112 description = "Public key";
113 example = "B735:1140:206F:225D:3E2B:D822:D7FD:691E:A1C3:3CC8:D666:8D0C:BE04:BFAB:CA43:FB79";
114 };
115 }; }));
116 };
117
118 tcpOnly = mkOption {
119 default = false;
120 type = types.bool;
121 description = ''
122 Force sending encrypted DNS queries to the upstream resolver over
123 TCP instead of UDP (on port 443). Use only if the UDP port is blocked.
124 '';
125 };
126
127 ephemeralKeys = mkOption {
128 default = false;
129 type = types.bool;
130 description = ''
131 Compute a new key pair for every query. Enabling this option
132 increases CPU usage, but makes it more difficult for the upstream
133 resolver to track your usage of their service across IP addresses.
134 The default is to re-use the public key pair for all queries, making
135 tracking trivial.
136 '';
137 };
138 };
139 };
140
141 config = mkIf cfg.enable {
142
143 assertions = [
144 { assertion = (cfg.customResolver != null) || (cfg.resolverName != null);
145 message = "please configure upstream DNSCrypt resolver";
146 }
147 ];
148
149 security.apparmor.profiles = mkIf apparmorEnabled (singleton (pkgs.writeText "apparmor-dnscrypt-proxy" ''
150 ${dnscrypt-proxy}/bin/dnscrypt-proxy {
151 /dev/null rw,
152 /dev/urandom r,
153
154 /etc/passwd r,
155 /etc/group r,
156 ${config.environment.etc."nsswitch.conf".source} r,
157
158 ${getLib pkgs.glibc}/lib/*.so mr,
159 ${pkgs.tzdata}/share/zoneinfo/** r,
160
161 network inet stream,
162 network inet6 stream,
163 network inet dgram,
164 network inet6 dgram,
165
166 ${getLib pkgs.gcc.cc}/lib/libssp.so.* mr,
167 ${getLib pkgs.libsodium}/lib/libsodium.so.* mr,
168 ${getLib pkgs.systemd}/lib/libsystemd.so.* mr,
169 ${getLib pkgs.xz}/lib/liblzma.so.* mr,
170 ${getLib pkgs.libgcrypt}/lib/libgcrypt.so.* mr,
171 ${getLib pkgs.libgpgerror}/lib/libgpg-error.so.* mr,
172 ${getLib pkgs.libcap}/lib/libcap.so.* mr,
173 ${getLib pkgs.lz4}/lib/liblz4.so.* mr,
174 ${getLib pkgs.attr}/lib/libattr.so.* mr,
175
176 ${cfg.resolverList} r,
177 }
178 ''));
179
180 users.users.dnscrypt-proxy = {
181 description = "dnscrypt-proxy daemon user";
182 isSystemUser = true;
183 group = "dnscrypt-proxy";
184 };
185 users.groups.dnscrypt-proxy = {};
186
187 systemd.sockets.dnscrypt-proxy = {
188 description = "dnscrypt-proxy listening socket";
189 socketConfig = {
190 ListenStream = "${localAddress}";
191 ListenDatagram = "${localAddress}";
192 };
193 wantedBy = [ "sockets.target" ];
194 };
195
196 systemd.services.dnscrypt-proxy = {
197 description = "dnscrypt-proxy daemon";
198
199 after = [ "network.target" ] ++ optional apparmorEnabled "apparmor.service";
200 requires = [ "dnscrypt-proxy.socket "] ++ optional apparmorEnabled "apparmor.service";
201
202 serviceConfig = {
203 Type = "simple";
204 NonBlocking = "true";
205 ExecStart = "${dnscrypt-proxy}/bin/dnscrypt-proxy ${toString daemonArgs}";
206
207 User = "dnscrypt-proxy";
208
209 PrivateTmp = true;
210 PrivateDevices = true;
211 ProtectHome = true;
212 };
213 };
214 };
215}