diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index a078e83..89ffd7d 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -2,6 +2,7 @@ on: push: tags: - 'v*' +name: release-build jobs: build: runs-on: ubuntu-latest diff --git a/README.md b/README.md index 9d64d73..e67c586 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # Trojan-Go -使用Golang实现的完整Trojan代理,和Trojan协议以及原版的配置文件格式兼容。安全,高效,轻巧,易用。 +使用Go实现的完整Trojan代理,和Trojan协议以及Trojan-GFW版本的配置文件格式兼容。安全,高效,轻巧,易用。 支持使用[多路复用](#多路复用)提升并发性能,使用[路由模块](#路由模块)实现国内直连。 -支持CDN[流量中转](#Websocket)(基于WebSocket over TLS/SSL) +支持[CDN流量中转](#Websocket)(基于WebSocket over TLS/SSL) 支持使用ACME协议从Let's Encrypt[自动申请和更新](#证书申请)HTTPS证书,只需提供域名和邮箱。 @@ -28,7 +28,7 @@ Trojan-Go支持并且兼容原版Trojan-GFW的绝大多数功能,包括但不 - TCP性能方面的选项,如TCP Fast Open,端口复用等 -同时,Trojan-Go还有更多高效易用的功能特性: +同时,Trojan-Go还扩展了更多高效易用的功能特性: - 简易模式,快速部署使用 @@ -166,7 +166,7 @@ Linux下,绑定80和443端口需要root权限,因此你需要使用sudo执 如果证书过期了,使用 ``` -sudo ./trojan-go -cert renew +sudo ./trojan-go -autocert renew ``` 更新证书,确保上面提到的四个文件在trojan-go所在目录,运行后trojan-go将自动更新证书文件。 @@ -187,7 +187,7 @@ Trojan-Go支持使用TLS+Websocket承载Trojan协议,使得利用CDN进行流 } ``` -完整的选项说明参见[Github Pages](https://p4gefau1t.github.io/trojan-go)。 +完整的选项说明参见[Trojan-Go 文档](https://p4gefau1t.github.io/trojan-go)。 服务端可以省略```hostname```, 但是服务器和客户端的```path```必须相同。服务器开启Websocket支持后可以同时支持Websocket和一般Trojan流量,未配置Websocket选项的客户端依然可以正常使用。 @@ -211,7 +211,7 @@ Trojan-Go支持多路复用([smux](https://github.com/xtaci/smux))。通过使 } ``` -完整的选项说明参见[Github Pages](https://p4gefau1t.github.io/trojan-go)。 +完整的选项说明参见[Trojan-Go 文档](https://p4gefau1t.github.io/trojan-go)。 你只需要设置客户端的配置文件即可,服务端会自动检测是否启用多路复用并提供支持。 @@ -251,7 +251,7 @@ Trojan-Go的客户端内建一个简单实用的路由模块用以方便实现 其中```bypass```,```block```, ```proxy```字段中填入相应的列表文件或者geo数据库tag。列表文件每行是一个域名或者IP地址段(CIDR)。geo数据库geoip和geosite为IP数据库和域名数据库。一旦匹配,则执行相应策略。 -完整的选项说明参见[Github Pages](https://p4gefau1t.github.io/trojan-go)。 +完整的选项说明参见[Trojan-Go 文档](https://p4gefau1t.github.io/trojan-go)。 下面是一个实现国内直连的选项,它将绕过中国大陆IP地址,中国大陆域名,以及内网IP等保留的私有IP地址,直接连接远端而不通过隧道代理。 @@ -270,7 +270,7 @@ Trojan-Go的客户端内建一个简单实用的路由模块用以方便实现 ## 构建 -确保你的Golang版本 >= 1.14,推荐使用snap安装Golang保持与上游同步。 +确保你的Go版本 >= 1.14,推荐使用snap安装Go保持与上游同步。 ``` git clone https://github.com/p4gefau1t/trojan-go.git @@ -278,7 +278,7 @@ cd trojan-go go build ``` -Golang支持通过设置环境变量进行交叉编译,例如 +Go支持通过设置环境变量进行交叉编译,例如 ``` CGO_ENABLE=0 GOOS=windows GOARCH=amd64 go build -o trojan-go.exe diff --git a/conf/parse.go b/conf/parse.go index cffe9a7..8ea17b0 100644 --- a/conf/parse.go +++ b/conf/parse.go @@ -15,7 +15,6 @@ import ( "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/log" - utls "github.com/refraction-networking/utls" "golang.org/x/crypto/pbkdf2" ) @@ -205,25 +204,6 @@ func loadClientConfig(config *GlobalConfig) error { log.Warn(err) } - //tls settings - if config.TLS.Fingerprint != "" && config.TLS.Fingerprint != "auto" { - table := map[string]*utls.ClientHelloID{ - "chrome": &utls.HelloChrome_Auto, - "firefox": &utls.HelloFirefox_Auto, - "ios": &utls.HelloIOS_Auto, - "randomized": &utls.HelloRandomized, - "randomized_alpn": &utls.HelloRandomizedALPN, - "randomized_no_alpn": &utls.HelloRandomizedNoALPN, - } - id, found := table[config.TLS.Fingerprint] - if found { - log.Debug("tls fingerprint loaded:", id.Str()) - config.TLS.ClientHelloID = id - } else { - log.Warn("invalid tls fingerprint:", config.TLS.Fingerprint, ", using default fingerprint") - } - } - if config.TLS.SNI == "" { log.Warn("SNI is unspecified, using remote_addr as SNI") config.TLS.SNI = config.RemoteHost @@ -333,10 +313,6 @@ func ParseJSON(data []byte) (*GlobalConfig, error) { config.TLS.VerifyHostname = true config.TLS.SessionTicket = true config.TLS.ReuseSession = true - config.TLS.ALPN = []string{ - "http/1.1", - "h2", - } config.Mux.IdleTimeout = 60 config.Mux.Concurrency = 8 config.MySQL.CheckRate = 60 diff --git a/docs/content/_index.md b/docs/content/_index.md index 5e6db14..92ab6b3 100644 --- a/docs/content/_index.md +++ b/docs/content/_index.md @@ -8,7 +8,7 @@ weight: 10 这里是Trojan-Go的文档,你可以在左侧的导航栏中找到一些使用技巧,以及完整的配置文件说明。 -Trojan-Go是使用Golang实现的完整的Trojan代理,和Trojan协议以及原版的配置文件格式兼容。 +Trojan-Go是使用Go语言实现的完整的Trojan代理,和Trojan协议以及原版的配置文件格式兼容。 Trojan-Go的的首要目标是保障传输安全性和隐蔽性。在此前提下,尽可能提升传输性能和易用性。 diff --git a/docs/content/basic/config.md b/docs/content/basic/config.md index 262579d..4123945 100644 --- a/docs/content/basic/config.md +++ b/docs/content/basic/config.md @@ -95,9 +95,10 @@ sudo ./trojan-go -autocert renew "remote_addr": "your_awesome_server", "remote_port": 443, "password": [ - "your_awesome_password" + "your_awesome_password" ], "ssl": { + "fingerprint": "firefox", "sni": "your_domain_name" } } @@ -107,4 +108,6 @@ sudo ./trojan-go -autocert renew 如果你在```remote_addr```中填写的是域名,```sni```可以省略。```sni```字段应当填写你申请证书的对应域名,或者你自己签发证书时证书的Common Name,而且必须一致。注意,```sni```字段目前的在TLS协议中是规定**明文传送**的(目的是使服务器提供相应证书),所以不要填写类似google.com等已经被封锁的域名,否则很有可能导致你的服务器也被封锁。 -更多关于配置文件的信息,可以在左侧导航栏中找到相应介绍。 \ No newline at end of file +```fingerprint```将设置Trojan-Go伪造Firefox浏览器的TLS请求指纹,使得Trojan-Go的流量混杂在正常的HTTPS流量中无法被识别。还可以设置为```ios```,```chrome```等。 + +更多关于配置文件的信息,可以在左侧导航栏中找到相应介绍。 diff --git a/docs/content/basic/fullconfig.md b/docs/content/basic/fullconfig.md index af8fd5d..6c6f43e 100644 --- a/docs/content/basic/fullconfig.md +++ b/docs/content/basic/fullconfig.md @@ -47,10 +47,7 @@ weight: 30 "curves": "", "prefer_server_cipher": false, "sni": "", - "alpn": [ - "http/1.1", - "h2" - ] + "alpn": [], "session_ticket": true, "reuse_session": true, "plain_http_response": "", @@ -128,11 +125,11 @@ weight: 30 ```prefer_server_cipher```客户端是否偏好选择服务端在协商中提供的密码学套件。 -```cipher```和```cipher13```指TLS使用的密码学套件。只有在你明确知道自己在做什么的情况下,才应该去填写此项以修改trojan-go使用的TLS密码学套件。**正常情况下,你应该将其留空或者不填**,trojan-go会根据当前硬件平台以及远端的情况,自动选择最合适的加密算法以提升性能和安全性。如果需要填写,密码学套件名用分号(":")分隔。Golang的TLS库中弃用了TLS1.2中不安全的密码学套件,并完全支持TLS1.3。默认情况下,trojan-go将优先使用更安全的TLS1.3 +```cipher```和```cipher13```指TLS使用的密码学套件。只有在你明确知道自己在做什么的情况下,才应该去填写此项以修改trojan-go使用的TLS密码学套件。**正常情况下,你应该将其留空或者不填**,trojan-go会根据当前硬件平台以及远端的情况,自动选择最合适的加密算法以提升性能和安全性。如果需要填写,密码学套件名用分号(":")分隔。Go的TLS库中弃用了TLS1.2中不安全的密码学套件,并完全支持TLS1.3。默认情况下,trojan-go将优先使用更安全的TLS1.3 ```curves```指定TLS在ECDHE中偏好使用的椭圆曲线。只有你明确知道自己在做什么的情况下,才应该填写此项。曲线名称用分号(":")分隔。 -```fingerprint```用于指定TLS Client Hello指纹伪造类型。trojan-go使用[utls](https://github.com/refraction-networking/utls)进行指纹伪造,避免遭到针对golang的TLS库的识别。合法的值有 +```fingerprint```用于指定TLS Client Hello指纹伪造类型,以抵抗GFW对于TLS Client Hello指纹的特征识别和阻断。trojan-go使用[utls](https://github.com/refraction-networking/utls)进行指纹伪造。合法的值有 - ""(空),默认,不使用指纹伪造 @@ -146,9 +143,9 @@ weight: 30 - "randomized",随机指纹 -一旦指纹的值被设置,```cipher```,```curves```,```alpn```等可能影响指纹的字段将使用该指纹的特定设置覆写。设置该选项有可能导致与服务器密钥协商失败,使用auto选项将自动尝试所有指纹并选出合适的一项。 +一旦指纹的值被设置,```cipher```,```curves```,```alpn```,```session_ticket```等有可能影响指纹的字段将使用该指纹的特定设置覆写。设置该选项有可能导致与服务器密钥协商失败,使用auto选项将自动尝试所有指纹并选出合适的一项。 -```plain_http_response```指定了当TLS握手失败时,明文发送的原始数据(原始TCP数据),这个字段填入该文件路径。推荐使用```fallback_port```而不是该字段。 +```plain_http_response```指定了当TLS握手失败时,明文发送的原始数据(原始TCP数据)。这个字段填入该文件路径。推荐使用```fallback_port```而不是该字段。 ```fallback_port```指TLS握手失败时,trojan-go将该连接代理到该端口上。这是trojan-go的特性,以便更好地隐蔽Trojan服务器,抵抗GFW的主动检测,使得服务器的443端口在遭遇非TLS协议的探测时,行为与正常服务器完全一致。当服务器接受了一个连接但无法进行TLS握手时,如果```fallback_port```不为空,则流量将会被代理至remote_addr:fallback_port。例如,你可以在本地使用nginx开启一个https服务,当你的服务器443端口被非TLS协议请求时(比如http请求),trojan-go将代理至本地https服务器,nginx将使用http协议明文返回一个400 Bad Request页面。你可以通过使用浏览器访问 http://your_domain_name.com:443 进行验证。 diff --git a/docs/content/developer/build.md b/docs/content/developer/build.md index e1d3e30..f6391d6 100644 --- a/docs/content/developer/build.md +++ b/docs/content/developer/build.md @@ -4,7 +4,7 @@ draft: false weight: 10 --- -编译需要Golang版本号高于0.14.x,请在编译前确认你的编译器版本。推荐使用snap安装和更新go。 +编译需要Go版本号高于0.14.x,请在编译前确认你的编译器版本。推荐使用snap安装和更新go。 编译过程非常简单 diff --git a/protocol/protocol.go b/protocol/protocol.go index 025d06b..4aa61a5 100644 --- a/protocol/protocol.go +++ b/protocol/protocol.go @@ -176,8 +176,13 @@ func ParsePort(addr net.Addr) (uint16, error) { var timeout time.Duration -func RandomizedTimeout(conn net.Conn) { - conn.SetDeadline(time.Now().Add(timeout)) +func GetRandomTimeoutDuration() time.Duration { + offset := time.Duration(rand.Intn(3000)) * time.Millisecond + return timeout + offset +} + +func SetRandomizedTimeout(conn net.Conn) { + conn.SetDeadline(time.Now().Add(GetRandomTimeoutDuration())) } func CancelTimeout(conn net.Conn) { diff --git a/protocol/trojan/websocket.go b/protocol/trojan/websocket.go index b5d152b..6528acc 100644 --- a/protocol/trojan/websocket.go +++ b/protocol/trojan/websocket.go @@ -190,9 +190,9 @@ func NewInboundWebsocket(ctx context.Context, conn net.Conn, config *conf.Global defer rewindConn.R.StopBuffering() bufrw := bufio.NewReadWriter(bufio.NewReader(rewindConn), bufio.NewWriter(rewindConn)) - httpRequest, obfErr := http.ReadRequest(bufrw.Reader) - if obfErr != nil { - log.Debug(common.NewError("not a http request:").Base(obfErr)) + httpRequest, err := http.ReadRequest(bufrw.Reader) + if err != nil { + log.Debug(common.NewError("not a http request:").Base(err)) return nil, nil } @@ -205,7 +205,7 @@ func NewInboundWebsocket(ctx context.Context, conn net.Conn, config *conf.Global shadowMan.CommitScapegoat(&shadow.Scapegoat{ Conn: rewindConn, ShadowAddress: config.RemoteAddress, - Info: "not a valid http upgrade request from " + conn.RemoteAddr().String(), + Info: "invalid http upgrade request from " + conn.RemoteAddr().String(), }) return nil, common.NewError("invalid ws url or hostname") } @@ -215,7 +215,7 @@ func NewInboundWebsocket(ctx context.Context, conn net.Conn, config *conf.Global rewindConn.R.SetBufferSize(0) url := "wss://" + config.Websocket.HostName + config.Websocket.Path origin := "https://" + config.Websocket.HostName - wsConfig, obfErr := websocket.NewConfig(url, origin) + wsConfig, err := websocket.NewConfig(url, origin) handshaked := make(chan struct{}) @@ -253,7 +253,7 @@ func NewInboundWebsocket(ctx context.Context, conn net.Conn, config *conf.Global return nil, common.NewError("failed to perform websocket handshake") } - //use ws to transport + //use ws to transfer var transport net.Conn rewindConn = common.NewRewindConn(wsConn) transport = rewindConn @@ -266,19 +266,19 @@ func NewInboundWebsocket(ctx context.Context, conn net.Conn, config *conf.Global log.Debug("ws obfs") //deadline for sending the iv and hash - rewindConn.SetDeadline(time.Now().Add(protocol.TCPTimeout)) - transport, obfErr = NewInboundObfReadWriteCloser(config.Websocket.ObfuscationKey, transport) - rewindConn.SetDeadline(time.Time{}) + protocol.SetRandomizedTimeout(rewindConn) + transport, err = NewInboundObfReadWriteCloser(config.Websocket.ObfuscationKey, transport) + protocol.CancelTimeout(rewindConn) - if obfErr != nil { + if err != nil { rewindConn.R.Rewind() //proxy this to our own ws server - obfErr = common.NewError("remote websocket conn:" + conn.RemoteAddr().String() + "didn't send any valid iv/hash").Base(obfErr) + err = common.NewError("remote websocket " + conn.RemoteAddr().String() + "didn't send any valid iv").Base(err) goat, err := getWebsocketScapegoat( config, url, origin, - obfErr.Error(), + err.Error(), rewindConn, ) if err != nil { @@ -287,7 +287,7 @@ func NewInboundWebsocket(ctx context.Context, conn net.Conn, config *conf.Global } else { shadowMan.CommitScapegoat(goat) } - return nil, obfErr + return nil, err } } if !config.Websocket.DoubleTLS { @@ -301,6 +301,7 @@ func NewInboundWebsocket(ctx context.Context, conn net.Conn, config *conf.Global SessionTicketsDisabled: !config.TLS.SessionTicket, } tlsConn := tls.Server(transport, tlsConfig) + protocol.SetRandomizedTimeout(tlsConn) if tlsErr := tlsConn.Handshake(); tlsErr != nil { rewindConn.R.Rewind() //proxy this to our own ws server @@ -320,6 +321,7 @@ func NewInboundWebsocket(ctx context.Context, conn net.Conn, config *conf.Global } return nil, tlsErr } + protocol.CancelTimeout(tlsConn) rewindConn.R.SetBufferSize(0) return tlsConn, nil } diff --git a/proxy/client/mux.go b/proxy/client/mux.go index 8d186f6..66b3d7e 100644 --- a/proxy/client/mux.go +++ b/proxy/client/mux.go @@ -55,6 +55,7 @@ func (m *MuxManager) newMuxClient() (*muxClientInfo, error) { } conn, err := trojan.NewOutboundConnSession(req, rwc, m.config) if err != nil { + rwc.Close() log.Error(common.NewError("failed to dial tls tunnel").Base(err)) return nil, err } diff --git a/proxy/client/tls.go b/proxy/client/tls.go index cca6a9a..e0406fc 100644 --- a/proxy/client/tls.go +++ b/proxy/client/tls.go @@ -6,107 +6,25 @@ import ( "math/rand" "net" "sync" - "time" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/conf" "github.com/p4gefau1t/trojan-go/log" + "github.com/p4gefau1t/trojan-go/protocol" "github.com/p4gefau1t/trojan-go/protocol/trojan" "github.com/p4gefau1t/trojan-go/sockopt" utls "github.com/refraction-networking/utls" ) -type Roller struct { - HelloIDs []utls.ClientHelloID - HelloIDMu sync.Mutex - WorkingHelloID *utls.ClientHelloID - TCPDialTimeout time.Duration - TLSHandshakeTimeout time.Duration - TLSConfig *utls.Config -} - -// NewRoller creates Roller object with default range of HelloIDs to cycle through until a -// working/unblocked one is found. -func NewRoller(config *utls.Config) *Roller { - tcpDialTimeoutInc := rand.Intn(14) - tcpDialTimeoutInc = 7 + tcpDialTimeoutInc - - tlsHandshakeTimeoutInc := rand.Intn(20) - tlsHandshakeTimeoutInc = 11 + tlsHandshakeTimeoutInc - - return &Roller{ - HelloIDs: []utls.ClientHelloID{ - utls.HelloChrome_Auto, - utls.HelloFirefox_Auto, - utls.HelloIOS_Auto, - utls.HelloRandomized, - }, - TCPDialTimeout: time.Second * time.Duration(tcpDialTimeoutInc), - TLSHandshakeTimeout: time.Second * time.Duration(tlsHandshakeTimeoutInc), - TLSConfig: config, - } -} - -func (c *Roller) Dial(network, addr, serverName string) (*utls.UConn, error) { - helloIDs := make([]utls.ClientHelloID, len(c.HelloIDs)) - copy(helloIDs, c.HelloIDs) - rand.Shuffle(len(c.HelloIDs), func(i, j int) { - helloIDs[i], helloIDs[j] = helloIDs[j], helloIDs[i] - }) - - c.HelloIDMu.Lock() - workingHelloID := c.WorkingHelloID // keep using same helloID, if it works - c.HelloIDMu.Unlock() - if workingHelloID != nil { - helloIDFound := false - for i, ID := range helloIDs { - if ID == *workingHelloID { - helloIDs[i] = helloIDs[0] - helloIDs[0] = *workingHelloID // push working hello ID first - helloIDFound = true - break - } - } - if !helloIDFound { - helloIDs = append([]utls.ClientHelloID{*workingHelloID}, helloIDs...) - } - } - - var tcpConn net.Conn - var err error - for _, helloID := range helloIDs { - tcpConn, err = net.DialTimeout(network, addr, c.TCPDialTimeout) - if err != nil { - return nil, err // on tcp Dial failure return with error right away - } - - client := utls.UClient(tcpConn, c.TLSConfig, helloID) - client.SetSNI(serverName) - client.SetDeadline(time.Now().Add(c.TLSHandshakeTimeout)) - err = client.Handshake() - client.SetDeadline(time.Time{}) // unset timeout - if err != nil { - log.Debug("hello id", helloID.Str(), "failed, err:", err) - continue // on tls Dial error keep trying HelloIDs - } - - log.Debug("found avaliable hello id:", helloID.Str()) - c.HelloIDMu.Lock() - c.WorkingHelloID = &client.ClientHelloID - c.HelloIDMu.Unlock() - return client, err - } - return nil, err -} - type TLSManager struct { TransportManager - utlsConfig *utls.Config - tlsConfig *tls.Config - autoClientHelloID *utls.ClientHelloID - config *conf.GlobalConfig - roller *Roller + helloIDs []utls.ClientHelloID + helloIDLock sync.Mutex + workingHelloID *utls.ClientHelloID + utlsConfig *utls.Config + tlsConfig *tls.Config + config *conf.GlobalConfig } func (m *TLSManager) printConnInfo(conn net.Conn) { @@ -139,57 +57,114 @@ func (m *TLSManager) printConnInfo(conn net.Conn) { } } -func (m *TLSManager) DialToServer() (io.ReadWriteCloser, error) { +func (m *TLSManager) dialTCP() (net.Conn, error) { network := "tcp" if m.config.TCP.PreferIPV4 { network = "tcp4" } - var tlsConn net.Conn - var err error - if m.config.TLS.Fingerprint == "auto" { - //use utls roller - tlsConn, err = m.roller.Dial(network, m.config.RemoteAddress.String(), m.config.TLS.SNI) - } else if m.config.TLS.ClientHelloID != nil { - //use utls fixed fingerprint - log.Debug("using fingerprint", m.config.TLS.ClientHelloID.Str()) - var conn net.Conn - conn, err = net.Dial(network, m.config.RemoteAddress.String()) - tlsConn = utls.UClient(conn, m.utlsConfig, *m.config.TLS.ClientHelloID) - } else { - //normal golang tls - conn, err := net.Dial(network, m.config.RemoteAddress.String()) + conn, err := net.DialTimeout(network, m.config.RemoteAddress.String(), protocol.GetRandomTimeoutDuration()) + if err != nil { + return nil, common.NewError("failed to dial to remote server").Base(err) + } + if err := sockopt.ApplyTCPConnOption(conn.(*net.TCPConn), &m.config.TCP); err != nil { + log.Warn(common.NewError("failed to apply tcp options").Base(err)) + } + return conn, nil +} + +func (m *TLSManager) dialTLSWithFakeFingerprint() (*utls.UConn, error) { + helloIDs := make([]utls.ClientHelloID, len(m.helloIDs)) + copy(helloIDs, m.helloIDs) + rand.Shuffle(len(m.helloIDs), func(i, j int) { + helloIDs[i], helloIDs[j] = helloIDs[j], helloIDs[i] + }) + + m.helloIDLock.Lock() + workingHelloID := m.workingHelloID // keep using same helloID, if it works + m.helloIDLock.Unlock() + if workingHelloID != nil { + helloIDFound := false + for i, ID := range helloIDs { + if ID == *workingHelloID { + helloIDs[i] = helloIDs[0] + helloIDs[0] = *workingHelloID // push working hello ID first + helloIDFound = true + break + } + } + if !helloIDFound { + helloIDs = append([]utls.ClientHelloID{*workingHelloID}, helloIDs...) + helloIDs[0], helloIDs[len(helloIDs)-1] = helloIDs[len(helloIDs)-1], helloIDs[0] + } + } + for _, helloID := range helloIDs { + tcpConn, err := m.dialTCP() + if err != nil { + return nil, err // on tcp Dial failure return with error right away + } + + client := utls.UClient(tcpConn, m.utlsConfig, helloID) + if m.config.Websocket.Enabled { + // HACK disable alpn (http/1.1, h2) to support websocket + client.HandshakeState.Hello.AlpnProtocols = []string{} + } + protocol.SetRandomizedTimeout(client) + err = client.Handshake() + protocol.CancelTimeout(client) + if err != nil { + log.Debug("hello id", helloID.Str(), "failed, err:", err) + continue // on tls Dial error keep trying HelloIDs + } + + log.Debug("found avaliable hello id:", helloID.Str()) + m.helloIDLock.Lock() + m.workingHelloID = &client.ClientHelloID + m.helloIDLock.Unlock() + return client, err + } + return nil, common.NewError("all client hello id tried but failed") +} + +func (m *TLSManager) DialToServer() (io.ReadWriteCloser, error) { + var transport net.Conn + if m.config.TLS.Fingerprint != "" { + //use utls fingerprints + tlsConn, err := m.dialTLSWithFakeFingerprint() if err != nil { return nil, err } - err = sockopt.ApplyTCPConnOption(conn.(*net.TCPConn), &m.config.TCP) + m.printConnInfo(tlsConn) + transport = tlsConn + } else { + //normal golang tls + tcpConn, err := m.dialTCP() if err != nil { - return nil, common.NewError("failed to apply tcp option").Base(err) + return nil, err } - tlsConn = tls.Client(conn, m.tlsConfig) - err = tlsConn.(*tls.Conn).Handshake() - } - if err != nil { - return nil, common.NewError("cannot dial to the remote server").Base(err) - } - m.printConnInfo(tlsConn) - var transport io.ReadWriteCloser = tlsConn - if m.config.Websocket.Enabled { - ws, err := trojan.NewOutboundWebosocket(tlsConn, m.config) + tlsConn := tls.Client(tcpConn, m.tlsConfig) + err = tlsConn.Handshake() if err != nil { + return nil, err + } + transport = tlsConn + m.printConnInfo(tlsConn) + } + if m.config.Websocket.Enabled { + ws, err := trojan.NewOutboundWebosocket(transport, m.config) + if err != nil { + transport.Close() return nil, common.NewError("failed to start websocket connection").Base(err) } - transport = ws + return ws, nil } return transport, nil } func NewTLSManager(config *conf.GlobalConfig) *TLSManager { utlsConfig := &utls.Config{ - RootCAs: config.TLS.CertPool, - ServerName: config.TLS.SNI, - InsecureSkipVerify: !config.TLS.Verify, - SessionTicketsDisabled: !config.TLS.SessionTicket, - ClientSessionCache: utls.NewLRUClientSessionCache(-1), + RootCAs: config.TLS.CertPool, + ServerName: config.TLS.SNI, + InsecureSkipVerify: !config.TLS.Verify, } tlsConfig := &tls.Config{ CipherSuites: config.TLS.CipherSuites, @@ -199,13 +174,38 @@ func NewTLSManager(config *conf.GlobalConfig) *TLSManager { SessionTicketsDisabled: !config.TLS.SessionTicket, CurvePreferences: config.TLS.CurvePreferences, NextProtos: config.TLS.ALPN, - ClientSessionCache: tls.NewLRUClientSessionCache(-1), + ClientSessionCache: tls.NewLRUClientSessionCache(192), } + m := &TLSManager{ config: config, utlsConfig: utlsConfig, tlsConfig: tlsConfig, - roller: NewRoller(utlsConfig), } + + if config.TLS.Fingerprint == "auto" { + m.helloIDs = []utls.ClientHelloID{ + utls.HelloChrome_Auto, + utls.HelloFirefox_Auto, + utls.HelloIOS_Auto, + utls.HelloRandomizedNoALPN, + } + } else if config.TLS.Fingerprint != "" { + table := map[string]*utls.ClientHelloID{ + "chrome": &utls.HelloChrome_Auto, + "firefox": &utls.HelloFirefox_Auto, + "ios": &utls.HelloIOS_Auto, + "randomized": &utls.HelloRandomizedNoALPN, + } + id, found := table[config.TLS.Fingerprint] + if found { + log.Debug("tls fingerprint loaded:", id.Str()) + m.helloIDs = []utls.ClientHelloID{*id} + } else { + log.Warn("invalid tls fingerprint:", config.TLS.Fingerprint, ", using default fingerprint") + config.TLS.Fingerprint = "" + } + } + return m } diff --git a/proxy/server/server.go b/proxy/server/server.go index a8b4570..1dd0095 100644 --- a/proxy/server/server.go +++ b/proxy/server/server.go @@ -63,7 +63,7 @@ func (s *Server) handleMuxConn(stream *smux.Stream) { } func (s *Server) handleConn(conn *tls.Conn) { - protocol.RandomizedTimeout(conn) + protocol.SetRandomizedTimeout(conn) inboundConn, req, err := trojan.NewInboundConnSession(s.ctx, conn, s.config, s.auth, s.shadow) if err != nil { //once the auth is failed, the conn will be took over by shadow manager. don't close it @@ -171,7 +171,7 @@ func (s *Server) Run() error { log.Info("conn accepted from", conn.RemoteAddr()) go func(conn net.Conn) { //using randomized timeout - protocol.RandomizedTimeout(conn) + protocol.SetRandomizedTimeout(conn) rewindConn := common.NewRewindConn(conn) rewindConn.R.SetBufferSize(512)