1{ config, lib, pkgs, ... }:
2with lib;
3
4let
5 cfg = config.services.dnscrypt-wrapper;
6 dataDir = "/var/lib/dnscrypt-wrapper";
7
8 mkPath = path: default:
9 if path != null
10 then toString path
11 else default;
12
13 publicKey = mkPath cfg.providerKey.public "${dataDir}/public.key";
14 secretKey = mkPath cfg.providerKey.secret "${dataDir}/secret.key";
15
16 daemonArgs = with cfg; [
17 "--listen-address=${address}:${toString port}"
18 "--resolver-address=${upstream.address}:${toString upstream.port}"
19 "--provider-name=${providerName}"
20 "--provider-publickey-file=${publicKey}"
21 "--provider-secretkey-file=${secretKey}"
22 "--provider-cert-file=${providerName}.crt"
23 "--crypt-secretkey-file=${providerName}.key"
24 ];
25
26 genKeys = ''
27 # generates time-limited keypairs
28 keyGen() {
29 dnscrypt-wrapper --gen-crypt-keypair \
30 --crypt-secretkey-file=${cfg.providerName}.key
31
32 dnscrypt-wrapper --gen-cert-file \
33 --crypt-secretkey-file=${cfg.providerName}.key \
34 --provider-cert-file=${cfg.providerName}.crt \
35 --provider-publickey-file=${publicKey} \
36 --provider-secretkey-file=${secretKey} \
37 --cert-file-expire-days=${toString cfg.keys.expiration}
38 }
39
40 cd ${dataDir}
41
42 # generate provider keypair (first run only)
43 ${optionalString (cfg.providerKey.public == null || cfg.providerKey.secret == null) ''
44 if [ ! -f ${publicKey} ] || [ ! -f ${secretKey} ]; then
45 dnscrypt-wrapper --gen-provider-keypair
46 fi
47 ''}
48
49 # generate new keys for rotation
50 if [ ! -f ${cfg.providerName}.key ] || [ ! -f ${cfg.providerName}.crt ]; then
51 keyGen
52 fi
53 '';
54
55 rotateKeys = ''
56 # check if keys are not expired
57 keyValid() {
58 fingerprint=$(dnscrypt-wrapper \
59 --show-provider-publickey \
60 --provider-publickey-file=${publicKey} \
61 | awk '{print $(NF)}')
62 dnscrypt-proxy --test=${toString (cfg.keys.checkInterval + 1)} \
63 --resolver-address=127.0.0.1:${toString cfg.port} \
64 --provider-name=${cfg.providerName} \
65 --provider-key=$fingerprint
66 }
67
68 cd ${dataDir}
69
70 # archive old keys and restart the service
71 if ! keyValid; then
72 echo "certificate soon to become invalid; backing up old cert"
73 mkdir -p oldkeys
74 mv -v ${cfg.providerName}.key oldkeys/${cfg.providerName}-$(date +%F-%T).key
75 mv -v ${cfg.providerName}.crt oldkeys/${cfg.providerName}-$(date +%F-%T).crt
76 systemctl restart dnscrypt-wrapper
77 fi
78 '';
79
80
81 # This is the fork of the original dnscrypt-proxy maintained by Dyne.org.
82 # dnscrypt-proxy2 doesn't provide the `--test` feature that is needed to
83 # correctly implement key rotation of dnscrypt-wrapper ephemeral keys.
84 dnscrypt-proxy1 = pkgs.callPackage
85 ({ stdenv, fetchFromGitHub, autoreconfHook
86 , pkg-config, libsodium, ldns, openssl, systemd }:
87
88 stdenv.mkDerivation rec {
89 pname = "dnscrypt-proxy";
90 version = "2019-08-20";
91
92 src = fetchFromGitHub {
93 owner = "dyne";
94 repo = "dnscrypt-proxy";
95 rev = "07ac3825b5069adc28e2547c16b1d983a8ed8d80";
96 sha256 = "0c4mq741q4rpmdn09agwmxap32kf0vgfz7pkhcdc5h54chc3g3xy";
97 };
98
99 configureFlags = optional stdenv.isLinux "--with-systemd";
100
101 nativeBuildInputs = [ autoreconfHook pkg-config ];
102
103 # <ldns/ldns.h> depends on <openssl/ssl.h>
104 buildInputs = [ libsodium openssl.dev ldns ] ++ optional stdenv.isLinux systemd;
105
106 postInstall = ''
107 # Previous versions required libtool files to load plugins; they are
108 # now strictly optional.
109 rm $out/lib/dnscrypt-proxy/*.la
110 '';
111
112 meta = {
113 description = "A tool for securing communications between a client and a DNS resolver";
114 homepage = "https://github.com/dyne/dnscrypt-proxy";
115 license = licenses.isc;
116 maintainers = with maintainers; [ rnhmjoj ];
117 platforms = platforms.linux;
118 };
119 }) { };
120
121in {
122
123
124 ###### interface
125
126 options.services.dnscrypt-wrapper = {
127 enable = mkEnableOption (lib.mdDoc "DNSCrypt wrapper");
128
129 address = mkOption {
130 type = types.str;
131 default = "127.0.0.1";
132 description = lib.mdDoc ''
133 The DNSCrypt wrapper will bind to this IP address.
134 '';
135 };
136
137 port = mkOption {
138 type = types.port;
139 default = 5353;
140 description = lib.mdDoc ''
141 The DNSCrypt wrapper will listen for DNS queries on this port.
142 '';
143 };
144
145 providerName = mkOption {
146 type = types.str;
147 default = "2.dnscrypt-cert.${config.networking.hostName}";
148 defaultText = literalExpression ''"2.dnscrypt-cert.''${config.networking.hostName}"'';
149 example = "2.dnscrypt-cert.myresolver";
150 description = lib.mdDoc ''
151 The name that will be given to this DNSCrypt resolver.
152 Note: the resolver name must start with `2.dnscrypt-cert.`.
153 '';
154 };
155
156 providerKey.public = mkOption {
157 type = types.nullOr types.path;
158 default = null;
159 example = "/etc/secrets/public.key";
160 description = lib.mdDoc ''
161 The filepath to the provider public key. If not given a new
162 provider key pair will be generated on the first run.
163 '';
164 };
165
166 providerKey.secret = mkOption {
167 type = types.nullOr types.path;
168 default = null;
169 example = "/etc/secrets/secret.key";
170 description = lib.mdDoc ''
171 The filepath to the provider secret key. If not given a new
172 provider key pair will be generated on the first run.
173 '';
174 };
175
176 upstream.address = mkOption {
177 type = types.str;
178 default = "127.0.0.1";
179 description = lib.mdDoc ''
180 The IP address of the upstream DNS server DNSCrypt will "wrap".
181 '';
182 };
183
184 upstream.port = mkOption {
185 type = types.port;
186 default = 53;
187 description = lib.mdDoc ''
188 The port of the upstream DNS server DNSCrypt will "wrap".
189 '';
190 };
191
192 keys.expiration = mkOption {
193 type = types.int;
194 default = 30;
195 description = lib.mdDoc ''
196 The duration (in days) of the time-limited secret key.
197 This will be automatically rotated before expiration.
198 '';
199 };
200
201 keys.checkInterval = mkOption {
202 type = types.int;
203 default = 1440;
204 description = lib.mdDoc ''
205 The time interval (in minutes) between key expiration checks.
206 '';
207 };
208
209 };
210
211
212 ###### implementation
213
214 config = mkIf cfg.enable {
215
216 users.users.dnscrypt-wrapper = {
217 description = "dnscrypt-wrapper daemon user";
218 home = "${dataDir}";
219 createHome = true;
220 isSystemUser = true;
221 group = "dnscrypt-wrapper";
222 };
223 users.groups.dnscrypt-wrapper = { };
224
225 security.polkit.extraConfig = ''
226 // Allow dnscrypt-wrapper user to restart dnscrypt-wrapper.service
227 polkit.addRule(function(action, subject) {
228 if (action.id == "org.freedesktop.systemd1.manage-units" &&
229 action.lookup("unit") == "dnscrypt-wrapper.service" &&
230 subject.user == "dnscrypt-wrapper") {
231 return polkit.Result.YES;
232 }
233 });
234 '';
235
236 systemd.services.dnscrypt-wrapper = {
237 description = "dnscrypt-wrapper daemon";
238 after = [ "network.target" ];
239 wantedBy = [ "multi-user.target" ];
240 path = [ pkgs.dnscrypt-wrapper ];
241
242 serviceConfig = {
243 User = "dnscrypt-wrapper";
244 WorkingDirectory = dataDir;
245 Restart = "on-failure";
246 ExecStart = "${pkgs.dnscrypt-wrapper}/bin/dnscrypt-wrapper ${toString daemonArgs}";
247 };
248
249 preStart = genKeys;
250 };
251
252
253 systemd.services.dnscrypt-wrapper-rotate = {
254 after = [ "network.target" ];
255 requires = [ "dnscrypt-wrapper.service" ];
256 description = "Rotates DNSCrypt wrapper keys if soon to expire";
257
258 path = with pkgs; [ dnscrypt-wrapper dnscrypt-proxy1 gawk ];
259 script = rotateKeys;
260 serviceConfig.User = "dnscrypt-wrapper";
261 };
262
263
264 systemd.timers.dnscrypt-wrapper-rotate = {
265 description = "Periodically check DNSCrypt wrapper keys for expiration";
266 wantedBy = [ "multi-user.target" ];
267
268 timerConfig = {
269 Unit = "dnscrypt-wrapper-rotate.service";
270 OnBootSec = "1min";
271 OnUnitActiveSec = cfg.keys.checkInterval * 60;
272 };
273 };
274
275 assertions = with cfg; [
276 { assertion = (providerKey.public == null && providerKey.secret == null) ||
277 (providerKey.secret != null && providerKey.public != null);
278 message = "The secret and public provider key must be set together.";
279 }
280 ];
281
282 };
283
284 meta.maintainers = with lib.maintainers; [ rnhmjoj ];
285
286}