From b3ea2ff8b6ab5dcb93cda20aa7679fb5b7deee12 Mon Sep 17 00:00:00 2001 From: yaling888 <73897884+yaling888@users.noreply.github.com> Date: Tue, 29 Mar 2022 23:50:41 +0800 Subject: [PATCH 1/2] Chore: adjust VLESS --- README.md | 14 ++- adapter/outbound/vless.go | 141 +++++++++++++++++------ listener/tun/device/tun/tun_wireguard.go | 1 - test/config/vless-ws.json | 35 ++++++ test/config/vless-xtls.json | 4 +- test/vless_test.go | 40 ++++++- transport/vless/vless.go | 9 -- 7 files changed, 193 insertions(+), 51 deletions(-) create mode 100644 test/config/vless-ws.json diff --git a/README.md b/README.md index d7af8a41..e9821aaa 100644 --- a/README.md +++ b/README.md @@ -132,14 +132,24 @@ Support outbound transport protocol `VLESS`. The XTLS only support TCP transport by the XRAY-CORE. ```yaml proxies: - - name: "vless-tcp" + - name: "vless-tls" type: vless server: server port: 443 uuid: uuid network: tcp servername: example.com - # flow: xtls-rprx-direct # xtls-rprx-origin # enable XTLS + udp: true + # skip-cert-verify: true + - name: "vless-xtls" + type: vless + server: server + port: 443 + uuid: uuid + network: tcp + servername: example.com + flow: xtls-rprx-direct # or xtls-rprx-origin + # flow-show: true # print the XTLS direct log # udp: true # skip-cert-verify: true ``` diff --git a/adapter/outbound/vless.go b/adapter/outbound/vless.go index 876613b2..908c4de8 100644 --- a/adapter/outbound/vless.go +++ b/adapter/outbound/vless.go @@ -6,9 +6,11 @@ import ( "encoding/binary" "errors" "fmt" + "io" "net" "net/http" "strconv" + "sync" "github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/resolver" @@ -20,6 +22,11 @@ import ( "golang.org/x/net/http2" ) +const ( + // max packet length + maxLength = 1024 << 3 +) + type Vless struct { *Base client *vless.Client @@ -39,7 +46,6 @@ type VlessOption struct { UUID string `proxy:"uuid"` Flow string `proxy:"flow,omitempty"` FlowShow bool `proxy:"flow-show,omitempty"` - TLS bool `proxy:"tls,omitempty"` UDP bool `proxy:"udp,omitempty"` Network string `proxy:"network,omitempty"` HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"` @@ -80,19 +86,19 @@ func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { wsOpts.Headers = header } - if v.option.TLS { - wsOpts.TLS = true - wsOpts.TLSConfig = &tls.Config{ - ServerName: host, - InsecureSkipVerify: v.option.SkipCertVerify, - NextProtos: []string{"http/1.1"}, - } - if v.option.ServerName != "" { - wsOpts.TLSConfig.ServerName = v.option.ServerName - } else if host := wsOpts.Headers.Get("Host"); host != "" { - wsOpts.TLSConfig.ServerName = host - } + wsOpts.TLS = true + wsOpts.TLSConfig = &tls.Config{ + MinVersion: tls.VersionTLS12, + ServerName: host, + InsecureSkipVerify: v.option.SkipCertVerify, + NextProtos: []string{"http/1.1"}, } + if v.option.ServerName != "" { + wsOpts.TLSConfig.ServerName = v.option.ServerName + } else if host := wsOpts.Headers.Get("Host"); host != "" { + wsOpts.TLSConfig.ServerName = host + } + c, err = vmess.StreamWebsocketConn(c, wsOpts) case "http": // readability first, so just copy default TLS logic @@ -160,7 +166,7 @@ func (v *Vless) streamTLSOrXTLSConn(conn net.Conn, isH2 bool) (net.Conn, error) return vless.StreamXTLSConn(conn, &xtlsOpts) - } else if v.option.TLS { + } else { tlsOpts := vmess.TLSConfig{ Host: host, SkipCertVerify: v.option.SkipCertVerify, @@ -176,8 +182,6 @@ func (v *Vless) streamTLSOrXTLSConn(conn net.Conn, isH2 bool) (net.Conn, error) return vmess.StreamTLSConn(conn, &tlsOpts) } - - return conn, nil } func (v *Vless) isXTLSEnabled() bool { @@ -282,30 +286,97 @@ func parseVlessAddr(metadata *C.Metadata) *vless.DstAddr { type vlessPacketConn struct { net.Conn - rAddr net.Addr - cache [2]byte + rAddr net.Addr + cache [2]byte + remain int + mux sync.Mutex } -func (uc *vlessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { - binary.BigEndian.PutUint16(uc.cache[:], uint16(len(b))) - _, _ = uc.Conn.Write(uc.cache[:]) - return uc.Conn.Write(b) -} - -func (uc *vlessPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { - n, err := uc.Conn.Read(uc.cache[:]) - if err != nil { - return n, uc.rAddr, err +func (vc *vlessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { + total := len(b) + if total == 0 { + return 0, nil } - n, err = uc.Conn.Read(b) - return n, uc.rAddr, err + + if total < maxLength { + return vc.writePacket(b) + } + + offset := 0 + for { + cursor := offset + maxLength + if cursor > total { + cursor = total + } + + n, err := vc.writePacket(b[offset:cursor]) + if err != nil { + return offset + n, err + } + + offset = cursor + if offset == total { + break + } + } + + return total, nil +} + +func (vc *vlessPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { + vc.mux.Lock() + defer vc.mux.Unlock() + + if vc.remain != 0 { + length := len(b) + if length > vc.remain { + length = vc.remain + } + + n, err := vc.Conn.Read(b[:length]) + if err != nil { + return 0, vc.rAddr, err + } + + vc.remain -= n + + return n, vc.rAddr, nil + } + + if _, err := vc.Conn.Read(b[:2]); err != nil { + return 0, vc.rAddr, err + } + + total := int(binary.BigEndian.Uint16(b[:2])) + if total == 0 { + return 0, vc.rAddr, nil + } + + length := len(b) + if length > total { + length = total + } + + if _, err := io.ReadFull(vc.Conn, b[:length]); err != nil { + return 0, vc.rAddr, errors.New("read packet error") + } + + vc.remain = total - length + + return length, vc.rAddr, nil +} + +func (vc *vlessPacketConn) writePacket(payload []byte) (int, error) { + binary.BigEndian.PutUint16(vc.cache[:], uint16(len(payload))) + + if _, err := vc.Conn.Write(vc.cache[:]); err != nil { + return 0, err + } + + return vc.Conn.Write(payload) } func NewVless(option VlessOption) (*Vless, error) { - if !option.TLS { - return nil, fmt.Errorf("TLS must be true with vless") - } - var addons *vless.Addons if option.Network != "ws" && len(option.Flow) >= 16 { option.Flow = option.Flow[:16] @@ -315,7 +386,7 @@ func NewVless(option VlessOption) (*Vless, error) { Flow: option.Flow, } default: - return nil, fmt.Errorf("unsupported vless flow type: %s", option.Flow) + return nil, fmt.Errorf("unsupported xtls flow type: %s", option.Flow) } } diff --git a/listener/tun/device/tun/tun_wireguard.go b/listener/tun/device/tun/tun_wireguard.go index 6e0ad15f..35008425 100644 --- a/listener/tun/device/tun/tun_wireguard.go +++ b/listener/tun/device/tun/tun_wireguard.go @@ -95,7 +95,6 @@ func (t *TUN) Write(packet []byte) (int, error) { return t.nt.Write(packet, t.offset) } - _ = t.cache[:t.offset] packet = append(t.cache[:t.offset], packet...) return t.nt.Write(packet, t.offset) diff --git a/test/config/vless-ws.json b/test/config/vless-ws.json new file mode 100644 index 00000000..9f3a5db8 --- /dev/null +++ b/test/config/vless-ws.json @@ -0,0 +1,35 @@ +{ + "inbounds": [ + { + "port": 10002, + "listen": "0.0.0.0", + "protocol": "vless", + "settings": { + "clients": [ + { + "id": "b831381d-6324-4d53-ad4f-8cda48b30811", + "level": 0, + "email": "ws@example.com" + } + ] + }, + "streamSettings": { + "network": "ws", + "security": "tls", + "tlsSettings": { + "certificates": [ + { + "certificateFile": "/etc/ssl/v2ray/fullchain.pem", + "keyFile": "/etc/ssl/v2ray/privkey.pem" + } + ] + } + } + } + ], + "outbounds": [ + { + "protocol": "freedom" + } + ] +} \ No newline at end of file diff --git a/test/config/vless-xtls.json b/test/config/vless-xtls.json index b4381d61..1d352c3f 100644 --- a/test/config/vless-xtls.json +++ b/test/config/vless-xtls.json @@ -8,9 +8,9 @@ "clients": [ { "id": "b831381d-6324-4d53-ad4f-8cda48b30811", + "email": "xtls@example.com", "flow": "xtls-rprx-direct", - "level": 0, - "email": "love@example.com" + "level": 0 } ], "decryption": "none" diff --git a/test/vless_test.go b/test/vless_test.go index b5fbec86..3f516925 100644 --- a/test/vless_test.go +++ b/test/vless_test.go @@ -37,7 +37,6 @@ func TestClash_VlessTLS(t *testing.T) { Server: localIP.String(), Port: 10002, UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", - TLS: true, SkipCertVerify: true, ServerName: "example.org", UDP: true, @@ -75,7 +74,6 @@ func TestClash_VlessXTLS(t *testing.T) { Server: localIP.String(), Port: 10002, UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", - TLS: true, SkipCertVerify: true, ServerName: "example.org", UDP: true, @@ -89,3 +87,41 @@ func TestClash_VlessXTLS(t *testing.T) { time.Sleep(waitTime) testSuit(t, proxy) } + +func TestClash_VlessWS(t *testing.T) { + cfg := &container.Config{ + Image: ImageVmess, + ExposedPorts: defaultExposedPorts, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + Binds: []string{ + fmt.Sprintf("%s:/etc/v2ray/config.json", C.Path.Resolve("vless-ws.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") + if err != nil { + assert.FailNow(t, err.Error()) + } + defer cleanContainer(id) + + proxy, err := outbound.NewVless(outbound.VlessOption{ + Name: "vless", + Server: localIP.String(), + Port: 10002, + UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", + SkipCertVerify: true, + ServerName: "example.org", + Network: "ws", + UDP: true, + }) + if err != nil { + assert.FailNow(t, err.Error()) + } + + time.Sleep(waitTime) + testSuit(t, proxy) +} diff --git a/transport/vless/vless.go b/transport/vless/vless.go index 458f54de..bb0ce881 100644 --- a/transport/vless/vless.go +++ b/transport/vless/vless.go @@ -35,15 +35,6 @@ type DstAddr struct { Port uint } -// Config of vless -type Config struct { - UUID string - AlterID uint16 - Security string - Port string - HostName string -} - // Client is vless connection generator type Client struct { uuid *uuid.UUID From 9ff1f5530e965773ae554529fa5558f4f18c1a63 Mon Sep 17 00:00:00 2001 From: yaling888 <73897884+yaling888@users.noreply.github.com> Date: Wed, 30 Mar 2022 00:15:39 +0800 Subject: [PATCH 2/2] Feature: Trojan XTLS --- README.md | 22 +++++++-- adapter/outbound/trojan.go | 32 ++++++++++++- test/config/trojan-xtls.json | 39 +++++++++++++++ test/trojan_test.go | 40 ++++++++++++++++ transport/trojan/trojan.go | 93 +++++++++++++++++++++++++++++------- 5 files changed, 205 insertions(+), 21 deletions(-) create mode 100644 test/config/trojan-xtls.json diff --git a/README.md b/README.md index e9821aaa..2eca8bbe 100644 --- a/README.md +++ b/README.md @@ -127,11 +127,14 @@ rules: ``` ### Proxies configuration -Support outbound transport protocol `VLESS`. +Support outbound protocol `VLESS`. -The XTLS only support TCP transport by the XRAY-CORE. +Support `Trojan` with XTLS. + +Currently XTLS only supports TCP transport. ```yaml proxies: + # VLESS - name: "vless-tls" type: vless server: server @@ -149,9 +152,22 @@ proxies: network: tcp servername: example.com flow: xtls-rprx-direct # or xtls-rprx-origin - # flow-show: true # print the XTLS direct log + # flow-show: true # print the XTLS direction log # udp: true # skip-cert-verify: true + + # Trojan + - name: "trojan-xtls" + type: trojan + server: server + port: 443 + password: yourpsk + network: tcp + flow: xtls-rprx-direct # or xtls-rprx-origin + # flow-show: true # print the XTLS direction log + # udp: true + # sni: example.com # aka server name + # skip-cert-verify: true ``` ### IPTABLES configuration diff --git a/adapter/outbound/trojan.go b/adapter/outbound/trojan.go index 064cd3c2..aa389b34 100644 --- a/adapter/outbound/trojan.go +++ b/adapter/outbound/trojan.go @@ -12,6 +12,7 @@ import ( C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/transport/gun" "github.com/Dreamacro/clash/transport/trojan" + "github.com/Dreamacro/clash/transport/vless" "golang.org/x/net/http2" ) @@ -40,6 +41,8 @@ type TrojanOption struct { Network string `proxy:"network,omitempty"` GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` WSOpts WSOptions `proxy:"ws-opts,omitempty"` + Flow string `proxy:"flow,omitempty"` + FlowShow bool `proxy:"flow-show,omitempty"` } func (t *Trojan) plainStream(c net.Conn) (net.Conn, error) { @@ -82,6 +85,11 @@ func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) return nil, fmt.Errorf("%s connect error: %w", t.addr, err) } + c, err = t.instance.PresetXTLSConn(c) + if err != nil { + return nil, err + } + err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata)) return c, err } @@ -95,6 +103,12 @@ func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ... return nil, err } + c, err = t.instance.PresetXTLSConn(c) + if err != nil { + c.Close() + return nil, err + } + if err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata)); err != nil { c.Close() return nil, err @@ -160,6 +174,17 @@ func NewTrojan(option TrojanOption) (*Trojan, error) { ALPN: option.ALPN, ServerName: option.Server, SkipCertVerify: option.SkipCertVerify, + FlowShow: option.FlowShow, + } + + if option.Network != "ws" && len(option.Flow) >= 16 { + option.Flow = option.Flow[:16] + switch option.Flow { + case vless.XRO, vless.XRD, vless.XRS: + tOption.Flow = option.Flow + default: + return nil, fmt.Errorf("unsupported xtls flow type: %s", option.Flow) + } } if option.SNI != "" { @@ -196,7 +221,12 @@ func NewTrojan(option TrojanOption) (*Trojan, error) { ServerName: tOption.ServerName, } - t.transport = gun.NewHTTP2Client(dialFn, tlsConfig) + if t.option.Flow != "" { + t.transport = gun.NewHTTP2XTLSClient(dialFn, tlsConfig) + } else { + t.transport = gun.NewHTTP2Client(dialFn, tlsConfig) + } + t.gunTLSConfig = tlsConfig t.gunConfig = &gun.Config{ ServiceName: option.GrpcOpts.GrpcServiceName, diff --git a/test/config/trojan-xtls.json b/test/config/trojan-xtls.json new file mode 100644 index 00000000..c3a72eee --- /dev/null +++ b/test/config/trojan-xtls.json @@ -0,0 +1,39 @@ +{ + "inbounds": [ + { + "port": 10002, + "listen": "0.0.0.0", + "protocol": "trojan", + "settings": { + "clients": [ + { + "password": "example", + "email": "xtls@example.com", + "flow": "xtls-rprx-direct", + "level": 0 + } + ] + }, + "streamSettings": { + "network": "tcp", + "security": "xtls", + "xtlsSettings": { + "certificates": [ + { + "certificateFile": "/etc/ssl/v2ray/fullchain.pem", + "keyFile": "/etc/ssl/v2ray/privkey.pem" + } + ] + } + } + } + ], + "outbounds": [ + { + "protocol": "freedom" + } + ], + "log": { + "loglevel": "debug" + } +} \ No newline at end of file diff --git a/test/trojan_test.go b/test/trojan_test.go index d1ab2a00..8b4e1745 100644 --- a/test/trojan_test.go +++ b/test/trojan_test.go @@ -131,6 +131,46 @@ func TestClash_TrojanWebsocket(t *testing.T) { testSuit(t, proxy) } +func TestClash_TrojanXTLS(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("trojan-xtls.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, "trojan-xtls") + if err != nil { + assert.FailNow(t, err.Error()) + } + defer cleanContainer(id) + + proxy, err := outbound.NewTrojan(outbound.TrojanOption{ + Name: "trojan", + Server: localIP.String(), + Port: 10002, + Password: "example", + SNI: "example.org", + SkipCertVerify: true, + UDP: true, + Network: "tcp", + Flow: "xtls-rprx-direct", + FlowShow: true, + }) + if err != nil { + assert.FailNow(t, err.Error()) + } + + time.Sleep(waitTime) + testSuit(t, proxy) +} + func Benchmark_Trojan(b *testing.B) { cfg := &container.Config{ Image: ImageTrojan, diff --git a/transport/trojan/trojan.go b/transport/trojan/trojan.go index ac9f17dd..a0e289f1 100644 --- a/transport/trojan/trojan.go +++ b/transport/trojan/trojan.go @@ -7,6 +7,7 @@ import ( "encoding/binary" "encoding/hex" "errors" + "fmt" "io" "net" "net/http" @@ -15,7 +16,10 @@ import ( "github.com/Dreamacro/clash/common/pool" C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/transport/socks5" + "github.com/Dreamacro/clash/transport/vless" "github.com/Dreamacro/clash/transport/vmess" + + xtls "github.com/xtls/go" ) const ( @@ -32,9 +36,13 @@ var ( type Command = byte -var ( +const ( CommandTCP byte = 1 CommandUDP byte = 3 + + // for XTLS + commandXRD byte = 0xf0 // XTLS direct mode + commandXRO byte = 0xf1 // XTLS origin mode ) type Option struct { @@ -42,6 +50,8 @@ type Option struct { ALPN []string ServerName string SkipCertVerify bool + Flow string + FlowShow bool } type WebsocketOption struct { @@ -62,23 +72,42 @@ func (t *Trojan) StreamConn(conn net.Conn) (net.Conn, error) { alpn = t.option.ALPN } - tlsConfig := &tls.Config{ - NextProtos: alpn, - MinVersion: tls.VersionTLS12, - InsecureSkipVerify: t.option.SkipCertVerify, - ServerName: t.option.ServerName, + if t.option.Flow != "" { + xtlsConfig := &xtls.Config{ + NextProtos: alpn, + MinVersion: xtls.VersionTLS12, + InsecureSkipVerify: t.option.SkipCertVerify, + ServerName: t.option.ServerName, + } + + xtlsConn := xtls.Client(conn, xtlsConfig) + + ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) + defer cancel() + if err := xtlsConn.HandshakeContext(ctx); err != nil { + return nil, err + } + + return xtlsConn, nil + } else { + tlsConfig := &tls.Config{ + NextProtos: alpn, + MinVersion: tls.VersionTLS12, + InsecureSkipVerify: t.option.SkipCertVerify, + ServerName: t.option.ServerName, + } + + tlsConn := tls.Client(conn, tlsConfig) + + // fix tls handshake not timeout + ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) + defer cancel() + if err := tlsConn.HandshakeContext(ctx); err != nil { + return nil, err + } + + return tlsConn, nil } - - tlsConn := tls.Client(conn, tlsConfig) - - // fix tls handshake not timeout - ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) - defer cancel() - if err := tlsConn.HandshakeContext(ctx); err != nil { - return nil, err - } - - return tlsConn, nil } func (t *Trojan) StreamWebsocketConn(conn net.Conn, wsOptions *WebsocketOption) (net.Conn, error) { @@ -104,7 +133,37 @@ func (t *Trojan) StreamWebsocketConn(conn net.Conn, wsOptions *WebsocketOption) }) } +func (t *Trojan) PresetXTLSConn(conn net.Conn) (net.Conn, error) { + switch t.option.Flow { + case vless.XRO, vless.XRD, vless.XRS: + if xtlsConn, ok := conn.(*xtls.Conn); ok { + xtlsConn.RPRX = true + xtlsConn.SHOW = t.option.FlowShow + xtlsConn.MARK = "XTLS" + if t.option.Flow == vless.XRS { + t.option.Flow = vless.XRD + } + + if t.option.Flow == vless.XRD { + xtlsConn.DirectMode = true + } + } else { + return nil, fmt.Errorf("failed to use %s, maybe \"security\" is not \"xtls\"", t.option.Flow) + } + } + + return conn, nil +} + func (t *Trojan) WriteHeader(w io.Writer, command Command, socks5Addr []byte) error { + if command == CommandTCP { + if t.option.Flow == vless.XRD { + command = commandXRD + } else if t.option.Flow == vless.XRO { + command = commandXRO + } + } + buf := pool.GetBuffer() defer pool.PutBuffer(buf)