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)