Feature: support trojan websocket
This commit is contained in:
parent
50a2222cd2
commit
1b7ad8f494
8 changed files with 154 additions and 23 deletions
|
@ -5,6 +5,7 @@ import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/component/dialer"
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
|
@ -18,6 +19,7 @@ import (
|
||||||
type Trojan struct {
|
type Trojan struct {
|
||||||
*Base
|
*Base
|
||||||
instance *trojan.Trojan
|
instance *trojan.Trojan
|
||||||
|
option *TrojanOption
|
||||||
|
|
||||||
// for gun mux
|
// for gun mux
|
||||||
gunTLSConfig *tls.Config
|
gunTLSConfig *tls.Config
|
||||||
|
@ -36,6 +38,34 @@ type TrojanOption struct {
|
||||||
UDP bool `proxy:"udp,omitempty"`
|
UDP bool `proxy:"udp,omitempty"`
|
||||||
Network string `proxy:"network,omitempty"`
|
Network string `proxy:"network,omitempty"`
|
||||||
GrpcOpts GrpcOptions `proxy:"grpc-opts,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
|
// StreamConn implements C.ProxyAdapter
|
||||||
|
@ -44,7 +74,7 @@ func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error)
|
||||||
if t.transport != nil {
|
if t.transport != nil {
|
||||||
c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig)
|
c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig)
|
||||||
} else {
|
} else {
|
||||||
c, err = t.instance.StreamConn(c)
|
c, err = t.plainStream(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -106,7 +136,7 @@ func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
|
||||||
}
|
}
|
||||||
defer safeConnClose(c, err)
|
defer safeConnClose(c, err)
|
||||||
tcpKeepAlive(c)
|
tcpKeepAlive(c)
|
||||||
c, err = t.instance.StreamConn(c)
|
c, err = t.plainStream(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
||||||
}
|
}
|
||||||
|
@ -143,6 +173,7 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
|
||||||
udp: option.UDP,
|
udp: option.UDP,
|
||||||
},
|
},
|
||||||
instance: trojan.New(tOption),
|
instance: trojan.New(tOption),
|
||||||
|
option: &option,
|
||||||
}
|
}
|
||||||
|
|
||||||
if option.Network == "grpc" {
|
if option.Network == "grpc" {
|
||||||
|
|
|
@ -105,8 +105,16 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||||
|
|
||||||
if v.option.TLS {
|
if v.option.TLS {
|
||||||
wsOpts.TLS = true
|
wsOpts.TLS = true
|
||||||
wsOpts.SkipCertVerify = v.option.SkipCertVerify
|
wsOpts.TLSConfig = &tls.Config{
|
||||||
wsOpts.ServerName = v.option.ServerName
|
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)
|
c, err = vmess.StreamWebsocketConn(c, wsOpts)
|
||||||
case "http":
|
case "http":
|
||||||
|
|
|
@ -31,6 +31,7 @@ const (
|
||||||
ImageShadowsocksRust = "ghcr.io/shadowsocks/ssserver-rust:latest"
|
ImageShadowsocksRust = "ghcr.io/shadowsocks/ssserver-rust:latest"
|
||||||
ImageVmess = "v2fly/v2fly-core:latest"
|
ImageVmess = "v2fly/v2fly-core:latest"
|
||||||
ImageTrojan = "trojangfw/trojan:latest"
|
ImageTrojan = "trojangfw/trojan:latest"
|
||||||
|
ImageTrojanGo = "p4gefau1t/trojan-go:latest"
|
||||||
ImageSnell = "icpz/snell-server:latest"
|
ImageSnell = "icpz/snell-server:latest"
|
||||||
ImageXray = "teddysun/xray:latest"
|
ImageXray = "teddysun/xray:latest"
|
||||||
)
|
)
|
||||||
|
@ -99,6 +100,7 @@ func init() {
|
||||||
ImageShadowsocksRust,
|
ImageShadowsocksRust,
|
||||||
ImageVmess,
|
ImageVmess,
|
||||||
ImageTrojan,
|
ImageTrojan,
|
||||||
|
ImageTrojanGo,
|
||||||
ImageSnell,
|
ImageSnell,
|
||||||
ImageXray,
|
ImageXray,
|
||||||
}
|
}
|
||||||
|
|
20
test/config/trojan-ws.json
Normal file
20
test/config/trojan-ws.json
Normal file
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -93,6 +93,44 @@ func TestClash_TrojanGrpc(t *testing.T) {
|
||||||
testSuit(t, proxy)
|
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) {
|
func Benchmark_Trojan(b *testing.B) {
|
||||||
cfg := &container.Config{
|
cfg := &container.Config{
|
||||||
Image: ImageTrojan,
|
Image: ImageTrojan,
|
||||||
|
|
|
@ -8,10 +8,12 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/common/pool"
|
"github.com/Dreamacro/clash/common/pool"
|
||||||
"github.com/Dreamacro/clash/transport/socks5"
|
"github.com/Dreamacro/clash/transport/socks5"
|
||||||
|
"github.com/Dreamacro/clash/transport/vmess"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -38,6 +40,13 @@ type Option struct {
|
||||||
SkipCertVerify bool
|
SkipCertVerify bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WebsocketOption struct {
|
||||||
|
Host string
|
||||||
|
Port string
|
||||||
|
Path string
|
||||||
|
Headers http.Header
|
||||||
|
}
|
||||||
|
|
||||||
type Trojan struct {
|
type Trojan struct {
|
||||||
option *Option
|
option *Option
|
||||||
hexPassword []byte
|
hexPassword []byte
|
||||||
|
@ -64,6 +73,29 @@ func (t *Trojan) StreamConn(conn net.Conn) (net.Conn, error) {
|
||||||
return tlsConn, nil
|
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 {
|
func (t *Trojan) WriteHeader(w io.Writer, command Command, socks5Addr []byte) error {
|
||||||
buf := pool.GetBuffer()
|
buf := pool.GetBuffer()
|
||||||
defer pool.PutBuffer(buf)
|
defer pool.PutBuffer(buf)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package obfs
|
package obfs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
@ -29,9 +30,19 @@ func NewV2rayObfs(conn net.Conn, option *Option) (net.Conn, error) {
|
||||||
Host: option.Host,
|
Host: option.Host,
|
||||||
Port: option.Port,
|
Port: option.Port,
|
||||||
Path: option.Path,
|
Path: option.Path,
|
||||||
TLS: option.TLS,
|
|
||||||
Headers: header,
|
Headers: header,
|
||||||
SkipCertVerify: option.SkipCertVerify,
|
}
|
||||||
|
|
||||||
|
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
|
var err error
|
||||||
|
|
|
@ -45,8 +45,7 @@ type WebsocketConfig struct {
|
||||||
Path string
|
Path string
|
||||||
Headers http.Header
|
Headers http.Header
|
||||||
TLS bool
|
TLS bool
|
||||||
SkipCertVerify bool
|
TLSConfig *tls.Config
|
||||||
ServerName string
|
|
||||||
MaxEarlyData int
|
MaxEarlyData int
|
||||||
EarlyDataHeaderName string
|
EarlyDataHeaderName string
|
||||||
}
|
}
|
||||||
|
@ -254,17 +253,7 @@ func streamWebsocketConn(conn net.Conn, c *WebsocketConfig, earlyData *bytes.Buf
|
||||||
scheme := "ws"
|
scheme := "ws"
|
||||||
if c.TLS {
|
if c.TLS {
|
||||||
scheme = "wss"
|
scheme = "wss"
|
||||||
dialer.TLSClientConfig = &tls.Config{
|
dialer.TLSClientConfig = c.TLSConfig
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uri := url.URL{
|
uri := url.URL{
|
||||||
|
|
Loading…
Reference in a new issue