at 23.11-pre 8.7 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 (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}