diff --git a/dialer/mtls/dialer.go b/dialer/mtls/dialer.go index 5786e7be..d5e78081 100644 --- a/dialer/mtls/dialer.go +++ b/dialer/mtls/dialer.go @@ -2,7 +2,6 @@ package mtls import ( "context" - "crypto/tls" "errors" "net" "sync" @@ -16,6 +15,7 @@ import ( "github.com/go-gost/x/internal/net/proxyproto" "github.com/go-gost/x/internal/util/mux" "github.com/go-gost/x/registry" + "github.com/go-gost/x/util" ) func init() { @@ -141,7 +141,7 @@ func (d *mtlsDialer) Handshake(ctx context.Context, conn net.Conn, options ...di } func (d *mtlsDialer) initSession(ctx context.Context, conn net.Conn) (*muxSession, error) { - tlsConn := tls.Client(conn, d.options.TLSConfig) + tlsConn := util.NewUTLSChromeClient(conn, d.options.TLSConfig) if err := tlsConn.HandshakeContext(ctx); err != nil { return nil, err } diff --git a/dialer/mws/dialer.go b/dialer/mws/dialer.go index 99b5a2d6..00c99207 100644 --- a/dialer/mws/dialer.go +++ b/dialer/mws/dialer.go @@ -17,6 +17,7 @@ import ( "github.com/go-gost/x/internal/util/mux" ws_util "github.com/go-gost/x/internal/util/ws" "github.com/go-gost/x/registry" + "github.com/go-gost/x/util" "github.com/gorilla/websocket" ) @@ -172,17 +173,30 @@ func (d *mwsDialer) initSession(ctx context.Context, host string, conn net.Conn, }, } - url := url.URL{Scheme: "ws", Host: host, Path: d.md.path} + urlObj := url.URL{Scheme: "ws", Host: host, Path: d.md.path} if d.tlsEnabled { - url.Scheme = "wss" + urlObj.Scheme = "wss" dialer.TLSClientConfig = d.options.TLSConfig + dialer.NetDialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + client, err := util.NewUTLSWebSocketClient(conn, d.options.TLSConfig, d.md.useH2) + if err != nil { + return nil, err + } + if err := client.Handshake(); err != nil { + return nil, err + } + return client, nil + } } if d.md.handshakeTimeout > 0 { conn.SetReadDeadline(time.Now().Add(d.md.handshakeTimeout)) } - - c, resp, err := dialer.DialContext(ctx, url.String(), d.md.header) + urlStr, errUnescape := url.QueryUnescape(urlObj.String()) + if errUnescape != nil { + d.options.Logger.Debugf("[mws] URL QueryUnescape Error URL.String() -> %s", urlObj.String()) + } + c, resp, err := dialer.DialContext(ctx, urlStr, d.md.header) if err != nil { return nil, err } diff --git a/dialer/mws/metadata.go b/dialer/mws/metadata.go index c341540c..7d201593 100644 --- a/dialer/mws/metadata.go +++ b/dialer/mws/metadata.go @@ -5,8 +5,8 @@ import ( "time" mdata "github.com/go-gost/core/metadata" - mdutil "github.com/go-gost/x/metadata/util" "github.com/go-gost/x/internal/util/mux" + mdutil "github.com/go-gost/x/metadata/util" ) const ( @@ -18,6 +18,8 @@ type metadata struct { host string path string + useH2 bool + handshakeTimeout time.Duration readHeaderTimeout time.Duration readBufferSize int @@ -77,5 +79,7 @@ func (d *mwsDialer) parseMetadata(md mdata.Metadata) (err error) { d.md.tcpKeepaliveInterval = mdutil.GetDuration(md, "tcp.keepalive.interval") d.md.tcpKeepaliveCount = mdutil.GetInt(md, "tcp.keepalive.count") + d.md.useH2 = mdutil.GetBool(md, "h2") + return } diff --git a/dialer/tls/dialer.go b/dialer/tls/dialer.go index 35227179..e5595626 100644 --- a/dialer/tls/dialer.go +++ b/dialer/tls/dialer.go @@ -2,7 +2,6 @@ package tls import ( "context" - "crypto/tls" "net" "time" @@ -13,6 +12,7 @@ import ( xnet "github.com/go-gost/x/internal/net" "github.com/go-gost/x/internal/net/proxyproto" "github.com/go-gost/x/registry" + "github.com/go-gost/x/util" ) func init() { @@ -77,7 +77,7 @@ func (d *tlsDialer) Handshake(ctx context.Context, conn net.Conn, options ...dia defer conn.SetDeadline(time.Time{}) } - tlsConn := tls.Client(conn, d.options.TLSConfig) + tlsConn := util.NewUTLSChromeClient(conn, d.options.TLSConfig) if err := tlsConn.HandshakeContext(ctx); err != nil { conn.Close() return nil, err diff --git a/dialer/utls/dialer.go b/dialer/utls/dialer.go index dc5bc7bb..5509e108 100644 --- a/dialer/utls/dialer.go +++ b/dialer/utls/dialer.go @@ -5,7 +5,6 @@ import ( "crypto/tls" "net" "time" - "unsafe" "github.com/go-gost/core/dialer" "github.com/go-gost/core/logger" @@ -14,6 +13,7 @@ import ( xnet "github.com/go-gost/x/internal/net" "github.com/go-gost/x/internal/net/proxyproto" "github.com/go-gost/x/registry" + "github.com/go-gost/x/util" utls "github.com/refraction-networking/utls" ) @@ -82,7 +82,7 @@ func (d *utlsDialer) Handshake(ctx context.Context, conn net.Conn, options ...di clientHelloID, ok := GetClientHelloID(d.md.fingerprint) if ok { d.log.Debugf("utls handshake with fingerprint: %s", d.md.fingerprint) - utlsCfg := (*utls.Config)(unsafe.Pointer(d.options.TLSConfig)) + utlsCfg := util.NewUTLSConfig(d.options.TLSConfig) uconn := utls.UClient(conn, utlsCfg, clientHelloID) if err := uconn.HandshakeContext(ctx); err != nil { conn.Close() diff --git a/dialer/ws/dialer.go b/dialer/ws/dialer.go index 1db6af29..519dadb1 100644 --- a/dialer/ws/dialer.go +++ b/dialer/ws/dialer.go @@ -13,6 +13,7 @@ import ( "github.com/go-gost/x/internal/net/proxyproto" ws_util "github.com/go-gost/x/internal/util/ws" "github.com/go-gost/x/registry" + "github.com/go-gost/x/util" "github.com/gorilla/websocket" ) @@ -110,13 +111,26 @@ func (d *wsDialer) Handshake(ctx context.Context, conn net.Conn, options ...dial }, } - url := url.URL{Scheme: "ws", Host: host, Path: d.md.path} + urlObj := url.URL{Scheme: "ws", Host: host, Path: d.md.path} if d.tlsEnabled { - url.Scheme = "wss" + urlObj.Scheme = "wss" dialer.TLSClientConfig = d.options.TLSConfig + dialer.NetDialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + client, err := util.NewUTLSWebSocketClient(conn, d.options.TLSConfig, d.md.useH2) + if err != nil { + return nil, err + } + if err := client.Handshake(); err != nil { + return nil, err + } + return client, nil + } } - - c, resp, err := dialer.DialContext(ctx, url.String(), d.md.header) + urlStr, errUnescape := url.QueryUnescape(urlObj.String()) + if errUnescape != nil { + d.options.Logger.Debugf("[ws] URL QueryUnescape Error URL.String() -> %s", urlObj.String()) + } + c, resp, err := dialer.DialContext(ctx, urlStr, d.md.header) if err != nil { return nil, err } diff --git a/dialer/ws/metadata.go b/dialer/ws/metadata.go index 17586108..d0f4ebea 100644 --- a/dialer/ws/metadata.go +++ b/dialer/ws/metadata.go @@ -14,8 +14,9 @@ const ( ) type metadata struct { - host string - path string + host string + path string + useH2 bool handshakeTimeout time.Duration readHeaderTimeout time.Duration @@ -66,5 +67,6 @@ func (d *wsDialer) parseMetadata(md mdata.Metadata) (err error) { d.md.tcpKeepaliveInterval = mdutil.GetDuration(md, "tcp.keepalive.interval") d.md.tcpKeepaliveCount = mdutil.GetInt(md, "tcp.keepalive.count") + d.md.useH2 = mdutil.GetBool(md, "h2") return } diff --git a/util/utls.go b/util/utls.go new file mode 100644 index 00000000..85cf677c --- /dev/null +++ b/util/utls.go @@ -0,0 +1,145 @@ +package util + +import ( + stdtls "crypto/tls" + "encoding/hex" + "net" + + tls "github.com/refraction-networking/utls" +) + +func NewUTLSClient(conn net.Conn, config *stdtls.Config, clientHelloID tls.ClientHelloID) *tls.UConn { + return tls.UClient(conn, NewUTLSConfig(config), clientHelloID) +} + +func NewUTLSChromeClient(conn net.Conn, config *stdtls.Config) *tls.UConn { + return NewUTLSClient(conn, config, tls.HelloChrome_Auto) +} + +func NewUTLSWebSocketClient(conn net.Conn, config *stdtls.Config, useH2 bool) (*tls.UConn, error) { + utlsConfig := NewLegacyUTLSConfig(config) + if useH2 { + return tls.UClient(conn, utlsConfig, tls.HelloChrome_Auto), nil + } + + client := tls.UClient(conn, utlsConfig, tls.HelloCustom) + spec, err := NewWebSocketClientHelloSpec() + if err != nil { + return nil, err + } + if err := client.ApplyPreset(spec); err != nil { + return nil, err + } + return client, nil +} + +func NewLegacyUTLSConfig(config *stdtls.Config) *tls.Config { + if config == nil { + return &tls.Config{} + } + + cfg := &tls.Config{ + InsecureSkipVerify: config.InsecureSkipVerify, + ServerName: config.ServerName, + ClientAuth: tls.ClientAuthType(config.ClientAuth), + ClientCAs: config.ClientCAs, + RootCAs: config.RootCAs, + } + + cfg.Certificates = make([]tls.Certificate, 0, len(config.Certificates)) + for _, cert := range config.Certificates { + cfg.Certificates = append(cfg.Certificates, tls.Certificate{ + Certificate: cert.Certificate, + PrivateKey: cert.PrivateKey, + OCSPStaple: cert.OCSPStaple, + SignedCertificateTimestamps: cert.SignedCertificateTimestamps, + Leaf: cert.Leaf, + }) + } + + return cfg +} + +func NewUTLSConfig(config *stdtls.Config) *tls.Config { + if config == nil { + return &tls.Config{} + } + + cfg := &tls.Config{ + Rand: config.Rand, + Time: config.Time, + RootCAs: config.RootCAs, + NextProtos: append([]string(nil), config.NextProtos...), + ServerName: config.ServerName, + ClientAuth: tls.ClientAuthType(config.ClientAuth), + ClientCAs: config.ClientCAs, + InsecureSkipVerify: config.InsecureSkipVerify, + CipherSuites: append([]uint16(nil), config.CipherSuites...), + SessionTicketsDisabled: config.SessionTicketsDisabled, + MinVersion: config.MinVersion, + MaxVersion: config.MaxVersion, + DynamicRecordSizingDisabled: config.DynamicRecordSizingDisabled, + Renegotiation: tls.RenegotiationSupport(config.Renegotiation), + KeyLogWriter: config.KeyLogWriter, + VerifyPeerCertificate: config.VerifyPeerCertificate, + } + + cfg.Certificates = make([]tls.Certificate, 0, len(config.Certificates)) + for _, cert := range config.Certificates { + cfg.Certificates = append(cfg.Certificates, toUTLSCertificate(cert)) + } + + cfg.CurvePreferences = make([]tls.CurveID, 0, len(config.CurvePreferences)) + for _, curve := range config.CurvePreferences { + cfg.CurvePreferences = append(cfg.CurvePreferences, tls.CurveID(curve)) + } + + if config.VerifyConnection != nil { + cfg.VerifyConnection = func(state tls.ConnectionState) error { + return config.VerifyConnection(toStdTLSConnectionState(state)) + } + } + + return cfg +} + +func toUTLSCertificate(cert stdtls.Certificate) tls.Certificate { + signatureAlgorithms := make([]tls.SignatureScheme, 0, len(cert.SupportedSignatureAlgorithms)) + for _, sig := range cert.SupportedSignatureAlgorithms { + signatureAlgorithms = append(signatureAlgorithms, tls.SignatureScheme(sig)) + } + + return tls.Certificate{ + Certificate: cert.Certificate, + PrivateKey: cert.PrivateKey, + SupportedSignatureAlgorithms: signatureAlgorithms, + OCSPStaple: cert.OCSPStaple, + SignedCertificateTimestamps: cert.SignedCertificateTimestamps, + Leaf: cert.Leaf, + } +} + +func toStdTLSConnectionState(state tls.ConnectionState) stdtls.ConnectionState { + return stdtls.ConnectionState{ + Version: state.Version, + HandshakeComplete: state.HandshakeComplete, + DidResume: state.DidResume, + CipherSuite: state.CipherSuite, + NegotiatedProtocol: state.NegotiatedProtocol, + NegotiatedProtocolIsMutual: state.NegotiatedProtocolIsMutual, + ServerName: state.ServerName, + PeerCertificates: state.PeerCertificates, + VerifiedChains: state.VerifiedChains, + SignedCertificateTimestamps: state.SignedCertificateTimestamps, + OCSPResponse: state.OCSPResponse, + TLSUnique: state.TLSUnique, + ECHAccepted: state.ECHAccepted, + } +} + +func NewWebSocketClientHelloSpec() (*tls.ClientHelloSpec, error) { + fingerprinter := &tls.Fingerprinter{} + rawCapturedClientHelloBytes, _ := hex.DecodeString("16030107b8010007b403036252c641762f4a4f55d93dcbe1a01e62839ba1b48e9082896737ca23dc6bd02120c719a58f491c56be1f21fcd68bd097f2ee5a370c576a7c512aba46b7ed73841a0020eaea130113021303c02bc02fc02cc030cca9cca8c013c014009c009d002f00350100074baaaa0000000a000c000a5a5a11ec001d00170018000d0012001004030804040105030805050108060601ff0100010000170000002b0007065a5a03040303001b00030200020010000b000908687474702f312e31000b00020100002d00020101fe0d00ba0000010001be0020a5fb3e1a8aa228913bdf2dc9c52670860d2032a04d71140d6dc08f4cb41b2a1c0090420727529f82e9a8b097215adb52126184c638a98f0a6f8121b04edb939a7ff2a9564b44a917160ceb0a41f7a2dc12b89c622bc6687a21759c813858c81fe2e9024b02c6501c4b4c9e87ed2bd1538a420b6045a0785a66b044e53260e19b6f09042b903fa6758ce571dff16b91e95a19dbe5da0a46cab946036f48ad1394534866ee874f8dc75b3e4efa444be660262f003304ef04ed5a5a00010011ec04c098b01e12861f0d1738ebdb83b5fb876e7a1382f50149279a62479352b110f9791d1710c99cd988348175a20884650682e5e6a48bd2b27bb52360882bd84c6f97b6bc142c300ba7ab420bbec1872c13ba6970501e2b473f5c325da09a1dda800060d3c14454a95e6a67ddf28be60140cc3b07d393826b8babaf9a074b97bfa6fb8a2da784e98c5a332c62d7a60b7ac5b85e6606d128a4e9d89056680c6ff3ad5c3a3018218006db0fd81843e4ccb3f3e42ec858554be50aaa5b9afb613a1c07a631551706c4840bdb11a41564db56cdd2e58d1c619fc34567b74a2c54755f4174a0d589bf5938388a0976456c2a2aca2c08b2af11a2b550da9b22e4a39e2a7759c887e884b362b0b0e8394a149b44e82b6f5e680cd280cd2167953c94b111c20c4d028b2fc0926ac1a0454977fdf4a6876a555048a0c8b23531215f8543b5af5a2a69fc4151a870f481204e3a1535ec35321697d0ea0a470239e7b40b99e9b7ee01205bf567c42479c137c346b8af4dd825d4324c6d34875dd50b1841a78c42b79075af259a0f45252f311997f39902337088ee6817565b2e63858924a2354d86b1ba77bfa324339ec4a72663134db7227e652aea43700f751f7f519c5a66a036e0274a48c58716a3eb8a6de4730d19d534dd52ae83b9a7f07ccbc8d63b073a5127e3170dc58031524b81b611c6c4cf2aaba01fc55747b643fb417c85c0a21aa823709a037e418fc8e9c16c51a0c105c07d7c2f56dbcd4516a17a10036531768fb04300f0cdcc7c5135517b1643c3204b47575b97d0a513287985a0353523d93d511b694d6bb5ecc0698823855a606b83c77723808dd6737c9129a5c50457e71a063f927299214e902861e89a81423715b60717e8551b29126f857499810a0328d7182b538276d15517aa21b828ba7afcc13c1cc4cd1574a1c97bcfe4490f766d0e95a0c2911bc0d7a47161bcf5c344558314e7a1ab6c4227950b76c3319acaa53a7776a91e3b62ecd38f0732504674247947855a95892cc43b7f56a09f0c4281976e2b73820ff48180f054f482638078b08cf01d2b221a35214002b5cc8e394a414230a4f562af739e70ec540c260c651009a0616c5830658e0cbb2d2a2f94dab9f539a9ef19750f4925f7106a3a9c0a68e4a7d442202678beeef25b92c8709d93133c08728603ac900719db6615d294ad1aa965546a0c655a6891ab8e9fea31b7262dbba5af94372262934a4feac79ad72a3277aa7e97ab48f9a06f6857d26a2d48306b80a4bb600785f5b26f00202d2eb6af0226774e93615a484fa42a91bfe6034e890f6642c95a979d8df8a39ab10977846b1dc1998b51a8dde62975dc54bf92ada8dc0d7c0a020f429d4ff0b11ba95b6e076ec0443d01f0cb963441ddca9244631b776a029f874b799534c9c14808863c756b2d8a0309a0fab26bc37db05601cd5741acf9acc4c84a9e6b76b1e8282908a8d2268562e72109c3c8cb93507e1accf48723a056bd4acab4317771d4e432216a0a39377792469757a8c186d8085078aa2a8bce201628bad8c7ce98bb41dc56d6715a1dc3cb1cab48cb5a8f18d04a651b9efdd751bd199c266aac08726177bb432ecba334b06e516c12449846dcf90a4080248e2b15cb435e021ee8c4a8c17c095babf8011d67e0a5f16e25dc7357fc1c78e5884fe32598695589294e9c431c94ac2f5d2053757f9a905df36b001d002022b1fc0ef89cbfbd8f197c3e230879a2e71c155a1a6cb1bc33136cf93380c41d00000011000f00000c7373682e6576616e2e72756e0012000000230000000500050100000000baba0001000029010b00e600e0d1ae9857a5731dd0d5ca9d726bf57748ff9f57a7bb61ee7cc84e0f06358291fd2694914ab4ab3abc95fec3a1b393f458cca49eddd4dc24c896429eba5464d54ed59c562be84fe5660c8f74263db3c5eefbd36f2c667b9bd8219e20d84162a545978844bce25f2dc0f1c56f032bcee7f0bcd9fc762cfa115f368d0fe3b2e03fce32673bfdafd419eb8bc36bcf9c4a0d49c8d750ea7373e41c2ac13eb97f8fdb6c619742e98951c6447b76b263e0040ae874c7bd486421af99541b3b40e5b6eb3c0ae555dc1dfa7448aee19f05e54f70eb6655a91eb03eb93015a45eb29adc73d82437927e002120022bd91157ac53b55c75c03be16f0bc0040f38fa0085949f0157f8e7db794cc5") + spec, err := fingerprinter.FingerprintClientHello(rawCapturedClientHelloBytes) + return spec, err +} diff --git a/util/utls_test.go b/util/utls_test.go new file mode 100644 index 00000000..adea1f73 --- /dev/null +++ b/util/utls_test.go @@ -0,0 +1,109 @@ +package util + +import ( + stdtls "crypto/tls" + "net" + "testing" + + tls "github.com/refraction-networking/utls" +) + +func TestNewUTLSConfigPreservesClientSettings(t *testing.T) { + called := false + cfg := NewUTLSConfig(&stdtls.Config{ + ServerName: "example.com", + InsecureSkipVerify: true, + NextProtos: []string{"h2", "http/1.1"}, + MinVersion: stdtls.VersionTLS12, + MaxVersion: stdtls.VersionTLS13, + CipherSuites: []uint16{stdtls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256}, + CurvePreferences: []stdtls.CurveID{stdtls.X25519, stdtls.CurveP256}, + DynamicRecordSizingDisabled: true, + Renegotiation: stdtls.RenegotiateOnceAsClient, + VerifyConnection: func(state stdtls.ConnectionState) error { + called = true + if state.ServerName != "example.com" { + t.Fatalf("ServerName = %q, want example.com", state.ServerName) + } + return nil + }, + }) + + if cfg.ServerName != "example.com" { + t.Fatalf("ServerName = %q, want example.com", cfg.ServerName) + } + if !cfg.InsecureSkipVerify { + t.Fatal("InsecureSkipVerify was not preserved") + } + if len(cfg.NextProtos) != 2 || cfg.NextProtos[0] != "h2" || cfg.NextProtos[1] != "http/1.1" { + t.Fatalf("NextProtos = %v", cfg.NextProtos) + } + if cfg.MinVersion != tls.VersionTLS12 || cfg.MaxVersion != tls.VersionTLS13 { + t.Fatalf("version range = %x-%x", cfg.MinVersion, cfg.MaxVersion) + } + if len(cfg.CipherSuites) != 1 || cfg.CipherSuites[0] != tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 { + t.Fatalf("CipherSuites = %v", cfg.CipherSuites) + } + if len(cfg.CurvePreferences) != 2 || cfg.CurvePreferences[0] != tls.X25519 || cfg.CurvePreferences[1] != tls.CurveP256 { + t.Fatalf("CurvePreferences = %v", cfg.CurvePreferences) + } + if !cfg.DynamicRecordSizingDisabled { + t.Fatal("DynamicRecordSizingDisabled was not preserved") + } + if cfg.Renegotiation != tls.RenegotiateOnceAsClient { + t.Fatalf("Renegotiation = %v", cfg.Renegotiation) + } + if cfg.VerifyConnection == nil { + t.Fatal("VerifyConnection was not preserved") + } + if err := cfg.VerifyConnection(tls.ConnectionState{ServerName: "example.com"}); err != nil { + t.Fatal(err) + } + if !called { + t.Fatal("VerifyConnection wrapper was not called") + } +} + +func TestNewUTLSChromeClientBuildsChromeHello(t *testing.T) { + c1, c2 := net.Pipe() + defer c1.Close() + defer c2.Close() + + client := NewUTLSChromeClient(c1, &stdtls.Config{ServerName: "example.com"}) + if client.ClientHelloID != tls.HelloChrome_Auto { + t.Fatalf("ClientHelloID = %+v, want %+v", client.ClientHelloID, tls.HelloChrome_Auto) + } + if err := client.BuildHandshakeStateWithoutSession(); err != nil { + t.Fatal(err) + } + if len(client.Extensions) == 0 { + t.Fatal("Chrome ClientHello has no extensions") + } +} + +func TestNewUTLSWebSocketClientAppliesCustomSpec(t *testing.T) { + c1, c2 := net.Pipe() + defer c1.Close() + defer c2.Close() + + client, err := NewUTLSWebSocketClient(c1, &stdtls.Config{ + ServerName: "example.com", + NextProtos: []string{"h2"}, + MinVersion: stdtls.VersionTLS13, + }, false) + if err != nil { + t.Fatal(err) + } + if client.ClientHelloID != tls.HelloCustom { + t.Fatalf("ClientHelloID = %+v, want %+v", client.ClientHelloID, tls.HelloCustom) + } + if len(client.Extensions) == 0 { + t.Fatal("custom WebSocket ClientHello preset has no extensions") + } + if err := client.BuildHandshakeStateWithoutSession(); err != nil { + t.Fatal(err) + } + if len(client.HandshakeState.Hello.AlpnProtocols) != 1 || client.HandshakeState.Hello.AlpnProtocols[0] != "http/1.1" { + t.Fatalf("ALPN = %v, want [http/1.1]", client.HandshakeState.Hello.AlpnProtocols) + } +}