1{ config, lib, pkgs, ... }:
2with lib;
3
4let
5 cfg = config.services.dnscrypt-wrapper;
6 dataDir = "/var/lib/dnscrypt-wrapper";
7
8 daemonArgs = with cfg; [
9 "--listen-address=${address}:${toString port}"
10 "--resolver-address=${upstream.address}:${toString upstream.port}"
11 "--provider-name=${providerName}"
12 "--provider-publickey-file=public.key"
13 "--provider-secretkey-file=secret.key"
14 "--provider-cert-file=${providerName}.crt"
15 "--crypt-secretkey-file=${providerName}.key"
16 ];
17
18 genKeys = ''
19 # generates time-limited keypairs
20 keyGen() {
21 dnscrypt-wrapper --gen-crypt-keypair \
22 --crypt-secretkey-file=${cfg.providerName}.key
23
24 dnscrypt-wrapper --gen-cert-file \
25 --crypt-secretkey-file=${cfg.providerName}.key \
26 --provider-cert-file=${cfg.providerName}.crt \
27 --provider-publickey-file=public.key \
28 --provider-secretkey-file=secret.key \
29 --cert-file-expire-days=${toString cfg.keys.expiration}
30 }
31
32 cd ${dataDir}
33
34 # generate provider keypair (first run only)
35 if [ ! -f public.key ] || [ ! -f secret.key ]; then
36 dnscrypt-wrapper --gen-provider-keypair
37 fi
38
39 # generate new keys for rotation
40 if [ ! -f ${cfg.providerName}.key ] || [ ! -f ${cfg.providerName}.crt ]; then
41 keyGen
42 fi
43 '';
44
45 rotateKeys = ''
46 # check if keys are not expired
47 keyValid() {
48 fingerprint=$(dnscrypt-wrapper --show-provider-publickey | awk '{print $(NF)}')
49 dnscrypt-proxy --test=${toString (cfg.keys.checkInterval + 1)} \
50 --resolver-address=127.0.0.1:${toString cfg.port} \
51 --provider-name=${cfg.providerName} \
52 --provider-key=$fingerprint
53 }
54
55 cd ${dataDir}
56
57 # archive old keys and restart the service
58 if ! keyValid; then
59 echo "certificate soon to become invalid; backing up old cert"
60 mkdir -p oldkeys
61 mv -v ${cfg.providerName}.key oldkeys/${cfg.providerName}-$(date +%F-%T).key
62 mv -v ${cfg.providerName}.crt oldkeys/${cfg.providerName}-$(date +%F-%T).crt
63 systemctl restart dnscrypt-wrapper
64 fi
65 '';
66
67in {
68
69
70 ###### interface
71
72 options.services.dnscrypt-wrapper = {
73 enable = mkEnableOption "DNSCrypt wrapper";
74
75 address = mkOption {
76 type = types.str;
77 default = "127.0.0.1";
78 description = ''
79 The DNSCrypt wrapper will bind to this IP address.
80 '';
81 };
82
83 port = mkOption {
84 type = types.int;
85 default = 5353;
86 description = ''
87 The DNSCrypt wrapper will listen for DNS queries on this port.
88 '';
89 };
90
91 providerName = mkOption {
92 type = types.str;
93 default = "2.dnscrypt-cert.${config.networking.hostName}";
94 example = "2.dnscrypt-cert.myresolver";
95 description = ''
96 The name that will be given to this DNSCrypt resolver.
97 Note: the resolver name must start with <literal>2.dnscrypt-cert.</literal>.
98 '';
99 };
100
101 upstream.address = mkOption {
102 type = types.str;
103 default = "127.0.0.1";
104 description = ''
105 The IP address of the upstream DNS server DNSCrypt will "wrap".
106 '';
107 };
108
109 upstream.port = mkOption {
110 type = types.int;
111 default = 53;
112 description = ''
113 The port of the upstream DNS server DNSCrypt will "wrap".
114 '';
115 };
116
117 keys.expiration = mkOption {
118 type = types.int;
119 default = 30;
120 description = ''
121 The duration (in days) of the time-limited secret key.
122 This will be automatically rotated before expiration.
123 '';
124 };
125
126 keys.checkInterval = mkOption {
127 type = types.int;
128 default = 1440;
129 description = ''
130 The time interval (in minutes) between key expiration checks.
131 '';
132 };
133
134 };
135
136
137 ###### implementation
138
139 config = mkIf cfg.enable {
140
141 users.users.dnscrypt-wrapper = {
142 description = "dnscrypt-wrapper daemon user";
143 home = "${dataDir}";
144 createHome = true;
145 };
146 users.groups.dnscrypt-wrapper = { };
147
148 security.polkit.extraConfig = ''
149 // Allow dnscrypt-wrapper user to restart dnscrypt-wrapper.service
150 polkit.addRule(function(action, subject) {
151 if (action.id == "org.freedesktop.systemd1.manage-units" &&
152 action.lookup("unit") == "dnscrypt-wrapper.service" &&
153 subject.user == "dnscrypt-wrapper") {
154 return polkit.Result.YES;
155 }
156 });
157 '';
158
159 systemd.services.dnscrypt-wrapper = {
160 description = "dnscrypt-wrapper daemon";
161 after = [ "network.target" ];
162 wantedBy = [ "multi-user.target" ];
163 path = [ pkgs.dnscrypt-wrapper ];
164
165 serviceConfig = {
166 User = "dnscrypt-wrapper";
167 WorkingDirectory = dataDir;
168 Restart = "on-failure";
169 ExecStart = "${pkgs.dnscrypt-wrapper}/bin/dnscrypt-wrapper ${toString daemonArgs}";
170 };
171
172 preStart = genKeys;
173 };
174
175
176 systemd.services.dnscrypt-wrapper-rotate = {
177 after = [ "network.target" ];
178 requires = [ "dnscrypt-wrapper.service" ];
179 description = "Rotates DNSCrypt wrapper keys if soon to expire";
180
181 path = with pkgs; [ dnscrypt-wrapper dnscrypt-proxy gawk ];
182 script = rotateKeys;
183 serviceConfig.User = "dnscrypt-wrapper";
184 };
185
186
187 systemd.timers.dnscrypt-wrapper-rotate = {
188 description = "Periodically check DNSCrypt wrapper keys for expiration";
189 wantedBy = [ "multi-user.target" ];
190
191 timerConfig = {
192 Unit = "dnscrypt-wrapper-rotate.service";
193 OnBootSec = "1min";
194 OnUnitActiveSec = cfg.keys.checkInterval * 60;
195 };
196 };
197
198 };
199}