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-fingerprint | 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 mkdir -p oldkeys
60 mv ${cfg.providerName}.key oldkeys/${cfg.providerName}-$(date +%F-%T).key
61 mv ${cfg.providerName}.crt oldkeys/${cfg.providerName}-$(date +%F-%T).crt
62 systemctl restart dnscrypt-wrapper
63 fi
64 '';
65
66in {
67
68
69 ###### interface
70
71 options.services.dnscrypt-wrapper = {
72 enable = mkEnableOption "DNSCrypt wrapper";
73
74 address = mkOption {
75 type = types.str;
76 default = "127.0.0.1";
77 description = ''
78 The DNSCrypt wrapper will bind to this IP address.
79 '';
80 };
81
82 port = mkOption {
83 type = types.int;
84 default = 5353;
85 description = ''
86 The DNSCrypt wrapper will listen for DNS queries on this port.
87 '';
88 };
89
90 providerName = mkOption {
91 type = types.str;
92 default = "2.dnscrypt-cert.${config.networking.hostName}";
93 example = "2.dnscrypt-cert.myresolver";
94 description = ''
95 The name that will be given to this DNSCrypt resolver.
96 Note: the resolver name must start with <literal>2.dnscrypt-cert.</literal>.
97 '';
98 };
99
100 upstream.address = mkOption {
101 type = types.str;
102 default = "127.0.0.1";
103 description = ''
104 The IP address of the upstream DNS server DNSCrypt will "wrap".
105 '';
106 };
107
108 upstream.port = mkOption {
109 type = types.int;
110 default = 53;
111 description = ''
112 The port of the upstream DNS server DNSCrypt will "wrap".
113 '';
114 };
115
116 keys.expiration = mkOption {
117 type = types.int;
118 default = 30;
119 description = ''
120 The duration (in days) of the time-limited secret key.
121 This will be automatically rotated before expiration.
122 '';
123 };
124
125 keys.checkInterval = mkOption {
126 type = types.int;
127 default = 1440;
128 description = ''
129 The time interval (in minutes) between key expiration checks.
130 '';
131 };
132
133 };
134
135
136 ###### implementation
137
138 config = mkIf cfg.enable {
139
140 users.users.dnscrypt-wrapper = {
141 description = "dnscrypt-wrapper daemon user";
142 home = "${dataDir}";
143 createHome = true;
144 };
145 users.groups.dnscrypt-wrapper = { };
146
147
148 systemd.services.dnscrypt-wrapper = {
149 description = "dnscrypt-wrapper daemon";
150 after = [ "network.target" ];
151 wantedBy = [ "multi-user.target" ];
152 path = [ pkgs.dnscrypt-wrapper ];
153
154 serviceConfig = {
155 User = "dnscrypt-wrapper";
156 WorkingDirectory = dataDir;
157 Restart = "on-failure";
158 ExecStart = "${pkgs.dnscrypt-wrapper}/bin/dnscrypt-wrapper ${toString daemonArgs}";
159 };
160
161 preStart = genKeys;
162 };
163
164
165 systemd.services.dnscrypt-wrapper-rotate = {
166 after = [ "network.target" ];
167 requires = [ "dnscrypt-wrapper.service" ];
168 description = "Rotates DNSCrypt wrapper keys if soon to expire";
169
170 path = with pkgs; [ dnscrypt-wrapper dnscrypt-proxy gawk ];
171 script = rotateKeys;
172 };
173
174
175 systemd.timers.dnscrypt-wrapper-rotate = {
176 description = "Periodically check DNSCrypt wrapper keys for expiration";
177 wantedBy = [ "multi-user.target" ];
178
179 timerConfig = {
180 Unit = "dnscrypt-wrapper-rotate.service";
181 OnBootSec = "1min";
182 OnUnitActiveSec = cfg.keys.checkInterval * 60;
183 };
184 };
185
186 };
187}