diff --git a/nixos/doc/manual/release-notes/rl-1803.xml b/nixos/doc/manual/release-notes/rl-1803.xml
index d2d4a0d32bb..12ff2e39a1b 100644
--- a/nixos/doc/manual/release-notes/rl-1803.xml
+++ b/nixos/doc/manual/release-notes/rl-1803.xml
@@ -139,12 +139,6 @@ following incompatible changes:
will be accessible at /run/memcached/memcached.sock.
-
-
- The DNSCrypt proxy module has been removed, the upstream project
- is no longer maintained.
-
-
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 8846bf9e8b1..8d329b5b4b2 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -446,6 +446,7 @@
./services/networking/dhcpd.nix
./services/networking/dnscache.nix
./services/networking/dnschain.nix
+ ./services/networking/dnscrypt-proxy.nix
./services/networking/dnscrypt-wrapper.nix
./services/networking/dnsmasq.nix
./services/networking/ejabberd.nix
diff --git a/nixos/modules/rename.nix b/nixos/modules/rename.nix
index cd6ae35c0d9..562be13a3f6 100644
--- a/nixos/modules/rename.nix
+++ b/nixos/modules/rename.nix
@@ -89,9 +89,6 @@ with lib;
# Tarsnap
(mkRenamedOptionModule [ "services" "tarsnap" "config" ] [ "services" "tarsnap" "archives" ])
- # dnscrypt-proxy
- (mkRemovedOptionModule [ "services" "dnscrypt-proxy" "enable" ] "")
-
# ibus
(mkRenamedOptionModule [ "programs" "ibus" "plugins" ] [ "i18n" "inputMethod" "ibus" "engines" ])
diff --git a/nixos/modules/services/networking/dnscrypt-proxy.nix b/nixos/modules/services/networking/dnscrypt-proxy.nix
new file mode 100644
index 00000000000..ed658258c7f
--- /dev/null
+++ b/nixos/modules/services/networking/dnscrypt-proxy.nix
@@ -0,0 +1,321 @@
+{ config, lib, pkgs, ... }:
+with lib;
+
+let
+ cfg = config.services.dnscrypt-proxy;
+
+ stateDirectory = "/var/lib/dnscrypt-proxy";
+
+ # The minisign public key used to sign the upstream resolver list.
+ # This is somewhat more flexible than preloading the key as an
+ # embedded string.
+ upstreamResolverListPubKey = pkgs.fetchurl {
+ url = https://raw.githubusercontent.com/jedisct1/dnscrypt-proxy/master/minisign.pub;
+ sha256 = "18lnp8qr6ghfc2sd46nn1rhcpr324fqlvgsp4zaigw396cd7vnnh";
+ };
+
+ # Internal flag indicating whether the upstream resolver list is used.
+ useUpstreamResolverList = cfg.customResolver == null;
+
+ # The final local address.
+ localAddress = "${cfg.localAddress}:${toString cfg.localPort}";
+
+ # The final resolvers list path.
+ resolverList = "${stateDirectory}/dnscrypt-resolvers.csv";
+
+ # Build daemon command line
+
+ resolverArgs =
+ if (cfg.customResolver == null)
+ then
+ [ "-L ${resolverList}"
+ "-R ${cfg.resolverName}"
+ ]
+ else with cfg.customResolver;
+ [ "-N ${name}"
+ "-k ${key}"
+ "-r ${address}:${toString port}"
+ ];
+
+ daemonArgs =
+ [ "-a ${localAddress}" ]
+ ++ resolverArgs
+ ++ cfg.extraArgs;
+in
+
+{
+ meta = {
+ maintainers = with maintainers; [ joachifm ];
+ doc = ./dnscrypt-proxy.xml;
+ };
+
+ options = {
+ # Before adding another option, consider whether it could
+ # equally well be passed via extraArgs.
+
+ services.dnscrypt-proxy = {
+ enable = mkOption {
+ default = false;
+ type = types.bool;
+ description = "Whether to enable the DNSCrypt client proxy";
+ };
+
+ localAddress = mkOption {
+ default = "127.0.0.1";
+ type = types.str;
+ description = ''
+ Listen for DNS queries to relay on this address. The only reason to
+ change this from its default value is to proxy queries on behalf
+ of other machines (typically on the local network).
+ '';
+ };
+
+ localPort = mkOption {
+ default = 53;
+ type = types.int;
+ description = ''
+ Listen for DNS queries to relay on this port. The default value
+ assumes that the DNSCrypt proxy should relay DNS queries directly.
+ When running as a forwarder for another DNS client, set this option
+ to a different value; otherwise leave the default.
+ '';
+ };
+
+ resolverName = mkOption {
+ default = "random";
+ example = "dnscrypt.eu-nl";
+ type = types.nullOr types.str;
+ description = ''
+ The name of the DNSCrypt resolver to use, taken from
+ ${resolverList}. The default is to
+ pick a random non-logging resolver that supports DNSSEC.
+ '';
+ };
+
+ customResolver = mkOption {
+ default = null;
+ description = ''
+ Use an unlisted resolver (e.g., a private DNSCrypt provider). For
+ advanced users only. If specified, this option takes precedence.
+ '';
+ type = types.nullOr (types.submodule ({ ... }: { options = {
+ address = mkOption {
+ type = types.str;
+ description = "IP address";
+ example = "208.67.220.220";
+ };
+
+ port = mkOption {
+ type = types.int;
+ description = "Port";
+ default = 443;
+ };
+
+ name = mkOption {
+ type = types.str;
+ description = "Fully qualified domain name";
+ example = "2.dnscrypt-cert.example.com";
+ };
+
+ key = mkOption {
+ type = types.str;
+ description = "Public key";
+ example = "B735:1140:206F:225D:3E2B:D822:D7FD:691E:A1C3:3CC8:D666:8D0C:BE04:BFAB:CA43:FB79";
+ };
+ }; }));
+ };
+
+ extraArgs = mkOption {
+ default = [];
+ type = types.listOf types.str;
+ description = ''
+ Additional command-line arguments passed verbatim to the daemon.
+ See dnscrypt-proxy
+ 8 for details.
+ '';
+ example = [ "-X libdcplugin_example_cache.so,--min-ttl=60" ];
+ };
+ };
+ };
+
+ config = mkIf cfg.enable (mkMerge [{
+ assertions = [
+ { assertion = (cfg.customResolver != null) || (cfg.resolverName != null);
+ message = "please configure upstream DNSCrypt resolver";
+ }
+ ];
+
+ users.users.dnscrypt-proxy = {
+ description = "dnscrypt-proxy daemon user";
+ isSystemUser = true;
+ group = "dnscrypt-proxy";
+ };
+ users.groups.dnscrypt-proxy = {};
+
+ systemd.sockets.dnscrypt-proxy = {
+ description = "dnscrypt-proxy listening socket";
+ documentation = [ "man:dnscrypt-proxy(8)" ];
+
+ wantedBy = [ "sockets.target" ];
+
+ socketConfig = {
+ ListenStream = localAddress;
+ ListenDatagram = localAddress;
+ };
+ };
+
+ systemd.services.dnscrypt-proxy = {
+ description = "dnscrypt-proxy daemon";
+ documentation = [ "man:dnscrypt-proxy(8)" ];
+
+ before = [ "nss-lookup.target" ];
+ after = [ "network.target" ];
+ requires = [ "dnscrypt-proxy.socket "];
+
+ serviceConfig = {
+ NonBlocking = "true";
+ ExecStart = "${pkgs.dnscrypt-proxy}/bin/dnscrypt-proxy ${toString daemonArgs}";
+ ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+
+ User = "dnscrypt-proxy";
+
+ PrivateTmp = true;
+ PrivateDevices = true;
+ ProtectHome = true;
+ };
+ };
+ }
+
+ (mkIf config.security.apparmor.enable {
+ systemd.services.dnscrypt-proxy.after = [ "apparmor.service" ];
+
+ security.apparmor.profiles = singleton (pkgs.writeText "apparmor-dnscrypt-proxy" ''
+ ${pkgs.dnscrypt-proxy}/bin/dnscrypt-proxy {
+ /dev/null rw,
+ /dev/urandom r,
+
+ /etc/passwd r,
+ /etc/group r,
+ ${config.environment.etc."nsswitch.conf".source} r,
+
+ ${getLib pkgs.glibc}/lib/*.so mr,
+ ${pkgs.tzdata}/share/zoneinfo/** r,
+
+ network inet stream,
+ network inet6 stream,
+ network inet dgram,
+ network inet6 dgram,
+
+ ${getLib pkgs.dnscrypt-proxy}/lib/dnscrypt-proxy/libdcplugin*.so mr,
+
+ ${getLib pkgs.gcc.cc}/lib/libssp.so.* mr,
+ ${getLib pkgs.libsodium}/lib/libsodium.so.* mr,
+ ${getLib pkgs.systemd}/lib/libsystemd.so.* mr,
+ ${getLib pkgs.xz}/lib/liblzma.so.* mr,
+ ${getLib pkgs.libgcrypt}/lib/libgcrypt.so.* mr,
+ ${getLib pkgs.libgpgerror}/lib/libgpg-error.so.* mr,
+ ${getLib pkgs.libcap}/lib/libcap.so.* mr,
+ ${getLib pkgs.lz4}/lib/liblz4.so.* mr,
+ ${getLib pkgs.attr}/lib/libattr.so.* mr, # */
+
+ ${resolverList} r,
+
+ /run/systemd/notify rw,
+ }
+ '');
+ })
+
+ (mkIf useUpstreamResolverList {
+ systemd.services.init-dnscrypt-proxy-statedir = {
+ description = "Initialize dnscrypt-proxy state directory";
+
+ wantedBy = [ "dnscrypt-proxy.service" ];
+ before = [ "dnscrypt-proxy.service" ];
+
+ script = ''
+ mkdir -pv ${stateDirectory}
+ chown -c dnscrypt-proxy:dnscrypt-proxy ${stateDirectory}
+ cp -uv \
+ ${pkgs.dnscrypt-proxy}/share/dnscrypt-proxy/dnscrypt-resolvers.csv \
+ ${stateDirectory}
+ '';
+
+ serviceConfig = {
+ Type = "oneshot";
+ RemainAfterExit = true;
+ };
+ };
+
+ systemd.services.update-dnscrypt-resolvers = {
+ description = "Update list of DNSCrypt resolvers";
+
+ requires = [ "init-dnscrypt-proxy-statedir.service" ];
+ after = [ "init-dnscrypt-proxy-statedir.service" ];
+
+ path = with pkgs; [ curl diffutils dnscrypt-proxy minisign ];
+ script = ''
+ cd ${stateDirectory}
+ domain=raw.githubusercontent.com
+ get="curl -fSs --resolve $domain:443:$(hostip -r 8.8.8.8 $domain | head -1)"
+ $get -o dnscrypt-resolvers.csv.tmp \
+ https://$domain/jedisct1/dnscrypt-proxy/master/dnscrypt-resolvers.csv
+ $get -o dnscrypt-resolvers.csv.minisig.tmp \
+ https://$domain/jedisct1/dnscrypt-proxy/master/dnscrypt-resolvers.csv.minisig
+ mv dnscrypt-resolvers.csv.minisig{.tmp,}
+ if ! minisign -q -V -p ${upstreamResolverListPubKey} \
+ -m dnscrypt-resolvers.csv.tmp -x dnscrypt-resolvers.csv.minisig ; then
+ echo "failed to verify resolver list!" >&2
+ exit 1
+ fi
+ [[ -f dnscrypt-resolvers.csv ]] && mv dnscrypt-resolvers.csv{,.old}
+ mv dnscrypt-resolvers.csv{.tmp,}
+ if cmp dnscrypt-resolvers.csv{,.old} ; then
+ echo "no change"
+ else
+ echo "resolver list updated"
+ fi
+ '';
+
+ serviceConfig = {
+ PrivateTmp = true;
+ PrivateDevices = true;
+ ProtectHome = true;
+ ProtectSystem = "strict";
+ ReadWritePaths = "${dirOf stateDirectory} ${stateDirectory}";
+ SystemCallFilter = "~@mount";
+ };
+ };
+
+ systemd.timers.update-dnscrypt-resolvers = {
+ wantedBy = [ "timers.target" ];
+ timerConfig = {
+ OnBootSec = "5min";
+ OnUnitActiveSec = "6h";
+ };
+ };
+ })
+ ]);
+
+ imports = [
+ (mkRenamedOptionModule [ "services" "dnscrypt-proxy" "port" ] [ "services" "dnscrypt-proxy" "localPort" ])
+
+ (mkChangedOptionModule
+ [ "services" "dnscrypt-proxy" "tcpOnly" ]
+ [ "services" "dnscrypt-proxy" "extraArgs" ]
+ (config:
+ let val = getAttrFromPath [ "services" "dnscrypt-proxy" "tcpOnly" ] config; in
+ optional val "-T"))
+
+ (mkChangedOptionModule
+ [ "services" "dnscrypt-proxy" "ephemeralKeys" ]
+ [ "services" "dnscrypt-proxy" "extraArgs" ]
+ (config:
+ let val = getAttrFromPath [ "services" "dnscrypt-proxy" "ephemeralKeys" ] config; in
+ optional val "-E"))
+
+ (mkRemovedOptionModule [ "services" "dnscrypt-proxy" "resolverList" ] ''
+ The current resolver listing from upstream is always used
+ unless a custom resolver is specified.
+ '')
+ ];
+}
diff --git a/nixos/modules/services/networking/dnscrypt-proxy.xml b/nixos/modules/services/networking/dnscrypt-proxy.xml
new file mode 100644
index 00000000000..555c6df4d55
--- /dev/null
+++ b/nixos/modules/services/networking/dnscrypt-proxy.xml
@@ -0,0 +1,69 @@
+
+
+ DNSCrypt client proxy
+
+
+ The DNSCrypt client proxy relays DNS queries to a DNSCrypt enabled
+ upstream resolver. The traffic between the client and the upstream
+ resolver is encrypted and authenticated, mitigating the risk of MITM
+ attacks, DNS poisoning attacks, and third-party snooping (assuming the
+ upstream is trustworthy).
+
+
+ Basic configuration
+
+
+ To enable the client proxy, set
+
+ services.dnscrypt-proxy.enable = true;
+
+
+
+
+ Enabling the client proxy does not alter the system nameserver; to
+ relay local queries, prepend 127.0.0.1 to
+ .
+
+
+
+
+ As a forwarder for another DNS client
+
+
+ To run the DNSCrypt proxy client as a forwarder for another
+ DNS client, change the default proxy listening port to a
+ non-standard value and point the other client to it:
+
+ services.dnscrypt-proxy.localPort = 43;
+
+
+
+ dnsmasq
+
+
+ {
+ services.dnsmasq.enable = true;
+ services.dnsmasq.servers = [ "127.0.0.1#43" ];
+ }
+
+
+
+
+ unbound
+
+
+ {
+ services.unbound.enable = true;
+ services.unbound.forwardAddresses = [ "127.0.0.1@43" ];
+ }
+
+
+
+
+
+
+
diff --git a/nixos/release.nix b/nixos/release.nix
index 5ae32928240..cf3fe6abd48 100644
--- a/nixos/release.nix
+++ b/nixos/release.nix
@@ -255,6 +255,7 @@ in rec {
tests.docker = hydraJob (import tests/docker.nix { system = "x86_64-linux"; });
tests.docker-edge = hydraJob (import tests/docker-edge.nix { system = "x86_64-linux"; });
tests.dovecot = callTest tests/dovecot.nix {};
+ tests.dnscrypt-proxy = callTest tests/dnscrypt-proxy.nix { system = "x86_64-linux"; };
tests.ecryptfs = callTest tests/ecryptfs.nix {};
tests.etcd = hydraJob (import tests/etcd.nix { system = "x86_64-linux"; });
tests.ec2-nixops = hydraJob (import tests/ec2.nix { system = "x86_64-linux"; }).boot-ec2-nixops;
diff --git a/nixos/tests/dnscrypt-proxy.nix b/nixos/tests/dnscrypt-proxy.nix
new file mode 100644
index 00000000000..84562336825
--- /dev/null
+++ b/nixos/tests/dnscrypt-proxy.nix
@@ -0,0 +1,32 @@
+import ./make-test.nix ({ pkgs, ... }: {
+ name = "dnscrypt-proxy";
+ meta = with pkgs.stdenv.lib.maintainers; {
+ maintainers = [ joachifm ];
+ };
+
+ nodes = {
+ # A client running the recommended setup: DNSCrypt proxy as a forwarder
+ # for a caching DNS client.
+ client =
+ { config, pkgs, ... }:
+ let localProxyPort = 43; in
+ {
+ security.apparmor.enable = true;
+
+ services.dnscrypt-proxy.enable = true;
+ services.dnscrypt-proxy.localPort = localProxyPort;
+ services.dnscrypt-proxy.extraArgs = [ "-X libdcplugin_example.so" ];
+
+ services.dnsmasq.enable = true;
+ services.dnsmasq.servers = [ "127.0.0.1#${toString localProxyPort}" ];
+ };
+ };
+
+ testScript = ''
+ $client->waitForUnit("dnsmasq");
+
+ # The daemon is socket activated; sending a single ping should activate it.
+ $client->execute("${pkgs.iputils}/bin/ping -c1 example.com");
+ $client->succeed("systemctl is-active dnscrypt-proxy");
+ '';
+})