diff --git a/api/client.go b/api/client.go index 6c34a6e..54b0706 100644 --- a/api/client.go +++ b/api/client.go @@ -36,15 +36,17 @@ func (s *ClientAPIService) QueryStats(ctx context.Context, req *StatsRequest) (* } func (s *ClientAPIService) calcSpeed() { - select { - case <-time.After(time.Second): - sent, recv := s.meter.Query("") - s.uploadSpeed = sent - s.lastSent - s.downloadSpeed = recv - s.lastRecv - s.lastSent = sent - s.lastRecv = recv - case <-s.ctx.Done(): - return + for { + select { + case <-time.After(time.Second): + sent, recv := s.meter.Query("") + s.uploadSpeed = sent - s.lastSent + s.downloadSpeed = recv - s.lastRecv + s.lastSent = sent + s.lastRecv = recv + case <-s.ctx.Done(): + return + } } } diff --git a/build-all.sh b/build-all.sh index 777194b..b2d0ae5 100755 --- a/build-all.sh +++ b/build-all.sh @@ -48,4 +48,6 @@ for name in trojan-go-*;do zip $name.zip client.json server.json trojan-go.service geoip.dat geosite.dat $name sha1sum $name.zip > $name.zip.sha1 rm $name -done \ No newline at end of file +done + +rm *.json *.service *.dat \ No newline at end of file diff --git a/conf/conf.go b/conf/conf.go index ede8a00..0408d44 100644 --- a/conf/conf.go +++ b/conf/conf.go @@ -5,6 +5,7 @@ import ( "crypto/x509" "github.com/p4gefau1t/trojan-go/common" + utls "github.com/refraction-networking/utls" ) type RunType string @@ -31,7 +32,9 @@ type TLSConfig struct { FallbackHost string `json:"fallback_addr"` FallbackPort int `json:"fallback_port"` ReuseSession bool `json:"reuse_session"` + Fingerprint string `json:"fingerprint"` + ClientHelloID utls.ClientHelloID FallbackAddress *common.Address CertPool *x509.CertPool KeyPair []tls.Certificate diff --git a/conf/parse.go b/conf/parse.go index e17f4f7..ba7b801 100644 --- a/conf/parse.go +++ b/conf/parse.go @@ -14,6 +14,7 @@ import ( "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/log" + utls "github.com/refraction-networking/utls" "golang.org/x/crypto/pbkdf2" ) @@ -46,7 +47,11 @@ func loadCommonConfig(config *GlobalConfig) error { config.TargetAddress = common.NewAddress(config.TargetHost, config.TargetPort, "tcp") if config.TLS.FallbackPort != 0 { - config.TLS.FallbackAddress = common.NewAddress(config.RemoteHost, config.TLS.FallbackPort, "tcp") + if config.TLS.FallbackHost == "" { + config.TLS.FallbackAddress = common.NewAddress(config.RemoteHost, config.TLS.FallbackPort, "tcp") + } else { + config.TLS.FallbackAddress = common.NewAddress(config.TLS.FallbackHost, config.TLS.FallbackPort, "tcp") + } } //api settings @@ -84,7 +89,7 @@ func loadCommonConfig(config *GlobalConfig) error { for _, c := range supportedSuites { list += c.Name + ":" } - log.Warn(list[0 : len(list)-1]) + log.Warn(list[:len(list)-1]) config.TLS.CipherSuites = nil } } else { @@ -192,6 +197,24 @@ func loadClientConfig(config *GlobalConfig) error { } //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 diff --git a/go.mod b/go.mod index 754b070..68f64ff 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/go-acme/lego/v3 v3.5.0 github.com/go-sql-driver/mysql v1.5.0 github.com/golang/protobuf v1.4.0 + github.com/refraction-networking/utls v0.0.0-20190909200633-43c36d3c1f57 github.com/smartystreets/goconvey v1.6.4 github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a github.com/xtaci/smux v1.5.12 diff --git a/go.sum b/go.sum index 9b4f883..4b6d55b 100644 --- a/go.sum +++ b/go.sum @@ -229,6 +229,8 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2/go.mod h1:7tZKcyumwBO6qip7RNQ5r77yrssm9bfCowcLEBcU5IA= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/refraction-networking/utls v0.0.0-20190909200633-43c36d3c1f57 h1:SL1K0QAuC1b54KoY1pjPWe6kSlsFHwK9/oC960fKrTY= +github.com/refraction-networking/utls v0.0.0-20190909200633-43c36d3c1f57/go.mod h1:tz9gX959MEFfFN5whTIocCLUG57WiILqtdVxI8c6Wj0= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= diff --git a/protocol/tproxy/tproxy.go b/protocol/tproxy/tproxy.go index 48477b4..0b552ac 100644 --- a/protocol/tproxy/tproxy.go +++ b/protocol/tproxy/tproxy.go @@ -1,4 +1,4 @@ -// +build linux +// +build linux darwin package tproxy diff --git a/proxy/client/tls.go b/proxy/client/tls.go index 3807443..26450db 100644 --- a/proxy/client/tls.go +++ b/proxy/client/tls.go @@ -3,30 +3,118 @@ package client import ( "crypto/tls" "io" + "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/trojan" + 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 - tlsConfig *tls.Config - config *conf.GlobalConfig + utlsConfig *utls.Config + tlsConfig *tls.Config + autoClientHelloID *utls.ClientHelloID + config *conf.GlobalConfig + roller *Roller } -func (m *TLSManager) DialToServer() (io.ReadWriteCloser, error) { - network := "tcp" - if m.config.TCP.PreferIPV4 { - network = "tcp4" +func (m *TLSManager) printConnInfo(conn net.Conn) { + if m.config.LogLevel != 0 { + return } - tlsConn, err := tls.Dial(network, m.config.RemoteAddress.String(), m.tlsConfig) - if err != nil { - return nil, common.NewError("cannot dial to the remote server").Base(err) - } - if m.config.LogLevel == 0 { + switch conn.(type) { + case *tls.Conn: + tlsConn := conn.(*tls.Conn) state := tlsConn.ConnectionState() chain := state.VerifiedChains log.Debug("tls handshaked", "cipher:", tls.CipherSuiteName(state.CipherSuite), "resume:", state.DidResume) @@ -35,19 +123,63 @@ func (m *TLSManager) DialToServer() (io.ReadWriteCloser, error) { log.Debug("subject:", chain[i][j].Subject, ", issuer:", chain[i][j].Issuer) } } + case *utls.UConn: + tlsConn := conn.(*utls.UConn) + state := tlsConn.ConnectionState() + chain := state.VerifiedChains + log.Debug("tls handshaked", "cipher:", tls.CipherSuiteName(state.CipherSuite), "resume:", state.DidResume) + for i := range chain { + for j := range chain[i] { + log.Debug("subject:", chain[i][j].Subject, ", issuer:", chain[i][j].Issuer) + } + } + default: + panic(conn) } - var conn io.ReadWriteCloser = tlsConn +} + +func (m *TLSManager) DialToServer() (io.ReadWriteCloser, 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.IsSet() { + //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 + tlsConn, err = tls.Dial(network, m.config.RemoteAddress.String(), m.tlsConfig) + } + m.printConnInfo(tlsConn) + if err != nil { + return nil, common.NewError("cannot dial to the remote server").Base(err) + } + var transport io.ReadWriteCloser = tlsConn if m.config.Websocket.Enabled { ws, err := trojan.NewOutboundWebosocket(tlsConn, m.config) if err != nil { return nil, common.NewError("failed to start websocket connection").Base(err) } - conn = ws + transport = ws } - return conn, 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, + } tlsConfig := &tls.Config{ CipherSuites: config.TLS.CipherSuites, RootCAs: config.TLS.CertPool, @@ -56,11 +188,14 @@ func NewTLSManager(config *conf.GlobalConfig) *TLSManager { SessionTicketsDisabled: !config.TLS.SessionTicket, } if config.TLS.ReuseSession { + utlsConfig.ClientSessionCache = utls.NewLRUClientSessionCache(-1) tlsConfig.ClientSessionCache = tls.NewLRUClientSessionCache(-1) } m := &TLSManager{ - config: config, - tlsConfig: tlsConfig, + config: config, + utlsConfig: utlsConfig, + tlsConfig: tlsConfig, + roller: NewRoller(utlsConfig), } return m } diff --git a/sockopt/sockopt.go b/sockopt/sockopt.go new file mode 100644 index 0000000..fc5c134 --- /dev/null +++ b/sockopt/sockopt.go @@ -0,0 +1 @@ +package sockopt diff --git a/test/proxy_test.go b/test/proxy_test.go index ceba952..e8f3478 100644 --- a/test/proxy_test.go +++ b/test/proxy_test.go @@ -8,6 +8,7 @@ import ( "crypto/x509" "io/ioutil" "net" + "sync" "testing" "time" @@ -98,6 +99,7 @@ func getTLSConfig() conf.TLSConfig { ReuseSession: true, SessionTicket: true, FallbackAddress: common.NewAddress("127.0.0.1", 10080, "tcp"), + Fingerprint: "auto", } return c } @@ -194,7 +196,6 @@ func RunServer(ctx context.Context, config *conf.GlobalConfig) { } func CheckClientServer(t *testing.T, clientConfig *conf.GlobalConfig, serverConfig *conf.GlobalConfig) { - time.Sleep(time.Second) ctx, cancel := context.WithCancel(context.Background()) go RunEchoTCPServer(ctx) go RunServer(ctx, serverConfig) @@ -216,8 +217,8 @@ func CheckClientServer(t *testing.T, clientConfig *conf.GlobalConfig, serverConf t.Fatal("not equal") } conn.Close() - cancel() + time.Sleep(time.Second) } func CheckForwardServer(t *testing.T, clientConfig *conf.GlobalConfig, serverConfig *conf.GlobalConfig) { @@ -267,17 +268,51 @@ func SingleThreadSpeedTestClientServer(b *testing.B, clientConfig *conf.GlobalCo common.Must(err) conn, err := dialer.Dial("tcp", "127.0.0.1:5000") common.Must(err) - mbytes := 512 + mbytes := 2048 payload := GeneratePayload(1024 * 1024 * mbytes) t1 := time.Now() conn.Write(payload) t2 := time.Now() speed := float64(mbytes) / t2.Sub(t1).Seconds() - b.Log("Single thread link speed:", speed*8/1024, "Gbps") + b.Log("single-thread link speed:", speed*8/1024, "Gbps") conn.Close() cancel() } +func MultiThreadSpeedTestClientServer(b *testing.B, clientConfig *conf.GlobalConfig, serverConfig *conf.GlobalConfig) { + time.Sleep(time.Second) + ctx, cancel := context.WithCancel(context.Background()) + go RunBlackHoleTCPServer(ctx) + go RunServer(ctx, serverConfig) + go RunClient(ctx, clientConfig) + + time.Sleep(time.Second) + dialer, err := proxy.SOCKS5("tcp", "127.0.0.1:4444", nil, nil) + common.Must(err) + mbytes := 2048 + threads := 16 + payload := GeneratePayload(1024 * 1024 * mbytes / threads) + + wg := sync.WaitGroup{} + wg.Add(threads) + t1 := time.Now() + for i := 0; i < threads; i++ { + go func() { + conn, err := dialer.Dial("tcp", "127.0.0.1:5000") + common.Must(err) + common.Must2(conn.Write(payload)) + wg.Done() + conn.Close() + }() + } + wg.Wait() + t2 := time.Now() + speed := float64(mbytes) / t2.Sub(t1).Seconds() + + b.Log("multi-thread link speed:", speed*8/1024, "Gbps") + cancel() +} + func TestIt(t *testing.T) { clientConfig := getBasicClientConfig() serverConfig := getBasicServerConfig() @@ -315,24 +350,28 @@ func BenchmarkNormal(b *testing.B) { clientConfig := getBasicClientConfig() serverConfig := getBasicServerConfig() SingleThreadSpeedTestClientServer(b, clientConfig, serverConfig) + MultiThreadSpeedTestClientServer(b, clientConfig, serverConfig) } func BenchmarkMux(b *testing.B) { clientConfig := addMuxConfig(getBasicClientConfig()) serverConfig := getBasicServerConfig() SingleThreadSpeedTestClientServer(b, clientConfig, serverConfig) + MultiThreadSpeedTestClientServer(b, clientConfig, serverConfig) } func BenchmarkWebsocket(b *testing.B) { clientConfig := addWsConfig(getBasicClientConfig()) serverConfig := addWsConfig(getBasicServerConfig()) SingleThreadSpeedTestClientServer(b, clientConfig, serverConfig) + MultiThreadSpeedTestClientServer(b, clientConfig, serverConfig) } func BenchmarkMuxWebsocket(b *testing.B) { clientConfig := addMuxConfig(addWsConfig(getBasicClientConfig())) serverConfig := addWsConfig(getBasicServerConfig()) SingleThreadSpeedTestClientServer(b, clientConfig, serverConfig) + MultiThreadSpeedTestClientServer(b, clientConfig, serverConfig) } func TestWebsocketShadow(t *testing.T) { @@ -410,3 +449,10 @@ func TestShadow(t *testing.T) { } cancel() } + +func TestAutoClientID(t *testing.T) { + serverConfig := getBasicServerConfig() + clientConfig := getBasicClientConfig() + clientConfig.TLS.Fingerprint = "auto" + CheckClientServer(t, clientConfig, serverConfig) +} diff --git a/test/target.go b/test/target.go index 1989917..ae1d8d8 100644 --- a/test/target.go +++ b/test/target.go @@ -69,7 +69,10 @@ func RunBlackHoleTCPServer(ctx context.Context) { common.Must(err) go func() { for { - conn, _ := listener.Accept() + conn, err := listener.Accept() + if err != nil { + return + } go func(conn net.Conn) { io.Copy(ioutil.Discard, conn) conn.Close() @@ -77,6 +80,7 @@ func RunBlackHoleTCPServer(ctx context.Context) { } }() <-ctx.Done() + listener.Close() } func RunHelloHTTPServer(ctx context.Context) {