1
2{ lib, pkgs, ... }:
3
4let
5 snakeoil = import ../common/acme/server/snakeoil-certs.nix;
6
7 hosts = lib.mkForce
8 { "fd::a" = [ "server" snakeoil.domain ];
9 "fd::b" = [ "client" ];
10 };
11in
12
13{
14 name = "dnscrypt-wrapper";
15 meta = with pkgs.lib.maintainers; {
16 maintainers = [ rnhmjoj ];
17 };
18
19 nodes = {
20 server = {
21 networking.hosts = hosts;
22 networking.interfaces.eth1.ipv6.addresses = lib.singleton
23 { address = "fd::a"; prefixLength = 64; };
24
25 services.dnscrypt-wrapper =
26 { enable = true;
27 address = "[::]";
28 port = 5353;
29 keys.expiration = 5; # days
30 keys.checkInterval = 2; # min
31 # The keypair was generated by the command:
32 # dnscrypt-wrapper --gen-provider-keypair \
33 # --provider-name=2.dnscrypt-cert.server \
34 providerKey.public = "${./public.key}";
35 providerKey.secret = "${./secret.key}";
36 };
37
38 # nameserver
39 services.bind.enable = true;
40 services.bind.zones = lib.singleton
41 { name = ".";
42 master = true;
43 file = pkgs.writeText "root.zone" ''
44 $TTL 3600
45 . IN SOA example.org. admin.example.org. ( 1 3h 1h 1w 1d )
46 . IN NS example.org.
47 example.org. IN AAAA 2001:db8::1
48 '';
49 };
50
51 # webserver
52 services.nginx.enable = true;
53 services.nginx.virtualHosts.${snakeoil.domain} =
54 { onlySSL = true;
55 listenAddresses = [ "localhost" ];
56 sslCertificate = snakeoil.${snakeoil.domain}.cert;
57 sslCertificateKey = snakeoil.${snakeoil.domain}.key;
58 locations."/ip".extraConfig = ''
59 default_type text/plain;
60 return 200 "Ciao $remote_addr!\n";
61 '';
62 };
63
64 # demultiplex HTTP and DNS from port 443
65 services.sslh =
66 { enable = true;
67 method = "ev";
68 settings.transparent = true;
69 settings.listen = lib.mkForce
70 [ { host = "server"; port = "443"; is_udp = false; }
71 { host = "server"; port = "443"; is_udp = true; }
72 ];
73 settings.protocols =
74 [ # Send TLS to webserver (TCP)
75 { name = "tls"; host= "localhost"; port= "443"; }
76 # Send DNSCrypt to dnscrypt-wrapper (TCP or UDP)
77 { name = "anyprot"; host = "localhost"; port = "5353"; }
78 { name = "anyprot"; host = "localhost"; port = "5353"; is_udp = true;}
79 ];
80 };
81
82 networking.firewall.allowedTCPPorts = [ 443 ];
83 networking.firewall.allowedUDPPorts = [ 443 ];
84 };
85
86 client = {
87 networking.hosts = hosts;
88 networking.interfaces.eth1.ipv6.addresses = lib.singleton
89 { address = "fd::b"; prefixLength = 64; };
90
91 services.dnscrypt-proxy2.enable = true;
92 services.dnscrypt-proxy2.upstreamDefaults = false;
93 services.dnscrypt-proxy2.settings =
94 { server_names = [ "server" ];
95 listen_addresses = [ "[::1]:53" ];
96 cache = false;
97 # Computed using https://dnscrypt.info/stamps/
98 static.server.stamp =
99 "sdns://AQAAAAAAAAAADzE5Mi4xNjguMS4yOjQ0MyAUQdg6"
100 +"_RIIpK6pHkINhrv7nxwIG5c7b_m5NJVT3A1AXRYyLmRuc2NyeXB0LWNlcnQuc2VydmVy";
101 };
102 networking.nameservers = [ "::1" ];
103 security.pki.certificateFiles = [ snakeoil.ca.cert ];
104 };
105
106 };
107
108 testScript = ''
109 with subtest("The server can generate the ephemeral keypair"):
110 server.wait_for_unit("dnscrypt-wrapper")
111 server.wait_for_file("/var/lib/dnscrypt-wrapper/2.dnscrypt-cert.server.key")
112 server.wait_for_file("/var/lib/dnscrypt-wrapper/2.dnscrypt-cert.server.crt")
113 almost_expiration = server.succeed("date --date '4days 23 hours 56min'").strip()
114
115 with subtest("The DNSCrypt client can connect to the server"):
116 server.wait_for_unit("sslh")
117 client.wait_until_succeeds("journalctl -u dnscrypt-proxy2 --grep '\[server\] OK'")
118
119 with subtest("HTTP client can connect to the server"):
120 server.wait_for_unit("nginx")
121 client.succeed("curl -s --fail https://${snakeoil.domain}/ip | grep -q fd::b")
122
123 with subtest("DNS queries over UDP are working"):
124 server.wait_for_unit("bind")
125 client.wait_for_open_port(53)
126 assert "2001:db8::1" in client.wait_until_succeeds(
127 "host -U example.org"
128 ), "The IP address of 'example.org' does not match 2001:db8::1"
129
130 with subtest("DNS queries over TCP are working"):
131 server.wait_for_unit("bind")
132 client.wait_for_open_port(53)
133 assert "2001:db8::1" in client.wait_until_succeeds(
134 "host -T example.org"
135 ), "The IP address of 'example.org' does not match 2001:db8::1"
136
137 with subtest("The server rotates the ephemeral keys"):
138 # advance time by a little less than 5 days
139 server.succeed(f"date -s '{almost_expiration}'")
140 client.succeed(f"date -s '{almost_expiration}'")
141 server.wait_for_file("/var/lib/dnscrypt-wrapper/oldkeys")
142
143 with subtest("The client can still connect to the server"):
144 client.systemctl("restart dnscrypt-proxy2")
145 client.wait_until_succeeds("host -T example.org")
146 client.wait_until_succeeds("host -U example.org")
147 '';
148}