diff --git a/u_quic.go b/u_quic.go index 6493ddbdb3..618f5782f1 100644 --- a/u_quic.go +++ b/u_quic.go @@ -90,6 +90,7 @@ func (q *UQUICConn) Close() error { return nil // never started } q.conn.quic.cancel() + <-q.conn.quic.signalc for range q.conn.quic.blockedc { // Wait for the handshake goroutine to return. } @@ -184,14 +185,16 @@ func (q *UQUICConn) SetTransportParameters(params []byte) { if params == nil { params = []byte{} } - q.conn.quic.transportParams = params // this won't be used for building ClientHello when using a preset + q.conn.quic.transportParams = params - // // instead, we set the transport parameters hold by the ClientHello - // for _, ext := range q.conn.Extensions { - // if qtp, ok := ext.(*QUICTransportParametersExtension); ok { - // qtp.TransportParametersExtData = params - // } - // } + // A ClientHello built from a preset marshals its own + // QUICTransportParametersExtension, which ApplyPreset cloned from the + // caller's spec. Put the parameters there, so they reach the peer. + for _, ext := range q.conn.Extensions { + if qtp, ok := ext.(*QUICTransportParametersExtension); ok { + qtp.marshalResult = append([]byte{}, params...) + } + } if q.conn.quic.started { <-q.conn.quic.signalc diff --git a/u_quic_preset_test.go b/u_quic_preset_test.go new file mode 100644 index 0000000000..427ee53337 --- /dev/null +++ b/u_quic_preset_test.go @@ -0,0 +1,110 @@ +// 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 + +import ( + "bytes" + "context" + "sync" + "testing" +) + +func newUQUICPresetClient(t *testing.T) *UQUICConn { + t.Helper() + clientConfig := testConfigClient.Clone() + clientConfig.NextProtos = []string{"h3"} + client := UQUICClient(&QUICConfig{TLSConfig: clientConfig}, HelloCustom) + preset := go127ClientHelloSpec([]SignatureScheme{ECDSAWithP256AndSHA256}) + preset.Extensions = append(preset.Extensions, + &ALPNExtension{AlpnProtocols: []string{"h3"}}, + &QUICTransportParametersExtension{}, + ) + if err := client.ApplyPreset(preset); err != nil { + t.Fatal(err) + } + return client +} + +func newUQUICTestServer() *QUICConn { + serverConfig := testConfigServer.Clone() + serverConfig.NextProtos = []string{"h3"} + return QUICServer(&QUICConfig{TLSConfig: serverConfig}) +} + +type uquicTestEndpoint interface { + Start(context.Context) error + NextEvent() QUICEvent + HandleData(QUICEncryptionLevel, []byte) error +} + +// runUQUICHandshake pumps events between the endpoints until both are idle +// or one fails, and returns the transport parameters each side received. +func runUQUICHandshake(ctx context.Context, client, server uquicTestEndpoint) (got [2][]byte, err error) { + endpoints := []uquicTestEndpoint{client, server} + for _, endpoint := range endpoints { + if err := endpoint.Start(ctx); err != nil { + return got, err + } + } + for current, idle := 0, 0; idle < 2; { + event := endpoints[current].NextEvent() + if event.Kind == QUICNoEvent { + idle++ + current = 1 - current + continue + } + idle = 0 + switch event.Kind { + case QUICWriteData: + if err := endpoints[1-current].HandleData(event.Level, event.Data); err != nil { + return got, err + } + case QUICTransportParameters: + got[current] = append([]byte{}, event.Data...) + } + } + return got, nil +} + +// SetTransportParameters must reach the ClientHello when a preset is applied: +// ApplyPreset clones the spec, so the extension the caller passed is no longer +// the one that is marshaled. +func TestUQUICPresetTransportParameters(t *testing.T) { + params := []byte{0x04, 0x04, 0x80, 0x10, 0x00, 0x00} // initial_max_data + client := newUQUICPresetClient(t) + defer client.Close() + client.SetTransportParameters(params) + server := newUQUICTestServer() + defer server.Close() + server.SetTransportParameters(nil) + + got, err := runUQUICHandshake(t.Context(), client, server) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got[1], params) { + t.Errorf("server received transport parameters %x, want %x", got[1], params) + } +} + +// Canceling the context concurrently with a handshake must not race with +// UQUICConn methods, as for QUICConn. See https://go.dev/issue/77274. +func TestUQUICContextCancelation(t *testing.T) { + for range 20 { + ctx, cancel := context.WithCancel(context.Background()) + client := newUQUICPresetClient(t) + client.SetTransportParameters(nil) + server := newUQUICTestServer() + server.SetTransportParameters(nil) + var wg sync.WaitGroup + wg.Go(func() { + _, _ = runUQUICHandshake(ctx, client, server) + client.Close() + server.Close() + }) + wg.Go(cancel) + wg.Wait() + } +}