|
|
@@ -0,0 +1,1396 @@
|
|
|
+package sudoku
|
|
|
+
|
|
|
+import (
|
|
|
+ "bytes"
|
|
|
+ "crypto/ecdh"
|
|
|
+ "crypto/rand"
|
|
|
+ cryptotls "crypto/tls"
|
|
|
+ "encoding/base64"
|
|
|
+ "encoding/hex"
|
|
|
+ "fmt"
|
|
|
+ "io"
|
|
|
+ stdnet "net"
|
|
|
+ "os"
|
|
|
+ "os/exec"
|
|
|
+ "os/signal"
|
|
|
+ "path/filepath"
|
|
|
+ "sync"
|
|
|
+ "syscall"
|
|
|
+ "testing"
|
|
|
+ "time"
|
|
|
+
|
|
|
+ "github.com/xtls/xray-core/app/dispatcher"
|
|
|
+ "github.com/xtls/xray-core/app/log"
|
|
|
+ "github.com/xtls/xray-core/app/proxyman"
|
|
|
+ clog "github.com/xtls/xray-core/common/log"
|
|
|
+ xnet "github.com/xtls/xray-core/common/net"
|
|
|
+ "github.com/xtls/xray-core/common/protocol"
|
|
|
+ "github.com/xtls/xray-core/common/protocol/tls/cert"
|
|
|
+ "github.com/xtls/xray-core/common/serial"
|
|
|
+ "github.com/xtls/xray-core/common/uuid"
|
|
|
+ core "github.com/xtls/xray-core/core"
|
|
|
+ "github.com/xtls/xray-core/proxy/dokodemo"
|
|
|
+ "github.com/xtls/xray-core/proxy/freedom"
|
|
|
+ hyproxy "github.com/xtls/xray-core/proxy/hysteria"
|
|
|
+ hyaccount "github.com/xtls/xray-core/proxy/hysteria/account"
|
|
|
+ "github.com/xtls/xray-core/proxy/vless"
|
|
|
+ vin "github.com/xtls/xray-core/proxy/vless/inbound"
|
|
|
+ vout "github.com/xtls/xray-core/proxy/vless/outbound"
|
|
|
+ testingtcp "github.com/xtls/xray-core/testing/servers/tcp"
|
|
|
+ "github.com/xtls/xray-core/transport/internet"
|
|
|
+ hytransport "github.com/xtls/xray-core/transport/internet/hysteria"
|
|
|
+ "github.com/xtls/xray-core/transport/internet/reality"
|
|
|
+ splithttp "github.com/xtls/xray-core/transport/internet/splithttp"
|
|
|
+ transtcp "github.com/xtls/xray-core/transport/internet/tcp"
|
|
|
+ xtls "github.com/xtls/xray-core/transport/internet/tls"
|
|
|
+ "google.golang.org/protobuf/proto"
|
|
|
+)
|
|
|
+
|
|
|
+var (
|
|
|
+ e2eBinaryOnce sync.Once
|
|
|
+ e2eBinaryPath string
|
|
|
+ e2eBinaryErr error
|
|
|
+)
|
|
|
+
|
|
|
+type trafficMode struct {
|
|
|
+ name string
|
|
|
+ config *Config
|
|
|
+}
|
|
|
+
|
|
|
+type protocolCase struct {
|
|
|
+ name string
|
|
|
+ transport string
|
|
|
+ run func(t *testing.T, bin string, mode trafficMode) caseResult
|
|
|
+}
|
|
|
+
|
|
|
+type caseResult struct {
|
|
|
+ Protocol string
|
|
|
+ Mode string
|
|
|
+ TotalBytes int
|
|
|
+ ASCIIBytes int
|
|
|
+ ASCIIRatio float64
|
|
|
+ AvgHammingOnes float64
|
|
|
+ RotationSeen int
|
|
|
+ RotationExpected int
|
|
|
+ DecodedUnits int
|
|
|
+ ClientToServer directionResult
|
|
|
+ ServerToClient directionResult
|
|
|
+}
|
|
|
+
|
|
|
+type directionResult struct {
|
|
|
+ RawBytes int
|
|
|
+ ASCIIBytes int
|
|
|
+ ASCIIRatio float64
|
|
|
+ AvgHammingOnes float64
|
|
|
+ RotationSeen int
|
|
|
+ DecodedUnits int
|
|
|
+}
|
|
|
+
|
|
|
+type tcpRelay struct {
|
|
|
+ listener stdnet.Listener
|
|
|
+ target string
|
|
|
+
|
|
|
+ mu sync.Mutex
|
|
|
+ captures []*tcpCapture
|
|
|
+ wg sync.WaitGroup
|
|
|
+ stopCh chan struct{}
|
|
|
+}
|
|
|
+
|
|
|
+type tcpCapture struct {
|
|
|
+ mu sync.Mutex
|
|
|
+ c2s []byte
|
|
|
+ s2c []byte
|
|
|
+}
|
|
|
+
|
|
|
+type udpRelay struct {
|
|
|
+ conn stdnet.PacketConn
|
|
|
+ target *stdnet.UDPAddr
|
|
|
+ clientMu sync.Mutex
|
|
|
+ client *stdnet.UDPAddr
|
|
|
+ stopCh chan struct{}
|
|
|
+ wg sync.WaitGroup
|
|
|
+ captureMu sync.Mutex
|
|
|
+ c2s [][]byte
|
|
|
+ s2c [][]byte
|
|
|
+}
|
|
|
+
|
|
|
+type tlsDecoy struct {
|
|
|
+ ln stdnet.Listener
|
|
|
+ done chan struct{}
|
|
|
+ wg sync.WaitGroup
|
|
|
+}
|
|
|
+
|
|
|
+func TestSudokuE2ETemp(t *testing.T) {
|
|
|
+ if testing.Short() {
|
|
|
+ t.Skip("skipping sudoku e2e harness in short mode")
|
|
|
+ }
|
|
|
+
|
|
|
+ bin := buildE2EBinary(t)
|
|
|
+ payloadSize := 192 * 1024
|
|
|
+ modes := []trafficMode{
|
|
|
+ {
|
|
|
+ name: "prefer_ascii",
|
|
|
+ config: &Config{
|
|
|
+ Password: "sudoku-e2e-shared-secret",
|
|
|
+ Ascii: "prefer_ascii",
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "prefer_entropy",
|
|
|
+ config: &Config{
|
|
|
+ Password: "sudoku-e2e-shared-secret",
|
|
|
+ Ascii: "prefer_entropy",
|
|
|
+ CustomTables: []string{
|
|
|
+ "xpxvvpvv",
|
|
|
+ "vxpvxvvp",
|
|
|
+ "pxvvxvvp",
|
|
|
+ "vpxvxvpv",
|
|
|
+ "xvpvvxpv",
|
|
|
+ "vvxpxpvv",
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ cases := []protocolCase{
|
|
|
+ {name: "vless-reality", transport: "tcp", run: func(t *testing.T, bin string, mode trafficMode) caseResult {
|
|
|
+ return runVLESSRealityCase(t, bin, mode, payloadSize)
|
|
|
+ }},
|
|
|
+ {name: "hysteria2", transport: "udp", run: func(t *testing.T, bin string, mode trafficMode) caseResult {
|
|
|
+ return runHysteria2Case(t, bin, mode, payloadSize)
|
|
|
+ }},
|
|
|
+ {name: "vless-enc", transport: "tcp", run: func(t *testing.T, bin string, mode trafficMode) caseResult {
|
|
|
+ return runVLesseEncCase(t, bin, mode, payloadSize)
|
|
|
+ }},
|
|
|
+ {name: "vless-xhttp", transport: "tcp", run: func(t *testing.T, bin string, mode trafficMode) caseResult {
|
|
|
+ return runVLESSXHTTPCase(t, bin, mode, payloadSize)
|
|
|
+ }},
|
|
|
+ }
|
|
|
+
|
|
|
+ results := make([]caseResult, 0, len(cases)*len(modes))
|
|
|
+ for _, tc := range cases {
|
|
|
+ tc := tc
|
|
|
+ t.Run(tc.name, func(t *testing.T) {
|
|
|
+ for _, mode := range modes {
|
|
|
+ mode := mode
|
|
|
+ t.Run(mode.name, func(t *testing.T) {
|
|
|
+ result := tc.run(t, bin, mode)
|
|
|
+ if mode.name == "prefer_ascii" && result.ASCIIRatio < 0.97 {
|
|
|
+ t.Fatalf("%s %s ascii ratio %.4f < 0.97", tc.name, mode.name, result.ASCIIRatio)
|
|
|
+ }
|
|
|
+ if mode.name == "prefer_entropy" {
|
|
|
+ if result.RotationSeen != result.RotationExpected {
|
|
|
+ t.Fatalf("%s %s saw %d/%d rotation tables", tc.name, mode.name, result.RotationSeen, result.RotationExpected)
|
|
|
+ }
|
|
|
+ if diff := result.AvgHammingOnes - 5.0; diff < -0.3 || diff > 0.3 {
|
|
|
+ t.Fatalf("%s %s average ones %.4f too far from 5", tc.name, mode.name, result.AvgHammingOnes)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ t.Logf(
|
|
|
+ "%s %s total=%d ascii=%.4f avg_ones=%.4f rotation=%d/%d c2s_ascii=%.4f s2c_ascii=%.4f",
|
|
|
+ tc.name,
|
|
|
+ mode.name,
|
|
|
+ result.TotalBytes,
|
|
|
+ result.ASCIIRatio,
|
|
|
+ result.AvgHammingOnes,
|
|
|
+ result.RotationSeen,
|
|
|
+ result.RotationExpected,
|
|
|
+ result.ClientToServer.ASCIIRatio,
|
|
|
+ result.ServerToClient.ASCIIRatio,
|
|
|
+ )
|
|
|
+ results = append(results, result)
|
|
|
+ })
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, result := range results {
|
|
|
+ t.Logf(
|
|
|
+ "summary protocol=%s mode=%s bytes=%d ascii=%.4f avg_ones=%.4f rotation=%d/%d decoded=%d",
|
|
|
+ result.Protocol,
|
|
|
+ result.Mode,
|
|
|
+ result.TotalBytes,
|
|
|
+ result.ASCIIRatio,
|
|
|
+ result.AvgHammingOnes,
|
|
|
+ result.RotationSeen,
|
|
|
+ result.RotationExpected,
|
|
|
+ result.DecodedUnits,
|
|
|
+ )
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func runVLESSRealityCase(t *testing.T, bin string, mode trafficMode, payloadSize int) caseResult {
|
|
|
+ backend := startXOREchoServer(t)
|
|
|
+ defer backend.Close()
|
|
|
+
|
|
|
+ decoyCert, _ := cert.MustGenerate(nil, cert.CommonName("localhost"), cert.DNSNames("localhost"))
|
|
|
+ decoy := startTLSEchoDecoy(t, decoyCert)
|
|
|
+ defer decoy.Close()
|
|
|
+
|
|
|
+ serverPort := testingtcp.PickPort()
|
|
|
+ relayPort := testingtcp.PickPort()
|
|
|
+ clientPort := testingtcp.PickPort()
|
|
|
+
|
|
|
+ relay := startTCPRelay(t, int(relayPort), fmt.Sprintf("127.0.0.1:%d", serverPort))
|
|
|
+ defer relay.Close()
|
|
|
+
|
|
|
+ userID := protocol.NewID(uuid.New())
|
|
|
+ realityPriv, realityPub := mustX25519Keypair(t)
|
|
|
+ shortID := mustDecodeHex(t, "0123456789abcdef")
|
|
|
+
|
|
|
+ serverConfig := defaultApps(&core.Config{
|
|
|
+ Inbound: []*core.InboundHandlerConfig{
|
|
|
+ {
|
|
|
+ ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
|
|
|
+ PortList: &xnet.PortList{Range: []*xnet.PortRange{xnet.SinglePortRange(serverPort)}},
|
|
|
+ Listen: xnet.NewIPOrDomain(xnet.LocalHostIP),
|
|
|
+ StreamSettings: &internet.StreamConfig{
|
|
|
+ ProtocolName: "tcp",
|
|
|
+ TransportSettings: []*internet.TransportConfig{
|
|
|
+ {
|
|
|
+ ProtocolName: "tcp",
|
|
|
+ Settings: serial.ToTypedMessage(&transtcp.Config{}),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ SecurityType: serial.GetMessageType(&reality.Config{}),
|
|
|
+ SecuritySettings: []*serial.TypedMessage{
|
|
|
+ serial.ToTypedMessage(&reality.Config{
|
|
|
+ Show: true,
|
|
|
+ Dest: fmt.Sprintf("localhost:%d", decoy.Port()),
|
|
|
+ ServerNames: []string{"localhost"},
|
|
|
+ PrivateKey: realityPriv,
|
|
|
+ ShortIds: [][]byte{shortID},
|
|
|
+ Type: "tcp",
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ Tcpmasks: []*serial.TypedMessage{serial.ToTypedMessage(cloneConfig(mode.config))},
|
|
|
+ },
|
|
|
+ }),
|
|
|
+ ProxySettings: serial.ToTypedMessage(&vin.Config{
|
|
|
+ Clients: []*protocol.User{
|
|
|
+ {
|
|
|
+ Account: serial.ToTypedMessage(&vless.Account{
|
|
|
+ Id: userID.String(),
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ Outbound: []*core.OutboundHandlerConfig{
|
|
|
+ {ProxySettings: serial.ToTypedMessage(&freedom.Config{})},
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ clientConfig := defaultApps(&core.Config{
|
|
|
+ Inbound: []*core.InboundHandlerConfig{
|
|
|
+ {
|
|
|
+ ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
|
|
|
+ PortList: &xnet.PortList{Range: []*xnet.PortRange{xnet.SinglePortRange(clientPort)}},
|
|
|
+ Listen: xnet.NewIPOrDomain(xnet.LocalHostIP),
|
|
|
+ }),
|
|
|
+ ProxySettings: serial.ToTypedMessage(&dokodemo.Config{
|
|
|
+ Address: xnet.NewIPOrDomain(backend.Address()),
|
|
|
+ Port: uint32(backend.Port()),
|
|
|
+ Networks: []xnet.Network{xnet.Network_TCP},
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ Outbound: []*core.OutboundHandlerConfig{
|
|
|
+ {
|
|
|
+ ProxySettings: serial.ToTypedMessage(&vout.Config{
|
|
|
+ Vnext: &protocol.ServerEndpoint{
|
|
|
+ Address: xnet.NewIPOrDomain(xnet.LocalHostIP),
|
|
|
+ Port: uint32(relayPort),
|
|
|
+ User: &protocol.User{
|
|
|
+ Account: serial.ToTypedMessage(&vless.Account{
|
|
|
+ Id: userID.String(),
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ }),
|
|
|
+ SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{
|
|
|
+ StreamSettings: &internet.StreamConfig{
|
|
|
+ ProtocolName: "tcp",
|
|
|
+ TransportSettings: []*internet.TransportConfig{
|
|
|
+ {
|
|
|
+ ProtocolName: "tcp",
|
|
|
+ Settings: serial.ToTypedMessage(&transtcp.Config{}),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ SecurityType: serial.GetMessageType(&reality.Config{}),
|
|
|
+ SecuritySettings: []*serial.TypedMessage{
|
|
|
+ serial.ToTypedMessage(&reality.Config{
|
|
|
+ Show: true,
|
|
|
+ Fingerprint: "chrome",
|
|
|
+ ServerName: "localhost",
|
|
|
+ PublicKey: realityPub,
|
|
|
+ ShortId: shortID,
|
|
|
+ SpiderX: "/",
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ Tcpmasks: []*serial.TypedMessage{serial.ToTypedMessage(cloneConfig(mode.config))},
|
|
|
+ },
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ serverCmd, clientCmd := runXrayPair(t, bin, serverConfig, clientConfig)
|
|
|
+ defer stopCmd(clientCmd)
|
|
|
+ defer stopCmd(serverCmd)
|
|
|
+ exerciseTCPClient(t, int(clientPort), payloadSize)
|
|
|
+
|
|
|
+ return analyzeTCPRelay(t, "vless-reality", mode, relay.Snapshots())
|
|
|
+}
|
|
|
+
|
|
|
+func runHysteria2Case(t *testing.T, bin string, mode trafficMode, payloadSize int) caseResult {
|
|
|
+ backend := startXOREchoServer(t)
|
|
|
+ defer backend.Close()
|
|
|
+
|
|
|
+ serverPort := testingtcp.PickPort()
|
|
|
+ relayPort := testingtcp.PickPort()
|
|
|
+ clientPort := testingtcp.PickPort()
|
|
|
+
|
|
|
+ relay := startUDPRelay(t, int(relayPort), int(serverPort))
|
|
|
+ defer relay.Close()
|
|
|
+
|
|
|
+ ct, ctHash := cert.MustGenerate(nil, cert.CommonName("localhost"), cert.DNSNames("localhost"))
|
|
|
+ auth := "hy2-auth-secret"
|
|
|
+
|
|
|
+ serverConfig := defaultApps(&core.Config{
|
|
|
+ Inbound: []*core.InboundHandlerConfig{
|
|
|
+ {
|
|
|
+ ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
|
|
|
+ PortList: &xnet.PortList{Range: []*xnet.PortRange{xnet.SinglePortRange(serverPort)}},
|
|
|
+ Listen: xnet.NewIPOrDomain(xnet.LocalHostIP),
|
|
|
+ StreamSettings: &internet.StreamConfig{
|
|
|
+ ProtocolName: "hysteria",
|
|
|
+ TransportSettings: []*internet.TransportConfig{
|
|
|
+ {
|
|
|
+ ProtocolName: "hysteria",
|
|
|
+ Settings: serial.ToTypedMessage(&hytransport.Config{
|
|
|
+ Version: 2,
|
|
|
+ Auth: auth,
|
|
|
+ Congestion: "bbr",
|
|
|
+ Up: 10 * 1024 * 1024,
|
|
|
+ Down: 10 * 1024 * 1024,
|
|
|
+ UdpIdleTimeout: 60,
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ SecurityType: serial.GetMessageType(&xtls.Config{}),
|
|
|
+ SecuritySettings: []*serial.TypedMessage{
|
|
|
+ serial.ToTypedMessage(&xtls.Config{
|
|
|
+ Certificate: []*xtls.Certificate{xtls.ParseCertificate(ct)},
|
|
|
+ NextProtocol: []string{"h3"},
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ Udpmasks: []*serial.TypedMessage{serial.ToTypedMessage(cloneConfig(mode.config))},
|
|
|
+ },
|
|
|
+ }),
|
|
|
+ ProxySettings: serial.ToTypedMessage(&hyproxy.ServerConfig{
|
|
|
+ Users: []*protocol.User{
|
|
|
+ {
|
|
|
+ Account: serial.ToTypedMessage(&hyaccount.Account{Auth: auth}),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ Outbound: []*core.OutboundHandlerConfig{
|
|
|
+ {ProxySettings: serial.ToTypedMessage(&freedom.Config{})},
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ clientConfig := defaultApps(&core.Config{
|
|
|
+ Inbound: []*core.InboundHandlerConfig{
|
|
|
+ {
|
|
|
+ ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
|
|
|
+ PortList: &xnet.PortList{Range: []*xnet.PortRange{xnet.SinglePortRange(clientPort)}},
|
|
|
+ Listen: xnet.NewIPOrDomain(xnet.LocalHostIP),
|
|
|
+ }),
|
|
|
+ ProxySettings: serial.ToTypedMessage(&dokodemo.Config{
|
|
|
+ Address: xnet.NewIPOrDomain(backend.Address()),
|
|
|
+ Port: uint32(backend.Port()),
|
|
|
+ Networks: []xnet.Network{xnet.Network_TCP},
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ Outbound: []*core.OutboundHandlerConfig{
|
|
|
+ {
|
|
|
+ ProxySettings: serial.ToTypedMessage(&hyproxy.ClientConfig{
|
|
|
+ Version: 2,
|
|
|
+ Server: &protocol.ServerEndpoint{
|
|
|
+ Address: xnet.NewIPOrDomain(xnet.LocalHostIP),
|
|
|
+ Port: uint32(relayPort),
|
|
|
+ User: &protocol.User{
|
|
|
+ Account: serial.ToTypedMessage(&hyaccount.Account{Auth: auth}),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ }),
|
|
|
+ SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{
|
|
|
+ StreamSettings: &internet.StreamConfig{
|
|
|
+ ProtocolName: "hysteria",
|
|
|
+ TransportSettings: []*internet.TransportConfig{
|
|
|
+ {
|
|
|
+ ProtocolName: "hysteria",
|
|
|
+ Settings: serial.ToTypedMessage(&hytransport.Config{
|
|
|
+ Version: 2,
|
|
|
+ Auth: auth,
|
|
|
+ Congestion: "bbr",
|
|
|
+ Up: 10 * 1024 * 1024,
|
|
|
+ Down: 10 * 1024 * 1024,
|
|
|
+ UdpIdleTimeout: 60,
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ SecurityType: serial.GetMessageType(&xtls.Config{}),
|
|
|
+ SecuritySettings: []*serial.TypedMessage{
|
|
|
+ serial.ToTypedMessage(&xtls.Config{
|
|
|
+ ServerName: "localhost",
|
|
|
+ PinnedPeerCertSha256: [][]byte{ctHash[:]},
|
|
|
+ NextProtocol: []string{"h3"},
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ Udpmasks: []*serial.TypedMessage{serial.ToTypedMessage(cloneConfig(mode.config))},
|
|
|
+ },
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ serverCmd, clientCmd := runXrayPair(t, bin, serverConfig, clientConfig)
|
|
|
+ defer stopCmd(clientCmd)
|
|
|
+ defer stopCmd(serverCmd)
|
|
|
+ if err := exerciseTCPClientErr(t, int(clientPort), payloadSize); err != nil {
|
|
|
+ c2s, s2c := relay.Snapshots()
|
|
|
+ t.Fatalf("hy2 traffic failed: %v (udp packets c2s=%d s2c=%d first_c2s=%d first_s2c=%d)", err, len(c2s), len(s2c), firstChunkLen(c2s), firstChunkLen(s2c))
|
|
|
+ }
|
|
|
+
|
|
|
+ c2s, s2c := relay.Snapshots()
|
|
|
+ return analyzeUDPRelay(t, "hysteria2", mode, c2s, s2c)
|
|
|
+}
|
|
|
+
|
|
|
+func runVLesseEncCase(t *testing.T, bin string, mode trafficMode, payloadSize int) caseResult {
|
|
|
+ backend := startXOREchoServer(t)
|
|
|
+ defer backend.Close()
|
|
|
+
|
|
|
+ serverPort := testingtcp.PickPort()
|
|
|
+ relayPort := testingtcp.PickPort()
|
|
|
+ clientPort := testingtcp.PickPort()
|
|
|
+
|
|
|
+ relay := startTCPRelay(t, int(relayPort), fmt.Sprintf("127.0.0.1:%d", serverPort))
|
|
|
+ defer relay.Close()
|
|
|
+
|
|
|
+ userID := protocol.NewID(uuid.New())
|
|
|
+ priv, pub := mustX25519Keypair(t)
|
|
|
+ pubB64 := base64.RawURLEncoding.EncodeToString(pub)
|
|
|
+ privB64 := base64.RawURLEncoding.EncodeToString(priv)
|
|
|
+
|
|
|
+ serverConfig := defaultApps(&core.Config{
|
|
|
+ Inbound: []*core.InboundHandlerConfig{
|
|
|
+ {
|
|
|
+ ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
|
|
|
+ PortList: &xnet.PortList{Range: []*xnet.PortRange{xnet.SinglePortRange(serverPort)}},
|
|
|
+ Listen: xnet.NewIPOrDomain(xnet.LocalHostIP),
|
|
|
+ StreamSettings: &internet.StreamConfig{
|
|
|
+ ProtocolName: "tcp",
|
|
|
+ TransportSettings: []*internet.TransportConfig{
|
|
|
+ {ProtocolName: "tcp", Settings: serial.ToTypedMessage(&transtcp.Config{})},
|
|
|
+ },
|
|
|
+ Tcpmasks: []*serial.TypedMessage{serial.ToTypedMessage(cloneConfig(mode.config))},
|
|
|
+ },
|
|
|
+ }),
|
|
|
+ ProxySettings: serial.ToTypedMessage(&vin.Config{
|
|
|
+ Clients: []*protocol.User{
|
|
|
+ {
|
|
|
+ Account: serial.ToTypedMessage(&vless.Account{
|
|
|
+ Id: userID.String(),
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ Decryption: privB64,
|
|
|
+ XorMode: 1,
|
|
|
+ SecondsFrom: 0,
|
|
|
+ SecondsTo: 0,
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ Outbound: []*core.OutboundHandlerConfig{
|
|
|
+ {ProxySettings: serial.ToTypedMessage(&freedom.Config{})},
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ clientConfig := defaultApps(&core.Config{
|
|
|
+ Inbound: []*core.InboundHandlerConfig{
|
|
|
+ {
|
|
|
+ ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
|
|
|
+ PortList: &xnet.PortList{Range: []*xnet.PortRange{xnet.SinglePortRange(clientPort)}},
|
|
|
+ Listen: xnet.NewIPOrDomain(xnet.LocalHostIP),
|
|
|
+ }),
|
|
|
+ ProxySettings: serial.ToTypedMessage(&dokodemo.Config{
|
|
|
+ Address: xnet.NewIPOrDomain(backend.Address()),
|
|
|
+ Port: uint32(backend.Port()),
|
|
|
+ Networks: []xnet.Network{xnet.Network_TCP},
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ Outbound: []*core.OutboundHandlerConfig{
|
|
|
+ {
|
|
|
+ ProxySettings: serial.ToTypedMessage(&vout.Config{
|
|
|
+ Vnext: &protocol.ServerEndpoint{
|
|
|
+ Address: xnet.NewIPOrDomain(xnet.LocalHostIP),
|
|
|
+ Port: uint32(relayPort),
|
|
|
+ User: &protocol.User{
|
|
|
+ Account: serial.ToTypedMessage(&vless.Account{
|
|
|
+ Id: userID.String(),
|
|
|
+ Encryption: pubB64,
|
|
|
+ XorMode: 1,
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ }),
|
|
|
+ SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{
|
|
|
+ StreamSettings: &internet.StreamConfig{
|
|
|
+ ProtocolName: "tcp",
|
|
|
+ TransportSettings: []*internet.TransportConfig{
|
|
|
+ {ProtocolName: "tcp", Settings: serial.ToTypedMessage(&transtcp.Config{})},
|
|
|
+ },
|
|
|
+ Tcpmasks: []*serial.TypedMessage{serial.ToTypedMessage(cloneConfig(mode.config))},
|
|
|
+ },
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ serverCmd, clientCmd := runXrayPair(t, bin, serverConfig, clientConfig)
|
|
|
+ defer stopCmd(clientCmd)
|
|
|
+ defer stopCmd(serverCmd)
|
|
|
+ exerciseTCPClient(t, int(clientPort), payloadSize)
|
|
|
+
|
|
|
+ return analyzeTCPRelay(t, "vless-enc", mode, relay.Snapshots())
|
|
|
+}
|
|
|
+
|
|
|
+func runVLESSXHTTPCase(t *testing.T, bin string, mode trafficMode, payloadSize int) caseResult {
|
|
|
+ backend := startXOREchoServer(t)
|
|
|
+ defer backend.Close()
|
|
|
+
|
|
|
+ serverPort := testingtcp.PickPort()
|
|
|
+ relayPort := testingtcp.PickPort()
|
|
|
+ clientPort := testingtcp.PickPort()
|
|
|
+
|
|
|
+ relay := startTCPRelay(t, int(relayPort), fmt.Sprintf("127.0.0.1:%d", serverPort))
|
|
|
+ defer relay.Close()
|
|
|
+
|
|
|
+ userID := protocol.NewID(uuid.New())
|
|
|
+ xhttpConfig := &splithttp.Config{
|
|
|
+ Host: "localhost",
|
|
|
+ Path: "/sudoku",
|
|
|
+ Mode: "auto",
|
|
|
+ }
|
|
|
+
|
|
|
+ serverConfig := defaultApps(&core.Config{
|
|
|
+ Inbound: []*core.InboundHandlerConfig{
|
|
|
+ {
|
|
|
+ ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
|
|
|
+ PortList: &xnet.PortList{Range: []*xnet.PortRange{xnet.SinglePortRange(serverPort)}},
|
|
|
+ Listen: xnet.NewIPOrDomain(xnet.LocalHostIP),
|
|
|
+ StreamSettings: &internet.StreamConfig{
|
|
|
+ ProtocolName: "splithttp",
|
|
|
+ TransportSettings: []*internet.TransportConfig{
|
|
|
+ {ProtocolName: "splithttp", Settings: serial.ToTypedMessage(xhttpConfig)},
|
|
|
+ },
|
|
|
+ Tcpmasks: []*serial.TypedMessage{serial.ToTypedMessage(cloneConfig(mode.config))},
|
|
|
+ },
|
|
|
+ }),
|
|
|
+ ProxySettings: serial.ToTypedMessage(&vin.Config{
|
|
|
+ Clients: []*protocol.User{
|
|
|
+ {
|
|
|
+ Account: serial.ToTypedMessage(&vless.Account{
|
|
|
+ Id: userID.String(),
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ Outbound: []*core.OutboundHandlerConfig{
|
|
|
+ {ProxySettings: serial.ToTypedMessage(&freedom.Config{})},
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ clientConfig := defaultApps(&core.Config{
|
|
|
+ Inbound: []*core.InboundHandlerConfig{
|
|
|
+ {
|
|
|
+ ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
|
|
|
+ PortList: &xnet.PortList{Range: []*xnet.PortRange{xnet.SinglePortRange(clientPort)}},
|
|
|
+ Listen: xnet.NewIPOrDomain(xnet.LocalHostIP),
|
|
|
+ }),
|
|
|
+ ProxySettings: serial.ToTypedMessage(&dokodemo.Config{
|
|
|
+ Address: xnet.NewIPOrDomain(backend.Address()),
|
|
|
+ Port: uint32(backend.Port()),
|
|
|
+ Networks: []xnet.Network{xnet.Network_TCP},
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ Outbound: []*core.OutboundHandlerConfig{
|
|
|
+ {
|
|
|
+ ProxySettings: serial.ToTypedMessage(&vout.Config{
|
|
|
+ Vnext: &protocol.ServerEndpoint{
|
|
|
+ Address: xnet.NewIPOrDomain(xnet.LocalHostIP),
|
|
|
+ Port: uint32(relayPort),
|
|
|
+ User: &protocol.User{
|
|
|
+ Account: serial.ToTypedMessage(&vless.Account{
|
|
|
+ Id: userID.String(),
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ }),
|
|
|
+ SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{
|
|
|
+ StreamSettings: &internet.StreamConfig{
|
|
|
+ ProtocolName: "splithttp",
|
|
|
+ TransportSettings: []*internet.TransportConfig{
|
|
|
+ {ProtocolName: "splithttp", Settings: serial.ToTypedMessage(xhttpConfig)},
|
|
|
+ },
|
|
|
+ Tcpmasks: []*serial.TypedMessage{serial.ToTypedMessage(cloneConfig(mode.config))},
|
|
|
+ },
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ serverCmd, clientCmd := runXrayPair(t, bin, serverConfig, clientConfig)
|
|
|
+ defer stopCmd(clientCmd)
|
|
|
+ defer stopCmd(serverCmd)
|
|
|
+ exerciseTCPClient(t, int(clientPort), payloadSize)
|
|
|
+
|
|
|
+ return analyzeTCPRelay(t, "vless-xhttp", mode, relay.Snapshots())
|
|
|
+}
|
|
|
+
|
|
|
+func analyzeTCPRelay(t *testing.T, protocol string, mode trafficMode, captures []*tcpCapture) caseResult {
|
|
|
+ tables, err := getTables(mode.config)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+
|
|
|
+ allC2S := make([][]byte, 0, len(captures))
|
|
|
+ allS2C := make([][]byte, 0, len(captures))
|
|
|
+ for _, capture := range captures {
|
|
|
+ c2s, s2c := capture.snapshot()
|
|
|
+ if len(c2s) > 0 {
|
|
|
+ allC2S = append(allC2S, c2s)
|
|
|
+ }
|
|
|
+ if len(s2c) > 0 {
|
|
|
+ allS2C = append(allS2C, s2c)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ c2sMetrics := metricFromBytes(flattenChunks(allC2S))
|
|
|
+ s2cMetrics := metricFromBytes(flattenChunks(allS2C))
|
|
|
+
|
|
|
+ c2sUsed, c2sDecoded, err := analyzePureChunks(tables, allC2S)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("%s %s pure decode failed: %v", protocol, mode.name, err)
|
|
|
+ }
|
|
|
+ s2cUsed, s2cDecoded, err := analyzePackedChunks(tables, allS2C)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("%s %s packed decode failed: %v", protocol, mode.name, err)
|
|
|
+ }
|
|
|
+
|
|
|
+ allBytes := append(append([]byte{}, flattenChunks(allC2S)...), flattenChunks(allS2C)...)
|
|
|
+ totalMetrics := metricFromBytes(allBytes)
|
|
|
+ rotationSeen := len(unionKeys(c2sUsed, s2cUsed))
|
|
|
+
|
|
|
+ return caseResult{
|
|
|
+ Protocol: protocol,
|
|
|
+ Mode: mode.name,
|
|
|
+ TotalBytes: len(allBytes),
|
|
|
+ ASCIIBytes: totalMetrics.asciiBytes,
|
|
|
+ ASCIIRatio: totalMetrics.asciiRatio,
|
|
|
+ AvgHammingOnes: totalMetrics.avgOnes,
|
|
|
+ RotationSeen: rotationSeen,
|
|
|
+ RotationExpected: expectedRotation(mode.config),
|
|
|
+ DecodedUnits: c2sDecoded + s2cDecoded,
|
|
|
+ ClientToServer: directionResult{
|
|
|
+ RawBytes: len(flattenChunks(allC2S)),
|
|
|
+ ASCIIBytes: c2sMetrics.asciiBytes,
|
|
|
+ ASCIIRatio: c2sMetrics.asciiRatio,
|
|
|
+ AvgHammingOnes: c2sMetrics.avgOnes,
|
|
|
+ RotationSeen: len(c2sUsed),
|
|
|
+ DecodedUnits: c2sDecoded,
|
|
|
+ },
|
|
|
+ ServerToClient: directionResult{
|
|
|
+ RawBytes: len(flattenChunks(allS2C)),
|
|
|
+ ASCIIBytes: s2cMetrics.asciiBytes,
|
|
|
+ ASCIIRatio: s2cMetrics.asciiRatio,
|
|
|
+ AvgHammingOnes: s2cMetrics.avgOnes,
|
|
|
+ RotationSeen: len(s2cUsed),
|
|
|
+ DecodedUnits: s2cDecoded,
|
|
|
+ },
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func analyzeUDPRelay(t *testing.T, protocol string, mode trafficMode, c2s [][]byte, s2c [][]byte) caseResult {
|
|
|
+ tables, err := getTables(mode.config)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+
|
|
|
+ c2sMetrics := metricFromBytes(flattenChunks(c2s))
|
|
|
+ s2cMetrics := metricFromBytes(flattenChunks(s2c))
|
|
|
+
|
|
|
+ c2sUsed, c2sDecoded, err := analyzePureChunks(tables, c2s)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("%s %s udp c2s decode failed: %v", protocol, mode.name, err)
|
|
|
+ }
|
|
|
+ s2cUsed, s2cDecoded, err := analyzePureChunks(tables, s2c)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("%s %s udp s2c decode failed: %v", protocol, mode.name, err)
|
|
|
+ }
|
|
|
+
|
|
|
+ allBytes := append(append([]byte{}, flattenChunks(c2s)...), flattenChunks(s2c)...)
|
|
|
+ totalMetrics := metricFromBytes(allBytes)
|
|
|
+ rotationSeen := len(unionKeys(c2sUsed, s2cUsed))
|
|
|
+
|
|
|
+ return caseResult{
|
|
|
+ Protocol: protocol,
|
|
|
+ Mode: mode.name,
|
|
|
+ TotalBytes: len(allBytes),
|
|
|
+ ASCIIBytes: totalMetrics.asciiBytes,
|
|
|
+ ASCIIRatio: totalMetrics.asciiRatio,
|
|
|
+ AvgHammingOnes: totalMetrics.avgOnes,
|
|
|
+ RotationSeen: rotationSeen,
|
|
|
+ RotationExpected: expectedRotation(mode.config),
|
|
|
+ DecodedUnits: c2sDecoded + s2cDecoded,
|
|
|
+ ClientToServer: directionResult{
|
|
|
+ RawBytes: len(flattenChunks(c2s)),
|
|
|
+ ASCIIBytes: c2sMetrics.asciiBytes,
|
|
|
+ ASCIIRatio: c2sMetrics.asciiRatio,
|
|
|
+ AvgHammingOnes: c2sMetrics.avgOnes,
|
|
|
+ RotationSeen: len(c2sUsed),
|
|
|
+ DecodedUnits: c2sDecoded,
|
|
|
+ },
|
|
|
+ ServerToClient: directionResult{
|
|
|
+ RawBytes: len(flattenChunks(s2c)),
|
|
|
+ ASCIIBytes: s2cMetrics.asciiBytes,
|
|
|
+ ASCIIRatio: s2cMetrics.asciiRatio,
|
|
|
+ AvgHammingOnes: s2cMetrics.avgOnes,
|
|
|
+ RotationSeen: len(s2cUsed),
|
|
|
+ DecodedUnits: s2cDecoded,
|
|
|
+ },
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+type byteMetrics struct {
|
|
|
+ asciiBytes int
|
|
|
+ asciiRatio float64
|
|
|
+ avgOnes float64
|
|
|
+}
|
|
|
+
|
|
|
+func metricFromBytes(b []byte) byteMetrics {
|
|
|
+ if len(b) == 0 {
|
|
|
+ return byteMetrics{}
|
|
|
+ }
|
|
|
+ var ascii, ones int
|
|
|
+ for _, v := range b {
|
|
|
+ if v < 0x80 {
|
|
|
+ ascii++
|
|
|
+ }
|
|
|
+ ones += bitsInByte(v)
|
|
|
+ }
|
|
|
+ return byteMetrics{
|
|
|
+ asciiBytes: ascii,
|
|
|
+ asciiRatio: float64(ascii) / float64(len(b)),
|
|
|
+ avgOnes: float64(ones) / float64(len(b)),
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func bitsInByte(b byte) int {
|
|
|
+ n := 0
|
|
|
+ for b != 0 {
|
|
|
+ n += int(b & 1)
|
|
|
+ b >>= 1
|
|
|
+ }
|
|
|
+ return n
|
|
|
+}
|
|
|
+
|
|
|
+func analyzePureChunks(tables []*table, chunks [][]byte) (map[int]int, int, error) {
|
|
|
+ if len(tables) == 0 {
|
|
|
+ return nil, 0, fmt.Errorf("no sudoku tables")
|
|
|
+ }
|
|
|
+ used := make(map[int]int)
|
|
|
+ decoded := 0
|
|
|
+ for _, chunk := range chunks {
|
|
|
+ hintBuf := make([]byte, 0, 4)
|
|
|
+ tableIndex := 0
|
|
|
+ for _, b := range chunk {
|
|
|
+ t := tables[tableIndex%len(tables)]
|
|
|
+ if !t.layout.isHint(b) {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ hintBuf = append(hintBuf, b)
|
|
|
+ if len(hintBuf) < 4 {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ keyBytes := sort4([4]byte{hintBuf[0], hintBuf[1], hintBuf[2], hintBuf[3]})
|
|
|
+ key := packKey(keyBytes)
|
|
|
+ if _, ok := t.decode[key]; !ok {
|
|
|
+ return nil, 0, fmt.Errorf("invalid pure tuple at table %d", tableIndex%len(tables))
|
|
|
+ }
|
|
|
+ used[tableIndex%len(tables)]++
|
|
|
+ decoded++
|
|
|
+ tableIndex++
|
|
|
+ hintBuf = hintBuf[:0]
|
|
|
+ }
|
|
|
+ if len(hintBuf) != 0 {
|
|
|
+ return nil, 0, fmt.Errorf("leftover pure hints")
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return used, decoded, nil
|
|
|
+}
|
|
|
+
|
|
|
+func analyzePackedChunks(tables []*table, chunks [][]byte) (map[int]int, int, error) {
|
|
|
+ layouts := tablesToLayouts(tables)
|
|
|
+ if len(layouts) == 0 {
|
|
|
+ return nil, 0, fmt.Errorf("no sudoku layouts")
|
|
|
+ }
|
|
|
+ used := make(map[int]int)
|
|
|
+ decoded := 0
|
|
|
+ for _, chunk := range chunks {
|
|
|
+ var bitBuf uint64
|
|
|
+ var bitCount int
|
|
|
+ groupIndex := 0
|
|
|
+ for _, b := range chunk {
|
|
|
+ layout := layouts[groupIndex%len(layouts)]
|
|
|
+ if !layout.isHint(b) {
|
|
|
+ if b == layout.padMarker {
|
|
|
+ bitBuf = 0
|
|
|
+ bitCount = 0
|
|
|
+ }
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ group, ok := layout.decodeGroup(b)
|
|
|
+ if !ok {
|
|
|
+ return nil, 0, fmt.Errorf("invalid packed byte %d", b)
|
|
|
+ }
|
|
|
+ used[groupIndex%len(layouts)]++
|
|
|
+ groupIndex++
|
|
|
+ bitBuf = (bitBuf << 6) | uint64(group)
|
|
|
+ bitCount += 6
|
|
|
+ for bitCount >= 8 {
|
|
|
+ bitCount -= 8
|
|
|
+ decoded++
|
|
|
+ if bitCount > 0 {
|
|
|
+ bitBuf &= (uint64(1) << bitCount) - 1
|
|
|
+ } else {
|
|
|
+ bitBuf = 0
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return used, decoded, nil
|
|
|
+}
|
|
|
+
|
|
|
+func expectedRotation(cfg *Config) int {
|
|
|
+ tables, err := getTables(cfg)
|
|
|
+ if err != nil {
|
|
|
+ return 0
|
|
|
+ }
|
|
|
+ return len(tables)
|
|
|
+}
|
|
|
+
|
|
|
+func unionKeys(a, b map[int]int) map[int]struct{} {
|
|
|
+ out := make(map[int]struct{}, len(a)+len(b))
|
|
|
+ for k := range a {
|
|
|
+ out[k] = struct{}{}
|
|
|
+ }
|
|
|
+ for k := range b {
|
|
|
+ out[k] = struct{}{}
|
|
|
+ }
|
|
|
+ return out
|
|
|
+}
|
|
|
+
|
|
|
+func flattenChunks(chunks [][]byte) []byte {
|
|
|
+ total := 0
|
|
|
+ for _, chunk := range chunks {
|
|
|
+ total += len(chunk)
|
|
|
+ }
|
|
|
+ out := make([]byte, 0, total)
|
|
|
+ for _, chunk := range chunks {
|
|
|
+ out = append(out, chunk...)
|
|
|
+ }
|
|
|
+ return out
|
|
|
+}
|
|
|
+
|
|
|
+func cloneConfig(cfg *Config) *Config {
|
|
|
+ if cfg == nil {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ out := proto.Clone(cfg).(*Config)
|
|
|
+ return out
|
|
|
+}
|
|
|
+
|
|
|
+func defaultApps(cfg *core.Config) *core.Config {
|
|
|
+ cfg.App = append(cfg.App,
|
|
|
+ serial.ToTypedMessage(&log.Config{
|
|
|
+ ErrorLogLevel: clog.Severity_Warning,
|
|
|
+ ErrorLogType: log.LogType_Console,
|
|
|
+ }),
|
|
|
+ serial.ToTypedMessage(&dispatcher.Config{}),
|
|
|
+ serial.ToTypedMessage(&proxyman.InboundConfig{}),
|
|
|
+ serial.ToTypedMessage(&proxyman.OutboundConfig{}),
|
|
|
+ )
|
|
|
+ return cfg
|
|
|
+}
|
|
|
+
|
|
|
+func buildE2EBinary(t *testing.T) string {
|
|
|
+ t.Helper()
|
|
|
+ e2eBinaryOnce.Do(func() {
|
|
|
+ tempDir, err := os.MkdirTemp("", "xray-sudoku-e2e-*")
|
|
|
+ if err != nil {
|
|
|
+ e2eBinaryErr = err
|
|
|
+ return
|
|
|
+ }
|
|
|
+ e2eBinaryPath = filepath.Join(tempDir, "xray.test")
|
|
|
+ cmd := exec.Command("go", "build", "-o", e2eBinaryPath, "./main")
|
|
|
+ cmd.Dir = repoRoot(t)
|
|
|
+ cmd.Stdout = os.Stdout
|
|
|
+ cmd.Stderr = os.Stderr
|
|
|
+ e2eBinaryErr = cmd.Run()
|
|
|
+ })
|
|
|
+ if e2eBinaryErr != nil {
|
|
|
+ t.Fatal(e2eBinaryErr)
|
|
|
+ }
|
|
|
+ return e2eBinaryPath
|
|
|
+}
|
|
|
+
|
|
|
+func repoRoot(t *testing.T) string {
|
|
|
+ t.Helper()
|
|
|
+ dir, err := os.Getwd()
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ for {
|
|
|
+ if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
|
|
|
+ return dir
|
|
|
+ }
|
|
|
+ parent := filepath.Dir(dir)
|
|
|
+ if parent == dir {
|
|
|
+ t.Fatal("failed to locate repo root")
|
|
|
+ }
|
|
|
+ dir = parent
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func runXrayPair(t *testing.T, bin string, serverCfg, clientCfg *core.Config) (*exec.Cmd, *exec.Cmd) {
|
|
|
+ t.Helper()
|
|
|
+ serverCmd := runXray(t, bin, serverCfg)
|
|
|
+
|
|
|
+ time.Sleep(500 * time.Millisecond)
|
|
|
+
|
|
|
+ clientCmd := runXray(t, bin, clientCfg)
|
|
|
+
|
|
|
+ time.Sleep(1500 * time.Millisecond)
|
|
|
+ return serverCmd, clientCmd
|
|
|
+}
|
|
|
+
|
|
|
+func runXray(t *testing.T, bin string, cfg *core.Config) *exec.Cmd {
|
|
|
+ t.Helper()
|
|
|
+ cfgBytes, err := proto.Marshal(cfg)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ cmd := exec.Command(bin, "-config=stdin:", "-format=pb")
|
|
|
+ cmd.Stdin = bytes.NewReader(cfgBytes)
|
|
|
+ cmd.Stdout = os.Stdout
|
|
|
+ cmd.Stderr = os.Stderr
|
|
|
+ if err := cmd.Start(); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ return cmd
|
|
|
+}
|
|
|
+
|
|
|
+func stopCmd(cmd *exec.Cmd) {
|
|
|
+ if cmd == nil || cmd.Process == nil {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ _ = cmd.Process.Signal(syscall.SIGTERM)
|
|
|
+ done := make(chan struct{})
|
|
|
+ go func() {
|
|
|
+ _, _ = cmd.Process.Wait()
|
|
|
+ close(done)
|
|
|
+ }()
|
|
|
+ select {
|
|
|
+ case <-done:
|
|
|
+ case <-time.After(3 * time.Second):
|
|
|
+ _ = cmd.Process.Kill()
|
|
|
+ <-done
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func startTCPRelay(t *testing.T, listenPort int, target string) *tcpRelay {
|
|
|
+ t.Helper()
|
|
|
+ ln, err := stdnet.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", listenPort))
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ r := &tcpRelay{
|
|
|
+ listener: ln,
|
|
|
+ target: target,
|
|
|
+ stopCh: make(chan struct{}),
|
|
|
+ }
|
|
|
+ r.wg.Add(1)
|
|
|
+ go func() {
|
|
|
+ defer r.wg.Done()
|
|
|
+ for {
|
|
|
+ conn, err := ln.Accept()
|
|
|
+ if err != nil {
|
|
|
+ select {
|
|
|
+ case <-r.stopCh:
|
|
|
+ return
|
|
|
+ default:
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
+ targetConn, err := stdnet.Dial("tcp", target)
|
|
|
+ if err != nil {
|
|
|
+ _ = conn.Close()
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ capture := &tcpCapture{}
|
|
|
+ r.mu.Lock()
|
|
|
+ r.captures = append(r.captures, capture)
|
|
|
+ r.mu.Unlock()
|
|
|
+ r.wg.Add(1)
|
|
|
+ go func(client, server stdnet.Conn, cap *tcpCapture) {
|
|
|
+ defer r.wg.Done()
|
|
|
+ defer client.Close()
|
|
|
+ defer server.Close()
|
|
|
+ var inner sync.WaitGroup
|
|
|
+ inner.Add(2)
|
|
|
+ go func() {
|
|
|
+ defer inner.Done()
|
|
|
+ _, _ = io.Copy(server, io.TeeReader(client, &captureWriter{capture: cap, dir: "c2s"}))
|
|
|
+ if tcp, ok := server.(*stdnet.TCPConn); ok {
|
|
|
+ _ = tcp.CloseWrite()
|
|
|
+ }
|
|
|
+ }()
|
|
|
+ go func() {
|
|
|
+ defer inner.Done()
|
|
|
+ _, _ = io.Copy(client, io.TeeReader(server, &captureWriter{capture: cap, dir: "s2c"}))
|
|
|
+ if tcp, ok := client.(*stdnet.TCPConn); ok {
|
|
|
+ _ = tcp.CloseWrite()
|
|
|
+ }
|
|
|
+ }()
|
|
|
+ inner.Wait()
|
|
|
+ }(conn, targetConn, capture)
|
|
|
+ }
|
|
|
+ }()
|
|
|
+ return r
|
|
|
+}
|
|
|
+
|
|
|
+func (r *tcpRelay) Close() {
|
|
|
+ close(r.stopCh)
|
|
|
+ _ = r.listener.Close()
|
|
|
+ r.wg.Wait()
|
|
|
+}
|
|
|
+
|
|
|
+func (r *tcpRelay) Snapshots() []*tcpCapture {
|
|
|
+ r.mu.Lock()
|
|
|
+ defer r.mu.Unlock()
|
|
|
+ out := make([]*tcpCapture, 0, len(r.captures))
|
|
|
+ for _, capture := range r.captures {
|
|
|
+ out = append(out, capture)
|
|
|
+ }
|
|
|
+ return out
|
|
|
+}
|
|
|
+
|
|
|
+func (c *tcpCapture) snapshot() ([]byte, []byte) {
|
|
|
+ c.mu.Lock()
|
|
|
+ defer c.mu.Unlock()
|
|
|
+ return append([]byte{}, c.c2s...), append([]byte{}, c.s2c...)
|
|
|
+}
|
|
|
+
|
|
|
+type captureWriter struct {
|
|
|
+ capture *tcpCapture
|
|
|
+ dir string
|
|
|
+}
|
|
|
+
|
|
|
+func (w *captureWriter) Write(p []byte) (int, error) {
|
|
|
+ w.capture.mu.Lock()
|
|
|
+ defer w.capture.mu.Unlock()
|
|
|
+ if w.dir == "c2s" {
|
|
|
+ w.capture.c2s = append(w.capture.c2s, p...)
|
|
|
+ } else {
|
|
|
+ w.capture.s2c = append(w.capture.s2c, p...)
|
|
|
+ }
|
|
|
+ return len(p), nil
|
|
|
+}
|
|
|
+
|
|
|
+func startUDPRelay(t *testing.T, listenPort, targetPort int) *udpRelay {
|
|
|
+ t.Helper()
|
|
|
+ conn, err := stdnet.ListenPacket("udp", fmt.Sprintf("127.0.0.1:%d", listenPort))
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ targetAddr := &stdnet.UDPAddr{IP: stdnet.IPv4(127, 0, 0, 1), Port: targetPort}
|
|
|
+ r := &udpRelay{
|
|
|
+ conn: conn,
|
|
|
+ target: targetAddr,
|
|
|
+ stopCh: make(chan struct{}),
|
|
|
+ }
|
|
|
+ r.wg.Add(1)
|
|
|
+ go func() {
|
|
|
+ defer r.wg.Done()
|
|
|
+ buf := make([]byte, 64*1024)
|
|
|
+ for {
|
|
|
+ n, addr, err := conn.ReadFrom(buf)
|
|
|
+ if err != nil {
|
|
|
+ select {
|
|
|
+ case <-r.stopCh:
|
|
|
+ return
|
|
|
+ default:
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
+ payload := append([]byte{}, buf[:n]...)
|
|
|
+ udpAddr := addr.(*stdnet.UDPAddr)
|
|
|
+ if udpAddr.IP.Equal(r.target.IP) && udpAddr.Port == r.target.Port {
|
|
|
+ r.captureMu.Lock()
|
|
|
+ r.s2c = append(r.s2c, payload)
|
|
|
+ r.captureMu.Unlock()
|
|
|
+ r.clientMu.Lock()
|
|
|
+ client := r.client
|
|
|
+ r.clientMu.Unlock()
|
|
|
+ if client != nil {
|
|
|
+ _, _ = conn.WriteTo(payload, client)
|
|
|
+ }
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ r.clientMu.Lock()
|
|
|
+ r.client = udpAddr
|
|
|
+ r.clientMu.Unlock()
|
|
|
+ r.captureMu.Lock()
|
|
|
+ r.c2s = append(r.c2s, payload)
|
|
|
+ r.captureMu.Unlock()
|
|
|
+ _, _ = conn.WriteTo(payload, r.target)
|
|
|
+ }
|
|
|
+ }()
|
|
|
+ return r
|
|
|
+}
|
|
|
+
|
|
|
+func (r *udpRelay) Close() {
|
|
|
+ close(r.stopCh)
|
|
|
+ _ = r.conn.Close()
|
|
|
+ r.wg.Wait()
|
|
|
+}
|
|
|
+
|
|
|
+func (r *udpRelay) Snapshots() ([][]byte, [][]byte) {
|
|
|
+ r.captureMu.Lock()
|
|
|
+ defer r.captureMu.Unlock()
|
|
|
+ c2s := make([][]byte, 0, len(r.c2s))
|
|
|
+ s2c := make([][]byte, 0, len(r.s2c))
|
|
|
+ for _, packet := range r.c2s {
|
|
|
+ c2s = append(c2s, append([]byte{}, packet...))
|
|
|
+ }
|
|
|
+ for _, packet := range r.s2c {
|
|
|
+ s2c = append(s2c, append([]byte{}, packet...))
|
|
|
+ }
|
|
|
+ return c2s, s2c
|
|
|
+}
|
|
|
+
|
|
|
+type xorEchoServer struct {
|
|
|
+ ln stdnet.Listener
|
|
|
+ wg sync.WaitGroup
|
|
|
+}
|
|
|
+
|
|
|
+func startXOREchoServer(t *testing.T) *xorEchoServer {
|
|
|
+ t.Helper()
|
|
|
+ ln, err := stdnet.Listen("tcp", "127.0.0.1:0")
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ s := &xorEchoServer{ln: ln}
|
|
|
+ s.wg.Add(1)
|
|
|
+ go func() {
|
|
|
+ defer s.wg.Done()
|
|
|
+ for {
|
|
|
+ conn, err := ln.Accept()
|
|
|
+ if err != nil {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ s.wg.Add(1)
|
|
|
+ go func(c stdnet.Conn) {
|
|
|
+ defer s.wg.Done()
|
|
|
+ defer c.Close()
|
|
|
+ buf := make([]byte, 4096)
|
|
|
+ for {
|
|
|
+ n, err := c.Read(buf)
|
|
|
+ if err != nil {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ for i := 0; i < n; i++ {
|
|
|
+ buf[i] ^= 'c'
|
|
|
+ }
|
|
|
+ if _, err := c.Write(buf[:n]); err != nil {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ for i := 0; i < n; i++ {
|
|
|
+ buf[i] ^= 'c'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }(conn)
|
|
|
+ }
|
|
|
+ }()
|
|
|
+ return s
|
|
|
+}
|
|
|
+
|
|
|
+func (s *xorEchoServer) Address() xnet.Address {
|
|
|
+ return xnet.IPAddress(s.ln.Addr().(*stdnet.TCPAddr).IP)
|
|
|
+}
|
|
|
+
|
|
|
+func (s *xorEchoServer) Port() xnet.Port {
|
|
|
+ return xnet.Port(s.ln.Addr().(*stdnet.TCPAddr).Port)
|
|
|
+}
|
|
|
+
|
|
|
+func (s *xorEchoServer) Close() {
|
|
|
+ _ = s.ln.Close()
|
|
|
+ s.wg.Wait()
|
|
|
+}
|
|
|
+
|
|
|
+func startTLSEchoDecoy(t *testing.T, c *cert.Certificate) *tlsDecoy {
|
|
|
+ t.Helper()
|
|
|
+ certPEM, keyPEM := c.ToPEM()
|
|
|
+ keyPair, err := cryptotls.X509KeyPair(certPEM, keyPEM)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ config := &cryptotls.Config{
|
|
|
+ Certificates: []cryptotls.Certificate{keyPair},
|
|
|
+ }
|
|
|
+ ln, err := stdnet.Listen("tcp", "127.0.0.1:0")
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ tlsLn := cryptotls.NewListener(ln, config)
|
|
|
+ d := &tlsDecoy{
|
|
|
+ ln: tlsLn,
|
|
|
+ done: make(chan struct{}),
|
|
|
+ }
|
|
|
+ d.wg.Add(1)
|
|
|
+ go func() {
|
|
|
+ defer d.wg.Done()
|
|
|
+ for {
|
|
|
+ conn, err := tlsLn.Accept()
|
|
|
+ if err != nil {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ d.wg.Add(1)
|
|
|
+ go func(c stdnet.Conn) {
|
|
|
+ defer d.wg.Done()
|
|
|
+ defer c.Close()
|
|
|
+ buf := make([]byte, 2048)
|
|
|
+ _, _ = c.Read(buf)
|
|
|
+ _, _ = c.Write([]byte("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK"))
|
|
|
+ }(conn)
|
|
|
+ }
|
|
|
+ }()
|
|
|
+ return d
|
|
|
+}
|
|
|
+
|
|
|
+func (d *tlsDecoy) Port() int {
|
|
|
+ return d.ln.Addr().(*stdnet.TCPAddr).Port
|
|
|
+}
|
|
|
+
|
|
|
+func (d *tlsDecoy) Close() {
|
|
|
+ _ = d.ln.Close()
|
|
|
+ d.wg.Wait()
|
|
|
+}
|
|
|
+
|
|
|
+func exerciseTCPClient(t *testing.T, port int, payloadSize int) {
|
|
|
+ t.Helper()
|
|
|
+ if err := exerciseTCPClientErr(t, port, payloadSize); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func exerciseTCPClientErr(t *testing.T, port int, payloadSize int) error {
|
|
|
+ conn := waitTCPConn(t, port, 10*time.Second)
|
|
|
+ defer conn.Close()
|
|
|
+ payload := make([]byte, payloadSize)
|
|
|
+ if _, err := rand.Read(payload); err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ offset := 0
|
|
|
+ for offset < len(payload) {
|
|
|
+ chunk := 1024
|
|
|
+ if remain := len(payload) - offset; remain < chunk {
|
|
|
+ chunk = remain
|
|
|
+ }
|
|
|
+ part := payload[offset : offset+chunk]
|
|
|
+ if _, err := conn.Write(part); err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ resp := make([]byte, chunk)
|
|
|
+ if _, err := io.ReadFull(conn, resp); err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ for i := range part {
|
|
|
+ if resp[i] != (part[i] ^ 'c') {
|
|
|
+ return fmt.Errorf("unexpected xor response at offset %d", offset+i)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ offset += chunk
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+func firstChunkLen(chunks [][]byte) int {
|
|
|
+ if len(chunks) == 0 {
|
|
|
+ return 0
|
|
|
+ }
|
|
|
+ return len(chunks[0])
|
|
|
+}
|
|
|
+
|
|
|
+func waitTCPConn(t *testing.T, port int, timeout time.Duration) stdnet.Conn {
|
|
|
+ t.Helper()
|
|
|
+ deadline := time.Now().Add(timeout)
|
|
|
+ for {
|
|
|
+ conn, err := stdnet.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), 500*time.Millisecond)
|
|
|
+ if err == nil {
|
|
|
+ return conn
|
|
|
+ }
|
|
|
+ if time.Now().After(deadline) {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ time.Sleep(100 * time.Millisecond)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func mustX25519Keypair(t *testing.T) ([]byte, []byte) {
|
|
|
+ t.Helper()
|
|
|
+ priv, err := ecdh.X25519().GenerateKey(rand.Reader)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ return priv.Bytes(), priv.PublicKey().Bytes()
|
|
|
+}
|
|
|
+
|
|
|
+func mustDecodeHex(t *testing.T, s string) []byte {
|
|
|
+ t.Helper()
|
|
|
+ out := make([]byte, len(s)/2)
|
|
|
+ if _, err := hex.Decode(out, []byte(s)); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ return out
|
|
|
+}
|
|
|
+
|
|
|
+func init() {
|
|
|
+ ch := make(chan os.Signal, 1)
|
|
|
+ signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
|
|
|
+ go func() {
|
|
|
+ <-ch
|
|
|
+ os.Exit(130)
|
|
|
+ }()
|
|
|
+}
|