Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions common.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,10 +249,15 @@ const (
var testingOnlyForceDowngradeCanary bool

// ConnectionMetrics contains basic metrics about the connection.
// Metrics are a snapshot and are final only after the handshake completes.
type ConnectionMetrics struct {
// ClientSentTicket is true if the client has sent a TLS 1.2 session ticket
// or a TLS 1.3 PSK in the ClientHello successfully.
ClientSentTicket bool

// ClientHelloFragmented is true if any eligible ClientHello was sent across
// multiple TLS records.
ClientHelloFragmented bool
}

// ConnectionState records basic TLS details about the connection.
Expand Down Expand Up @@ -574,6 +579,15 @@ const (
RenegotiateFreelyAsClient
)

// ClientHelloFragFunc returns a ClientHello split offset. The input
// is the marshaled handshake message, excluding the TLS record header.
// Returning 0 < n < len(clientHello) splits at n; any other value sends one
// record.
//
// Called for non-QUIC, non-ECH ClientHellos in the initial client handshake,
// including TLS 1.3 HRR retries. It must not modify or retain clientHello.
type ClientHelloFragFunc func(clientHello []byte) int

// A Config structure is used to configure a TLS client or server.
// After one has been passed to a TLS function it must not be
// modified. A Config may be reused; the tls package will also not
Expand Down Expand Up @@ -752,6 +766,10 @@ type Config struct {
// controls whether the extension is omitted.
AlwaysIncludePSK bool // [uTLS]

// FragmentClientHello enables ClientHello TLS record fragmentation.
// If nil, ClientHellos are sent normally.
FragmentClientHello ClientHelloFragFunc // [uTLS]

// InsecureServerNameToVerify is used to verify the hostname on the returned
// certificates. It is intended to use with spoofed ServerName.
// If InsecureServerNameToVerify is "*", crypto/tls will do normal
Expand Down Expand Up @@ -1087,6 +1105,7 @@ func (c *Config) Clone() *Config {
InsecureServerNameToVerify: c.InsecureServerNameToVerify,
OmitEmptyPsk: c.OmitEmptyPsk,
AlwaysIncludePSK: c.AlwaysIncludePSK,
FragmentClientHello: c.FragmentClientHello,
CipherSuites: c.CipherSuites,
PreferServerCipherSuites: c.PreferServerCipherSuites,
SessionTicketsDisabled: c.SessionTicketsDisabled,
Expand Down
11 changes: 11 additions & 0 deletions conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ type Conn struct {
handshakes int
extMasterSecret bool
clientSentTicket bool // whether the client sent a session ticket or a PSK in the Client Hello

// clientHelloFragmented records whether any eligible ClientHello was sent
// across multiple TLS records.
clientHelloFragmented bool

didResume bool // whether this connection was a session resumption
didHRR bool // whether a HelloRetryRequest was sent/received
cipherSuite uint16
Expand Down Expand Up @@ -1065,6 +1070,11 @@ func (c *Conn) writeHandshakeRecord(msg handshakeMessage, transcript transcriptH
transcript.Write(data)
}

// [uTLS] Optionally split an eligible ClientHello across two records.
if c.shouldFragmentClientHello(data) {
return c.writeFragmentedClientHello(data)
}

return c.writeRecordLocked(recordTypeHandshake, data)
}

Expand Down Expand Up @@ -1725,5 +1735,6 @@ func (c *Conn) ConnectionMetrics() ConnectionMetrics {
defer c.handshakeMutex.Unlock()
var metrics ConnectionMetrics
metrics.ClientSentTicket = c.clientSentTicket
metrics.ClientHelloFragmented = c.clientHelloFragmented
return metrics
}
9 changes: 7 additions & 2 deletions tls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -818,7 +818,7 @@ func TestWarningAlertFlood(t *testing.T) {
}

func TestCloneFuncFields(t *testing.T) {
const expectedCount = 10
const expectedCount = 11
called := 0

c1 := Config{
Expand Down Expand Up @@ -862,6 +862,10 @@ func TestCloneFuncFields(t *testing.T) {
called |= 1 << 9
return nil, nil
},
FragmentClientHello: func([]byte) int {
called |= 1 << 10
return 0
},
}

c2 := c1.Clone()
Expand All @@ -876,6 +880,7 @@ func TestCloneFuncFields(t *testing.T) {
c2.WrapSession(ConnectionState{}, nil)
c2.EncryptedClientHelloRejectionVerify(ConnectionState{})
c2.GetEncryptedClientHelloKeys(nil)
c2.FragmentClientHello(nil)

if called != (1<<expectedCount)-1 {
t.Fatalf("expected %d calls but saw calls %b", expectedCount, called)
Expand All @@ -894,7 +899,7 @@ func TestCloneNonFuncFields(t *testing.T) {
switch fn := typ.Field(i).Name; fn {
case "Rand":
f.Set(reflect.ValueOf(io.Reader(os.Stdin)))
case "Time", "GetCertificate", "GetConfigForClient", "VerifyPeerCertificate", "VerifyConnection", "GetClientCertificate", "WrapSession", "UnwrapSession", "EncryptedClientHelloRejectionVerify", "GetEncryptedClientHelloKeys":
case "Time", "GetCertificate", "GetConfigForClient", "VerifyPeerCertificate", "VerifyConnection", "GetClientCertificate", "WrapSession", "UnwrapSession", "EncryptedClientHelloRejectionVerify", "GetEncryptedClientHelloKeys", "FragmentClientHello":
// DeepEqual can't compare functions. If you add a
// function field to this list, you must also change
// TestCloneFuncFields to ensure that the func field is
Expand Down
44 changes: 44 additions & 0 deletions u_client_hello_fragmentation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright 2026 The uTLS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package tls

// shouldFragmentClientHello reports whether data is an eligible
// initial-handshake ClientHello.
func (c *Conn) shouldFragmentClientHello(data []byte) bool {
return c != nil &&
c.config != nil &&
c.config.FragmentClientHello != nil &&
len(c.config.EncryptedClientHelloConfigList) == 0 &&
c.isClient &&
c.quic == nil &&
c.handshakes == 0 &&
len(data) > 0 &&
data[0] == typeClientHello
}

// writeFragmentedClientHello writes data as one or two handshake
// records. Invalid split points fall back to one record.
func (c *Conn) writeFragmentedClientHello(data []byte) (int, error) {
split := c.config.FragmentClientHello(data)
if split <= 0 || split >= len(data) {
n, err := c.writeRecordLocked(recordTypeHandshake, data)
if err != nil {
return n, err
}
return n, nil
}

n, err := c.writeRecordLocked(recordTypeHandshake, data[:split])
if err != nil {
return n, err
}
n2, err := c.writeRecordLocked(recordTypeHandshake, data[split:])
if err != nil {
return n + n2, err
}
c.clientHelloFragmented = true

return len(data), nil
}
Loading
Loading