UTLS兼容

This commit is contained in:
wenyifan
2026-06-28 12:08:30 +08:00
parent f63f36805b
commit 3993c95700
9 changed files with 305 additions and 17 deletions
+2 -2
View File
@@ -2,7 +2,6 @@ package mtls
import ( import (
"context" "context"
"crypto/tls"
"errors" "errors"
"net" "net"
"sync" "sync"
@@ -16,6 +15,7 @@ import (
"github.com/go-gost/x/internal/net/proxyproto" "github.com/go-gost/x/internal/net/proxyproto"
"github.com/go-gost/x/internal/util/mux" "github.com/go-gost/x/internal/util/mux"
"github.com/go-gost/x/registry" "github.com/go-gost/x/registry"
"github.com/go-gost/x/util"
) )
func init() { 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) { 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 { if err := tlsConn.HandshakeContext(ctx); err != nil {
return nil, err return nil, err
} }
+18 -4
View File
@@ -17,6 +17,7 @@ import (
"github.com/go-gost/x/internal/util/mux" "github.com/go-gost/x/internal/util/mux"
ws_util "github.com/go-gost/x/internal/util/ws" ws_util "github.com/go-gost/x/internal/util/ws"
"github.com/go-gost/x/registry" "github.com/go-gost/x/registry"
"github.com/go-gost/x/util"
"github.com/gorilla/websocket" "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 { if d.tlsEnabled {
url.Scheme = "wss" urlObj.Scheme = "wss"
dialer.TLSClientConfig = d.options.TLSConfig 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 { if d.md.handshakeTimeout > 0 {
conn.SetReadDeadline(time.Now().Add(d.md.handshakeTimeout)) conn.SetReadDeadline(time.Now().Add(d.md.handshakeTimeout))
} }
urlStr, errUnescape := url.QueryUnescape(urlObj.String())
c, resp, err := dialer.DialContext(ctx, url.String(), d.md.header) 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 { if err != nil {
return nil, err return nil, err
} }
+5 -1
View File
@@ -5,8 +5,8 @@ import (
"time" "time"
mdata "github.com/go-gost/core/metadata" mdata "github.com/go-gost/core/metadata"
mdutil "github.com/go-gost/x/metadata/util"
"github.com/go-gost/x/internal/util/mux" "github.com/go-gost/x/internal/util/mux"
mdutil "github.com/go-gost/x/metadata/util"
) )
const ( const (
@@ -18,6 +18,8 @@ type metadata struct {
host string host string
path string path string
useH2 bool
handshakeTimeout time.Duration handshakeTimeout time.Duration
readHeaderTimeout time.Duration readHeaderTimeout time.Duration
readBufferSize int 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.tcpKeepaliveInterval = mdutil.GetDuration(md, "tcp.keepalive.interval")
d.md.tcpKeepaliveCount = mdutil.GetInt(md, "tcp.keepalive.count") d.md.tcpKeepaliveCount = mdutil.GetInt(md, "tcp.keepalive.count")
d.md.useH2 = mdutil.GetBool(md, "h2")
return return
} }
+2 -2
View File
@@ -2,7 +2,6 @@ package tls
import ( import (
"context" "context"
"crypto/tls"
"net" "net"
"time" "time"
@@ -13,6 +12,7 @@ import (
xnet "github.com/go-gost/x/internal/net" xnet "github.com/go-gost/x/internal/net"
"github.com/go-gost/x/internal/net/proxyproto" "github.com/go-gost/x/internal/net/proxyproto"
"github.com/go-gost/x/registry" "github.com/go-gost/x/registry"
"github.com/go-gost/x/util"
) )
func init() { func init() {
@@ -77,7 +77,7 @@ func (d *tlsDialer) Handshake(ctx context.Context, conn net.Conn, options ...dia
defer conn.SetDeadline(time.Time{}) 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 { if err := tlsConn.HandshakeContext(ctx); err != nil {
conn.Close() conn.Close()
return nil, err return nil, err
+2 -2
View File
@@ -5,7 +5,6 @@ import (
"crypto/tls" "crypto/tls"
"net" "net"
"time" "time"
"unsafe"
"github.com/go-gost/core/dialer" "github.com/go-gost/core/dialer"
"github.com/go-gost/core/logger" "github.com/go-gost/core/logger"
@@ -14,6 +13,7 @@ import (
xnet "github.com/go-gost/x/internal/net" xnet "github.com/go-gost/x/internal/net"
"github.com/go-gost/x/internal/net/proxyproto" "github.com/go-gost/x/internal/net/proxyproto"
"github.com/go-gost/x/registry" "github.com/go-gost/x/registry"
"github.com/go-gost/x/util"
utls "github.com/refraction-networking/utls" 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) clientHelloID, ok := GetClientHelloID(d.md.fingerprint)
if ok { if ok {
d.log.Debugf("utls handshake with fingerprint: %s", d.md.fingerprint) 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) uconn := utls.UClient(conn, utlsCfg, clientHelloID)
if err := uconn.HandshakeContext(ctx); err != nil { if err := uconn.HandshakeContext(ctx); err != nil {
conn.Close() conn.Close()
+18 -4
View File
@@ -13,6 +13,7 @@ import (
"github.com/go-gost/x/internal/net/proxyproto" "github.com/go-gost/x/internal/net/proxyproto"
ws_util "github.com/go-gost/x/internal/util/ws" ws_util "github.com/go-gost/x/internal/util/ws"
"github.com/go-gost/x/registry" "github.com/go-gost/x/registry"
"github.com/go-gost/x/util"
"github.com/gorilla/websocket" "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 { if d.tlsEnabled {
url.Scheme = "wss" urlObj.Scheme = "wss"
dialer.TLSClientConfig = d.options.TLSConfig 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
}
} }
urlStr, errUnescape := url.QueryUnescape(urlObj.String())
c, resp, err := dialer.DialContext(ctx, url.String(), d.md.header) 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 { if err != nil {
return nil, err return nil, err
} }
+4 -2
View File
@@ -14,8 +14,9 @@ const (
) )
type metadata struct { type metadata struct {
host string host string
path string path string
useH2 bool
handshakeTimeout time.Duration handshakeTimeout time.Duration
readHeaderTimeout 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.tcpKeepaliveInterval = mdutil.GetDuration(md, "tcp.keepalive.interval")
d.md.tcpKeepaliveCount = mdutil.GetInt(md, "tcp.keepalive.count") d.md.tcpKeepaliveCount = mdutil.GetInt(md, "tcp.keepalive.count")
d.md.useH2 = mdutil.GetBool(md, "h2")
return return
} }
+145
View File
@@ -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
}
+109
View File
@@ -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)
}
}