at 21.11-pre 8.5 kB view raw
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 "DNSCrypt wrapper"; 128 129 address = mkOption { 130 type = types.str; 131 default = "127.0.0.1"; 132 description = '' 133 The DNSCrypt wrapper will bind to this IP address. 134 ''; 135 }; 136 137 port = mkOption { 138 type = types.int; 139 default = 5353; 140 description = '' 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 example = "2.dnscrypt-cert.myresolver"; 149 description = '' 150 The name that will be given to this DNSCrypt resolver. 151 Note: the resolver name must start with <literal>2.dnscrypt-cert.</literal>. 152 ''; 153 }; 154 155 providerKey.public = mkOption { 156 type = types.nullOr types.path; 157 default = null; 158 example = "/etc/secrets/public.key"; 159 description = '' 160 The filepath to the provider public key. If not given a new 161 provider key pair will be generated on the first run. 162 ''; 163 }; 164 165 providerKey.secret = mkOption { 166 type = types.nullOr types.path; 167 default = null; 168 example = "/etc/secrets/secret.key"; 169 description = '' 170 The filepath to the provider secret key. If not given a new 171 provider key pair will be generated on the first run. 172 ''; 173 }; 174 175 upstream.address = mkOption { 176 type = types.str; 177 default = "127.0.0.1"; 178 description = '' 179 The IP address of the upstream DNS server DNSCrypt will "wrap". 180 ''; 181 }; 182 183 upstream.port = mkOption { 184 type = types.int; 185 default = 53; 186 description = '' 187 The port of the upstream DNS server DNSCrypt will "wrap". 188 ''; 189 }; 190 191 keys.expiration = mkOption { 192 type = types.int; 193 default = 30; 194 description = '' 195 The duration (in days) of the time-limited secret key. 196 This will be automatically rotated before expiration. 197 ''; 198 }; 199 200 keys.checkInterval = mkOption { 201 type = types.int; 202 default = 1440; 203 description = '' 204 The time interval (in minutes) between key expiration checks. 205 ''; 206 }; 207 208 }; 209 210 211 ###### implementation 212 213 config = mkIf cfg.enable { 214 215 users.users.dnscrypt-wrapper = { 216 description = "dnscrypt-wrapper daemon user"; 217 home = "${dataDir}"; 218 createHome = true; 219 isSystemUser = true; 220 }; 221 users.groups.dnscrypt-wrapper = { }; 222 223 security.polkit.extraConfig = '' 224 // Allow dnscrypt-wrapper user to restart dnscrypt-wrapper.service 225 polkit.addRule(function(action, subject) { 226 if (action.id == "org.freedesktop.systemd1.manage-units" && 227 action.lookup("unit") == "dnscrypt-wrapper.service" && 228 subject.user == "dnscrypt-wrapper") { 229 return polkit.Result.YES; 230 } 231 }); 232 ''; 233 234 systemd.services.dnscrypt-wrapper = { 235 description = "dnscrypt-wrapper daemon"; 236 after = [ "network.target" ]; 237 wantedBy = [ "multi-user.target" ]; 238 path = [ pkgs.dnscrypt-wrapper ]; 239 240 serviceConfig = { 241 User = "dnscrypt-wrapper"; 242 WorkingDirectory = dataDir; 243 Restart = "on-failure"; 244 ExecStart = "${pkgs.dnscrypt-wrapper}/bin/dnscrypt-wrapper ${toString daemonArgs}"; 245 }; 246 247 preStart = genKeys; 248 }; 249 250 251 systemd.services.dnscrypt-wrapper-rotate = { 252 after = [ "network.target" ]; 253 requires = [ "dnscrypt-wrapper.service" ]; 254 description = "Rotates DNSCrypt wrapper keys if soon to expire"; 255 256 path = with pkgs; [ dnscrypt-wrapper dnscrypt-proxy1 gawk ]; 257 script = rotateKeys; 258 serviceConfig.User = "dnscrypt-wrapper"; 259 }; 260 261 262 systemd.timers.dnscrypt-wrapper-rotate = { 263 description = "Periodically check DNSCrypt wrapper keys for expiration"; 264 wantedBy = [ "multi-user.target" ]; 265 266 timerConfig = { 267 Unit = "dnscrypt-wrapper-rotate.service"; 268 OnBootSec = "1min"; 269 OnUnitActiveSec = cfg.keys.checkInterval * 60; 270 }; 271 }; 272 273 assertions = with cfg; [ 274 { assertion = (providerKey.public == null && providerKey.secret == null) || 275 (providerKey.secret != null && providerKey.public != null); 276 message = "The secret and public provider key must be set together."; 277 } 278 ]; 279 280 }; 281 282 meta.maintainers = with lib.maintainers; [ rnhmjoj ]; 283 284}