Repository: quic-go/quic-go
Pull request #5158 (2025-05-28 03:56:57)

wire: add support for the reset_stream_at transport parameter
For #4139.


Patch:
diff --git a/internal/wire/transport_parameters.go b/internal/wire/transport_parameters.go
index d39189a0..2437892a 100644
--- a/internal/wire/transport_parameters.go
+++ b/internal/wire/transport_parameters.go
@@ -45,6 +45,8 @@ const (
 	retrySourceConnectionIDParameterID         transportParameterID = 0x10
 	// RFC 9221
 	maxDatagramFrameSizeParameterID transportParameterID = 0x20
+	// https://datatracker.ietf.org/doc/draft-ietf-quic-reliable-stream-reset/06/
+	resetStreamAtParameterID transportParameterID = 0x17f7586d2cb571
 )
 
 // PreferredAddress is the value encoding in the preferred_address transport parameter
@@ -82,7 +84,8 @@ type TransportParameters struct {
 	StatelessResetToken     *protocol.StatelessResetToken
 	ActiveConnectionIDLimit uint64
 
-	MaxDatagramFrameSize protocol.ByteCount
+	MaxDatagramFrameSize protocol.ByteCount // RFC 9221
+	EnableResetStreamAt  bool               // https://datatracker.ietf.org/doc/draft-ietf-quic-reliable-stream-reset/06/
 }
 
 // Unmarshal the transport parameters
@@ -199,6 +202,11 @@ func (p *TransportParameters) unmarshal(b []byte, sentBy protocol.Perspective, f
 			connID := protocol.ParseConnectionID(b[:paramLen])
 			b = b[paramLen:]
 			p.RetrySourceConnectionID = &connID
+		case resetStreamAtParameterID:
+			if paramLen != 0 {
+				return fmt.Errorf("wrong length for reset_stream_at: %d (expected empty)", paramLen)
+			}
+			p.EnableResetStreamAt = true
 		default:
 			b = b[paramLen:]
 		}
@@ -428,9 +436,15 @@ func (p *TransportParameters) Marshal(pers protocol.Perspective) []byte {
 		b = quicvarint.Append(b, uint64(p.RetrySourceConnectionID.Len()))
 		b = append(b, p.RetrySourceConnectionID.Bytes()...)
 	}
+	// QUIC datagrams
 	if p.MaxDatagramFrameSize != protocol.InvalidByteCount {
 		b = p.marshalVarintParam(b, maxDatagramFrameSizeParameterID, uint64(p.MaxDatagramFrameSize))
 	}
+	// QUIC Stream Resets with Partial Delivery
+	if p.EnableResetStreamAt {
+		b = quicvarint.Append(b, uint64(resetStreamAtParameterID))
+		b = quicvarint.Append(b, 0)
+	}
 
 	if pers == protocol.PerspectiveClient && len(AdditionalTransportParametersClient) > 0 {
 		for k, v := range AdditionalTransportParametersClient {
@@ -472,12 +486,18 @@ func (p *TransportParameters) MarshalForSessionTicket(b []byte) []byte {
 	b = p.marshalVarintParam(b, initialMaxStreamsBidiParameterID, uint64(p.MaxBidiStreamNum))
 	// initial_max_uni_streams
 	b = p.marshalVarintParam(b, initialMaxStreamsUniParameterID, uint64(p.MaxUniStreamNum))
+	// active_connection_id_limit
+	b = p.marshalVarintParam(b, activeConnectionIDLimitParameterID, p.ActiveConnectionIDLimit)
 	// max_datagram_frame_size
 	if p.MaxDatagramFrameSize != protocol.InvalidByteCount {
 		b = p.marshalVarintParam(b, maxDatagramFrameSizeParameterID, uint64(p.MaxDatagramFrameSize))
 	}
-	// active_connection_id_limit
-	return p.marshalVarintParam(b, activeConnectionIDLimitParameterID, p.ActiveConnectionIDLimit)
+	// reset_stream_at
+	if p.EnableResetStreamAt {
+		b = quicvarint.Append(b, uint64(resetStreamAtParameterID))
+		b = quicvarint.Append(b, 0)
+	}
+	return b
 }
 
 // UnmarshalFromSessionTicket unmarshals transport parameters from a session ticket.
@@ -524,13 +544,13 @@ func (p *TransportParameters) ValidForUpdate(saved *TransportParameters) bool {
 // String returns a string representation, intended for logging.
 func (p *TransportParameters) String() string {
 	logString := "&wire.TransportParameters{OriginalDestinationConnectionID: %s, InitialSourceConnectionID: %s, "
-	logParams := []interface{}{p.OriginalDestinationConnectionID, p.InitialSourceConnectionID}
+	logParams := []any{p.OriginalDestinationConnectionID, p.InitialSourceConnectionID}
 	if p.RetrySourceConnectionID != nil {
 		logString += "RetrySourceConnectionID: %s, "
 		logParams = append(logParams, p.RetrySourceConnectionID)
 	}
 	logString += "InitialMaxStreamDataBidiLocal: %d, InitialMaxStreamDataBidiRemote: %d, InitialMaxStreamDataUni: %d, InitialMaxData: %d, MaxBidiStreamNum: %d, MaxUniStreamNum: %d, MaxIdleTimeout: %s, AckDelayExponent: %d, MaxAckDelay: %s, ActiveConnectionIDLimit: %d"
-	logParams = append(logParams, []interface{}{p.InitialMaxStreamDataBidiLocal, p.InitialMaxStreamDataBidiRemote, p.InitialMaxStreamDataUni, p.InitialMaxData, p.MaxBidiStreamNum, p.MaxUniStreamNum, p.MaxIdleTimeout, p.AckDelayExponent, p.MaxAckDelay, p.ActiveConnectionIDLimit}...)
+	logParams = append(logParams, []any{p.InitialMaxStreamDataBidiLocal, p.InitialMaxStreamDataBidiRemote, p.InitialMaxStreamDataUni, p.InitialMaxData, p.MaxBidiStreamNum, p.MaxUniStreamNum, p.MaxIdleTimeout, p.AckDelayExponent, p.MaxAckDelay, p.ActiveConnectionIDLimit}...)
 	if p.StatelessResetToken != nil { // the client never sends a stateless reset token
 		logString += ", StatelessResetToken: %#x"
 		logParams = append(logParams, *p.StatelessResetToken)
@@ -539,6 +559,8 @@ func (p *TransportParameters) String() string {
 		logString += ", MaxDatagramFrameSize: %d"
 		logParams = append(logParams, p.MaxDatagramFrameSize)
 	}
+	logString += ", EnableResetStreamAt: %t"
+	logParams = append(logParams, p.EnableResetStreamAt)
 	logString += "}"
 	return fmt.Sprintf(logString, logParams...)
 }


Test patch:
diff --git a/internal/wire/transport_parameter_test.go b/internal/wire/transport_parameter_test.go
index 2970cdbc..040937a4 100644
--- a/internal/wire/transport_parameter_test.go
+++ b/internal/wire/transport_parameter_test.go
@@ -48,8 +48,9 @@ func TestTransportParametersStringRepresentation(t *testing.T) {
 		StatelessResetToken:             &protocol.StatelessResetToken{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00},
 		ActiveConnectionIDLimit:         123,
 		MaxDatagramFrameSize:            876,
+		EnableResetStreamAt:             true,
 	}
-	expected := "&wire.TransportParameters{OriginalDestinationConnectionID: deadbeef, InitialSourceConnectionID: decafbad, RetrySourceConnectionID: deadc0de, InitialMaxStreamDataBidiLocal: 1234, InitialMaxStreamDataBidiRemote: 2345, InitialMaxStreamDataUni: 3456, InitialMaxData: 4567, MaxBidiStreamNum: 1337, MaxUniStreamNum: 7331, MaxIdleTimeout: 42s, AckDelayExponent: 14, MaxAckDelay: 37ms, ActiveConnectionIDLimit: 123, StatelessResetToken: 0x112233445566778899aabbccddeeff00, MaxDatagramFrameSize: 876}"
+	expected := "&wire.TransportParameters{OriginalDestinationConnectionID: deadbeef, InitialSourceConnectionID: decafbad, RetrySourceConnectionID: deadc0de, InitialMaxStreamDataBidiLocal: 1234, InitialMaxStreamDataBidiRemote: 2345, InitialMaxStreamDataUni: 3456, InitialMaxData: 4567, MaxBidiStreamNum: 1337, MaxUniStreamNum: 7331, MaxIdleTimeout: 42s, AckDelayExponent: 14, MaxAckDelay: 37ms, ActiveConnectionIDLimit: 123, StatelessResetToken: 0x112233445566778899aabbccddeeff00, MaxDatagramFrameSize: 876, EnableResetStreamAt: true}"
 	require.Equal(t, expected, p.String())
 }
 
@@ -69,7 +70,7 @@ func TestTransportParametersStringRepresentationWithoutOptionalFields(t *testing
 		ActiveConnectionIDLimit:         89,
 		MaxDatagramFrameSize:            protocol.InvalidByteCount,
 	}
-	expected := "&wire.TransportParameters{OriginalDestinationConnectionID: deadbeef, InitialSourceConnectionID: (empty), InitialMaxStreamDataBidiLocal: 1234, InitialMaxStreamDataBidiRemote: 2345, InitialMaxStreamDataUni: 3456, InitialMaxData: 4567, MaxBidiStreamNum: 1337, MaxUniStreamNum: 7331, MaxIdleTimeout: 42s, AckDelayExponent: 14, MaxAckDelay: 37s, ActiveConnectionIDLimit: 89}"
+	expected := "&wire.TransportParameters{OriginalDestinationConnectionID: deadbeef, InitialSourceConnectionID: (empty), InitialMaxStreamDataBidiLocal: 1234, InitialMaxStreamDataBidiRemote: 2345, InitialMaxStreamDataUni: 3456, InitialMaxData: 4567, MaxBidiStreamNum: 1337, MaxUniStreamNum: 7331, MaxIdleTimeout: 42s, AckDelayExponent: 14, MaxAckDelay: 37s, ActiveConnectionIDLimit: 89, EnableResetStreamAt: false}"
 	require.Equal(t, expected, p.String())
 }
 
@@ -95,6 +96,7 @@ func TestMarshalAndUnmarshalTransportParameters(t *testing.T) {
 		ActiveConnectionIDLimit:         2 + getRandomValueUpTo(quicvarint.Max-2),
 		MaxUDPPayloadSize:               1200 + protocol.ByteCount(getRandomValueUpTo(quicvarint.Max-1200)),
 		MaxDatagramFrameSize:            protocol.ByteCount(getRandomValue()),
+		EnableResetStreamAt:             getRandomValue()%2 == 0,
 	}
 	data := params.Marshal(protocol.PerspectiveServer)
 
@@ -117,6 +119,7 @@ func TestMarshalAndUnmarshalTransportParameters(t *testing.T) {
 	require.Equal(t, params.ActiveConnectionIDLimit, p.ActiveConnectionIDLimit)
 	require.Equal(t, params.MaxUDPPayloadSize, p.MaxUDPPayloadSize)
 	require.Equal(t, params.MaxDatagramFrameSize, p.MaxDatagramFrameSize)
+	require.Equal(t, params.EnableResetStreamAt, p.EnableResetStreamAt)
 }
 
 func TestMarshalAdditionalTransportParameters(t *testing.T) {
@@ -369,6 +372,17 @@ func TestTransportParameterErrors(t *testing.T) {
 			perspective:    protocol.PerspectiveClient,
 			expectedErrMsg: "invalid value for max_ack_delay: 3689348814741910323ms (maximum 16383ms)",
 		},
+		{
+			name: "invalid value for reset_stream_at",
+			data: func() []byte {
+				b := quicvarint.Append(nil, uint64(resetStreamAtParameterID))
+				b = quicvarint.Append(b, 1)
+				b = quicvarint.Append(b, 1)
+				return appendInitialSourceConnectionID(b)
+			}(),
+			perspective:    protocol.PerspectiveClient,
+			expectedErrMsg: "wrong length for reset_stream_at: 1 (expected empty)",
+		},
 	}
 
 	for _, tt := range tests {
@@ -550,6 +564,7 @@ func TestTransportParametersFromSessionTicket(t *testing.T) {
 		MaxUniStreamNum:                protocol.StreamNum(getRandomValueUpTo(uint64(protocol.MaxStreamCount))),
 		ActiveConnectionIDLimit:        2 + getRandomValueUpTo(quicvarint.Max-2),
 		MaxDatagramFrameSize:           protocol.ByteCount(getRandomValueUpTo(uint64(MaxDatagramSize))),
+		EnableResetStreamAt:            getRandomValue()%2 == 0,
 	}
 	require.True(t, params.ValidFor0RTT(params))
 	b := params.MarshalForSessionTicket(nil)
@@ -563,6 +578,7 @@ func TestTransportParametersFromSessionTicket(t *testing.T) {
 	require.Equal(t, params.MaxUniStreamNum, tp.MaxUniStreamNum)
 	require.Equal(t, params.ActiveConnectionIDLimit, tp.ActiveConnectionIDLimit)
 	require.Equal(t, params.MaxDatagramFrameSize, tp.MaxDatagramFrameSize)
+	require.Equal(t, params.EnableResetStreamAt, tp.EnableResetStreamAt)
 }
 
 func TestSessionTicketInvalidTransportParameters(t *testing.T) {