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