crypto/tls: add QUICErrorEvent

Add a new QUICEvent type for reporting errors.
This provides a way to report errors that don't occur as a result of
QUICConn.Start, QUICConn.HandleData, or QUICConn.SendSessionTicket.

Fixes #75108

Change-Id: I941371a21f26b940e75287a66d7e0211fc0baab1
Reviewed-on: https://go-review.googlesource.com/c/go/+/719040
Auto-Submit: Damien Neil <dneil@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Roland Shoemaker <roland@golang.org>
This commit is contained in:
Damien Neil 2025-11-08 11:22:59 -08:00 committed by Gopher Robot
parent 3ad2e113fc
commit bd2b117c2c
4 changed files with 73 additions and 0 deletions

3
api/next/75108.txt Normal file
View file

@ -0,0 +1,3 @@
pkg crypto/tls, const QUICErrorEvent = 10 #75108
pkg crypto/tls, const QUICErrorEvent QUICEventKind #75108
pkg crypto/tls, type QUICEvent struct, Err error #75108

View file

@ -0,0 +1,2 @@
The [QUICConn] type used by QUIC implementations includes new event
for reporting TLS handshake errors.

View file

@ -117,6 +117,11 @@ const (
// The application may modify the [SessionState] before storing it. // The application may modify the [SessionState] before storing it.
// This event only occurs on client connections. // This event only occurs on client connections.
QUICStoreSession QUICStoreSession
// QUICErrorEvent indicates that a fatal error has occurred.
// The handshake cannot proceed and the connection must be closed.
// QUICEvent.Err is set.
QUICErrorEvent
) )
// A QUICEvent is an event occurring on a QUIC connection. // A QUICEvent is an event occurring on a QUIC connection.
@ -138,6 +143,10 @@ type QUICEvent struct {
// Set for QUICResumeSession and QUICStoreSession. // Set for QUICResumeSession and QUICStoreSession.
SessionState *SessionState SessionState *SessionState
// Set for QUICErrorEvent.
// The error will wrap AlertError.
Err error
} }
type quicState struct { type quicState struct {
@ -157,6 +166,7 @@ type quicState struct {
cancel context.CancelFunc cancel context.CancelFunc
waitingForDrain bool waitingForDrain bool
errorReturned bool
// readbuf is shared between HandleData and the handshake goroutine. // readbuf is shared between HandleData and the handshake goroutine.
// HandshakeCryptoData passes ownership to the handshake goroutine by // HandshakeCryptoData passes ownership to the handshake goroutine by
@ -229,6 +239,15 @@ func (q *QUICConn) NextEvent() QUICEvent {
<-qs.signalc <-qs.signalc
<-qs.blockedc <-qs.blockedc
} }
if err := q.conn.handshakeErr; err != nil {
if qs.errorReturned {
return QUICEvent{Kind: QUICNoEvent}
}
qs.errorReturned = true
qs.events = nil
qs.nextEvent = 0
return QUICEvent{Kind: QUICErrorEvent, Err: q.conn.handshakeErr}
}
if qs.nextEvent >= len(qs.events) { if qs.nextEvent >= len(qs.events) {
qs.events = qs.events[:0] qs.events = qs.events[:0]
qs.nextEvent = 0 qs.nextEvent = 0

View file

@ -8,6 +8,7 @@ import (
"bytes" "bytes"
"context" "context"
"errors" "errors"
"fmt"
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
@ -21,6 +22,7 @@ type testQUICConn struct {
ticketOpts QUICSessionTicketOptions ticketOpts QUICSessionTicketOptions
onResumeSession func(*SessionState) onResumeSession func(*SessionState)
gotParams []byte gotParams []byte
gotError error
earlyDataRejected bool earlyDataRejected bool
complete bool complete bool
} }
@ -109,6 +111,9 @@ func runTestQUICConnection(ctx context.Context, cli, srv *testQUICConn, onEvent
if onEvent != nil && onEvent(e, a, b) { if onEvent != nil && onEvent(e, a, b) {
continue continue
} }
if a.gotError != nil && e.Kind != QUICNoEvent {
return fmt.Errorf("unexpected event %v after QUICErrorEvent", e.Kind)
}
switch e.Kind { switch e.Kind {
case QUICNoEvent: case QUICNoEvent:
idleCount++ idleCount++
@ -152,6 +157,11 @@ func runTestQUICConnection(ctx context.Context, cli, srv *testQUICConn, onEvent
} }
case QUICRejectedEarlyData: case QUICRejectedEarlyData:
a.earlyDataRejected = true a.earlyDataRejected = true
case QUICErrorEvent:
if e.Err == nil {
return errors.New("unexpected QUICErrorEvent with no Err")
}
a.gotError = e.Err
} }
if e.Kind != QUICNoEvent { if e.Kind != QUICNoEvent {
idleCount = 0 idleCount = 0
@ -371,6 +381,45 @@ func TestQUICHandshakeError(t *testing.T) {
if _, ok := errors.AsType[*CertificateVerificationError](err); !ok { if _, ok := errors.AsType[*CertificateVerificationError](err); !ok {
t.Errorf("connection handshake terminated with error %q, want CertificateVerificationError", err) t.Errorf("connection handshake terminated with error %q, want CertificateVerificationError", err)
} }
ev := cli.conn.NextEvent()
if ev.Kind != QUICErrorEvent {
t.Errorf("client.NextEvent: no QUICErrorEvent, want one")
}
if ev.Err != err {
t.Errorf("client.NextEvent: want same error returned by Start, got %v", ev.Err)
}
}
// Test that we can report an error produced by the GetEncryptedClientHelloKeys function.
func TestQUICECHKeyError(t *testing.T) {
getECHKeysError := errors.New("error returned by GetEncryptedClientHelloKeys")
config := &QUICConfig{TLSConfig: testConfig.Clone()}
config.TLSConfig.MinVersion = VersionTLS13
config.TLSConfig.NextProtos = []string{"h3"}
config.TLSConfig.GetEncryptedClientHelloKeys = func(*ClientHelloInfo) ([]EncryptedClientHelloKey, error) {
return nil, getECHKeysError
}
cli := newTestQUICClient(t, config)
cli.conn.SetTransportParameters(nil)
srv := newTestQUICServer(t, config)
if err := runTestQUICConnection(context.Background(), cli, srv, nil); err != errTransportParametersRequired {
t.Fatalf("handshake with no client parameters: %v; want errTransportParametersRequired", err)
}
srv.conn.SetTransportParameters(nil)
if err := runTestQUICConnection(context.Background(), cli, srv, nil); err == nil {
t.Fatalf("handshake with GetEncryptedClientHelloKeys errors: nil, want error")
}
if srv.gotError == nil {
t.Fatalf("after GetEncryptedClientHelloKeys error, server did not see QUICErrorEvent")
}
if _, ok := errors.AsType[AlertError](srv.gotError); !ok {
t.Errorf("connection handshake terminated with error %T, want AlertError", srv.gotError)
}
if !errors.Is(srv.gotError, getECHKeysError) {
t.Errorf("connection handshake terminated with error %v, want error returned by GetEncryptedClientHelloKeys", srv.gotError)
}
} }
// Test that QUICConn.ConnectionState can be used during the handshake, // Test that QUICConn.ConnectionState can be used during the handshake,