From d8dc44e78613146a8117358838f7317fb5e54705 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 14 Jun 2022 13:21:22 +0800 Subject: [PATCH] Refactor: vmess Add support for vmess length masking/packetaddr/authenticated length Add support for zero/aes-128-cfb protcol --- adapter/outbound/vmess.go | 133 ++++++++++++++++++-------------------- go.mod | 3 +- go.sum | 6 +- test/clash_test.go | 13 ++-- test/go.mod | 5 +- test/go.sum | 10 +-- test/vmess_test.go | 68 +++++++++++++++++++ 7 files changed, 152 insertions(+), 86 deletions(-) diff --git a/adapter/outbound/vmess.go b/adapter/outbound/vmess.go index 1ad5036fc..c9b9852f9 100644 --- a/adapter/outbound/vmess.go +++ b/adapter/outbound/vmess.go @@ -5,17 +5,20 @@ import ( "crypto/tls" "errors" "fmt" - "github.com/Dreamacro/clash/common/convert" "net" "net/http" "strconv" "strings" + "github.com/Dreamacro/clash/common/convert" "github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/resolver" C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/transport/gun" - "github.com/Dreamacro/clash/transport/vmess" + clashVMess "github.com/Dreamacro/clash/transport/vmess" + "github.com/sagernet/sing-vmess" + "github.com/sagernet/sing-vmess/packetaddr" + M "github.com/sagernet/sing/common/metadata" ) type Vmess struct { @@ -31,21 +34,23 @@ type Vmess struct { type VmessOption struct { BasicOption - Name string `proxy:"name"` - Server string `proxy:"server"` - Port int `proxy:"port"` - UUID string `proxy:"uuid"` - AlterID int `proxy:"alterId"` - Cipher string `proxy:"cipher"` - UDP bool `proxy:"udp,omitempty"` - Network string `proxy:"network,omitempty"` - TLS bool `proxy:"tls,omitempty"` - SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` - ServerName string `proxy:"servername,omitempty"` - HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"` - HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"` - GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` - WSOpts WSOptions `proxy:"ws-opts,omitempty"` + Name string `proxy:"name"` + Server string `proxy:"server"` + Port int `proxy:"port"` + UUID string `proxy:"uuid"` + AlterID int `proxy:"alterId"` + Cipher string `proxy:"cipher"` + UDP bool `proxy:"udp,omitempty"` + Network string `proxy:"network,omitempty"` + TLS bool `proxy:"tls,omitempty"` + SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` + ServerName string `proxy:"servername,omitempty"` + HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"` + HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"` + GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` + WSOpts WSOptions `proxy:"ws-opts,omitempty"` + PacketAddr bool `proxy:"packet-addr,omitempty"` + AuthenticatedLength bool `proxy:"authenticated-length,omitempty"` // TODO: compatible with VMESS WS older version configurations WSHeaders map[string]string `proxy:"ws-headers,omitempty"` @@ -81,7 +86,7 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { case "ws": host, port, _ := net.SplitHostPort(v.addr) - wsOpts := &vmess.WebsocketConfig{ + wsOpts := &clashVMess.WebsocketConfig{ Host: host, Port: port, Path: v.option.WSOpts.Path, @@ -114,12 +119,12 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { convert.SetUserAgent(wsOpts.Headers) } } - c, err = vmess.StreamWebsocketConn(c, wsOpts) + c, err = clashVMess.StreamWebsocketConn(c, wsOpts) case "http": // readability first, so just copy default TLS logic if v.option.TLS { host, _, _ := net.SplitHostPort(v.addr) - tlsOpts := &vmess.TLSConfig{ + tlsOpts := &clashVMess.TLSConfig{ Host: host, SkipCertVerify: v.option.SkipCertVerify, } @@ -128,24 +133,24 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { tlsOpts.Host = v.option.ServerName } - c, err = vmess.StreamTLSConn(c, tlsOpts) + c, err = clashVMess.StreamTLSConn(c, tlsOpts) if err != nil { return nil, err } } host, _, _ := net.SplitHostPort(v.addr) - httpOpts := &vmess.HTTPConfig{ + httpOpts := &clashVMess.HTTPConfig{ Host: host, Method: v.option.HTTPOpts.Method, Path: v.option.HTTPOpts.Path, Headers: v.option.HTTPOpts.Headers, } - c = vmess.StreamHTTPConn(c, httpOpts) + c = clashVMess.StreamHTTPConn(c, httpOpts) case "h2": host, _, _ := net.SplitHostPort(v.addr) - tlsOpts := vmess.TLSConfig{ + tlsOpts := clashVMess.TLSConfig{ Host: host, SkipCertVerify: v.option.SkipCertVerify, NextProtos: []string{"h2"}, @@ -155,24 +160,24 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { tlsOpts.Host = v.option.ServerName } - c, err = vmess.StreamTLSConn(c, &tlsOpts) + c, err = clashVMess.StreamTLSConn(c, &tlsOpts) if err != nil { return nil, err } - h2Opts := &vmess.H2Config{ + h2Opts := &clashVMess.H2Config{ Hosts: v.option.HTTP2Opts.Host, Path: v.option.HTTP2Opts.Path, } - c, err = vmess.StreamH2Conn(c, h2Opts) + c, err = clashVMess.StreamH2Conn(c, h2Opts) case "grpc": c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig) default: // handle TLS if v.option.TLS { host, _, _ := net.SplitHostPort(v.addr) - tlsOpts := &vmess.TLSConfig{ + tlsOpts := &clashVMess.TLSConfig{ Host: host, SkipCertVerify: v.option.SkipCertVerify, } @@ -181,15 +186,18 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { tlsOpts.Host = v.option.ServerName } - c, err = vmess.StreamTLSConn(c, tlsOpts) + c, err = clashVMess.StreamTLSConn(c, tlsOpts) } } if err != nil { return nil, err } - - return v.client.StreamConn(c, parseVmessAddr(metadata)) + if metadata.NetWork == C.UDP { + return v.client.DialPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress())) + } else { + return v.client.DialConn(c, M.ParseSocksaddr(metadata.RemoteAddress())) + } } // DialContext implements C.ProxyAdapter @@ -202,7 +210,7 @@ func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d } defer safeConnClose(c, err) - c, err = v.client.StreamConn(c, parseVmessAddr(metadata)) + c, err = v.client.DialConn(c, M.ParseSocksaddr(metadata.RemoteAddress())) if err != nil { return nil, err } @@ -232,6 +240,11 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o metadata.DstIP = ip } + if v.option.PacketAddr { + metadata.Host = packetaddr.SeqPacketMagicAddress + metadata.DstPort = "443" + } + var c net.Conn // gun transport if v.transport != nil && len(opts) == 0 { @@ -241,7 +254,7 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o } defer safeConnClose(c, err) - c, err = v.client.StreamConn(c, parseVmessAddr(metadata)) + c, err = v.client.DialPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress())) } else { c, err = dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...) if err != nil { @@ -257,11 +270,21 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o return nil, fmt.Errorf("new vmess client error: %v", err) } - return v.ListenPacketOnStreamConn(c, metadata) + if v.option.PacketAddr { + return newPacketConn(packetaddr.NewBindClient(c), v), nil + } else if pc, ok := c.(net.PacketConn); ok { + return newPacketConn(pc, v), nil + } + return newPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil } // ListenPacketOnStreamConn implements C.ProxyAdapter func (v *Vmess) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) { + if v.option.PacketAddr { + return newPacketConn(packetaddr.NewBindClient(c), v), nil + } else if pc, ok := c.(net.PacketConn); ok { + return newPacketConn(pc, v), nil + } return newPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil } @@ -272,14 +295,11 @@ func (v *Vmess) SupportUOT() bool { func NewVmess(option VmessOption) (*Vmess, error) { security := strings.ToLower(option.Cipher) - client, err := vmess.NewClient(vmess.Config{ - UUID: option.UUID, - AlterID: uint16(option.AlterID), - Security: security, - HostName: option.Server, - Port: strconv.Itoa(option.Port), - IsAead: option.AlterID == 0, - }) + var options []vmess.ClientOption + if option.AuthenticatedLength { + options = append(options, vmess.ClientWithAuthenticatedLength()) + } + client, err := vmess.NewClient(option.UUID, security, option.AlterID, options...) if err != nil { return nil, err } @@ -338,38 +358,9 @@ func NewVmess(option VmessOption) (*Vmess, error) { v.gunConfig = gunConfig v.transport = gun.NewHTTP2Client(dialFn, tlsConfig) } - return v, nil } -func parseVmessAddr(metadata *C.Metadata) *vmess.DstAddr { - var addrType byte - var addr []byte - switch metadata.AddrType { - case C.AtypIPv4: - addrType = byte(vmess.AtypIPv4) - addr = make([]byte, net.IPv4len) - copy(addr[:], metadata.DstIP.AsSlice()) - case C.AtypIPv6: - addrType = byte(vmess.AtypIPv6) - addr = make([]byte, net.IPv6len) - copy(addr[:], metadata.DstIP.AsSlice()) - case C.AtypDomainName: - addrType = byte(vmess.AtypDomainName) - addr = make([]byte, len(metadata.Host)+1) - addr[0] = byte(len(metadata.Host)) - copy(addr[1:], []byte(metadata.Host)) - } - - port, _ := strconv.ParseUint(metadata.DstPort, 10, 16) - return &vmess.DstAddr{ - UDP: metadata.NetWork == C.UDP, - AddrType: addrType, - Addr: addr, - Port: uint(port), - } -} - type vmessPacketConn struct { net.Conn rAddr net.Addr diff --git a/go.mod b/go.mod index 0d792322f..d31402cf4 100644 --- a/go.mod +++ b/go.mod @@ -13,8 +13,9 @@ require ( github.com/lucas-clemente/quic-go v0.27.2 github.com/miekg/dns v1.1.49 github.com/oschwald/geoip2-golang v1.7.0 - github.com/sagernet/sing v0.0.0-20220610074707-a30d5506be41 + github.com/sagernet/sing v0.0.0-20220614034114-7caa1d0c0851 github.com/sagernet/sing-shadowsocks v0.0.0-20220610074818-432dcbdb1d7c + github.com/sagernet/sing-vmess v0.0.0-20220614042419-6f7c1431421a github.com/sirupsen/logrus v1.8.1 github.com/stretchr/testify v1.7.2 github.com/tobyxdd/hysteria v1.0.4 diff --git a/go.sum b/go.sum index 24658b9e2..97c6185ef 100644 --- a/go.sum +++ b/go.sum @@ -305,10 +305,12 @@ github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0 github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= -github.com/sagernet/sing v0.0.0-20220610074707-a30d5506be41 h1:vquZangXPhZJJz5PMCn/6qOYk+c4dtk/fb7HpDlof2I= -github.com/sagernet/sing v0.0.0-20220610074707-a30d5506be41/go.mod h1:ZEo7wZBfJmzm8uwnbCtWEHw9GsIfSThSylZYcR+H/Zw= +github.com/sagernet/sing v0.0.0-20220614034114-7caa1d0c0851 h1:Pp+9IPHtlwxrkiKMSrTOPS/eg+qJLdfZoeM89QX40BI= +github.com/sagernet/sing v0.0.0-20220614034114-7caa1d0c0851/go.mod h1:ZEo7wZBfJmzm8uwnbCtWEHw9GsIfSThSylZYcR+H/Zw= github.com/sagernet/sing-shadowsocks v0.0.0-20220610074818-432dcbdb1d7c h1:dapkcUcFbOwqnBwut6Dct7L695PVS6GoEqJeSRMWe0k= github.com/sagernet/sing-shadowsocks v0.0.0-20220610074818-432dcbdb1d7c/go.mod h1:ty1OoG/SgB6IccsWUS5DZBkEGKUKM8nRSWxLlqYUPx0= +github.com/sagernet/sing-vmess v0.0.0-20220614042419-6f7c1431421a h1:vEJtwy2Ysw9ftf+5TALig2ZiRFf1pDnWGxhwRqDs+Cs= +github.com/sagernet/sing-vmess v0.0.0-20220614042419-6f7c1431421a/go.mod h1:KZxqBTXh1v6q7TnVm36y+oCzPQLQKgv0z/TeBuozb2s= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= diff --git a/test/clash_test.go b/test/clash_test.go index 15a634b0a..2cc5bd9a5 100644 --- a/test/clash_test.go +++ b/test/clash_test.go @@ -31,6 +31,7 @@ const ( ImageShadowsocks = "mritd/shadowsocks:latest" ImageShadowsocksRust = "ghcr.io/shadowsocks/ssserver-rust:latest" ImageVmess = "v2fly/v2fly-core:latest" + ImageVmessLatest = "sagernet/v2fly-core:latest" ImageVless = "teddysun/xray:latest" ImageTrojan = "trojangfw/trojan:latest" ImageTrojanGo = "p4gefau1t/trojan-go:latest" @@ -450,26 +451,26 @@ func testLargeDataWithPacketConn(t *testing.T, pc net.PacketConn) error { writeRandData := func(pc net.PacketConn, addr net.Addr) (map[int][]byte, error) { hashMap := map[int][]byte{} mux := sync.Mutex{} - for i := 0; i < times; i++ { - go func(idx int) { + go func() { + for i := 0; i < times; i++ { buf := make([]byte, chunkSize) if _, err := rand.Read(buf[1:]); err != nil { t.Log(err.Error()) return } - buf[0] = byte(idx) + buf[0] = byte(i) hash := md5.Sum(buf) mux.Lock() - hashMap[idx] = hash[:] + hashMap[i] = hash[:] mux.Unlock() if _, err := pc.WriteTo(buf, addr); err != nil { t.Log(err.Error()) return } - }(i) - } + } + }() return hashMap, nil } diff --git a/test/go.mod b/test/go.mod index ac4e1756f..1046817e7 100644 --- a/test/go.mod +++ b/test/go.mod @@ -61,8 +61,9 @@ require ( github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.32.1 // indirect github.com/prometheus/procfs v0.7.3 // indirect - github.com/sagernet/sing v0.0.0-20220609091055-86d0144940e7 // indirect - github.com/sagernet/sing-shadowsocks v0.0.0-20220609092835-699292971c13 // indirect + github.com/sagernet/sing v0.0.0-20220614034114-7caa1d0c0851 // indirect + github.com/sagernet/sing-shadowsocks v0.0.0-20220610074818-432dcbdb1d7c // indirect + github.com/sagernet/sing-vmess v0.0.0-20220614042419-6f7c1431421a // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/tobyxdd/hysteria v1.0.4 // indirect github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf // indirect diff --git a/test/go.sum b/test/go.sum index 5ea06f65e..15e2096ff 100644 --- a/test/go.sum +++ b/test/go.sum @@ -323,10 +323,12 @@ github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0 github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= -github.com/sagernet/sing v0.0.0-20220609091055-86d0144940e7 h1:Q+uNKLNSKqpx+p96qcBTVFh8RUKiQFr4IrNVi5Q5yl0= -github.com/sagernet/sing v0.0.0-20220609091055-86d0144940e7/go.mod h1:w2HnJzXKHpD6F5Z/9XlSD4qbcpHY2RSZuQnFzqgELMg= -github.com/sagernet/sing-shadowsocks v0.0.0-20220609092835-699292971c13 h1:bQN0hjTHdB7SyaD9yjEYAl+bDl/kXW9zC0xNa+LMTrA= -github.com/sagernet/sing-shadowsocks v0.0.0-20220609092835-699292971c13/go.mod h1:Fp/9+odJhtgDmiHbZClMLnxaVvmDRJxwA7u/+uXWDiQ= +github.com/sagernet/sing v0.0.0-20220614034114-7caa1d0c0851 h1:Pp+9IPHtlwxrkiKMSrTOPS/eg+qJLdfZoeM89QX40BI= +github.com/sagernet/sing v0.0.0-20220614034114-7caa1d0c0851/go.mod h1:ZEo7wZBfJmzm8uwnbCtWEHw9GsIfSThSylZYcR+H/Zw= +github.com/sagernet/sing-shadowsocks v0.0.0-20220610074818-432dcbdb1d7c h1:dapkcUcFbOwqnBwut6Dct7L695PVS6GoEqJeSRMWe0k= +github.com/sagernet/sing-shadowsocks v0.0.0-20220610074818-432dcbdb1d7c/go.mod h1:ty1OoG/SgB6IccsWUS5DZBkEGKUKM8nRSWxLlqYUPx0= +github.com/sagernet/sing-vmess v0.0.0-20220614042419-6f7c1431421a h1:vEJtwy2Ysw9ftf+5TALig2ZiRFf1pDnWGxhwRqDs+Cs= +github.com/sagernet/sing-vmess v0.0.0-20220614042419-6f7c1431421a/go.mod h1:KZxqBTXh1v6q7TnVm36y+oCzPQLQKgv0z/TeBuozb2s= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= diff --git a/test/vmess_test.go b/test/vmess_test.go index c3f47a010..fd83fff8b 100644 --- a/test/vmess_test.go +++ b/test/vmess_test.go @@ -44,6 +44,74 @@ func TestClash_Vmess(t *testing.T) { testSuit(t, proxy) } +func TestClash_VmessAuthenticatedLength(t *testing.T) { + configPath := C.Path.Resolve("vmess.json") + + cfg := &container.Config{ + Image: ImageVmess, + ExposedPorts: defaultExposedPorts, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + Binds: []string{fmt.Sprintf("%s:/etc/v2ray/config.json", configPath)}, + } + + id, err := startContainer(cfg, hostCfg, "vmess") + require.NoError(t, err) + + t.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewVmess(outbound.VmessOption{ + Name: "vmess", + Server: localIP.String(), + Port: 10002, + UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", + Cipher: "auto", + UDP: true, + AuthenticatedLength: true, + }) + require.NoError(t, err) + + time.Sleep(waitTime) + testSuit(t, proxy) +} + +func TestClash_VmessPacketAddr(t *testing.T) { + configPath := C.Path.Resolve("vmess.json") + + cfg := &container.Config{ + Image: ImageVmessLatest, + ExposedPorts: defaultExposedPorts, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + Binds: []string{fmt.Sprintf("%s:/etc/v2ray/config.json", configPath)}, + } + + id, err := startContainer(cfg, hostCfg, "vmess") + require.NoError(t, err) + + t.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewVmess(outbound.VmessOption{ + Name: "vmess", + Server: localIP.String(), + Port: 10002, + UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", + Cipher: "auto", + UDP: true, + PacketAddr: true, + }) + require.NoError(t, err) + + time.Sleep(waitTime) + testSuit(t, proxy) +} + func TestClash_VmessTLS(t *testing.T) { cfg := &container.Config{ Image: ImageVmess,