at master 15 kB view raw
1{ lib, pkgs, ... }: 2let 3 wg-keys = import ./wireguard/snakeoil-keys.nix; 4 5 target_host = "acme.test"; 6 server_host = "sing-box.test"; 7 8 hosts = { 9 "${target_host}" = "1.1.1.1"; 10 "${server_host}" = "1.1.1.2"; 11 }; 12 hostsEntries = lib.mapAttrs' (k: v: { 13 name = v; 14 value = lib.singleton k; 15 }) hosts; 16 17 vmessPort = 1080; 18 vmessUUID = "bf000d23-0752-40b4-affe-68f7707a9661"; 19 vmessInbound = { 20 type = "vmess"; 21 tag = "inbound:vmess"; 22 listen = "0.0.0.0"; 23 listen_port = vmessPort; 24 users = [ 25 { 26 name = "sekai"; 27 uuid = vmessUUID; 28 alterId = 0; 29 } 30 ]; 31 }; 32 vmessOutbound = { 33 type = "vmess"; 34 tag = "outbound:vmess"; 35 server = server_host; 36 server_port = vmessPort; 37 uuid = vmessUUID; 38 security = "auto"; 39 alter_id = 0; 40 }; 41 42 tunInbound = { 43 type = "tun"; 44 tag = "inbound:tun"; 45 interface_name = "tun0"; 46 address = [ 47 "172.16.0.1/30" 48 "fd00::1/126" 49 ]; 50 auto_route = true; 51 iproute2_table_index = 2024; 52 iproute2_rule_index = 9001; 53 route_address = [ 54 "${hosts."${target_host}"}/32" 55 ]; 56 route_exclude_address = [ 57 "${hosts."${server_host}"}/32" 58 ]; 59 strict_route = false; 60 }; 61 62 tproxyPort = 1081; 63 tproxyPost = pkgs.writeShellApplication { 64 name = "exe"; 65 runtimeInputs = with pkgs; [ 66 iproute2 67 iptables 68 ]; 69 text = '' 70 ip route add local default dev lo table 100 71 ip rule add fwmark 1 table 100 72 73 iptables -t mangle -N SING_BOX 74 iptables -t mangle -A SING_BOX -d 100.64.0.0/10 -j RETURN 75 iptables -t mangle -A SING_BOX -d 127.0.0.0/8 -j RETURN 76 iptables -t mangle -A SING_BOX -d 169.254.0.0/16 -j RETURN 77 iptables -t mangle -A SING_BOX -d 172.16.0.0/12 -j RETURN 78 iptables -t mangle -A SING_BOX -d 192.0.0.0/24 -j RETURN 79 iptables -t mangle -A SING_BOX -d 224.0.0.0/4 -j RETURN 80 iptables -t mangle -A SING_BOX -d 240.0.0.0/4 -j RETURN 81 iptables -t mangle -A SING_BOX -d 255.255.255.255/32 -j RETURN 82 83 iptables -t mangle -A SING_BOX -d ${hosts."${server_host}"}/32 -p tcp -j RETURN 84 iptables -t mangle -A SING_BOX -d ${hosts."${server_host}"}/32 -p udp -j RETURN 85 86 iptables -t mangle -A SING_BOX -d ${hosts."${target_host}"}/32 -p tcp -j TPROXY --on-port ${toString tproxyPort} --tproxy-mark 1 87 iptables -t mangle -A SING_BOX -d ${hosts."${target_host}"}/32 -p udp -j TPROXY --on-port ${toString tproxyPort} --tproxy-mark 1 88 iptables -t mangle -A PREROUTING -j SING_BOX 89 90 iptables -t mangle -N SING_BOX_SELF 91 iptables -t mangle -A SING_BOX_SELF -d 100.64.0.0/10 -j RETURN 92 iptables -t mangle -A SING_BOX_SELF -d 127.0.0.0/8 -j RETURN 93 iptables -t mangle -A SING_BOX_SELF -d 169.254.0.0/16 -j RETURN 94 iptables -t mangle -A SING_BOX_SELF -d 172.16.0.0/12 -j RETURN 95 iptables -t mangle -A SING_BOX_SELF -d 192.0.0.0/24 -j RETURN 96 iptables -t mangle -A SING_BOX_SELF -d 224.0.0.0/4 -j RETURN 97 iptables -t mangle -A SING_BOX_SELF -d 240.0.0.0/4 -j RETURN 98 iptables -t mangle -A SING_BOX_SELF -d 255.255.255.255/32 -j RETURN 99 iptables -t mangle -A SING_BOX_SELF -j RETURN -m mark --mark 1234 100 101 iptables -t mangle -A SING_BOX_SELF -d ${hosts."${server_host}"}/32 -p tcp -j RETURN 102 iptables -t mangle -A SING_BOX_SELF -d ${hosts."${server_host}"}/32 -p udp -j RETURN 103 iptables -t mangle -A SING_BOX_SELF -p tcp -j MARK --set-mark 1 104 iptables -t mangle -A SING_BOX_SELF -p udp -j MARK --set-mark 1 105 iptables -t mangle -A OUTPUT -j SING_BOX_SELF 106 ''; 107 }; 108in 109{ 110 111 name = "sing-box"; 112 113 meta = { 114 maintainers = with lib.maintainers; [ 115 nickcao 116 prince213 117 ]; 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 route = { 224 default_interface = "eth1"; 225 }; 226 }; 227 }; 228 }; 229 230 tun = 231 { pkgs, ... }: 232 { 233 networking = { 234 firewall.enable = false; 235 hosts = hostsEntries; 236 useDHCP = false; 237 interfaces.eth1 = { 238 ipv4.addresses = [ 239 { 240 address = "1.1.1.3"; 241 prefixLength = 24; 242 } 243 ]; 244 }; 245 }; 246 247 security.pki.certificates = [ 248 (builtins.readFile ./common/acme/server/ca.cert.pem) 249 ]; 250 251 environment.systemPackages = [ 252 pkgs.curlHTTP3 253 pkgs.iproute2 254 ]; 255 256 services.sing-box = { 257 enable = true; 258 settings = { 259 inbounds = [ 260 tunInbound 261 ]; 262 outbounds = [ 263 { 264 type = "block"; 265 tag = "outbound:block"; 266 } 267 { 268 type = "direct"; 269 tag = "outbound:direct"; 270 } 271 vmessOutbound 272 ]; 273 route = { 274 default_interface = "eth1"; 275 final = "outbound:block"; 276 rules = [ 277 { 278 inbound = [ 279 "inbound:tun" 280 ]; 281 outbound = "outbound:vmess"; 282 } 283 ]; 284 }; 285 }; 286 }; 287 }; 288 289 wireguard = 290 { pkgs, ... }: 291 { 292 networking = { 293 firewall.enable = false; 294 hosts = hostsEntries; 295 useDHCP = false; 296 interfaces.eth1 = { 297 ipv4.addresses = [ 298 { 299 address = "1.1.1.4"; 300 prefixLength = 24; 301 } 302 ]; 303 }; 304 }; 305 306 security.pki.certificates = [ 307 (builtins.readFile ./common/acme/server/ca.cert.pem) 308 ]; 309 310 environment.systemPackages = [ 311 pkgs.curlHTTP3 312 pkgs.iproute2 313 ]; 314 315 services.sing-box = { 316 enable = true; 317 settings = { 318 outbounds = [ 319 { 320 type = "block"; 321 tag = "outbound:block"; 322 } 323 ]; 324 endpoints = [ 325 { 326 type = "wireguard"; 327 tag = "outbound:wireguard"; 328 name = "wg0"; 329 address = [ "10.23.42.2/32" ]; 330 mtu = 1280; 331 private_key = wg-keys.peer1.privateKey; 332 peers = [ 333 { 334 address = server_host; 335 port = 2408; 336 public_key = wg-keys.peer0.publicKey; 337 allowed_ips = [ "0.0.0.0/0" ]; 338 } 339 ]; 340 system = true; 341 } 342 ]; 343 route = { 344 default_interface = "eth1"; 345 final = "outbound:block"; 346 }; 347 }; 348 }; 349 }; 350 351 tproxy = 352 { pkgs, ... }: 353 { 354 networking = { 355 firewall.enable = false; 356 hosts = hostsEntries; 357 useDHCP = false; 358 interfaces.eth1 = { 359 ipv4.addresses = [ 360 { 361 address = "1.1.1.5"; 362 prefixLength = 24; 363 } 364 ]; 365 }; 366 }; 367 368 security.pki.certificates = [ 369 (builtins.readFile ./common/acme/server/ca.cert.pem) 370 ]; 371 372 environment.systemPackages = [ pkgs.curlHTTP3 ]; 373 374 systemd.services.sing-box.serviceConfig.ExecStartPost = [ 375 "+${tproxyPost}/bin/exe" 376 ]; 377 378 services.sing-box = { 379 enable = true; 380 settings = { 381 inbounds = [ 382 { 383 tag = "inbound:tproxy"; 384 type = "tproxy"; 385 listen = "0.0.0.0"; 386 listen_port = tproxyPort; 387 udp_fragment = true; 388 } 389 ]; 390 outbounds = [ 391 { 392 type = "block"; 393 tag = "outbound:block"; 394 } 395 { 396 type = "direct"; 397 tag = "outbound:direct"; 398 } 399 vmessOutbound 400 ]; 401 route = { 402 default_interface = "eth1"; 403 final = "outbound:block"; 404 rules = [ 405 { 406 inbound = [ 407 "inbound:tproxy" 408 ]; 409 outbound = "outbound:vmess"; 410 } 411 ]; 412 }; 413 }; 414 }; 415 }; 416 417 fakeip = 418 { pkgs, ... }: 419 { 420 networking = { 421 firewall.enable = false; 422 hosts = hostsEntries; 423 useDHCP = false; 424 interfaces.eth1 = { 425 ipv4.addresses = [ 426 { 427 address = "1.1.1.6"; 428 prefixLength = 24; 429 } 430 ]; 431 }; 432 }; 433 434 environment.systemPackages = [ pkgs.dnsutils ]; 435 436 services.sing-box = { 437 enable = true; 438 settings = { 439 dns = { 440 final = "dns:default"; 441 independent_cache = true; 442 servers = [ 443 { 444 type = "udp"; 445 tag = "dns:default"; 446 server = hosts."${target_host}"; 447 } 448 { 449 type = "fakeip"; 450 tag = "dns:fakeip"; 451 inet4_range = "198.18.0.0/16"; 452 } 453 { 454 type = "resolved"; 455 tag = "dns:resolved"; 456 service = "service:resolved"; 457 accept_default_resolvers = true; 458 } 459 ]; 460 rules = [ 461 { 462 query_type = [ 463 "A" 464 "AAAA" 465 ]; 466 server = "dns:fakeip"; 467 } 468 ]; 469 }; 470 inbounds = [ 471 tunInbound 472 ]; 473 outbounds = [ 474 { 475 type = "block"; 476 tag = "outbound:block"; 477 } 478 { 479 type = "direct"; 480 tag = "outbound:direct"; 481 } 482 ]; 483 route = { 484 default_domain_resolver = "dns:default"; 485 default_interface = "eth1"; 486 final = "outbound:direct"; 487 rules = [ 488 { 489 action = "sniff"; 490 } 491 { 492 protocol = "dns"; 493 action = "hijack-dns"; 494 } 495 ]; 496 }; 497 services = [ 498 { 499 type = "resolved"; 500 tag = "service:resolved"; 501 } 502 ]; 503 }; 504 }; 505 }; 506 }; 507 508 testScript = '' 509 target.wait_for_unit("nginx.service") 510 target.wait_for_open_port(443) 511 target.wait_for_unit("dnsmasq.service") 512 target.wait_for_open_port(53) 513 514 server.wait_for_unit("sing-box.service") 515 server.wait_for_open_port(1080) 516 server.wait_for_unit("wg-quick-wg0.service") 517 server.wait_for_file("/sys/class/net/wg0") 518 519 def test_curl(machine, extra_args=""): 520 assert ( 521 machine.succeed(f"curl --fail --max-time 10 --http2 https://${target_host} {extra_args}") 522 == "HTTP/2.0 ${hosts.${server_host}}" 523 ) 524 assert ( 525 machine.succeed(f"curl --fail --max-time 10 --http3-only https://${target_host} {extra_args}") 526 == "HTTP/3.0 ${hosts.${server_host}}" 527 ) 528 529 with subtest("tun"): 530 tun.wait_for_unit("sing-box.service") 531 tun.wait_for_unit("sys-devices-virtual-net-${tunInbound.interface_name}.device") 532 tun.wait_until_succeeds("ip route get ${hosts."${target_host}"} | grep 'dev ${tunInbound.interface_name}'") 533 tun.succeed("ip addr show ${tunInbound.interface_name}") 534 tun.succeed("ip route show table ${toString tunInbound.iproute2_table_index} | grep ${tunInbound.interface_name}") 535 assert ( 536 tun.succeed("ip rule list table ${toString tunInbound.iproute2_table_index} | sort | head -1 | awk -F: '{print $1}' | tr -d '\n'") 537 == "${toString tunInbound.iproute2_rule_index}" 538 ) 539 test_curl(tun) 540 541 with subtest("wireguard"): 542 wireguard.wait_for_unit("sing-box.service") 543 wireguard.wait_for_unit("sys-devices-virtual-net-wg0.device") 544 wireguard.succeed("ip addr show wg0") 545 test_curl(wireguard, "--interface wg0") 546 547 with subtest("tproxy"): 548 tproxy.wait_for_unit("sing-box.service") 549 test_curl(tproxy) 550 551 with subtest("fakeip"): 552 fakeip.wait_for_unit("sing-box.service") 553 fakeip.wait_for_unit("sys-devices-virtual-net-${tunInbound.interface_name}.device") 554 fakeip.wait_until_succeeds("ip route get ${hosts."${target_host}"} | grep 'dev ${tunInbound.interface_name}'") 555 fakeip.succeed("dig +short A ${target_host} @${target_host} | grep '^198.18.'") 556 ''; 557 558}