Unverified Commit 89284118 authored by Nick Cao's avatar Nick Cao Committed by GitHub
Browse files

nixos/sing-box: test distribution specific features (#343641)

parents 0d0d0787 10fb67f9
Loading
Loading
Loading
Loading
+530 −39
Original line number Diff line number Diff line
import ./make-test-python.nix ({ lib, pkgs, ... }: {
import ./make-test-python.nix (
  { lib, pkgs, ... }:
  let
    wg-keys = import ./wireguard/snakeoil-keys.nix;

    target_host = "acme.test";
    server_host = "sing-box.test";

    hosts = {
      "${target_host}" = "1.1.1.1";
      "${server_host}" = "1.1.1.2";
    };
    hostsEntries = lib.mapAttrs' (k: v: {
      name = v;
      value = lib.singleton k;
    }) hosts;

    vmessPort = 1080;
    vmessUUID = "bf000d23-0752-40b4-affe-68f7707a9661";
    vmessInbound = {
      type = "vmess";
      tag = "inbound:vmess";
      listen = "0.0.0.0";
      listen_port = vmessPort;
      users = [
        {
          name = "sekai";
          uuid = vmessUUID;
          alterId = 0;
        }
      ];
    };
    vmessOutbound = {
      type = "vmess";
      tag = "outbound:vmess";
      server = server_host;
      server_port = vmessPort;
      uuid = vmessUUID;
      security = "auto";
      alter_id = 0;
    };

    tunInbound = {
      type = "tun";
      tag = "inbound:tun";
      interface_name = "tun0";
      inet4_address = "172.16.0.1/30";
      inet6_address = "fd00::1/126";
      auto_route = true;
      inet4_route_address = [
        "${hosts."${target_host}"}/32"
      ];
      inet4_route_exclude_address = [
        "${hosts."${server_host}"}/32"
      ];
      strict_route = false;
      sniff = true;
      sniff_override_destination = false;
    };

    tproxyPort = 1081;
    tproxyPost = pkgs.writeShellApplication {
      name = "exe";
      runtimeInputs = with pkgs; [
        iproute2
        iptables
      ];
      text = ''
        ip route add local default dev lo table 100
        ip rule add fwmark 1 table 100

        iptables -t mangle -N SING_BOX
        iptables -t mangle -A SING_BOX -d 100.64.0.0/10 -j RETURN
        iptables -t mangle -A SING_BOX -d 127.0.0.0/8 -j RETURN
        iptables -t mangle -A SING_BOX -d 169.254.0.0/16 -j RETURN
        iptables -t mangle -A SING_BOX -d 172.16.0.0/12 -j RETURN
        iptables -t mangle -A SING_BOX -d 192.0.0.0/24 -j RETURN
        iptables -t mangle -A SING_BOX -d 224.0.0.0/4 -j RETURN
        iptables -t mangle -A SING_BOX -d 240.0.0.0/4 -j RETURN
        iptables -t mangle -A SING_BOX -d 255.255.255.255/32 -j RETURN

        iptables -t mangle -A SING_BOX -d ${hosts."${server_host}"}/32 -p tcp -j RETURN
        iptables -t mangle -A SING_BOX -d ${hosts."${server_host}"}/32 -p udp -j RETURN

        iptables -t mangle -A SING_BOX -d ${hosts."${target_host}"}/32 -p tcp -j TPROXY --on-port ${toString tproxyPort} --tproxy-mark 1
        iptables -t mangle -A SING_BOX -d ${hosts."${target_host}"}/32 -p udp -j TPROXY --on-port ${toString tproxyPort} --tproxy-mark 1
        iptables -t mangle -A PREROUTING -j SING_BOX

        iptables -t mangle -N SING_BOX_SELF
        iptables -t mangle -A SING_BOX_SELF -d 100.64.0.0/10 -j RETURN
        iptables -t mangle -A SING_BOX_SELF -d 127.0.0.0/8 -j RETURN
        iptables -t mangle -A SING_BOX_SELF -d 169.254.0.0/16 -j RETURN
        iptables -t mangle -A SING_BOX_SELF -d 172.16.0.0/12 -j RETURN
        iptables -t mangle -A SING_BOX_SELF -d 192.0.0.0/24 -j RETURN
        iptables -t mangle -A SING_BOX_SELF -d 224.0.0.0/4 -j RETURN
        iptables -t mangle -A SING_BOX_SELF -d 240.0.0.0/4 -j RETURN
        iptables -t mangle -A SING_BOX_SELF -d 255.255.255.255/32 -j RETURN
        iptables -t mangle -A SING_BOX_SELF  -j RETURN -m mark --mark 1234

        iptables -t mangle -A SING_BOX_SELF -d ${hosts."${server_host}"}/32 -p tcp -j RETURN
        iptables -t mangle -A SING_BOX_SELF -d ${hosts."${server_host}"}/32 -p udp -j RETURN
        iptables -t mangle -A SING_BOX_SELF -p tcp -j MARK --set-mark 1
        iptables -t mangle -A SING_BOX_SELF -p udp -j MARK --set-mark 1
        iptables -t mangle -A OUTPUT -j SING_BOX_SELF
      '';
    };
  in
  {

    name = "sing-box";

@@ -6,43 +113,427 @@ import ./make-test-python.nix ({ lib, pkgs, ... }: {
      maintainers = with lib.maintainers; [ nickcao ];
    };

  nodes.machine = { pkgs, ... }: {
    environment.systemPackages = [ pkgs.curl ];
    nodes = {
      target =
        { pkgs, ... }:
        {
          networking = {
            firewall.enable = false;
            hosts = hostsEntries;
            useDHCP = false;
            interfaces.eth1 = {
              ipv4.addresses = [
                {
                  address = hosts."${target_host}";
                  prefixLength = 24;
                }
              ];
            };
          };

          services.dnsmasq.enable = true;

          services.nginx = {
            enable = true;
      statusPage = true;
            package = pkgs.nginxQuic;

            virtualHosts."${target_host}" = {
              onlySSL = true;
              sslCertificate = ./common/acme/server/acme.test.cert.pem;
              sslCertificateKey = ./common/acme/server/acme.test.key.pem;
              http2 = true;
              http3 = true;
              http3_hq = false;
              quic = true;
              reuseport = true;
              locations."/" = {
                extraConfig = ''
                  default_type text/plain;
                  return 200 "$server_protocol $remote_addr";
                  allow ${hosts."${server_host}"}/32;
                  deny all;
                '';
              };
            };
          };
        };

      server =
        { pkgs, ... }:
        {
          boot.kernel.sysctl = {
            "net.ipv4.conf.all.forwarding" = 1;
          };

          networking = {
            firewall.enable = false;
            hosts = hostsEntries;
            useDHCP = false;
            interfaces.eth1 = {
              ipv4.addresses = [
                {
                  address = hosts."${server_host}";
                  prefixLength = 24;
                }
              ];
            };
          };

          systemd.network.wait-online.ignoredInterfaces = [ "wg0" ];

          networking.wg-quick.interfaces.wg0 = {
            address = [
              "10.23.42.1/24"
            ];
            listenPort = 2408;
            mtu = 1500;

            inherit (wg-keys.peer0) privateKey;

            peers = lib.singleton {
              allowedIPs = [
                "10.23.42.2/32"
              ];

              inherit (wg-keys.peer1) publicKey;
            };

            postUp = ''
              ${pkgs.iptables}/bin/iptables -A FORWARD -i wg0 -j ACCEPT
              ${pkgs.iptables}/bin/iptables -t nat -A POSTROUTING -s 10.23.42.0/24 -o eth1 -j MASQUERADE
            '';
          };

          services.sing-box = {
            enable = true;
            settings = {
              inbounds = [
                vmessInbound
              ];
              outbounds = [
                {
                  type = "direct";
                  tag = "outbound:direct";
                }
              ];
            };
          };
        };

      tun =
        { pkgs, ... }:
        {
          networking = {
            firewall.enable = false;
            hosts = hostsEntries;
            useDHCP = false;
            interfaces.eth1 = {
              ipv4.addresses = [
                {
                  address = "1.1.1.3";
                  prefixLength = 24;
                }
              ];
            };
          };

          security.pki.certificates = [
            (builtins.readFile ./common/acme/server/ca.cert.pem)
          ];

          environment.systemPackages = [
            pkgs.curlHTTP3
            pkgs.iproute2
          ];

          services.sing-box = {
            enable = true;
            settings = {
              inbounds = [
                tunInbound
              ];
              outbounds = [
                {
                  type = "block";
                  tag = "outbound:block";
                }
                {
                  type = "direct";
                  tag = "outbound:direct";
                }
                vmessOutbound
              ];
              route = {
                final = "outbound:block";
                rules = [
                  {
                    inbound = [
                      "inbound:tun"
                    ];
                    outbound = "outbound:vmess";
                  }
                ];
              };
            };
          };
        };

      wireguard =
        { pkgs, ... }:
        {
          networking = {
            firewall.enable = false;
            hosts = hostsEntries;
            useDHCP = false;
            interfaces.eth1 = {
              ipv4.addresses = [
                {
                  address = "1.1.1.4";
                  prefixLength = 24;
                }
              ];
            };
          };

          security.pki.certificates = [
            (builtins.readFile ./common/acme/server/ca.cert.pem)
          ];

          environment.systemPackages = [
            pkgs.curlHTTP3
            pkgs.iproute2
          ];

          services.sing-box = {
            enable = true;
            settings = {
              outbounds = [
                {
                  type = "block";
                  tag = "outbound:block";
                }
                {
                  type = "direct";
                  tag = "outbound:direct";
                }
                {
                  detour = "outbound:direct";
                  type = "wireguard";
                  tag = "outbound:wireguard";
                  interface_name = "wg0";
                  local_address = [ "10.23.42.2/32" ];
                  mtu = 1280;
                  private_key = wg-keys.peer1.privateKey;
                  peer_public_key = wg-keys.peer0.publicKey;
                  server = server_host;
                  server_port = 2408;
                  system_interface = true;
                }
              ];
              route = {
                final = "outbound:block";
              };
            };
          };
        };

      tproxy =
        { pkgs, ... }:
        {
          networking = {
            firewall.enable = false;
            hosts = hostsEntries;
            useDHCP = false;
            interfaces.eth1 = {
              ipv4.addresses = [
                {
                  address = "1.1.1.5";
                  prefixLength = 24;
                }
              ];
            };
          };

          security.pki.certificates = [
            (builtins.readFile ./common/acme/server/ca.cert.pem)
          ];

          environment.systemPackages = [ pkgs.curlHTTP3 ];

          systemd.services.sing-box.serviceConfig.ExecStartPost = [
            "+${tproxyPost}/bin/exe"
          ];

          services.sing-box = {
            enable = true;
            settings = {
              inbounds = [
                {
                  tag = "inbound:tproxy";
                  type = "tproxy";
                  listen = "0.0.0.0";
                  listen_port = tproxyPort;
                  udp_fragment = true;
                  sniff = true;
                  sniff_override_destination = false;
                }
              ];
              outbounds = [
                {
                  type = "block";
                  tag = "outbound:block";
                }
                {
                  type = "direct";
                  tag = "outbound:direct";
                }
                vmessOutbound
              ];
              route = {
                final = "outbound:block";
                rules = [
                  {
                    inbound = [
                      "inbound:tproxy"
                    ];
                    outbound = "outbound:vmess";
                  }
                ];
              };
            };
          };
        };

      fakeip =
        { pkgs, ... }:
        {
          networking = {
            firewall.enable = false;
            hosts = hostsEntries;
            useDHCP = false;
            interfaces.eth1 = {
              ipv4.addresses = [
                {
                  address = "1.1.1.6";
                  prefixLength = 24;
                }
              ];
            };
          };

          environment.systemPackages = [ pkgs.dnsutils ];

          services.sing-box = {
            enable = true;
            settings = {
        inbounds = [{
          type = "mixed";
          tag = "inbound";
          listen = "127.0.0.1";
          listen_port = 1080;
          users = [{
            username = "user";
            password = { _secret = pkgs.writeText "password" "supersecret"; };
          }];
        }];
        outbounds = [{
              dns = {
                final = "dns:default";
                independent_cache = true;
                fakeip = {
                  enabled = true;
                  "inet4_range" = "198.18.0.0/16";
                };
                servers = [
                  {
                    detour = "outbound:direct";
                    tag = "dns:default";
                    address = hosts."${target_host}";
                  }
                  {
                    tag = "dns:fakeip";
                    address = "fakeip";
                  }
                ];
                rules = [
                  {
                    outbound = [ "any" ];
                    server = "dns:default";
                  }
                  {
                    query_type = [
                      "A"
                      "AAAA"
                    ];
                    server = "dns:fakeip";

                  }
                ];
              };
              inbounds = [
                tunInbound
              ];
              outbounds = [
                {
                  type = "block";
                  tag = "outbound:block";
                }
                {
                  type = "direct";
          tag = "outbound";
        }];
                  tag = "outbound:direct";
                }
                {
                  type = "dns";
                  tag = "outbound:dns";
                }
              ];
              route = {
                final = "outbound:direct";
                rules = [
                  {
                    protocol = "dns";
                    outbound = "outbound:dns";
                  }
                ];
              };
            };
          };
        };
    };

    testScript = ''
    machine.wait_for_unit("nginx.service")
    machine.wait_for_unit("sing-box.service")
      target.wait_for_unit("nginx.service")
      target.wait_for_open_port(443)
      target.wait_for_unit("dnsmasq.service")
      target.wait_for_open_port(53)

      server.wait_for_unit("sing-box.service")
      server.wait_for_open_port(1080)
      server.wait_for_unit("wg-quick-wg0.service")
      server.wait_for_file("/sys/class/net/wg0")

      def test_curl(machine, extra_args=""):
        assert (
          machine.succeed(f"curl --fail --max-time 10 --http2 https://${target_host} {extra_args}")
          == "HTTP/2.0 ${hosts.${server_host}}"
        )
        assert (
          machine.succeed(f"curl --fail --max-time 10 --http3-only https://${target_host} {extra_args}")
          == "HTTP/3.0 ${hosts.${server_host}}"
        )

      with subtest("tun"):
        tun.wait_for_unit("sing-box.service")
        tun.wait_for_unit("sys-devices-virtual-net-tun0.device")
        tun.wait_until_succeeds("ip route get ${hosts."${target_host}"} | grep 'dev tun0'")
        tun.succeed("ip addr show tun0")
        test_curl(tun)

      with subtest("wireguard"):
        wireguard.wait_for_unit("sing-box.service")
        wireguard.wait_for_unit("sys-devices-virtual-net-wg0.device")
        wireguard.succeed("ip addr show wg0")
        test_curl(wireguard, "--interface wg0")

    machine.wait_for_open_port(80)
    machine.wait_for_open_port(1080)
      with subtest("tproxy"):
        tproxy.wait_for_unit("sing-box.service")
        test_curl(tproxy)

    machine.succeed("curl --fail --max-time 10 --proxy http://user:supersecret@localhost:1080 http://localhost")
    machine.fail("curl --fail --max-time 10 --proxy http://user:supervillain@localhost:1080 http://localhost")
    machine.succeed("curl --fail --max-time 10 --proxy socks5://user:supersecret@localhost:1080 http://localhost")
      with subtest("fakeip"):
        fakeip.wait_for_unit("sing-box.service")
        fakeip.wait_for_unit("sys-devices-virtual-net-tun0.device")
        fakeip.wait_until_succeeds("ip route get ${hosts."${target_host}"} | grep 'dev tun0'")
        fakeip.succeed("dig +short A ${target_host} @${target_host} | grep '^198.18.'")
    '';

})
  }
)