1import ./make-test-python.nix (
2 { lib, pkgs, ... }:
3 let
4 wg-keys = import ./wireguard/snakeoil-keys.nix;
5
6 target_host = "acme.test";
7 server_host = "sing-box.test";
8
9 hosts = {
10 "${target_host}" = "1.1.1.1";
11 "${server_host}" = "1.1.1.2";
12 };
13 hostsEntries = lib.mapAttrs' (k: v: {
14 name = v;
15 value = lib.singleton k;
16 }) hosts;
17
18 vmessPort = 1080;
19 vmessUUID = "bf000d23-0752-40b4-affe-68f7707a9661";
20 vmessInbound = {
21 type = "vmess";
22 tag = "inbound:vmess";
23 listen = "0.0.0.0";
24 listen_port = vmessPort;
25 users = [
26 {
27 name = "sekai";
28 uuid = vmessUUID;
29 alterId = 0;
30 }
31 ];
32 };
33 vmessOutbound = {
34 type = "vmess";
35 tag = "outbound:vmess";
36 server = server_host;
37 server_port = vmessPort;
38 uuid = vmessUUID;
39 security = "auto";
40 alter_id = 0;
41 };
42
43 tunInbound = {
44 type = "tun";
45 tag = "inbound:tun";
46 interface_name = "tun0";
47 address = [
48 "172.16.0.1/30"
49 "fd00::1/126"
50 ];
51 auto_route = true;
52 iproute2_table_index = 2024;
53 iproute2_rule_index = 9001;
54 route_address = [
55 "${hosts."${target_host}"}/32"
56 ];
57 route_exclude_address = [
58 "${hosts."${server_host}"}/32"
59 ];
60 strict_route = false;
61 sniff = true;
62 sniff_override_destination = false;
63 };
64
65 tproxyPort = 1081;
66 tproxyPost = pkgs.writeShellApplication {
67 name = "exe";
68 runtimeInputs = with pkgs; [
69 iproute2
70 iptables
71 ];
72 text = ''
73 ip route add local default dev lo table 100
74 ip rule add fwmark 1 table 100
75
76 iptables -t mangle -N SING_BOX
77 iptables -t mangle -A SING_BOX -d 100.64.0.0/10 -j RETURN
78 iptables -t mangle -A SING_BOX -d 127.0.0.0/8 -j RETURN
79 iptables -t mangle -A SING_BOX -d 169.254.0.0/16 -j RETURN
80 iptables -t mangle -A SING_BOX -d 172.16.0.0/12 -j RETURN
81 iptables -t mangle -A SING_BOX -d 192.0.0.0/24 -j RETURN
82 iptables -t mangle -A SING_BOX -d 224.0.0.0/4 -j RETURN
83 iptables -t mangle -A SING_BOX -d 240.0.0.0/4 -j RETURN
84 iptables -t mangle -A SING_BOX -d 255.255.255.255/32 -j RETURN
85
86 iptables -t mangle -A SING_BOX -d ${hosts."${server_host}"}/32 -p tcp -j RETURN
87 iptables -t mangle -A SING_BOX -d ${hosts."${server_host}"}/32 -p udp -j RETURN
88
89 iptables -t mangle -A SING_BOX -d ${hosts."${target_host}"}/32 -p tcp -j TPROXY --on-port ${toString tproxyPort} --tproxy-mark 1
90 iptables -t mangle -A SING_BOX -d ${hosts."${target_host}"}/32 -p udp -j TPROXY --on-port ${toString tproxyPort} --tproxy-mark 1
91 iptables -t mangle -A PREROUTING -j SING_BOX
92
93 iptables -t mangle -N SING_BOX_SELF
94 iptables -t mangle -A SING_BOX_SELF -d 100.64.0.0/10 -j RETURN
95 iptables -t mangle -A SING_BOX_SELF -d 127.0.0.0/8 -j RETURN
96 iptables -t mangle -A SING_BOX_SELF -d 169.254.0.0/16 -j RETURN
97 iptables -t mangle -A SING_BOX_SELF -d 172.16.0.0/12 -j RETURN
98 iptables -t mangle -A SING_BOX_SELF -d 192.0.0.0/24 -j RETURN
99 iptables -t mangle -A SING_BOX_SELF -d 224.0.0.0/4 -j RETURN
100 iptables -t mangle -A SING_BOX_SELF -d 240.0.0.0/4 -j RETURN
101 iptables -t mangle -A SING_BOX_SELF -d 255.255.255.255/32 -j RETURN
102 iptables -t mangle -A SING_BOX_SELF -j RETURN -m mark --mark 1234
103
104 iptables -t mangle -A SING_BOX_SELF -d ${hosts."${server_host}"}/32 -p tcp -j RETURN
105 iptables -t mangle -A SING_BOX_SELF -d ${hosts."${server_host}"}/32 -p udp -j RETURN
106 iptables -t mangle -A SING_BOX_SELF -p tcp -j MARK --set-mark 1
107 iptables -t mangle -A SING_BOX_SELF -p udp -j MARK --set-mark 1
108 iptables -t mangle -A OUTPUT -j SING_BOX_SELF
109 '';
110 };
111 in
112 {
113
114 name = "sing-box";
115
116 meta = {
117 maintainers = with lib.maintainers; [ nickcao ];
118 };
119
120 nodes = {
121 target =
122 { pkgs, ... }:
123 {
124 networking = {
125 firewall.enable = false;
126 hosts = hostsEntries;
127 useDHCP = false;
128 interfaces.eth1 = {
129 ipv4.addresses = [
130 {
131 address = hosts."${target_host}";
132 prefixLength = 24;
133 }
134 ];
135 };
136 };
137
138 services.dnsmasq.enable = true;
139
140 services.nginx = {
141 enable = true;
142 package = pkgs.nginxQuic;
143
144 virtualHosts."${target_host}" = {
145 onlySSL = true;
146 sslCertificate = ./common/acme/server/acme.test.cert.pem;
147 sslCertificateKey = ./common/acme/server/acme.test.key.pem;
148 http2 = true;
149 http3 = true;
150 http3_hq = false;
151 quic = true;
152 reuseport = true;
153 locations."/" = {
154 extraConfig = ''
155 default_type text/plain;
156 return 200 "$server_protocol $remote_addr";
157 allow ${hosts."${server_host}"}/32;
158 deny all;
159 '';
160 };
161 };
162 };
163 };
164
165 server =
166 { pkgs, ... }:
167 {
168 boot.kernel.sysctl = {
169 "net.ipv4.conf.all.forwarding" = 1;
170 };
171
172 networking = {
173 firewall.enable = false;
174 hosts = hostsEntries;
175 useDHCP = false;
176 interfaces.eth1 = {
177 ipv4.addresses = [
178 {
179 address = hosts."${server_host}";
180 prefixLength = 24;
181 }
182 ];
183 };
184 };
185
186 systemd.network.wait-online.ignoredInterfaces = [ "wg0" ];
187
188 networking.wg-quick.interfaces.wg0 = {
189 address = [
190 "10.23.42.1/24"
191 ];
192 listenPort = 2408;
193 mtu = 1500;
194
195 inherit (wg-keys.peer0) privateKey;
196
197 peers = lib.singleton {
198 allowedIPs = [
199 "10.23.42.2/32"
200 ];
201
202 inherit (wg-keys.peer1) publicKey;
203 };
204
205 postUp = ''
206 ${pkgs.iptables}/bin/iptables -A FORWARD -i wg0 -j ACCEPT
207 ${pkgs.iptables}/bin/iptables -t nat -A POSTROUTING -s 10.23.42.0/24 -o eth1 -j MASQUERADE
208 '';
209 };
210
211 services.sing-box = {
212 enable = true;
213 settings = {
214 inbounds = [
215 vmessInbound
216 ];
217 outbounds = [
218 {
219 type = "direct";
220 tag = "outbound:direct";
221 }
222 ];
223 };
224 };
225 };
226
227 tun =
228 { pkgs, ... }:
229 {
230 networking = {
231 firewall.enable = false;
232 hosts = hostsEntries;
233 useDHCP = false;
234 interfaces.eth1 = {
235 ipv4.addresses = [
236 {
237 address = "1.1.1.3";
238 prefixLength = 24;
239 }
240 ];
241 };
242 };
243
244 security.pki.certificates = [
245 (builtins.readFile ./common/acme/server/ca.cert.pem)
246 ];
247
248 environment.systemPackages = [
249 pkgs.curlHTTP3
250 pkgs.iproute2
251 ];
252
253 services.sing-box = {
254 enable = true;
255 settings = {
256 inbounds = [
257 tunInbound
258 ];
259 outbounds = [
260 {
261 type = "block";
262 tag = "outbound:block";
263 }
264 {
265 type = "direct";
266 tag = "outbound:direct";
267 }
268 vmessOutbound
269 ];
270 route = {
271 final = "outbound:block";
272 rules = [
273 {
274 inbound = [
275 "inbound:tun"
276 ];
277 outbound = "outbound:vmess";
278 }
279 ];
280 };
281 };
282 };
283 };
284
285 wireguard =
286 { pkgs, ... }:
287 {
288 networking = {
289 firewall.enable = false;
290 hosts = hostsEntries;
291 useDHCP = false;
292 interfaces.eth1 = {
293 ipv4.addresses = [
294 {
295 address = "1.1.1.4";
296 prefixLength = 24;
297 }
298 ];
299 };
300 };
301
302 security.pki.certificates = [
303 (builtins.readFile ./common/acme/server/ca.cert.pem)
304 ];
305
306 environment.systemPackages = [
307 pkgs.curlHTTP3
308 pkgs.iproute2
309 ];
310
311 services.sing-box = {
312 enable = true;
313 settings = {
314 outbounds = [
315 {
316 type = "block";
317 tag = "outbound:block";
318 }
319 {
320 type = "direct";
321 tag = "outbound:direct";
322 }
323 {
324 detour = "outbound:direct";
325 type = "wireguard";
326 tag = "outbound:wireguard";
327 interface_name = "wg0";
328 local_address = [ "10.23.42.2/32" ];
329 mtu = 1280;
330 private_key = wg-keys.peer1.privateKey;
331 peer_public_key = wg-keys.peer0.publicKey;
332 server = server_host;
333 server_port = 2408;
334 system_interface = true;
335 }
336 ];
337 route = {
338 final = "outbound:block";
339 };
340 };
341 };
342 };
343
344 tproxy =
345 { pkgs, ... }:
346 {
347 networking = {
348 firewall.enable = false;
349 hosts = hostsEntries;
350 useDHCP = false;
351 interfaces.eth1 = {
352 ipv4.addresses = [
353 {
354 address = "1.1.1.5";
355 prefixLength = 24;
356 }
357 ];
358 };
359 };
360
361 security.pki.certificates = [
362 (builtins.readFile ./common/acme/server/ca.cert.pem)
363 ];
364
365 environment.systemPackages = [ pkgs.curlHTTP3 ];
366
367 systemd.services.sing-box.serviceConfig.ExecStartPost = [
368 "+${tproxyPost}/bin/exe"
369 ];
370
371 services.sing-box = {
372 enable = true;
373 settings = {
374 inbounds = [
375 {
376 tag = "inbound:tproxy";
377 type = "tproxy";
378 listen = "0.0.0.0";
379 listen_port = tproxyPort;
380 udp_fragment = true;
381 sniff = true;
382 sniff_override_destination = false;
383 }
384 ];
385 outbounds = [
386 {
387 type = "block";
388 tag = "outbound:block";
389 }
390 {
391 type = "direct";
392 tag = "outbound:direct";
393 }
394 vmessOutbound
395 ];
396 route = {
397 final = "outbound:block";
398 rules = [
399 {
400 inbound = [
401 "inbound:tproxy"
402 ];
403 outbound = "outbound:vmess";
404 }
405 ];
406 };
407 };
408 };
409 };
410
411 fakeip =
412 { pkgs, ... }:
413 {
414 networking = {
415 firewall.enable = false;
416 hosts = hostsEntries;
417 useDHCP = false;
418 interfaces.eth1 = {
419 ipv4.addresses = [
420 {
421 address = "1.1.1.6";
422 prefixLength = 24;
423 }
424 ];
425 };
426 };
427
428 environment.systemPackages = [ pkgs.dnsutils ];
429
430 services.sing-box = {
431 enable = true;
432 settings = {
433 dns = {
434 final = "dns:default";
435 independent_cache = true;
436 fakeip = {
437 enabled = true;
438 "inet4_range" = "198.18.0.0/16";
439 };
440 servers = [
441 {
442 detour = "outbound:direct";
443 tag = "dns:default";
444 address = hosts."${target_host}";
445 }
446 {
447 tag = "dns:fakeip";
448 address = "fakeip";
449 }
450 ];
451 rules = [
452 {
453 outbound = [ "any" ];
454 server = "dns:default";
455 }
456 {
457 query_type = [
458 "A"
459 "AAAA"
460 ];
461 server = "dns:fakeip";
462
463 }
464 ];
465 };
466 inbounds = [
467 tunInbound
468 ];
469 outbounds = [
470 {
471 type = "block";
472 tag = "outbound:block";
473 }
474 {
475 type = "direct";
476 tag = "outbound:direct";
477 }
478 {
479 type = "dns";
480 tag = "outbound:dns";
481 }
482 ];
483 route = {
484 final = "outbound:direct";
485 rules = [
486 {
487 protocol = "dns";
488 outbound = "outbound:dns";
489 }
490 ];
491 };
492 };
493 };
494 };
495 };
496
497 testScript = ''
498 target.wait_for_unit("nginx.service")
499 target.wait_for_open_port(443)
500 target.wait_for_unit("dnsmasq.service")
501 target.wait_for_open_port(53)
502
503 server.wait_for_unit("sing-box.service")
504 server.wait_for_open_port(1080)
505 server.wait_for_unit("wg-quick-wg0.service")
506 server.wait_for_file("/sys/class/net/wg0")
507
508 def test_curl(machine, extra_args=""):
509 assert (
510 machine.succeed(f"curl --fail --max-time 10 --http2 https://${target_host} {extra_args}")
511 == "HTTP/2.0 ${hosts.${server_host}}"
512 )
513 assert (
514 machine.succeed(f"curl --fail --max-time 10 --http3-only https://${target_host} {extra_args}")
515 == "HTTP/3.0 ${hosts.${server_host}}"
516 )
517
518 with subtest("tun"):
519 tun.wait_for_unit("sing-box.service")
520 tun.wait_for_unit("sys-devices-virtual-net-${tunInbound.interface_name}.device")
521 tun.wait_until_succeeds("ip route get ${hosts."${target_host}"} | grep 'dev ${tunInbound.interface_name}'")
522 tun.succeed("ip addr show ${tunInbound.interface_name}")
523 tun.succeed("ip route show table ${toString tunInbound.iproute2_table_index} | grep ${tunInbound.interface_name}")
524 assert (
525 tun.succeed("ip rule list table ${toString tunInbound.iproute2_table_index} | sort | head -1 | awk -F: '{print $1}' | tr -d '\n'")
526 == "${toString tunInbound.iproute2_rule_index}"
527 )
528 test_curl(tun)
529
530 with subtest("wireguard"):
531 wireguard.wait_for_unit("sing-box.service")
532 wireguard.wait_for_unit("sys-devices-virtual-net-wg0.device")
533 wireguard.succeed("ip addr show wg0")
534 test_curl(wireguard, "--interface wg0")
535
536 with subtest("tproxy"):
537 tproxy.wait_for_unit("sing-box.service")
538 test_curl(tproxy)
539
540 with subtest("fakeip"):
541 fakeip.wait_for_unit("sing-box.service")
542 fakeip.wait_for_unit("sys-devices-virtual-net-${tunInbound.interface_name}.device")
543 fakeip.wait_until_succeeds("ip route get ${hosts."${target_host}"} | grep 'dev ${tunInbound.interface_name}'")
544 fakeip.succeed("dig +short A ${target_host} @${target_host} | grep '^198.18.'")
545 '';
546
547 }
548)