1{ pkgs, ... }:
2let
3 certs = import ../common/acme/server/snakeoil-certs.nix;
4 domain = certs.domain;
5in
6{
7 name = "firezone";
8 meta.maintainers = with pkgs.lib.maintainers; [ oddlama ];
9
10 nodes = {
11 server =
12 {
13 config,
14 lib,
15 pkgs,
16 ...
17 }:
18 {
19 security.pki.certificateFiles = [ certs.ca.cert ];
20
21 networking.extraHosts = ''
22 ${config.networking.primaryIPAddress} ${domain}
23 ${config.networking.primaryIPv6Address} ${domain}
24 '';
25
26 networking.firewall.allowedTCPPorts = [
27 80
28 443
29 ];
30
31 services.nginx = {
32 enable = true;
33 virtualHosts.${domain} = {
34 sslCertificate = certs.${domain}.cert;
35 sslCertificateKey = certs.${domain}.key;
36 };
37 };
38
39 services.firezone.server = {
40 enable = true;
41 enableLocalDB = true;
42 nginx.enable = true;
43
44 # Doesn't need to work for this test, but needs to be configured
45 # otherwise the server will not start.
46 smtp = {
47 from = "firezone@example.com";
48 host = "mail.localhost";
49 port = 465;
50 implicitTls = true;
51 username = "firezone@example.com";
52 passwordFile = pkgs.writeText "tmpmailpasswd" "supermailpassword";
53 };
54
55 provision = {
56 enable = true;
57 accounts.main = {
58 name = "My Account";
59 relayGroups.my-relays.name = "Relays";
60 gatewayGroups.site.name = "Site";
61 actors = {
62 admin = {
63 type = "account_admin_user";
64 name = "Admin";
65 email = "admin@example.com";
66 };
67 client = {
68 type = "service_account";
69 name = "A client";
70 email = "client@example.com";
71 };
72 };
73 resources.res1 = {
74 type = "dns";
75 name = "Dns Resource";
76 address = "resource.example.com";
77 gatewayGroups = [ "site" ];
78 filters = [
79 { protocol = "icmp"; }
80 {
81 protocol = "tcp";
82 ports = [ 80 ];
83 }
84 ];
85 };
86 resources.res2 = {
87 type = "ip";
88 name = "Ip Resource";
89 address = "172.20.2.1";
90 gatewayGroups = [ "site" ];
91 };
92 resources.res3 = {
93 type = "cidr";
94 name = "Cidr Resource";
95 address = "172.20.1.0/24";
96 gatewayGroups = [ "site" ];
97 };
98 policies.pol1 = {
99 description = "Allow anyone res1 access";
100 group = "everyone";
101 resource = "res1";
102 };
103 policies.pol2 = {
104 description = "Allow anyone res2 access";
105 group = "everyone";
106 resource = "res2";
107 };
108 policies.pol3 = {
109 description = "Allow anyone res3 access";
110 group = "everyone";
111 resource = "res3";
112 };
113 };
114 };
115
116 api.externalUrl = "https://${domain}/api/";
117 web.externalUrl = "https://${domain}/";
118 };
119
120 systemd.services.firezone-server-domain.postStart = lib.mkAfter ''
121 ${lib.getExe config.services.firezone.server.domain.package} rpc 'Code.eval_file("${./create-tokens.exs}")'
122 '';
123 };
124
125 relay =
126 {
127 nodes,
128 config,
129 lib,
130 ...
131 }:
132 {
133 security.pki.certificateFiles = [ certs.ca.cert ];
134 networking.extraHosts = ''
135 ${nodes.server.networking.primaryIPAddress} ${domain}
136 ${nodes.server.networking.primaryIPv6Address} ${domain}
137 '';
138
139 services.firezone.relay = {
140 enable = true;
141 logLevel = "debug";
142 name = "test-relay";
143 apiUrl = "wss://${domain}/api/";
144 tokenFile = "/tmp/shared/relay_token.txt";
145 publicIpv4 = config.networking.primaryIPAddress;
146 publicIpv6 = config.networking.primaryIPv6Address;
147 openFirewall = true;
148 };
149
150 # Don't auto-start so we can wait until the token was provisioned
151 systemd.services.firezone-relay.wantedBy = lib.mkForce [ ];
152 };
153
154 # A resource that is only connected to the gateway,
155 # allowing us to confirm the VPN works
156 resource = {
157 virtualisation.vlans = [
158 1
159 2
160 ];
161
162 networking.interfaces.eth1.ipv4.addresses = [
163 {
164 address = "172.20.1.1";
165 prefixLength = 24;
166 }
167 ];
168
169 networking.interfaces.eth2.ipv4.addresses = [
170 {
171 address = "172.20.2.1";
172 prefixLength = 24;
173 }
174 ];
175
176 networking.firewall.allowedTCPPorts = [
177 80
178 ];
179
180 services.nginx = {
181 enable = true;
182 virtualHosts = {
183 "localhost" = {
184 default = true;
185 locations."/".extraConfig = ''
186 return 200 'greetings from the resource';
187 add_header Content-Type text/plain;
188 '';
189 };
190 };
191 };
192 };
193
194 gateway =
195 {
196 nodes,
197 lib,
198 ...
199 }:
200 {
201 virtualisation.vlans = [
202 1
203 2
204 ];
205
206 networking = {
207 interfaces.eth1.ipv4.addresses = [
208 {
209 address = "172.20.1.2";
210 prefixLength = 24;
211 }
212 ];
213
214 interfaces.eth2.ipv4.addresses = [
215 {
216 address = "172.20.2.2";
217 prefixLength = 24;
218 }
219 ];
220
221 firewall.enable = false;
222 nftables.enable = true;
223 nftables.tables."filter".family = "inet";
224 nftables.tables."filter".content = ''
225 chain incoming {
226 type filter hook input priority 0; policy accept;
227 }
228
229 chain postrouting {
230 type nat hook postrouting priority srcnat; policy accept;
231 meta protocol ip iifname "tun-firezone" oifname { "eth1", "eth2" } masquerade random
232 }
233
234 chain forward {
235 type filter hook forward priority 0; policy drop;
236 iifname "tun-firezone" accept
237 oifname "tun-firezone" accept
238 }
239
240 chain output {
241 type filter hook output priority 0; policy accept;
242 }
243 '';
244 };
245
246 boot.kernel.sysctl."net.ipv4.ip_forward" = "1";
247 # boot.kernel.sysctl."net.ipv4.conf.all.src_valid_mark" = "1";
248 boot.kernel.sysctl."net.ipv6.conf.default.forwarding" = "1";
249 boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = "1";
250
251 security.pki.certificateFiles = [ certs.ca.cert ];
252 networking.extraHosts = ''
253 ${nodes.server.networking.primaryIPAddress} ${domain}
254 ${nodes.server.networking.primaryIPv6Address} ${domain}
255 172.20.1.1 resource.example.com
256 '';
257
258 services.firezone.gateway = {
259 enable = true;
260 logLevel = "debug";
261 name = "test-gateway";
262 apiUrl = "wss://${domain}/api/";
263 tokenFile = "/tmp/shared/gateway_token.txt";
264 };
265
266 # Don't auto-start so we can wait until the token was provisioned
267 systemd.services.firezone-gateway.wantedBy = lib.mkForce [ ];
268 };
269
270 client =
271 {
272 nodes,
273 lib,
274 ...
275 }:
276 {
277 security.pki.certificateFiles = [ certs.ca.cert ];
278 networking.useNetworkd = true;
279 networking.extraHosts = ''
280 ${nodes.server.networking.primaryIPAddress} ${domain}
281 ${nodes.server.networking.primaryIPv6Address} ${domain}
282 '';
283
284 services.firezone.headless-client = {
285 enable = true;
286 logLevel = "debug";
287 name = "test-client-somebody";
288 apiUrl = "wss://${domain}/api/";
289 tokenFile = "/tmp/shared/client_token.txt";
290 };
291
292 # Don't auto-start so we can wait until the token was provisioned
293 systemd.services.firezone-headless-client.wantedBy = lib.mkForce [ ];
294 };
295 };
296
297 testScript =
298 { ... }:
299 ''
300 start_all()
301
302 with subtest("Start server"):
303 server.wait_for_unit("firezone.target")
304 server.wait_until_succeeds("curl -Lsf https://${domain} | grep 'Welcome to Firezone'")
305 server.wait_until_succeeds("curl -Ls https://${domain}/api | grep 'Not Found'")
306
307 # Wait for tokens and copy them to shared folder
308 server.wait_for_file("/var/lib/private/firezone/relay_token.txt")
309 server.wait_for_file("/var/lib/private/firezone/gateway_token.txt")
310 server.wait_for_file("/var/lib/private/firezone/client_token.txt")
311 server.succeed("cp /var/lib/private/firezone/*_token.txt /tmp/shared")
312
313 with subtest("Connect relay"):
314 relay.succeed("systemctl start firezone-relay")
315 relay.wait_for_unit("firezone-relay.service")
316 relay.wait_until_succeeds("journalctl --since -2m --unit firezone-relay.service --grep 'Connected to portal.*${domain}'", timeout=30)
317
318 with subtest("Connect gateway"):
319 gateway.succeed("systemctl start firezone-gateway")
320 gateway.wait_for_unit("firezone-gateway.service")
321 gateway.wait_until_succeeds("journalctl --since -2m --unit firezone-gateway.service --grep 'Connected to portal.*${domain}'", timeout=30)
322 relay.wait_until_succeeds("journalctl --since -2m --unit firezone-relay.service --grep 'Created allocation.*IPv4'", timeout=30)
323 relay.wait_until_succeeds("journalctl --since -2m --unit firezone-relay.service --grep 'Created allocation.*IPv6'", timeout=30)
324
325 # Assert both relay ips are known
326 gateway.wait_until_succeeds("journalctl --since -2m --unit firezone-gateway.service --grep 'Updated allocation.*relay_ip4.*Some.*relay_ip6.*Some'", timeout=30)
327
328 with subtest("Connect headless-client"):
329 client.succeed("systemctl start firezone-headless-client")
330 client.wait_for_unit("firezone-headless-client.service")
331 client.wait_until_succeeds("journalctl --since -2m --unit firezone-headless-client.service --grep 'Connected to portal.*${domain}'", timeout=30)
332 client.wait_until_succeeds("journalctl --since -2m --unit firezone-headless-client.service --grep 'Tunnel ready'", timeout=30)
333
334 with subtest("Check DNS based access"):
335 # Check that we can access the resource through the VPN via DNS
336 client.wait_until_succeeds("curl -4 -Lsf http://resource.example.com | grep 'greetings from the resource'")
337 client.wait_until_succeeds("curl -6 -Lsf http://resource.example.com | grep 'greetings from the resource'")
338
339 with subtest("Check CIDR based access"):
340 # Check that we can access the resource through the VPN via CIDR
341 client.wait_until_succeeds("ping -c1 -W1 172.20.1.1")
342
343 with subtest("Check IP based access"):
344 # Check that we can access the resource through the VPN via IP
345 client.wait_until_succeeds("ping -c1 -W1 172.20.2.1")
346 '';
347}