diff --git a/adapter/outbound/vless.go b/adapter/outbound/vless.go new file mode 100644 index 0000000..c9f1ad3 --- /dev/null +++ b/adapter/outbound/vless.go @@ -0,0 +1,10 @@ +package outbound + +type ( + Vless = Vmess + VlessOption = VmessOption +) + +func NewVless(option VlessOption) (*Vless, error) { + return newVmess(option, true) +} diff --git a/adapter/outbound/vmess.go b/adapter/outbound/vmess.go index dd3ba44..19fd54b 100644 --- a/adapter/outbound/vmess.go +++ b/adapter/outbound/vmess.go @@ -39,8 +39,8 @@ type VmessOption struct { Server string `proxy:"server"` Port int `proxy:"port"` UUID string `proxy:"uuid"` - AlterID int `proxy:"alterId"` - Cipher string `proxy:"cipher"` + AlterID int `proxy:"alterId,omitempty"` + Cipher string `proxy:"cipher,omitempty"` UDP bool `proxy:"udp,omitempty"` Network string `proxy:"network,omitempty"` TLS bool `proxy:"tls,omitempty"` @@ -264,7 +264,14 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o } func NewVmess(option VmessOption) (*Vmess, error) { + return newVmess(option, false) +} + +func newVmess(option VmessOption, isVless bool) (*Vmess, error) { security := strings.ToLower(option.Cipher) + if security == "" { + security = "auto" + } client, err := vmess.NewClient(vmess.Config{ UUID: option.UUID, AlterID: uint16(option.AlterID), @@ -272,6 +279,7 @@ func NewVmess(option VmessOption) (*Vmess, error) { HostName: option.Server, Port: strconv.Itoa(option.Port), IsAead: option.AlterID == 0, + IsVless: isVless, }) if err != nil { return nil, err @@ -284,11 +292,16 @@ func NewVmess(option VmessOption) (*Vmess, error) { } } + tp := C.Vmess + if isVless { + tp = C.Vless + } + v := &Vmess{ Base: &Base{ name: option.Name, addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), - tp: C.Vmess, + tp: tp, udp: option.UDP, iface: option.Interface, rmark: option.RoutingMark, diff --git a/adapter/parser.go b/adapter/parser.go index 4ee7b48..663cba7 100644 --- a/adapter/parser.go +++ b/adapter/parser.go @@ -48,6 +48,8 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) { break } proxy = outbound.NewHttp(*httpOption) + case "vless": + fallthrough case "vmess": vmessOption := &outbound.VmessOption{ HTTPOpts: outbound.HTTPOptions{ @@ -59,7 +61,11 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) { if err != nil { break } - proxy, err = outbound.NewVmess(*vmessOption) + if proxyType == "vless" { + proxy, err = outbound.NewVless(*vmessOption) + } else { + proxy, err = outbound.NewVmess(*vmessOption) + } case "snell": snellOption := &outbound.SnellOption{} err = decoder.Decode(mapping, snellOption) diff --git a/constant/adapters.go b/constant/adapters.go index 8200e46..c034487 100644 --- a/constant/adapters.go +++ b/constant/adapters.go @@ -19,6 +19,7 @@ const ( Snell Socks5 Http + Vless Vmess Trojan @@ -140,6 +141,8 @@ func (at AdapterType) String() string { return "Socks5" case Http: return "Http" + case Vless: + return "Vless" case Vmess: return "Vmess" case Trojan: diff --git a/test/config/vless-grpc.json b/test/config/vless-grpc.json new file mode 100644 index 0000000..8b1d55b --- /dev/null +++ b/test/config/vless-grpc.json @@ -0,0 +1,40 @@ +{ + "inbounds": [ + { + "port": 10002, + "listen": "0.0.0.0", + "protocol": "vless", + "settings": { + "clients": [ + { + "id": "b831381d-6324-4d53-ad4f-8cda48b30811" + } + ], + "decryption": "none" + }, + "streamSettings": { + "network": "grpc", + "security": "tls", + "tlsSettings": { + "certificates": [ + { + "certificateFile": "/etc/ssl/v2ray/fullchain.pem", + "keyFile": "/etc/ssl/v2ray/privkey.pem" + } + ] + }, + "grpcSettings": { + "serviceName": "example!" + } + } + } + ], + "outbounds": [ + { + "protocol": "freedom" + } + ], + "log": { + "loglevel": "debug" + } +} diff --git a/test/config/vless-http.json b/test/config/vless-http.json new file mode 100644 index 0000000..7cead85 --- /dev/null +++ b/test/config/vless-http.json @@ -0,0 +1,55 @@ +{ + "inbounds": [ + { + "port": 10002, + "listen": "0.0.0.0", + "protocol": "vless", + "settings": { + "clients": [ + { + "id": "b831381d-6324-4d53-ad4f-8cda48b30811" + } + ], + "decryption": "none" + }, + "streamSettings": { + "network": "tcp", + "tcpSettings": { + "header": { + "type": "http", + "response": { + "version": "1.1", + "status": "200", + "reason": "OK", + "headers": { + "Content-Type": [ + "application/octet-stream", + "video/mpeg", + "application/x-msdownload", + "text/html", + "application/x-shockwave-flash" + ], + "Transfer-Encoding": [ + "chunked" + ], + "Connection": [ + "keep-alive" + ], + "Pragma": "no-cache" + } + } + } + }, + "security": "none" + } + } + ], + "outbounds": [ + { + "protocol": "freedom" + } + ], + "log": { + "loglevel": "debug" + } +} diff --git a/test/config/vless-http2.json b/test/config/vless-http2.json new file mode 100644 index 0000000..cab4eda --- /dev/null +++ b/test/config/vless-http2.json @@ -0,0 +1,43 @@ +{ + "inbounds": [ + { + "port": 10002, + "listen": "0.0.0.0", + "protocol": "vless", + "settings": { + "clients": [ + { + "id": "b831381d-6324-4d53-ad4f-8cda48b30811" + } + ], + "decryption": "none" + }, + "streamSettings": { + "network": "http", + "security": "tls", + "tlsSettings": { + "certificates": [ + { + "certificateFile": "/etc/ssl/v2ray/fullchain.pem", + "keyFile": "/etc/ssl/v2ray/privkey.pem" + } + ] + }, + "httpSettings": { + "host": [ + "example.org" + ], + "path": "/test" + } + } + } + ], + "outbounds": [ + { + "protocol": "freedom" + } + ], + "log": { + "loglevel": "debug" + } +} diff --git a/test/config/vless-tls.json b/test/config/vless-tls.json new file mode 100644 index 0000000..2485f59 --- /dev/null +++ b/test/config/vless-tls.json @@ -0,0 +1,37 @@ +{ + "inbounds": [ + { + "port": 10002, + "listen": "0.0.0.0", + "protocol": "vless", + "settings": { + "clients": [ + { + "id": "b831381d-6324-4d53-ad4f-8cda48b30811" + } + ], + "decryption": "none" + }, + "streamSettings": { + "network": "tcp", + "security": "tls", + "tlsSettings": { + "certificates": [ + { + "certificateFile": "/etc/ssl/v2ray/fullchain.pem", + "keyFile": "/etc/ssl/v2ray/privkey.pem" + } + ] + } + } + } + ], + "outbounds": [ + { + "protocol": "freedom" + } + ], + "log": { + "loglevel": "debug" + } +} diff --git a/test/config/vless-ws-0rtt.json b/test/config/vless-ws-0rtt.json new file mode 100644 index 0000000..32188c3 --- /dev/null +++ b/test/config/vless-ws-0rtt.json @@ -0,0 +1,30 @@ +{ + "inbounds": [ + { + "port": 10002, + "listen": "0.0.0.0", + "protocol": "vless", + "settings": { + "clients": [ + { + "id": "b831381d-6324-4d53-ad4f-8cda48b30811" + } + ], + "decryption": "none" + }, + "streamSettings": { + "network": "ws", + "security": "none", + "wsSettings": { + "maxEarlyData": 128, + "earlyDataHeaderName": "Sec-WebSocket-Protocol" + } + } + } + ], + "outbounds": [ + { + "protocol": "freedom" + } + ] +} diff --git a/test/config/vless-ws-tls-zero.json b/test/config/vless-ws-tls-zero.json new file mode 100644 index 0000000..3640798 --- /dev/null +++ b/test/config/vless-ws-tls-zero.json @@ -0,0 +1,34 @@ +{ + "inbounds": [ + { + "port": 10002, + "listen": "0.0.0.0", + "protocol": "vless", + "settings": { + "clients": [ + { + "id": "b831381d-6324-4d53-ad4f-8cda48b30811" + } + ], + "decryption": "none" + }, + "streamSettings": { + "network": "ws", + "security": "tls", + "tlsSettings": { + "certificates": [ + { + "certificateFile": "/etc/ssl/v2ray/fullchain.pem", + "keyFile": "/etc/ssl/v2ray/privkey.pem" + } + ] + } + } + } + ], + "outbounds": [ + { + "protocol": "freedom" + } + ] +} diff --git a/test/config/vless-ws-tls.json b/test/config/vless-ws-tls.json new file mode 100644 index 0000000..3640798 --- /dev/null +++ b/test/config/vless-ws-tls.json @@ -0,0 +1,34 @@ +{ + "inbounds": [ + { + "port": 10002, + "listen": "0.0.0.0", + "protocol": "vless", + "settings": { + "clients": [ + { + "id": "b831381d-6324-4d53-ad4f-8cda48b30811" + } + ], + "decryption": "none" + }, + "streamSettings": { + "network": "ws", + "security": "tls", + "tlsSettings": { + "certificates": [ + { + "certificateFile": "/etc/ssl/v2ray/fullchain.pem", + "keyFile": "/etc/ssl/v2ray/privkey.pem" + } + ] + } + } + } + ], + "outbounds": [ + { + "protocol": "freedom" + } + ] +} diff --git a/test/config/vless-ws.json b/test/config/vless-ws.json new file mode 100644 index 0000000..8c31c1f --- /dev/null +++ b/test/config/vless-ws.json @@ -0,0 +1,26 @@ +{ + "inbounds": [ + { + "port": 10002, + "listen": "0.0.0.0", + "protocol": "vless", + "settings": { + "clients": [ + { + "id": "b831381d-6324-4d53-ad4f-8cda48b30811" + } + ], + "decryption": "none" + }, + "streamSettings": { + "network": "ws", + "security": "none" + } + } + ], + "outbounds": [ + { + "protocol": "freedom" + } + ] +} diff --git a/test/config/vless.json b/test/config/vless.json new file mode 100644 index 0000000..be3168b --- /dev/null +++ b/test/config/vless.json @@ -0,0 +1,28 @@ +{ + "inbounds": [ + { + "port": 10002, + "listen": "0.0.0.0", + "protocol": "vless", + "settings": { + "clients": [ + { + "id": "b831381d-6324-4d53-ad4f-8cda48b30811" + } + ], + "decryption": "none" + }, + "streamSettings": { + "network": "tcp" + } + } + ], + "outbounds": [ + { + "protocol": "freedom" + } + ], + "log": { + "loglevel": "debug" + } +} diff --git a/test/vless_test.go b/test/vless_test.go new file mode 100644 index 0000000..4dc4214 --- /dev/null +++ b/test/vless_test.go @@ -0,0 +1,452 @@ +package main + +import ( + "fmt" + "testing" + "time" + + "github.com/docker/docker/api/types/container" + "github.com/stretchr/testify/require" + + "github.com/Dreamacro/clash/adapter/outbound" + C "github.com/Dreamacro/clash/constant" +) + +func TestClash_Vless(t *testing.T) { + configPath := C.Path.Resolve("vless.json") + + cfg := &container.Config{ + Image: ImageVmess, + ExposedPorts: defaultExposedPorts, + Entrypoint: []string{"/usr/bin/v2ray"}, + Cmd: []string{"run", "-c", "/etc/v2ray/config.json"}, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + Binds: []string{fmt.Sprintf("%s:/etc/v2ray/config.json", configPath)}, + } + + id, err := startContainer(cfg, hostCfg, "vless") + require.NoError(t, err) + + t.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewVless(outbound.VlessOption{ + Name: "vless", + Server: localIP.String(), + Port: 10002, + UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", + Cipher: "auto", + UDP: true, + }) + require.NoError(t, err) + + time.Sleep(waitTime) + testSuit(t, proxy) +} + +func TestClash_VlessTLS(t *testing.T) { + cfg := &container.Config{ + Image: ImageVmess, + ExposedPorts: defaultExposedPorts, + Entrypoint: []string{"/usr/bin/v2ray"}, + Cmd: []string{"run", "-c", "/etc/v2ray/config.json"}, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + Binds: []string{ + fmt.Sprintf("%s:/etc/v2ray/config.json", C.Path.Resolve("vless-tls.json")), + fmt.Sprintf("%s:/etc/ssl/v2ray/fullchain.pem", C.Path.Resolve("example.org.pem")), + fmt.Sprintf("%s:/etc/ssl/v2ray/privkey.pem", C.Path.Resolve("example.org-key.pem")), + }, + } + + id, err := startContainer(cfg, hostCfg, "vless-tls") + require.NoError(t, err) + t.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewVless(outbound.VlessOption{ + Name: "vless", + Server: localIP.String(), + Port: 10002, + UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", + Cipher: "auto", + TLS: true, + SkipCertVerify: true, + ServerName: "example.org", + UDP: true, + }) + require.NoError(t, err) + + time.Sleep(waitTime) + testSuit(t, proxy) +} + +func TestClash_VlessHTTP2(t *testing.T) { + cfg := &container.Config{ + Image: ImageVmess, + ExposedPorts: defaultExposedPorts, + Entrypoint: []string{"/usr/bin/v2ray"}, + Cmd: []string{"run", "-c", "/etc/v2ray/config.json"}, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + Binds: []string{ + fmt.Sprintf("%s:/etc/v2ray/config.json", C.Path.Resolve("vless-http2.json")), + fmt.Sprintf("%s:/etc/ssl/v2ray/fullchain.pem", C.Path.Resolve("example.org.pem")), + fmt.Sprintf("%s:/etc/ssl/v2ray/privkey.pem", C.Path.Resolve("example.org-key.pem")), + }, + } + + id, err := startContainer(cfg, hostCfg, "vless-http2") + require.NoError(t, err) + t.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewVless(outbound.VlessOption{ + Name: "vless", + Server: localIP.String(), + Port: 10002, + UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", + Cipher: "auto", + Network: "h2", + TLS: true, + SkipCertVerify: true, + ServerName: "example.org", + UDP: true, + HTTP2Opts: outbound.HTTP2Options{ + Host: []string{"example.org"}, + Path: "/test", + }, + }) + require.NoError(t, err) + + time.Sleep(waitTime) + testSuit(t, proxy) +} + +func TestClash_VlessHTTP(t *testing.T) { + cfg := &container.Config{ + Image: ImageVmess, + ExposedPorts: defaultExposedPorts, + Entrypoint: []string{"/usr/bin/v2ray"}, + Cmd: []string{"run", "-c", "/etc/v2ray/config.json"}, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + Binds: []string{ + fmt.Sprintf("%s:/etc/v2ray/config.json", C.Path.Resolve("vless-http.json")), + }, + } + + id, err := startContainer(cfg, hostCfg, "vless-http") + require.NoError(t, err) + t.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewVless(outbound.VlessOption{ + Name: "vless", + Server: localIP.String(), + Port: 10002, + UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", + Cipher: "auto", + Network: "http", + UDP: true, + HTTPOpts: outbound.HTTPOptions{ + Method: "GET", + Path: []string{"/"}, + Headers: map[string][]string{ + "Host": {"www.amazon.com"}, + "User-Agent": { + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36 Edg/84.0.522.49", + }, + "Accept-Encoding": { + "gzip, deflate", + }, + "Connection": { + "keep-alive", + }, + "Pragma": {"no-cache"}, + }, + }, + }) + require.NoError(t, err) + + time.Sleep(waitTime) + testSuit(t, proxy) +} + +func TestClash_VlessWebsocket(t *testing.T) { + cfg := &container.Config{ + Image: ImageVmess, + ExposedPorts: defaultExposedPorts, + Entrypoint: []string{"/usr/bin/v2ray"}, + Cmd: []string{"run", "-c", "/etc/v2ray/config.json"}, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + Binds: []string{ + fmt.Sprintf("%s:/etc/v2ray/config.json", C.Path.Resolve("vless-ws.json")), + }, + } + + id, err := startContainer(cfg, hostCfg, "vless-ws") + require.NoError(t, err) + t.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewVless(outbound.VlessOption{ + Name: "vless", + Server: localIP.String(), + Port: 10002, + UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", + Cipher: "auto", + Network: "ws", + UDP: true, + }) + require.NoError(t, err) + + time.Sleep(waitTime) + testSuit(t, proxy) +} + +func TestClash_VlessWebsocketTLS(t *testing.T) { + cfg := &container.Config{ + Image: ImageVmess, + ExposedPorts: defaultExposedPorts, + Entrypoint: []string{"/usr/bin/v2ray"}, + Cmd: []string{"run", "-c", "/etc/v2ray/config.json"}, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + Binds: []string{ + fmt.Sprintf("%s:/etc/v2ray/config.json", C.Path.Resolve("vless-ws-tls.json")), + fmt.Sprintf("%s:/etc/ssl/v2ray/fullchain.pem", C.Path.Resolve("example.org.pem")), + fmt.Sprintf("%s:/etc/ssl/v2ray/privkey.pem", C.Path.Resolve("example.org-key.pem")), + }, + } + + id, err := startContainer(cfg, hostCfg, "vless-ws-tls") + require.NoError(t, err) + t.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewVless(outbound.VlessOption{ + Name: "vless", + Server: localIP.String(), + Port: 10002, + UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", + Cipher: "auto", + Network: "ws", + TLS: true, + SkipCertVerify: true, + UDP: true, + }) + require.NoError(t, err) + + time.Sleep(waitTime) + testSuit(t, proxy) +} + +func TestClash_VlessWebsocketTLSZero(t *testing.T) { + cfg := &container.Config{ + Image: ImageVmess, + ExposedPorts: defaultExposedPorts, + Entrypoint: []string{"/usr/bin/v2ray"}, + Cmd: []string{"run", "-c", "/etc/v2ray/config.json"}, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + Binds: []string{ + fmt.Sprintf("%s:/etc/v2ray/config.json", C.Path.Resolve("vless-ws-tls-zero.json")), + fmt.Sprintf("%s:/etc/ssl/v2ray/fullchain.pem", C.Path.Resolve("example.org.pem")), + fmt.Sprintf("%s:/etc/ssl/v2ray/privkey.pem", C.Path.Resolve("example.org-key.pem")), + }, + } + + id, err := startContainer(cfg, hostCfg, "vless-ws-tls-zero") + require.NoError(t, err) + t.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewVless(outbound.VlessOption{ + Name: "vless", + Server: localIP.String(), + Port: 10002, + UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", + Cipher: "zero", + Network: "ws", + TLS: true, + SkipCertVerify: true, + UDP: true, + }) + require.NoError(t, err) + + time.Sleep(waitTime) + testSuit(t, proxy) +} + +func TestClash_VlessGrpc(t *testing.T) { + cfg := &container.Config{ + Image: ImageVmess, + ExposedPorts: defaultExposedPorts, + Entrypoint: []string{"/usr/bin/v2ray"}, + Cmd: []string{"run", "-c", "/etc/v2ray/config.json"}, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + Binds: []string{ + fmt.Sprintf("%s:/etc/v2ray/config.json", C.Path.Resolve("vless-grpc.json")), + fmt.Sprintf("%s:/etc/ssl/v2ray/fullchain.pem", C.Path.Resolve("example.org.pem")), + fmt.Sprintf("%s:/etc/ssl/v2ray/privkey.pem", C.Path.Resolve("example.org-key.pem")), + }, + } + + id, err := startContainer(cfg, hostCfg, "vless-grpc") + require.NoError(t, err) + t.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewVless(outbound.VlessOption{ + Name: "vless", + Server: localIP.String(), + Port: 10002, + UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", + Cipher: "auto", + Network: "grpc", + TLS: true, + SkipCertVerify: true, + UDP: true, + ServerName: "example.org", + GrpcOpts: outbound.GrpcOptions{ + GrpcServiceName: "example!", + }, + }) + require.NoError(t, err) + + time.Sleep(waitTime) + testSuit(t, proxy) +} + +func TestClash_VlessWebsocket0RTT(t *testing.T) { + cfg := &container.Config{ + Image: ImageVmess, + ExposedPorts: defaultExposedPorts, + Entrypoint: []string{"/usr/bin/v2ray"}, + Cmd: []string{"run", "-c", "/etc/v2ray/config.json"}, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + Binds: []string{ + fmt.Sprintf("%s:/etc/v2ray/config.json", C.Path.Resolve("vless-ws-0rtt.json")), + }, + } + + id, err := startContainer(cfg, hostCfg, "vless-ws-0rtt") + require.NoError(t, err) + t.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewVless(outbound.VlessOption{ + Name: "vless", + Server: localIP.String(), + Port: 10002, + UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", + Cipher: "auto", + Network: "ws", + UDP: true, + ServerName: "example.org", + WSOpts: outbound.WSOptions{ + MaxEarlyData: 2048, + EarlyDataHeaderName: "Sec-WebSocket-Protocol", + }, + }) + require.NoError(t, err) + + time.Sleep(waitTime) + testSuit(t, proxy) +} + +func TestClash_VlessWebsocketXray0RTT(t *testing.T) { + cfg := &container.Config{ + Image: ImageXray, + ExposedPorts: defaultExposedPorts, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + Binds: []string{ + fmt.Sprintf("%s:/etc/xray/config.json", C.Path.Resolve("vless-ws-0rtt.json")), + }, + } + + id, err := startContainer(cfg, hostCfg, "vless-xray-ws-0rtt") + require.NoError(t, err) + t.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewVless(outbound.VlessOption{ + Name: "vless", + Server: localIP.String(), + Port: 10002, + UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", + Cipher: "auto", + Network: "ws", + UDP: true, + ServerName: "example.org", + WSOpts: outbound.WSOptions{ + Path: "/?ed=2048", + }, + }) + require.NoError(t, err) + + time.Sleep(waitTime) + testSuit(t, proxy) +} + +func Benchmark_Vless(b *testing.B) { + configPath := C.Path.Resolve("vless.json") + + cfg := &container.Config{ + Image: ImageVmess, + ExposedPorts: defaultExposedPorts, + Entrypoint: []string{"/usr/bin/v2ray"}, + Cmd: []string{"run", "-c", "/etc/v2ray/config.json"}, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + Binds: []string{fmt.Sprintf("%s:/etc/v2ray/config.json", configPath)}, + } + + id, err := startContainer(cfg, hostCfg, "vless-bench") + require.NoError(b, err) + + b.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewVless(outbound.VlessOption{ + Name: "vless", + Server: localIP.String(), + Port: 10002, + UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", + Cipher: "auto", + AlterID: 0, + UDP: true, + }) + require.NoError(b, err) + + time.Sleep(waitTime) + benchmarkProxy(b, proxy) +} diff --git a/transport/vmess/conn.go b/transport/vmess/conn.go index bb77cef..5d8d2b4 100644 --- a/transport/vmess/conn.go +++ b/transport/vmess/conn.go @@ -34,6 +34,7 @@ type Conn struct { security byte option byte isAead bool + isVless bool received bool } @@ -55,6 +56,29 @@ func (vc *Conn) Read(b []byte) (int, error) { } func (vc *Conn) sendRequest() error { + if vc.isVless { + buf := protobytes.BytesWriter{} + buf.PutUint8(0) // Protocol Version + buf.PutSlice(vc.id.UUID.Bytes()) // UUID + buf.PutUint8(0) // Addons Length + // buf.PutString("") // Addons Data + + // Command + if vc.dst.UDP { + buf.PutUint8(CommandUDP) + } else { + buf.PutUint8(CommandTCP) + } + + // Port AddrType Addr + buf.PutUint16be(uint16(vc.dst.Port)) + buf.PutUint8(vc.dst.AddrType) + buf.PutSlice(vc.dst.Addr) + + _, err := vc.Conn.Write(buf.Bytes()) + return err + } + timestamp := time.Now() mbuf := protobytes.BytesWriter{} @@ -119,6 +143,24 @@ func (vc *Conn) sendRequest() error { } func (vc *Conn) recvResponse() error { + if vc.isVless { + var buffer [2]byte + if _, err := io.ReadFull(vc.Conn, buffer[:]); err != nil { + return err + } + + if buffer[0] != 0 { + return errors.New("unexpected response version") + } + + length := int64(buffer[1]) + if length != 0 { // addon data length > 0 + io.CopyN(io.Discard, vc.Conn, length) // just discard + } + + return nil + } + var buf []byte if !vc.isAead { block, err := aes.NewCipher(vc.respBodyKey[:]) @@ -194,31 +236,37 @@ func hashTimestamp(t time.Time) []byte { } // newConn return a Conn instance -func newConn(conn net.Conn, id *ID, dst *DstAddr, security Security, isAead bool) (*Conn, error) { - randBytes := make([]byte, 33) - rand.Read(randBytes) - reqBodyIV := make([]byte, 16) - reqBodyKey := make([]byte, 16) - copy(reqBodyIV[:], randBytes[:16]) - copy(reqBodyKey[:], randBytes[16:32]) - respV := randBytes[32] - option := OptionChunkStream - +func newConn(conn net.Conn, id *ID, dst *DstAddr, security Security, isAead bool, isVless bool) (*Conn, error) { var ( + reqBodyKey []byte + reqBodyIV []byte respBodyKey []byte respBodyIV []byte + respV byte + option byte ) - if isAead { - bodyKey := sha256.Sum256(reqBodyKey) - bodyIV := sha256.Sum256(reqBodyIV) - respBodyKey = bodyKey[:16] - respBodyIV = bodyIV[:16] - } else { - bodyKey := md5.Sum(reqBodyKey) - bodyIV := md5.Sum(reqBodyIV) - respBodyKey = bodyKey[:] - respBodyIV = bodyIV[:] + if !isVless { + randBytes := make([]byte, 33) + rand.Read(randBytes) + reqBodyIV = make([]byte, 16) + reqBodyKey = make([]byte, 16) + copy(reqBodyIV[:], randBytes[:16]) + copy(reqBodyKey[:], randBytes[16:32]) + respV = randBytes[32] + option = OptionChunkStream + + if isAead { + bodyKey := sha256.Sum256(reqBodyKey) + bodyIV := sha256.Sum256(reqBodyIV) + respBodyKey = bodyKey[:16] + respBodyIV = bodyIV[:16] + } else { + bodyKey := md5.Sum(reqBodyKey) + bodyIV := md5.Sum(reqBodyIV) + respBodyKey = bodyKey[:] + respBodyIV = bodyIV[:] + } } var writer io.Writer @@ -276,6 +324,7 @@ func newConn(conn net.Conn, id *ID, dst *DstAddr, security Security, isAead bool security: security, option: option, isAead: isAead, + isVless: isVless, } if err := c.sendRequest(); err != nil { return nil, err diff --git a/transport/vmess/vmess.go b/transport/vmess/vmess.go index 331a0a8..ab9d3b0 100644 --- a/transport/vmess/vmess.go +++ b/transport/vmess/vmess.go @@ -56,6 +56,7 @@ type Client struct { uuid *uuid.UUID security Security isAead bool + isVless bool } // Config of vmess @@ -66,12 +67,13 @@ type Config struct { Port string HostName string IsAead bool + IsVless bool } // StreamConn return a Conn with net.Conn and DstAddr func (c *Client) StreamConn(conn net.Conn, dst *DstAddr) (net.Conn, error) { r := rand.Intn(len(c.user)) - return newConn(conn, c.user[r], dst, c.security, c.isAead) + return newConn(conn, c.user[r], dst, c.security, c.isAead, c.isVless) } // NewClient return Client instance @@ -81,6 +83,11 @@ func NewClient(config Config) (*Client, error) { return nil, err } + if config.IsVless { + config.AlterID = 0 + config.Security = "zero" + } + var security Security switch config.Security { case "aes-128-gcm": @@ -105,5 +112,6 @@ func NewClient(config Config) (*Client, error) { uuid: &uid, security: security, isAead: config.IsAead, + isVless: config.IsVless, }, nil }