Skip to content

Commit 434956a

Browse files
committed
quic: include more detail in connection close errors
When closing a connection with an error, include a reason string in the CONNECTION_CLOSE frame as well as the error code, when the code isn't sufficient to explain the error. Change-Id: I055a4e11b222e87d1ff01d8c45fcb7cc17fe4196 Reviewed-on: https://go-review.googlesource.com/c/net/+/539342 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Jonathan Amsterdam <[email protected]>
1 parent ec29a94 commit 434956a

15 files changed

+178
-61
lines changed

internal/quic/conn_close.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ func (c *Conn) enterDraining(err error) {
156156
if c.isDraining() {
157157
return
158158
}
159-
if e, ok := c.lifetime.localErr.(localTransportError); ok && transportError(e) != errNo {
159+
if e, ok := c.lifetime.localErr.(localTransportError); ok && e.code != errNo {
160160
// If we've terminated the connection due to a peer protocol violation,
161161
// record the final error on the connection as our reason for termination.
162162
c.lifetime.finalErr = c.lifetime.localErr
@@ -220,7 +220,7 @@ func (c *Conn) Wait(ctx context.Context) error {
220220
// Otherwise, Abort sends a transport error of APPLICATION_ERROR with the error's text.
221221
func (c *Conn) Abort(err error) {
222222
if err == nil {
223-
err = localTransportError(errNo)
223+
err = localTransportError{code: errNo}
224224
}
225225
c.sendMsg(func(now time.Time, c *Conn) {
226226
c.abort(now, err)

internal/quic/conn_flow.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,10 @@ func (c *Conn) shouldUpdateFlowControl(credit int64) bool {
9090
func (c *Conn) handleStreamBytesReceived(n int64) error {
9191
c.streams.inflow.usedLimit += n
9292
if c.streams.inflow.usedLimit > c.streams.inflow.sentLimit {
93-
return localTransportError(errFlowControl)
93+
return localTransportError{
94+
code: errFlowControl,
95+
reason: "stream exceeded flow control limit",
96+
}
9497
}
9598
return nil
9699
}

internal/quic/conn_id.go

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -210,25 +210,40 @@ func (s *connIDState) validateTransportParameters(c *Conn, isRetry bool, p trans
210210
// the transient remote connection ID we chose (client)
211211
// or is empty (server).
212212
if !bytes.Equal(s.originalDstConnID, p.originalDstConnID) {
213-
return localTransportError(errTransportParameter)
213+
return localTransportError{
214+
code: errTransportParameter,
215+
reason: "original_destination_connection_id mismatch",
216+
}
214217
}
215218
s.originalDstConnID = nil // we have no further need for this
216219
// Verify retry_source_connection_id matches the value from
217220
// the server's Retry packet (when one was sent), or is empty.
218221
if !bytes.Equal(p.retrySrcConnID, s.retrySrcConnID) {
219-
return localTransportError(errTransportParameter)
222+
return localTransportError{
223+
code: errTransportParameter,
224+
reason: "retry_source_connection_id mismatch",
225+
}
220226
}
221227
s.retrySrcConnID = nil // we have no further need for this
222228
// Verify initial_source_connection_id matches the first remote connection ID.
223229
if len(s.remote) == 0 || s.remote[0].seq != 0 {
224-
return localTransportError(errInternal)
230+
return localTransportError{
231+
code: errInternal,
232+
reason: "remote connection id missing",
233+
}
225234
}
226235
if !bytes.Equal(p.initialSrcConnID, s.remote[0].cid) {
227-
return localTransportError(errTransportParameter)
236+
return localTransportError{
237+
code: errTransportParameter,
238+
reason: "initial_source_connection_id mismatch",
239+
}
228240
}
229241
if len(p.statelessResetToken) > 0 {
230242
if c.side == serverSide {
231-
return localTransportError(errTransportParameter)
243+
return localTransportError{
244+
code: errTransportParameter,
245+
reason: "client sent stateless_reset_token",
246+
}
232247
}
233248
token := statelessResetToken(p.statelessResetToken)
234249
s.remote[0].resetToken = token
@@ -255,17 +270,6 @@ func (s *connIDState) handlePacket(c *Conn, ptype packetType, srcConnID []byte)
255270
},
256271
}
257272
}
258-
case ptype == packetTypeInitial && c.side == serverSide:
259-
if len(s.remote) == 0 {
260-
// We're a server connection processing the first Initial packet
261-
// from the client. Set the client's connection ID.
262-
s.remote = append(s.remote, remoteConnID{
263-
connID: connID{
264-
seq: 0,
265-
cid: cloneBytes(srcConnID),
266-
},
267-
})
268-
}
269273
case ptype == packetTypeHandshake && c.side == serverSide:
270274
if len(s.local) > 0 && s.local[0].seq == -1 && !s.local[0].retired {
271275
// We're a server connection processing the first Handshake packet from
@@ -294,7 +298,10 @@ func (s *connIDState) handleNewConnID(c *Conn, seq, retire int64, cid []byte, re
294298
// Destination Connection ID MUST treat receipt of a NEW_CONNECTION_ID
295299
// frame as a connection error of type PROTOCOL_VIOLATION."
296300
// https://www.rfc-editor.org/rfc/rfc9000.html#section-19.15-6
297-
return localTransportError(errProtocolViolation)
301+
return localTransportError{
302+
code: errProtocolViolation,
303+
reason: "NEW_CONNECTION_ID from peer with zero-length DCID",
304+
}
298305
}
299306

300307
if retire > s.retireRemotePriorTo {
@@ -316,7 +323,10 @@ func (s *connIDState) handleNewConnID(c *Conn, seq, retire int64, cid []byte, re
316323
}
317324
if rcid.seq == seq {
318325
if !bytes.Equal(rcid.cid, cid) {
319-
return localTransportError(errProtocolViolation)
326+
return localTransportError{
327+
code: errProtocolViolation,
328+
reason: "NEW_CONNECTION_ID does not match prior id",
329+
}
320330
}
321331
have = true // yes, we've seen this sequence number
322332
}
@@ -350,7 +360,10 @@ func (s *connIDState) handleNewConnID(c *Conn, seq, retire int64, cid []byte, re
350360
// Retired connection IDs (including newly-retired ones) do not count
351361
// against the limit.
352362
// https://www.rfc-editor.org/rfc/rfc9000.html#section-5.1.1-5
353-
return localTransportError(errConnectionIDLimit)
363+
return localTransportError{
364+
code: errConnectionIDLimit,
365+
reason: "active_connection_id_limit exceeded",
366+
}
354367
}
355368

356369
// "An endpoint SHOULD limit the number of connection IDs it has retired locally
@@ -360,7 +373,10 @@ func (s *connIDState) handleNewConnID(c *Conn, seq, retire int64, cid []byte, re
360373
// Set a limit of four times the active_connection_id_limit for
361374
// the total number of remote connection IDs we keep state for locally.
362375
if len(s.remote) > 4*activeConnIDLimit {
363-
return localTransportError(errConnectionIDLimit)
376+
return localTransportError{
377+
code: errConnectionIDLimit,
378+
reason: "too many unacknowledged RETIRE_CONNECTION_ID frames",
379+
}
364380
}
365381

366382
return nil
@@ -375,7 +391,10 @@ func (s *connIDState) retireRemote(rcid *remoteConnID) {
375391

376392
func (s *connIDState) handleRetireConnID(c *Conn, seq int64) error {
377393
if seq >= s.nextLocalSeq {
378-
return localTransportError(errProtocolViolation)
394+
return localTransportError{
395+
code: errProtocolViolation,
396+
reason: "RETIRE_CONNECTION_ID for unissued sequence number",
397+
}
379398
}
380399
for i := range s.local {
381400
if s.local[i].seq == seq {

internal/quic/conn_recv.go

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,18 @@ func (c *Conn) handleLongHeader(now time.Time, ptype packetType, space numberSpa
7979
if buf[0]&reservedLongBits != 0 {
8080
// Reserved header bits must be 0.
8181
// https://www.rfc-editor.org/rfc/rfc9000#section-17.2-8.2.1
82-
c.abort(now, localTransportError(errProtocolViolation))
82+
c.abort(now, localTransportError{
83+
code: errProtocolViolation,
84+
reason: "reserved header bits are not zero",
85+
})
8386
return -1
8487
}
8588
if p.version != quicVersion1 {
8689
// The peer has changed versions on us mid-handshake?
87-
c.abort(now, localTransportError(errProtocolViolation))
90+
c.abort(now, localTransportError{
91+
code: errProtocolViolation,
92+
reason: "protocol version changed during handshake",
93+
})
8894
return -1
8995
}
9096

@@ -129,7 +135,10 @@ func (c *Conn) handle1RTT(now time.Time, buf []byte) int {
129135
if buf[0]&reserved1RTTBits != 0 {
130136
// Reserved header bits must be 0.
131137
// https://www.rfc-editor.org/rfc/rfc9000#section-17.3.1-4.8.1
132-
c.abort(now, localTransportError(errProtocolViolation))
138+
c.abort(now, localTransportError{
139+
code: errProtocolViolation,
140+
reason: "reserved header bits are not zero",
141+
})
133142
return -1
134143
}
135144

@@ -222,7 +231,10 @@ func (c *Conn) handleFrames(now time.Time, ptype packetType, space numberSpace,
222231
// "An endpoint MUST treat receipt of a packet containing no frames
223232
// as a connection error of type PROTOCOL_VIOLATION."
224233
// https://www.rfc-editor.org/rfc/rfc9000#section-12.4-3
225-
c.abort(now, localTransportError(errProtocolViolation))
234+
c.abort(now, localTransportError{
235+
code: errProtocolViolation,
236+
reason: "packet contains no frames",
237+
})
226238
return false
227239
}
228240
// frameOK verifies that ptype is one of the packets in mask.
@@ -232,7 +244,10 @@ func (c *Conn) handleFrames(now time.Time, ptype packetType, space numberSpace,
232244
// that is not permitted as a connection error of type
233245
// PROTOCOL_VIOLATION."
234246
// https://www.rfc-editor.org/rfc/rfc9000#section-12.4-3
235-
c.abort(now, localTransportError(errProtocolViolation))
247+
c.abort(now, localTransportError{
248+
code: errProtocolViolation,
249+
reason: "frame not allowed in packet",
250+
})
236251
return false
237252
}
238253
return true
@@ -347,7 +362,10 @@ func (c *Conn) handleFrames(now time.Time, ptype packetType, space numberSpace,
347362
n = c.handleHandshakeDoneFrame(now, space, payload)
348363
}
349364
if n < 0 {
350-
c.abort(now, localTransportError(errFrameEncoding))
365+
c.abort(now, localTransportError{
366+
code: errFrameEncoding,
367+
reason: "frame encoding error",
368+
})
351369
return false
352370
}
353371
payload = payload[n:]
@@ -360,7 +378,10 @@ func (c *Conn) handleAckFrame(now time.Time, space numberSpace, payload []byte)
360378
largest, ackDelay, n := consumeAckFrame(payload, func(rangeIndex int, start, end packetNumber) {
361379
if end > c.loss.nextNumber(space) {
362380
// Acknowledgement of a packet we never sent.
363-
c.abort(now, localTransportError(errProtocolViolation))
381+
c.abort(now, localTransportError{
382+
code: errProtocolViolation,
383+
reason: "acknowledgement for unsent packet",
384+
})
364385
return
365386
}
366387
c.loss.receiveAckRange(now, space, rangeIndex, start, end, c.handleAckOrLoss)
@@ -521,7 +542,10 @@ func (c *Conn) handleHandshakeDoneFrame(now time.Time, space numberSpace, payloa
521542
if c.side == serverSide {
522543
// Clients should never send HANDSHAKE_DONE.
523544
// https://www.rfc-editor.org/rfc/rfc9000#section-19.20-4
524-
c.abort(now, localTransportError(errProtocolViolation))
545+
c.abort(now, localTransportError{
546+
code: errProtocolViolation,
547+
reason: "client sent HANDSHAKE_DONE",
548+
})
525549
return -1
526550
}
527551
if !c.isClosingOrDraining() {

internal/quic/conn_send.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ func (c *Conn) appendConnectionCloseFrame(now time.Time, space numberSpace, err
328328
c.lifetime.connCloseSentTime = now
329329
switch e := err.(type) {
330330
case localTransportError:
331-
c.w.appendConnectionCloseTransportFrame(transportError(e), 0, "")
331+
c.w.appendConnectionCloseTransportFrame(e.code, 0, e.reason)
332332
case *ApplicationError:
333333
if space != appDataSpace {
334334
// "CONNECTION_CLOSE frames signaling application errors (type 0x1d)

internal/quic/conn_streams.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,10 @@ func (c *Conn) streamForFrame(now time.Time, id streamID, ftype streamFrameType)
127127
if (id.initiator() == c.side) != (ftype == sendStream) {
128128
// Received an invalid frame for unidirectional stream.
129129
// For example, a RESET_STREAM frame for a send-only stream.
130-
c.abort(now, localTransportError(errStreamState))
130+
c.abort(now, localTransportError{
131+
code: errStreamState,
132+
reason: "invalid frame for unidirectional stream",
133+
})
131134
return nil
132135
}
133136
}
@@ -148,7 +151,10 @@ func (c *Conn) streamForFrame(now time.Time, id streamID, ftype streamFrameType)
148151
}
149152
// Received a frame for a stream that should be originated by us,
150153
// but which we never created.
151-
c.abort(now, localTransportError(errStreamState))
154+
c.abort(now, localTransportError{
155+
code: errStreamState,
156+
reason: "received frame for unknown stream",
157+
})
152158
return nil
153159
} else {
154160
// if isOpen, this is a stream that was implicitly opened by a

internal/quic/conn_test.go

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,20 @@ func (tc *testConn) wantDatagram(expectation string, want *testDatagram) {
594594
}
595595
}
596596

597+
func datagramEqual(a, b *testDatagram) bool {
598+
if a.paddedSize != b.paddedSize ||
599+
a.addr != b.addr ||
600+
len(a.packets) != len(b.packets) {
601+
return false
602+
}
603+
for i := range a.packets {
604+
if !packetEqual(a.packets[i], b.packets[i]) {
605+
return false
606+
}
607+
}
608+
return true
609+
}
610+
597611
// wantPacket indicates that we expect the Conn to send a packet.
598612
func (tc *testConn) wantPacket(expectation string, want *testPacket) {
599613
tc.t.Helper()
@@ -603,6 +617,25 @@ func (tc *testConn) wantPacket(expectation string, want *testPacket) {
603617
}
604618
}
605619

620+
func packetEqual(a, b *testPacket) bool {
621+
ac := *a
622+
ac.frames = nil
623+
bc := *b
624+
bc.frames = nil
625+
if !reflect.DeepEqual(ac, bc) {
626+
return false
627+
}
628+
if len(a.frames) != len(b.frames) {
629+
return false
630+
}
631+
for i := range a.frames {
632+
if !frameEqual(a.frames[i], b.frames[i]) {
633+
return false
634+
}
635+
}
636+
return true
637+
}
638+
606639
// wantFrame indicates that we expect the Conn to send a frame.
607640
func (tc *testConn) wantFrame(expectation string, wantType packetType, want debugFrame) {
608641
tc.t.Helper()
@@ -613,11 +646,20 @@ func (tc *testConn) wantFrame(expectation string, wantType packetType, want debu
613646
if gotType != wantType {
614647
tc.t.Fatalf("%v:\ngot %v packet, want %v\ngot frame: %v", expectation, gotType, wantType, got)
615648
}
616-
if !reflect.DeepEqual(got, want) {
649+
if !frameEqual(got, want) {
617650
tc.t.Fatalf("%v:\ngot frame: %v\nwant frame: %v", expectation, got, want)
618651
}
619652
}
620653

654+
func frameEqual(a, b debugFrame) bool {
655+
switch af := a.(type) {
656+
case debugFrameConnectionCloseTransport:
657+
bf, ok := b.(debugFrameConnectionCloseTransport)
658+
return ok && af.code == bf.code
659+
}
660+
return reflect.DeepEqual(a, b)
661+
}
662+
621663
// wantFrameType indicates that we expect the Conn to send a frame,
622664
// although we don't care about the contents.
623665
func (tc *testConn) wantFrameType(expectation string, wantType packetType, want debugFrame) {

internal/quic/crypto_stream.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ type cryptoStream struct {
3030
func (s *cryptoStream) handleCrypto(off int64, b []byte, f func([]byte) error) error {
3131
end := off + int64(len(b))
3232
if end-s.inset.min() > cryptoBufferSize {
33-
return localTransportError(errCryptoBufferExceeded)
33+
return localTransportError{
34+
code: errCryptoBufferExceeded,
35+
reason: "crypto buffer exceeded",
36+
}
3437
}
3538
s.inset.add(off, end)
3639
if off == s.in.start {

internal/quic/errors.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,16 @@ func (e transportError) String() string {
8383
}
8484

8585
// A localTransportError is an error sent to the peer.
86-
type localTransportError transportError
86+
type localTransportError struct {
87+
code transportError
88+
reason string
89+
}
8790

8891
func (e localTransportError) Error() string {
89-
return "closed connection: " + transportError(e).String()
92+
if e.reason == "" {
93+
return fmt.Sprintf("closed connection: %v", e.code)
94+
}
95+
return fmt.Sprintf("closed connection: %v: %q", e.code, e.reason)
9096
}
9197

9298
// A peerTransportError is an error received from the peer.

internal/quic/listener.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ func (l *Listener) Close(ctx context.Context) error {
107107
if !l.closing {
108108
l.closing = true
109109
for c := range l.conns {
110-
c.Abort(localTransportError(errNo))
110+
c.Abort(localTransportError{code: errNo})
111111
}
112112
if len(l.conns) == 0 {
113113
l.udpConn.Close()

0 commit comments

Comments
 (0)