浏览代码

Add ws compatibility test

世界 3 年之前
父节点
当前提交
4a0df713aa

+ 52 - 0
test/config/vmess-ws-client.json

@@ -0,0 +1,52 @@
+{
+  "log": {
+    "loglevel": "debug"
+  },
+  "inbounds": [
+    {
+      "listen": "127.0.0.1",
+      "port": "1080",
+      "protocol": "socks",
+      "settings": {
+        "auth": "noauth",
+        "udp": true,
+        "ip": "127.0.0.1"
+      }
+    }
+  ],
+  "outbounds": [
+    {
+      "protocol": "vmess",
+      "settings": {
+        "vnext": [
+          {
+            "address": "127.0.0.1",
+            "port": 1234,
+            "users": [
+              {
+                "id": ""
+              }
+            ]
+          }
+        ]
+      },
+      "streamSettings": {
+        "network": "ws",
+        "security": "tls",
+        "tlsSettings": {
+          "serverName": "example.org",
+          "certificates": [
+            {
+              "certificateFile": "/path/to/certificate.crt",
+              "keyFile": "/path/to/private.key"
+            }
+          ]
+        },
+        "wsSettings": {
+          "maxEarlyData": 2048,
+          "earlyDataHeaderName": ""
+        }
+      }
+    }
+  ]
+}

+ 41 - 0
test/config/vmess-ws-server.json

@@ -0,0 +1,41 @@
+{
+  "log": {
+    "loglevel": "debug"
+  },
+  "inbounds": [
+    {
+      "listen": "0.0.0.0",
+      "port": 1234,
+      "protocol": "vmess",
+      "settings": {
+        "clients": [
+          {
+            "id": "b831381d-6324-4d53-ad4f-8cda48b30811"
+          }
+        ]
+      },
+      "streamSettings": {
+        "network": "ws",
+        "security": "tls",
+        "tlsSettings": {
+          "serverName": "example.org",
+          "certificates": [
+            {
+              "certificateFile": "/path/to/certificate.crt",
+              "keyFile": "/path/to/private.key"
+            }
+          ]
+        },
+        "wsSettings": {
+          "maxEarlyData": 2048,
+          "earlyDataHeaderName": ""
+        }
+      }
+    }
+  ],
+  "outbounds": [
+    {
+      "protocol": "freedom"
+    }
+  ]
+}

+ 0 - 3
test/direct_test.go

@@ -10,9 +10,6 @@ import (
 
 func TestProxyProtocol(t *testing.T) {
 	startInstance(t, option.Options{
-		Log: &option.LogOptions{
-			Level: "error",
-		},
 		Inbounds: []option.Inbound{
 			{
 				Type: C.TypeMixed,

+ 4 - 4
test/go.mod

@@ -62,22 +62,22 @@ require (
 	github.com/sagernet/sing-dns v0.0.0-20220822023312-3e086b06d666 // indirect
 	github.com/sagernet/sing-tun v0.0.0-20220828031750-185b6c880a83 // indirect
 	github.com/sagernet/sing-vmess v0.0.0-20220829020559-33915075430c // indirect
-	github.com/sagernet/smux v0.0.0-20220812084127-e2d085ee3939 // indirect
+	github.com/sagernet/smux v0.0.0-20220831015742-e0f1988e3195 // indirect
 	github.com/sirupsen/logrus v1.8.1 // indirect
 	github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect
 	go.uber.org/atomic v1.10.0 // indirect
 	go.uber.org/multierr v1.6.0 // indirect
 	go.uber.org/zap v1.22.0 // indirect
 	go4.org/netipx v0.0.0-20220812043211-3cc044ffd68d // indirect
-	golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d // indirect
+	golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 // indirect
 	golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect
 	golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
-	golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 // indirect
+	golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 // indirect
 	golang.org/x/text v0.3.7 // indirect
 	golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect
 	golang.org/x/tools v0.1.11-0.20220513221640-090b14e8501f // indirect
 	golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect
-	golang.zx2c4.com/wireguard v0.0.0-20220703234212-c31a7b1ab478 // indirect
+	golang.zx2c4.com/wireguard v0.0.0-20220829161405-d1d08426b27b // indirect
 	google.golang.org/genproto v0.0.0-20210722135532-667f2b7c528f // indirect
 	google.golang.org/grpc v1.49.0 // indirect
 	google.golang.org/protobuf v1.28.1 // indirect

+ 8 - 8
test/go.sum

@@ -165,8 +165,8 @@ github.com/sagernet/sing-tun v0.0.0-20220828031750-185b6c880a83 h1:SoWiHYuOCVedq
 github.com/sagernet/sing-tun v0.0.0-20220828031750-185b6c880a83/go.mod h1:76r07HS1WRcEI4mE9pFsohfTBUt1j/G9Avz6DaOP3VU=
 github.com/sagernet/sing-vmess v0.0.0-20220829020559-33915075430c h1:92Gn78/z/t6CkzZ4XWG/uPiCxhUmjPULFEHFMDY6K8k=
 github.com/sagernet/sing-vmess v0.0.0-20220829020559-33915075430c/go.mod h1:82O6gzbxLha/W/jxSVQbsqf2lVdRTjMIgyLug0lpJps=
-github.com/sagernet/smux v0.0.0-20220812084127-e2d085ee3939 h1:pB1Dh1NbwVrLhQhotr4O4Hs3yhiBzmg3AvnUyYjL4x4=
-github.com/sagernet/smux v0.0.0-20220812084127-e2d085ee3939/go.mod h1:yedWtra8nyGJ+SyI+ziwuaGMzBatbB10P1IOOZbbSK8=
+github.com/sagernet/smux v0.0.0-20220831015742-e0f1988e3195 h1:5VBIbVw9q7aKbrFdT83mjkyvQ+VaRsQ6yflTepfln38=
+github.com/sagernet/smux v0.0.0-20220831015742-e0f1988e3195/go.mod h1:yedWtra8nyGJ+SyI+ziwuaGMzBatbB10P1IOOZbbSK8=
 github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
 github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
 github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
@@ -205,8 +205,8 @@ golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaE
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
-golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d h1:3qF+Z8Hkrw9sOhrFHti9TlB1Hkac1x+DNRkv0XQiFjo=
-golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
+golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA=
 golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
@@ -275,8 +275,8 @@ golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 h1:UiNENfZ8gDvpiWw7IpOMQ27spWmThO1RwwdQVbJahJM=
-golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY=
+golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -311,8 +311,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 h1:Ug9qvr1myri/zFN6xL17LSCBGFDnphBBhzmILHsM5TY=
 golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
-golang.zx2c4.com/wireguard v0.0.0-20220703234212-c31a7b1ab478 h1:vDy//hdR+GnROE3OdYbQKt9rdtNdHkDtONvpRwmls/0=
-golang.zx2c4.com/wireguard v0.0.0-20220703234212-c31a7b1ab478/go.mod h1:bVQfyl2sCM/QIIGHpWbFGfHPuDvqnCNkT6MQLTCjO/U=
+golang.zx2c4.com/wireguard v0.0.0-20220829161405-d1d08426b27b h1:qgrKnOfe1zyURRNdmDlGbN32i38Zjmw0B1+TMdHcOvg=
+golang.zx2c4.com/wireguard v0.0.0-20220829161405-d1d08426b27b/go.mod h1:6y4CqPAy54NwiN4nC8K+R1eMpQDB1P2d25qmunh2RSA=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=

+ 0 - 9
test/hysteria_test.go

@@ -14,9 +14,6 @@ func TestHysteriaSelf(t *testing.T) {
 	}
 	_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
 	startInstance(t, option.Options{
-		Log: &option.LogOptions{
-			Level: "error",
-		},
 		Inbounds: []option.Inbound{
 			{
 				Type: C.TypeMixed,
@@ -92,9 +89,6 @@ func TestHysteriaInbound(t *testing.T) {
 	}
 	caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
 	startInstance(t, option.Options{
-		Log: &option.LogOptions{
-			Level: "error",
-		},
 		Inbounds: []option.Inbound{
 			{
 				Type: C.TypeHysteria,
@@ -145,9 +139,6 @@ func TestHysteriaOutbound(t *testing.T) {
 		},
 	})
 	startInstance(t, option.Options{
-		Log: &option.LogOptions{
-			Level: "error",
-		},
 		Inbounds: []option.Inbound{
 			{
 				Type: C.TypeMixed,

+ 0 - 3
test/inbound_detour_test.go

@@ -13,9 +13,6 @@ func TestChainedInbound(t *testing.T) {
 	method := shadowaead_2022.List[0]
 	password := mkBase64(t, 16)
 	startInstance(t, option.Options{
-		Log: &option.LogOptions{
-			Level: "error",
-		},
 		Inbounds: []option.Inbound{
 			{
 				Type: C.TypeMixed,

+ 0 - 6
test/mux_test.go

@@ -37,9 +37,6 @@ func testShadowsocksMux(t *testing.T, protocol string) {
 	method := shadowaead_2022.List[0]
 	password := mkBase64(t, 16)
 	startInstance(t, option.Options{
-		Log: &option.LogOptions{
-			Level: "error",
-		},
 		Inbounds: []option.Inbound{
 			{
 				Type: C.TypeMixed,
@@ -101,9 +98,6 @@ func testShadowsocksMux(t *testing.T, protocol string) {
 func testVMessMux(t *testing.T, protocol string) {
 	user, _ := uuid.NewV4()
 	startInstance(t, option.Options{
-		Log: &option.LogOptions{
-			Level: "error",
-		},
 		Inbounds: []option.Inbound{
 			{
 				Type: C.TypeMixed,

+ 0 - 9
test/naive_test.go

@@ -13,9 +13,6 @@ import (
 func TestNaiveInboundWithNginx(t *testing.T) {
 	caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
 	startInstance(t, option.Options{
-		Log: &option.LogOptions{
-			Level: "error",
-		},
 		Inbounds: []option.Inbound{
 			{
 				Type: C.TypeNaive,
@@ -62,9 +59,6 @@ func TestNaiveInboundWithNginx(t *testing.T) {
 func TestNaiveInbound(t *testing.T) {
 	caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
 	startInstance(t, option.Options{
-		Log: &option.LogOptions{
-			Level: "error",
-		},
 		Inbounds: []option.Inbound{
 			{
 				Type: C.TypeNaive,
@@ -110,9 +104,6 @@ func TestNaiveHTTP3Inbound(t *testing.T) {
 	}
 	caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
 	startInstance(t, option.Options{
-		Log: &option.LogOptions{
-			Level: "error",
-		},
 		Inbounds: []option.Inbound{
 			{
 				Type: C.TypeNaive,

+ 0 - 12
test/shadowsocks_test.go

@@ -77,9 +77,6 @@ func testShadowsocksInboundWithShadowsocksRust(t *testing.T, method string, pass
 		Cmd:        []string{"-s", F.ToString("127.0.0.1:", serverPort), "-b", F.ToString("0.0.0.0:", clientPort), "-m", method, "-k", password, "-U"},
 	})
 	startInstance(t, option.Options{
-		Log: &option.LogOptions{
-			Level: "error",
-		},
 		Inbounds: []option.Inbound{
 			{
 				Type: C.TypeShadowsocks,
@@ -105,9 +102,6 @@ func testShadowsocksOutboundWithShadowsocksRust(t *testing.T, method string, pas
 		Cmd:        []string{"-s", F.ToString("0.0.0.0:", serverPort), "-m", method, "-k", password, "-U"},
 	})
 	startInstance(t, option.Options{
-		Log: &option.LogOptions{
-			Level: "error",
-		},
 		Inbounds: []option.Inbound{
 			{
 				Type: C.TypeMixed,
@@ -138,9 +132,6 @@ func testShadowsocksOutboundWithShadowsocksRust(t *testing.T, method string, pas
 
 func testShadowsocksSelf(t *testing.T, method string, password string) {
 	startInstance(t, option.Options{
-		Log: &option.LogOptions{
-			Level: "error",
-		},
 		Inbounds: []option.Inbound{
 			{
 				Type: C.TypeMixed,
@@ -199,9 +190,6 @@ func TestShadowsocksUoT(t *testing.T) {
 	method := shadowaead_2022.List[0]
 	password := mkBase64(t, 16)
 	startInstance(t, option.Options{
-		Log: &option.LogOptions{
-			Level: "error",
-		},
 		Inbounds: []option.Inbound{
 			{
 				Type: C.TypeMixed,

+ 0 - 6
test/trojan_test.go

@@ -20,9 +20,6 @@ func TestTrojanOutbound(t *testing.T) {
 		},
 	})
 	startInstance(t, option.Options{
-		Log: &option.LogOptions{
-			Level: "error",
-		},
 		Inbounds: []option.Inbound{
 			{
 				Type: C.TypeMixed,
@@ -58,9 +55,6 @@ func TestTrojanOutbound(t *testing.T) {
 func TestTrojanSelf(t *testing.T) {
 	_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
 	startInstance(t, option.Options{
-		Log: &option.LogOptions{
-			Level: "error",
-		},
 		Inbounds: []option.Inbound{
 			{
 				Type: C.TypeMixed,

+ 0 - 6
test/v2ray_grpc_test.go

@@ -27,9 +27,6 @@ func testV2RayGRPCInbound(t *testing.T, forceLite bool) {
 	require.NoError(t, err)
 	_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
 	startInstance(t, option.Options{
-		Log: &option.LogOptions{
-			Level: "error",
-		},
 		Inbounds: []option.Inbound{
 			{
 				Type: C.TypeVMess,
@@ -125,9 +122,6 @@ func testV2RayGRPCOutbound(t *testing.T, forceLite bool) {
 		},
 	})
 	startInstance(t, option.Options{
-		Log: &option.LogOptions{
-			Level: "error",
-		},
 		Inbounds: []option.Inbound{
 			{
 				Type: C.TypeMixed,

+ 0 - 37
test/v2ray_transport_test.go

@@ -11,31 +11,6 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-func TestV2RayWebscoketSelf(t *testing.T) {
-	t.Run("basic", func(t *testing.T) {
-		testV2RayTransportSelf(t, &option.V2RayTransportOptions{
-			Type: C.V2RayTransportTypeWebsocket,
-		})
-	})
-	t.Run("v2ray early data", func(t *testing.T) {
-		testV2RayTransportSelf(t, &option.V2RayTransportOptions{
-			Type: C.V2RayTransportTypeWebsocket,
-			WebsocketOptions: option.V2RayWebsocketOptions{
-				MaxEarlyData: 2048,
-			},
-		})
-	})
-	t.Run("xray early data", func(t *testing.T) {
-		testV2RayTransportSelf(t, &option.V2RayTransportOptions{
-			Type: C.V2RayTransportTypeWebsocket,
-			WebsocketOptions: option.V2RayWebsocketOptions{
-				MaxEarlyData:        2048,
-				EarlyDataHeaderName: "Sec-WebSocket-Protocol",
-			},
-		})
-	})
-}
-
 func TestV2RayHTTPSelf(t *testing.T) {
 	testV2RayTransportSelf(t, &option.V2RayTransportOptions{
 		Type: C.V2RayTransportTypeHTTP,
@@ -69,9 +44,6 @@ func testVMessTransportSelf(t *testing.T, server *option.V2RayTransportOptions,
 	require.NoError(t, err)
 	_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
 	startInstance(t, option.Options{
-		Log: &option.LogOptions{
-			Level: "error",
-		},
 		Inbounds: []option.Inbound{
 			{
 				Type: C.TypeMixed,
@@ -148,9 +120,6 @@ func testTrojanTransportSelf(t *testing.T, server *option.V2RayTransportOptions,
 	require.NoError(t, err)
 	_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
 	startInstance(t, option.Options{
-		Log: &option.LogOptions{
-			Level: "error",
-		},
 		Inbounds: []option.Inbound{
 			{
 				Type: C.TypeMixed,
@@ -229,9 +198,6 @@ func TestVMessQUICSelf(t *testing.T) {
 	require.NoError(t, err)
 	_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
 	startInstance(t, option.Options{
-		Log: &option.LogOptions{
-			Level: "error",
-		},
 		Inbounds: []option.Inbound{
 			{
 				Type: C.TypeMixed,
@@ -307,9 +273,6 @@ func testV2RayTransportNOTLSSelf(t *testing.T, transport *option.V2RayTransportO
 	user, err := uuid.DefaultGenerator.NewV4()
 	require.NoError(t, err)
 	startInstance(t, option.Options{
-		Log: &option.LogOptions{
-			Level: "error",
-		},
 		Inbounds: []option.Inbound{
 			{
 				Type: C.TypeMixed,

+ 197 - 0
test/v2ray_ws_test.go

@@ -0,0 +1,197 @@
+package main
+
+import (
+	"net/netip"
+	"os"
+	"testing"
+
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/option"
+
+	"github.com/gofrs/uuid"
+	"github.com/spyzhov/ajson"
+	"github.com/stretchr/testify/require"
+)
+
+func TestV2RayWebsocket(t *testing.T) {
+	t.Run("self", func(t *testing.T) {
+		testV2RayTransportSelf(t, &option.V2RayTransportOptions{
+			Type: C.V2RayTransportTypeWebsocket,
+		})
+	})
+	t.Run("self-early-data", func(t *testing.T) {
+		testV2RayTransportSelf(t, &option.V2RayTransportOptions{
+			Type: C.V2RayTransportTypeWebsocket,
+			WebsocketOptions: option.V2RayWebsocketOptions{
+				MaxEarlyData: 2048,
+			},
+		})
+	})
+	t.Run("self-xray-early-data", func(t *testing.T) {
+		testV2RayTransportSelf(t, &option.V2RayTransportOptions{
+			Type: C.V2RayTransportTypeWebsocket,
+			WebsocketOptions: option.V2RayWebsocketOptions{
+				MaxEarlyData:        2048,
+				EarlyDataHeaderName: "Sec-WebSocket-Protocol",
+			},
+		})
+	})
+	t.Run("inbound", func(t *testing.T) {
+		testV2RayWebsocketInbound(t, 0, "")
+	})
+	t.Run("inbound-early-data", func(t *testing.T) {
+		testV2RayWebsocketInbound(t, 2048, "")
+	})
+	t.Run("inbound-xray-early-data", func(t *testing.T) {
+		testV2RayWebsocketInbound(t, 2048, "Sec-WebSocket-Protocol")
+	})
+	t.Run("outbound", func(t *testing.T) {
+		testV2RayWebsocketOutbound(t, 0, "")
+	})
+	t.Run("outbound-early-data", func(t *testing.T) {
+		testV2RayWebsocketOutbound(t, 2048, "")
+	})
+	t.Run("outbound-xray-early-data", func(t *testing.T) {
+		testV2RayWebsocketOutbound(t, 2048, "Sec-WebSocket-Protocol")
+	})
+}
+
+func testV2RayWebsocketInbound(t *testing.T, maxEarlyData uint32, earlyDataHeaderName string) {
+	userId, err := uuid.DefaultGenerator.NewV4()
+	require.NoError(t, err)
+	_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
+	startInstance(t, option.Options{
+		Inbounds: []option.Inbound{
+			{
+				Type: C.TypeVMess,
+				VMessOptions: option.VMessInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.ListenAddress(netip.IPv4Unspecified()),
+						ListenPort: serverPort,
+					},
+					Users: []option.VMessUser{
+						{
+							Name: "sekai",
+							UUID: userId.String(),
+						},
+					},
+					TLS: &option.InboundTLSOptions{
+						Enabled:         true,
+						ServerName:      "example.org",
+						CertificatePath: certPem,
+						KeyPath:         keyPem,
+					},
+					Transport: &option.V2RayTransportOptions{
+						Type: C.V2RayTransportTypeWebsocket,
+						WebsocketOptions: option.V2RayWebsocketOptions{
+							MaxEarlyData:        maxEarlyData,
+							EarlyDataHeaderName: earlyDataHeaderName,
+						},
+					},
+				},
+			},
+		},
+	})
+	content, err := os.ReadFile("config/vmess-ws-client.json")
+	require.NoError(t, err)
+	config, err := ajson.Unmarshal(content)
+	require.NoError(t, err)
+
+	config.MustKey("inbounds").MustIndex(0).MustKey("port").SetNumeric(float64(clientPort))
+	outbound := config.MustKey("outbounds").MustIndex(0)
+	settings := outbound.MustKey("settings").MustKey("vnext").MustIndex(0)
+	settings.MustKey("port").SetNumeric(float64(serverPort))
+	user := settings.MustKey("users").MustIndex(0)
+	user.MustKey("id").SetString(userId.String())
+	wsSettings := outbound.MustKey("streamSettings").MustKey("wsSettings")
+	wsSettings.MustKey("maxEarlyData").SetNumeric(float64(maxEarlyData))
+	wsSettings.MustKey("earlyDataHeaderName").SetString(earlyDataHeaderName)
+	content, err = ajson.Marshal(config)
+	require.NoError(t, err)
+
+	startDockerContainer(t, DockerOptions{
+		Image:      ImageV2RayCore,
+		Ports:      []uint16{serverPort, testPort},
+		EntryPoint: "v2ray",
+		Stdin:      content,
+		Bind: map[string]string{
+			certPem: "/path/to/certificate.crt",
+			keyPem:  "/path/to/private.key",
+		},
+	})
+
+	testSuitSimple(t, clientPort, testPort)
+}
+
+func testV2RayWebsocketOutbound(t *testing.T, maxEarlyData uint32, earlyDataHeaderName string) {
+	userId, err := uuid.DefaultGenerator.NewV4()
+	require.NoError(t, err)
+	_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
+
+	content, err := os.ReadFile("config/vmess-ws-server.json")
+	require.NoError(t, err)
+	config, err := ajson.Unmarshal(content)
+	require.NoError(t, err)
+
+	inbound := config.MustKey("inbounds").MustIndex(0)
+	inbound.MustKey("port").SetNumeric(float64(serverPort))
+	inbound.MustKey("settings").MustKey("clients").MustIndex(0).MustKey("id").SetString(userId.String())
+	wsSettings := inbound.MustKey("streamSettings").MustKey("wsSettings")
+	wsSettings.MustKey("maxEarlyData").SetNumeric(float64(maxEarlyData))
+	wsSettings.MustKey("earlyDataHeaderName").SetString(earlyDataHeaderName)
+	content, err = ajson.Marshal(config)
+	require.NoError(t, err)
+
+	startDockerContainer(t, DockerOptions{
+		Image:      ImageV2RayCore,
+		Ports:      []uint16{serverPort, testPort},
+		EntryPoint: "v2ray",
+		Stdin:      content,
+		Env:        []string{"V2RAY_VMESS_AEAD_FORCED=false"},
+		Bind: map[string]string{
+			certPem: "/path/to/certificate.crt",
+			keyPem:  "/path/to/private.key",
+		},
+	})
+	startInstance(t, option.Options{
+		Inbounds: []option.Inbound{
+			{
+				Type: C.TypeMixed,
+				Tag:  "mixed-in",
+				MixedOptions: option.HTTPMixedInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.ListenAddress(netip.IPv4Unspecified()),
+						ListenPort: clientPort,
+					},
+				},
+			},
+		},
+		Outbounds: []option.Outbound{
+			{
+				Type: C.TypeVMess,
+				Tag:  "vmess-out",
+				VMessOptions: option.VMessOutboundOptions{
+					ServerOptions: option.ServerOptions{
+						Server:     "127.0.0.1",
+						ServerPort: serverPort,
+					},
+					UUID:     userId.String(),
+					Security: "zero",
+					TLS: &option.OutboundTLSOptions{
+						Enabled:         true,
+						ServerName:      "example.org",
+						CertificatePath: certPem,
+					},
+					Transport: &option.V2RayTransportOptions{
+						Type: C.V2RayTransportTypeWebsocket,
+						WebsocketOptions: option.V2RayWebsocketOptions{
+							MaxEarlyData:        maxEarlyData,
+							EarlyDataHeaderName: earlyDataHeaderName,
+						},
+					},
+				},
+			},
+		},
+	})
+	testSuitSimple(t, clientPort, testPort)
+}

+ 0 - 9
test/vmess_test.go

@@ -182,9 +182,6 @@ func testVMessInboundWithV2Ray(t *testing.T, security string, uuid uuid.UUID, al
 	})
 
 	startInstance(t, option.Options{
-		Log: &option.LogOptions{
-			Level: "error",
-		},
 		Inbounds: []option.Inbound{
 			{
 				Type: C.TypeVMess,
@@ -231,9 +228,6 @@ func testVMessOutboundWithV2Ray(t *testing.T, security string, uuid uuid.UUID, g
 	})
 
 	startInstance(t, option.Options{
-		Log: &option.LogOptions{
-			Level: "error",
-		},
 		Inbounds: []option.Inbound{
 			{
 				Type: C.TypeMixed,
@@ -267,9 +261,6 @@ func testVMessOutboundWithV2Ray(t *testing.T, security string, uuid uuid.UUID, g
 
 func testVMessSelf(t *testing.T, security string, uuid uuid.UUID, alterId int, globalPadding bool, authenticatedLength bool, packetAddr bool) {
 	startInstance(t, option.Options{
-		Log: &option.LogOptions{
-			Level: "error",
-		},
 		Inbounds: []option.Inbound{
 			{
 				Type: C.TypeMixed,

+ 0 - 3
test/wireguard_test.go

@@ -21,9 +21,6 @@ func TestWireGuard(t *testing.T) {
 	})
 	time.Sleep(5 * time.Second)
 	startInstance(t, option.Options{
-		Log: &option.LogOptions{
-			Level: "error",
-		},
 		Inbounds: []option.Inbound{
 			{
 				Type: C.TypeMixed,

+ 3 - 0
transport/v2raywebsocket/conn.go

@@ -167,6 +167,9 @@ func (c *EarlyWebsocketConn) SetWriteDeadline(t time.Time) error {
 
 func wrapError(err error) error {
 	if websocket.IsCloseError(err, websocket.CloseNormalClosure) {
+		return io.EOF
+	}
+	if websocket.IsCloseError(err, websocket.CloseAbnormalClosure) {
 		return net.ErrClosed
 	}
 	return err