1import ./make-test-python.nix (
2 { pkgs, ... }:
3 let
4 deviceName = "rp0";
5
6 server = {
7 ip = "fe80::1";
8 wg = {
9 public = "mQufmDFeQQuU/fIaB2hHgluhjjm1ypK4hJr1cW3WqAw=";
10 secret = "4N5Y1dldqrpsbaEiY8O0XBUGUFf8vkvtBtm8AoOX7Eo=";
11 listen = 10000;
12 };
13 };
14 client = {
15 ip = "fe80::2";
16 wg = {
17 public = "Mb3GOlT7oS+F3JntVKiaD7SpHxLxNdtEmWz/9FMnRFU=";
18 secret = "uC5dfGMv7Oxf5UDfdPkj6rZiRZT2dRWp5x8IQxrNcUE=";
19 };
20 };
21 in
22 {
23 name = "rosenpass";
24
25 nodes =
26 let
27 shared =
28 peer:
29 { config, modulesPath, ... }:
30 {
31 imports = [ "${modulesPath}/services/networking/rosenpass.nix" ];
32
33 boot.kernelModules = [ "wireguard" ];
34
35 services.rosenpass = {
36 enable = true;
37 defaultDevice = deviceName;
38 settings = {
39 verbosity = "Verbose";
40 public_key = "/etc/rosenpass/pqpk";
41 secret_key = "/etc/rosenpass/pqsk";
42 };
43 };
44
45 networking.firewall.allowedUDPPorts = [ 9999 ];
46
47 systemd.network = {
48 enable = true;
49 networks."rosenpass" = {
50 matchConfig.Name = deviceName;
51 networkConfig.IPv4Forwarding = true;
52 networkConfig.IPv6Forwarding = true;
53 address = [ "${peer.ip}/64" ];
54 };
55
56 netdevs."10-rp0" = {
57 netdevConfig = {
58 Kind = "wireguard";
59 Name = deviceName;
60 };
61 wireguardConfig.PrivateKeyFile = "/etc/wireguard/wgsk";
62 };
63 };
64
65 environment.etc."wireguard/wgsk" = {
66 text = peer.wg.secret;
67 user = "systemd-network";
68 group = "systemd-network";
69 };
70 };
71 in
72 {
73 server = {
74 imports = [ (shared server) ];
75
76 networking.firewall.allowedUDPPorts = [ server.wg.listen ];
77
78 systemd.network.netdevs."10-${deviceName}" = {
79 wireguardConfig.ListenPort = server.wg.listen;
80 wireguardPeers = [
81 {
82 AllowedIPs = [ "::/0" ];
83 PublicKey = client.wg.public;
84 }
85 ];
86 };
87
88 services.rosenpass.settings = {
89 listen = [ "0.0.0.0:9999" ];
90 peers = [
91 {
92 public_key = "/etc/rosenpass/peers/client/pqpk";
93 peer = client.wg.public;
94 }
95 ];
96 };
97 };
98 client = {
99 imports = [ (shared client) ];
100
101 systemd.network.netdevs."10-${deviceName}".wireguardPeers = [
102 {
103 AllowedIPs = [ "::/0" ];
104 PublicKey = server.wg.public;
105 Endpoint = "server:${builtins.toString server.wg.listen}";
106 }
107 ];
108
109 services.rosenpass.settings.peers = [
110 {
111 public_key = "/etc/rosenpass/peers/server/pqpk";
112 endpoint = "server:9999";
113 peer = server.wg.public;
114 }
115 ];
116 };
117 };
118
119 testScript =
120 { ... }:
121 ''
122 from os import system
123
124 # Full path to rosenpass in the store, to avoid fiddling with `$PATH`.
125 rosenpass = "${pkgs.rosenpass}/bin/rosenpass"
126
127 # Path in `/etc` where keys will be placed.
128 etc = "/etc/rosenpass"
129
130 start_all()
131
132 for machine in [server, client]:
133 machine.wait_for_unit("multi-user.target")
134
135 # Gently stop Rosenpass to avoid crashes during key generation/distribution.
136 for machine in [server, client]:
137 machine.execute("systemctl stop rosenpass.service")
138
139 for (name, machine, remote) in [("server", server, client), ("client", client, server)]:
140 pk, sk = f"{name}.pqpk", f"{name}.pqsk"
141 system(f"{rosenpass} gen-keys --force --secret-key {sk} --public-key {pk}")
142 machine.copy_from_host(sk, f"{etc}/pqsk")
143 machine.copy_from_host(pk, f"{etc}/pqpk")
144 remote.copy_from_host(pk, f"{etc}/peers/{name}/pqpk")
145
146 for machine in [server, client]:
147 machine.execute("systemctl start rosenpass.service")
148
149 for machine in [server, client]:
150 machine.wait_for_unit("rosenpass.service")
151
152 with subtest("ping"):
153 client.succeed("ping -c 2 -i 0.5 ${server.ip}%${deviceName}")
154
155 with subtest("preshared-keys"):
156 # Rosenpass works by setting the WireGuard preshared key at regular intervals.
157 # Thus, if it is not active, then no key will be set, and the output of `wg show` will contain "none".
158 # Otherwise, if it is active, then the key will be set and "none" will not be found in the output of `wg show`.
159 for machine in [server, client]:
160 machine.wait_until_succeeds("wg show all preshared-keys | grep --invert-match none", timeout=5)
161 '';
162
163 # NOTE: Below configuration is for "interactive" (=developing/debugging) only.
164 interactive.nodes =
165 let
166 inherit (import ./ssh-keys.nix pkgs) snakeOilPublicKey snakeOilPrivateKey;
167
168 sshAndKeyGeneration = {
169 services.openssh.enable = true;
170 users.users.root.openssh.authorizedKeys.keys = [ snakeOilPublicKey ];
171 environment.systemPackages = [
172 (pkgs.writeShellApplication {
173 name = "gen-keys";
174 runtimeInputs = [ pkgs.rosenpass ];
175 text = ''
176 HOST="$(hostname)"
177 if [ "$HOST" == "server" ]
178 then
179 PEER="client"
180 else
181 PEER="server"
182 fi
183
184 # Generate keypair.
185 mkdir -vp /etc/rosenpass/peers/$PEER
186 rosenpass gen-keys --force --secret-key /etc/rosenpass/pqsk --public-key /etc/rosenpass/pqpk
187
188 # Set up SSH key.
189 mkdir -p /root/.ssh
190 cp ${snakeOilPrivateKey} /root/.ssh/id_ecdsa
191 chmod 0400 /root/.ssh/id_ecdsa
192
193 # Copy public key to other peer.
194 # shellcheck disable=SC2029
195 ssh -o StrictHostKeyChecking=no $PEER "mkdir -pv /etc/rosenpass/peers/$HOST"
196 scp /etc/rosenpass/pqpk "$PEER:/etc/rosenpass/peers/$HOST/pqpk"
197 '';
198 })
199 ];
200 };
201
202 # Use kmscon <https://www.freedesktop.org/wiki/Software/kmscon/>
203 # to provide a slightly nicer console, and while we're at it,
204 # also use a nice font.
205 # With kmscon, we can for example zoom in/out using [Ctrl] + [+]
206 # and [Ctrl] + [-]
207 niceConsoleAndAutologin.services.kmscon = {
208 enable = true;
209 autologinUser = "root";
210 fonts = [
211 {
212 name = "Fira Code";
213 package = pkgs.fira-code;
214 }
215 ];
216 };
217 in
218 {
219 server = sshAndKeyGeneration // niceConsoleAndAutologin;
220 client = sshAndKeyGeneration // niceConsoleAndAutologin;
221 };
222 }
223)