{ lib, pkgs, ... }: let snakeoil = import ../common/acme/server/snakeoil-certs.nix; hosts = lib.mkForce { "fd::a" = [ "server" snakeoil.domain ]; "fd::b" = [ "client" ]; }; in { name = "dnscrypt-wrapper"; meta = with pkgs.lib.maintainers; { maintainers = [ rnhmjoj ]; }; nodes = { server = { networking.hosts = hosts; networking.interfaces.eth1.ipv6.addresses = lib.singleton { address = "fd::a"; prefixLength = 64; }; services.dnscrypt-wrapper = { enable = true; address = "[::]"; port = 5353; keys.expiration = 5; # days keys.checkInterval = 2; # min # The keypair was generated by the command: # dnscrypt-wrapper --gen-provider-keypair \ # --provider-name=2.dnscrypt-cert.server \ providerKey.public = "${./public.key}"; providerKey.secret = "${./secret.key}"; }; # nameserver services.bind.enable = true; services.bind.zones = lib.singleton { name = "."; master = true; file = pkgs.writeText "root.zone" '' $TTL 3600 . IN SOA example.org. admin.example.org. ( 1 3h 1h 1w 1d ) . IN NS example.org. example.org. IN AAAA 2001:db8::1 ''; }; # webserver services.nginx.enable = true; services.nginx.virtualHosts.${snakeoil.domain} = { onlySSL = true; listenAddresses = [ "localhost" ]; sslCertificate = snakeoil.${snakeoil.domain}.cert; sslCertificateKey = snakeoil.${snakeoil.domain}.key; locations."/ip".extraConfig = '' default_type text/plain; return 200 "Ciao $remote_addr!\n"; ''; }; # demultiplex HTTP and DNS from port 443 services.sslh = { enable = true; method = "ev"; settings.transparent = true; settings.listen = lib.mkForce [ { host = "server"; port = "443"; is_udp = false; } { host = "server"; port = "443"; is_udp = true; } ]; settings.protocols = [ # Send TLS to webserver (TCP) { name = "tls"; host= "localhost"; port= "443"; } # Send DNSCrypt to dnscrypt-wrapper (TCP or UDP) { name = "anyprot"; host = "localhost"; port = "5353"; } { name = "anyprot"; host = "localhost"; port = "5353"; is_udp = true;} ]; }; networking.firewall.allowedTCPPorts = [ 443 ]; networking.firewall.allowedUDPPorts = [ 443 ]; }; client = { networking.hosts = hosts; networking.interfaces.eth1.ipv6.addresses = lib.singleton { address = "fd::b"; prefixLength = 64; }; services.dnscrypt-proxy2.enable = true; services.dnscrypt-proxy2.upstreamDefaults = false; services.dnscrypt-proxy2.settings = { server_names = [ "server" ]; listen_addresses = [ "[::1]:53" ]; cache = false; # Computed using https://dnscrypt.info/stamps/ static.server.stamp = "sdns://AQAAAAAAAAAADzE5Mi4xNjguMS4yOjQ0MyAUQdg6" +"_RIIpK6pHkINhrv7nxwIG5c7b_m5NJVT3A1AXRYyLmRuc2NyeXB0LWNlcnQuc2VydmVy"; }; networking.nameservers = [ "::1" ]; security.pki.certificateFiles = [ snakeoil.ca.cert ]; }; }; testScript = '' with subtest("The server can generate the ephemeral keypair"): server.wait_for_unit("dnscrypt-wrapper") server.wait_for_file("/var/lib/dnscrypt-wrapper/2.dnscrypt-cert.server.key") server.wait_for_file("/var/lib/dnscrypt-wrapper/2.dnscrypt-cert.server.crt") almost_expiration = server.succeed("date --date '4days 23 hours 56min'").strip() with subtest("The DNSCrypt client can connect to the server"): server.wait_for_unit("sslh") client.wait_until_succeeds("journalctl -u dnscrypt-proxy2 --grep '\[server\] OK'") with subtest("HTTP client can connect to the server"): server.wait_for_unit("nginx") client.succeed("curl -s --fail https://${snakeoil.domain}/ip | grep -q fd::b") with subtest("DNS queries over UDP are working"): server.wait_for_unit("bind") client.wait_for_open_port(53) assert "2001:db8::1" in client.wait_until_succeeds( "host -U example.org" ), "The IP address of 'example.org' does not match 2001:db8::1" with subtest("DNS queries over TCP are working"): server.wait_for_unit("bind") client.wait_for_open_port(53) assert "2001:db8::1" in client.wait_until_succeeds( "host -T example.org" ), "The IP address of 'example.org' does not match 2001:db8::1" with subtest("The server rotates the ephemeral keys"): # advance time by a little less than 5 days server.succeed(f"date -s '{almost_expiration}'") client.succeed(f"date -s '{almost_expiration}'") server.wait_for_file("/var/lib/dnscrypt-wrapper/oldkeys") with subtest("The client can still connect to the server"): client.systemctl("restart dnscrypt-proxy2") client.wait_until_succeeds("host -T example.org") client.wait_until_succeeds("host -U example.org") ''; }