200 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
			
		
		
	
	
			200 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
| { config, lib, pkgs, ... }:
 | |
| with lib;
 | |
| 
 | |
| let
 | |
|   cfg     = config.services.dnscrypt-wrapper;
 | |
|   dataDir = "/var/lib/dnscrypt-wrapper";
 | |
| 
 | |
|   daemonArgs = with cfg; [
 | |
|     "--listen-address=${address}:${toString port}"
 | |
|     "--resolver-address=${upstream.address}:${toString upstream.port}"
 | |
|     "--provider-name=${providerName}"
 | |
|     "--provider-publickey-file=public.key"
 | |
|     "--provider-secretkey-file=secret.key"
 | |
|     "--provider-cert-file=${providerName}.crt"
 | |
|     "--crypt-secretkey-file=${providerName}.key"
 | |
|   ];
 | |
| 
 | |
|   genKeys = ''
 | |
|     # generates time-limited keypairs
 | |
|     keyGen() {
 | |
|       dnscrypt-wrapper --gen-crypt-keypair \
 | |
|         --crypt-secretkey-file=${cfg.providerName}.key
 | |
| 
 | |
|       dnscrypt-wrapper --gen-cert-file \
 | |
|         --crypt-secretkey-file=${cfg.providerName}.key \
 | |
|         --provider-cert-file=${cfg.providerName}.crt \
 | |
|         --provider-publickey-file=public.key \
 | |
|         --provider-secretkey-file=secret.key \
 | |
|         --cert-file-expire-days=${toString cfg.keys.expiration}
 | |
|     }
 | |
| 
 | |
|     cd ${dataDir}
 | |
| 
 | |
|     # generate provider keypair (first run only)
 | |
|     if [ ! -f public.key ] || [ ! -f secret.key ]; then
 | |
|       dnscrypt-wrapper --gen-provider-keypair
 | |
|     fi
 | |
| 
 | |
|     # generate new keys for rotation
 | |
|     if [ ! -f ${cfg.providerName}.key ] || [ ! -f ${cfg.providerName}.crt ]; then
 | |
|       keyGen
 | |
|     fi
 | |
|   '';
 | |
| 
 | |
|   rotateKeys = ''
 | |
|     # check if keys are not expired
 | |
|     keyValid() {
 | |
|       fingerprint=$(dnscrypt-wrapper --show-provider-publickey | awk '{print $(NF)}')
 | |
|       dnscrypt-proxy --test=${toString (cfg.keys.checkInterval + 1)} \
 | |
|         --resolver-address=127.0.0.1:${toString cfg.port} \
 | |
|         --provider-name=${cfg.providerName} \
 | |
|         --provider-key=$fingerprint
 | |
|     }
 | |
| 
 | |
|     cd ${dataDir}
 | |
| 
 | |
|     # archive old keys and restart the service
 | |
|     if ! keyValid; then
 | |
|       echo "certificate soon to become invalid; backing up old cert"
 | |
|       mkdir -p oldkeys
 | |
|       mv -v ${cfg.providerName}.key oldkeys/${cfg.providerName}-$(date +%F-%T).key
 | |
|       mv -v ${cfg.providerName}.crt oldkeys/${cfg.providerName}-$(date +%F-%T).crt
 | |
|       systemctl restart dnscrypt-wrapper
 | |
|     fi
 | |
|   '';
 | |
| 
 | |
| in {
 | |
| 
 | |
| 
 | |
|   ###### interface
 | |
| 
 | |
|   options.services.dnscrypt-wrapper = {
 | |
|     enable = mkEnableOption "DNSCrypt wrapper";
 | |
| 
 | |
|     address = mkOption {
 | |
|       type = types.str;
 | |
|       default = "127.0.0.1";
 | |
|       description = ''
 | |
|         The DNSCrypt wrapper will bind to this IP address.
 | |
|       '';
 | |
|     };
 | |
| 
 | |
|     port = mkOption {
 | |
|       type = types.int;
 | |
|       default = 5353;
 | |
|       description = ''
 | |
|         The DNSCrypt wrapper will listen for DNS queries on this port.
 | |
|       '';
 | |
|     };
 | |
| 
 | |
|     providerName = mkOption {
 | |
|       type = types.str;
 | |
|       default = "2.dnscrypt-cert.${config.networking.hostName}";
 | |
|       example = "2.dnscrypt-cert.myresolver";
 | |
|       description = ''
 | |
|         The name that will be given to this DNSCrypt resolver.
 | |
|         Note: the resolver name must start with <literal>2.dnscrypt-cert.</literal>.
 | |
|       '';
 | |
|     };
 | |
| 
 | |
|     upstream.address = mkOption {
 | |
|       type = types.str;
 | |
|       default = "127.0.0.1";
 | |
|       description = ''
 | |
|         The IP address of the upstream DNS server DNSCrypt will "wrap".
 | |
|       '';
 | |
|     };
 | |
| 
 | |
|     upstream.port = mkOption {
 | |
|       type = types.int;
 | |
|       default = 53;
 | |
|       description = ''
 | |
|         The port of the upstream DNS server DNSCrypt will "wrap".
 | |
|       '';
 | |
|     };
 | |
| 
 | |
|     keys.expiration = mkOption {
 | |
|       type = types.int;
 | |
|       default = 30;
 | |
|       description = ''
 | |
|         The duration (in days) of the time-limited secret key.
 | |
|         This will be automatically rotated before expiration.
 | |
|       '';
 | |
|     };
 | |
| 
 | |
|     keys.checkInterval = mkOption {
 | |
|       type = types.int;
 | |
|       default = 1440;
 | |
|       description = ''
 | |
|         The time interval (in minutes) between key expiration checks.
 | |
|       '';
 | |
|     };
 | |
| 
 | |
|   };
 | |
| 
 | |
| 
 | |
|   ###### implementation
 | |
| 
 | |
|   config = mkIf cfg.enable {
 | |
| 
 | |
|     users.users.dnscrypt-wrapper = {
 | |
|       description = "dnscrypt-wrapper daemon user";
 | |
|       home = "${dataDir}";
 | |
|       createHome = true;
 | |
|     };
 | |
|     users.groups.dnscrypt-wrapper = { };
 | |
| 
 | |
|     security.polkit.extraConfig = ''
 | |
|       // Allow dnscrypt-wrapper user to restart dnscrypt-wrapper.service
 | |
|       polkit.addRule(function(action, subject) {
 | |
|           if (action.id == "org.freedesktop.systemd1.manage-units" &&
 | |
|               action.lookup("unit") == "dnscrypt-wrapper.service" &&
 | |
|               subject.user == "dnscrypt-wrapper") {
 | |
|               return polkit.Result.YES;
 | |
|           }
 | |
|         });
 | |
|     '';
 | |
| 
 | |
|     systemd.services.dnscrypt-wrapper = {
 | |
|       description = "dnscrypt-wrapper daemon";
 | |
|       after    = [ "network.target" ];
 | |
|       wantedBy = [ "multi-user.target" ];
 | |
|       path     = [ pkgs.dnscrypt-wrapper ];
 | |
| 
 | |
|       serviceConfig = {
 | |
|         User = "dnscrypt-wrapper";
 | |
|         WorkingDirectory = dataDir;
 | |
|         Restart   = "on-failure";
 | |
|         ExecStart = "${pkgs.dnscrypt-wrapper}/bin/dnscrypt-wrapper ${toString daemonArgs}";
 | |
|       };
 | |
| 
 | |
|       preStart = genKeys;
 | |
|     };
 | |
| 
 | |
| 
 | |
|     systemd.services.dnscrypt-wrapper-rotate = {
 | |
|       after    = [ "network.target" ];
 | |
|       requires = [ "dnscrypt-wrapper.service" ];
 | |
|       description = "Rotates DNSCrypt wrapper keys if soon to expire";
 | |
| 
 | |
|       path   = with pkgs; [ dnscrypt-wrapper dnscrypt-proxy gawk ];
 | |
|       script = rotateKeys;
 | |
|       serviceConfig.User = "dnscrypt-wrapper";
 | |
|     };
 | |
| 
 | |
| 
 | |
|     systemd.timers.dnscrypt-wrapper-rotate = {
 | |
|       description = "Periodically check DNSCrypt wrapper keys for expiration";
 | |
|       wantedBy = [ "multi-user.target" ];
 | |
| 
 | |
|       timerConfig = {
 | |
|         Unit = "dnscrypt-wrapper-rotate.service";
 | |
|         OnBootSec = "1min";
 | |
|         OnUnitActiveSec = cfg.keys.checkInterval * 60;
 | |
|       };
 | |
|     };
 | |
| 
 | |
|   };
 | |
| }
 | 
