Skip to content

Commit

Permalink
Feature: support trojan websocket
Browse files Browse the repository at this point in the history
  • Loading branch information
Dreamacro committed Oct 16, 2021
1 parent 68753b4 commit df3a491
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 23 deletions.
35 changes: 33 additions & 2 deletions adapter/outbound/trojan.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"crypto/tls"
"fmt"
"net"
"net/http"
"strconv"

"github.com/Dreamacro/clash/component/dialer"
Expand All @@ -18,6 +19,7 @@ import (
type Trojan struct {
*Base
instance *trojan.Trojan
option *TrojanOption

// for gun mux
gunTLSConfig *tls.Config
Expand All @@ -36,6 +38,34 @@ type TrojanOption struct {
UDP bool `proxy:"udp,omitempty"`
Network string `proxy:"network,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
WSOpts WSOptions `proxy:"ws-opts,omitempty"`
}

func (t *Trojan) plainStream(c net.Conn) (net.Conn, error) {
if t.option.Network == "ws" {
host, port, _ := net.SplitHostPort(t.addr)
wsOpts := &trojan.WebsocketOption{
Host: host,
Port: port,
Path: t.option.WSOpts.Path,
}

if t.option.SNI != "" {
wsOpts.Host = t.option.SNI
}

if len(t.option.WSOpts.Headers) != 0 {
header := http.Header{}
for key, value := range t.option.WSOpts.Headers {
header.Add(key, value)
}
wsOpts.Headers = header
}

return t.instance.StreamWebsocketConn(c, wsOpts)
}

return t.instance.StreamConn(c)
}

// StreamConn implements C.ProxyAdapter
Expand All @@ -44,7 +74,7 @@ func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error)
if t.transport != nil {
c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig)
} else {
c, err = t.instance.StreamConn(c)
c, err = t.plainStream(c)
}

if err != nil {
Expand Down Expand Up @@ -106,7 +136,7 @@ func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
}
defer safeConnClose(c, err)
tcpKeepAlive(c)
c, err = t.instance.StreamConn(c)
c, err = t.plainStream(c)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
Expand Down Expand Up @@ -143,6 +173,7 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
udp: option.UDP,
},
instance: trojan.New(tOption),
option: &option,
}

if option.Network == "grpc" {
Expand Down
12 changes: 10 additions & 2 deletions adapter/outbound/vmess.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,16 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {

if v.option.TLS {
wsOpts.TLS = true
wsOpts.SkipCertVerify = v.option.SkipCertVerify
wsOpts.ServerName = v.option.ServerName
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
}
}
c, err = vmess.StreamWebsocketConn(c, wsOpts)
case "http":
Expand Down
2 changes: 2 additions & 0 deletions test/clash_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const (
ImageShadowsocksRust = "ghcr.io/shadowsocks/ssserver-rust:latest"
ImageVmess = "v2fly/v2fly-core:latest"
ImageTrojan = "trojangfw/trojan:latest"
ImageTrojanGo = "p4gefau1t/trojan-go:latest"
ImageSnell = "icpz/snell-server:latest"
ImageXray = "teddysun/xray:latest"
)
Expand Down Expand Up @@ -99,6 +100,7 @@ func init() {
ImageShadowsocksRust,
ImageVmess,
ImageTrojan,
ImageTrojanGo,
ImageSnell,
ImageXray,
}
Expand Down
20 changes: 20 additions & 0 deletions test/config/trojan-ws.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"run_type": "server",
"local_addr": "0.0.0.0",
"local_port": 10002,
"disable_http_check": true,
"password": [
"example"
],
"websocket": {
"enabled": true,
"path": "/",
"host": "example.org"
},
"ssl": {
"verify": true,
"cert": "/fullchain.pem",
"key": "/privkey.pem",
"sni": "example.org"
}
}
38 changes: 38 additions & 0 deletions test/trojan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,44 @@ func TestClash_TrojanGrpc(t *testing.T) {
testSuit(t, proxy)
}

func TestClash_TrojanWebsocket(t *testing.T) {
cfg := &container.Config{
Image: ImageTrojanGo,
ExposedPorts: defaultExposedPorts,
}
hostCfg := &container.HostConfig{
PortBindings: defaultPortBindings,
Binds: []string{
fmt.Sprintf("%s:/etc/trojan-go/config.json", C.Path.Resolve("trojan-ws.json")),
fmt.Sprintf("%s:/fullchain.pem", C.Path.Resolve("example.org.pem")),
fmt.Sprintf("%s:/privkey.pem", C.Path.Resolve("example.org-key.pem")),
},
}

id, err := startContainer(cfg, hostCfg, "trojan-ws")
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: "ws",
})
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,
Expand Down
32 changes: 32 additions & 0 deletions transport/trojan/trojan.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import (
"errors"
"io"
"net"
"net/http"
"sync"

"github.com/Dreamacro/clash/common/pool"
"github.com/Dreamacro/clash/transport/socks5"
"github.com/Dreamacro/clash/transport/vmess"
)

const (
Expand All @@ -38,6 +40,13 @@ type Option struct {
SkipCertVerify bool
}

type WebsocketOption struct {
Host string
Port string
Path string
Headers http.Header
}

type Trojan struct {
option *Option
hexPassword []byte
Expand All @@ -64,6 +73,29 @@ func (t *Trojan) StreamConn(conn net.Conn) (net.Conn, error) {
return tlsConn, nil
}

func (t *Trojan) StreamWebsocketConn(conn net.Conn, wsOptions *WebsocketOption) (net.Conn, error) {
alpn := defaultALPN
if len(t.option.ALPN) != 0 {
alpn = t.option.ALPN
}

tlsConfig := &tls.Config{
NextProtos: alpn,
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: t.option.SkipCertVerify,
ServerName: t.option.ServerName,
}

return vmess.StreamWebsocketConn(conn, &vmess.WebsocketConfig{
Host: wsOptions.Host,
Port: wsOptions.Port,
Path: wsOptions.Path,
Headers: wsOptions.Headers,
TLS: true,
TLSConfig: tlsConfig,
})
}

func (t *Trojan) WriteHeader(w io.Writer, command Command, socks5Addr []byte) error {
buf := pool.GetBuffer()
defer pool.PutBuffer(buf)
Expand Down
23 changes: 17 additions & 6 deletions transport/v2ray-plugin/websocket.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package obfs

import (
"crypto/tls"
"net"
"net/http"

Expand All @@ -26,12 +27,22 @@ func NewV2rayObfs(conn net.Conn, option *Option) (net.Conn, error) {
}

config := &vmess.WebsocketConfig{
Host: option.Host,
Port: option.Port,
Path: option.Path,
TLS: option.TLS,
Headers: header,
SkipCertVerify: option.SkipCertVerify,
Host: option.Host,
Port: option.Port,
Path: option.Path,
Headers: header,
}

if option.TLS {
config.TLS = true
config.TLSConfig = &tls.Config{
ServerName: option.Host,
InsecureSkipVerify: option.SkipCertVerify,
NextProtos: []string{"http/1.1"},
}
if host := config.Headers.Get("Host"); host != "" {
config.TLSConfig.ServerName = host
}
}

var err error
Expand Down
15 changes: 2 additions & 13 deletions transport/vmess/websocket.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,7 @@ type WebsocketConfig struct {
Path string
Headers http.Header
TLS bool
SkipCertVerify bool
ServerName string
TLSConfig *tls.Config
MaxEarlyData int
EarlyDataHeaderName string
}
Expand Down Expand Up @@ -254,17 +253,7 @@ func streamWebsocketConn(conn net.Conn, c *WebsocketConfig, earlyData *bytes.Buf
scheme := "ws"
if c.TLS {
scheme = "wss"
dialer.TLSClientConfig = &tls.Config{
ServerName: c.Host,
InsecureSkipVerify: c.SkipCertVerify,
NextProtos: []string{"http/1.1"},
}

if c.ServerName != "" {
dialer.TLSClientConfig.ServerName = c.ServerName
} else if host := c.Headers.Get("Host"); host != "" {
dialer.TLSClientConfig.ServerName = host
}
dialer.TLSClientConfig = c.TLSConfig
}

uri := url.URL{
Expand Down

0 comments on commit df3a491

Please sign in to comment.