From 5d7ee61f5fd18b925a96cdf90888dfc9d4689e4a Mon Sep 17 00:00:00 2001 From: Marten Seemann Date: Wed, 28 May 2025 12:30:02 +0800 Subject: [PATCH] wire: add support for the reset_stream_at transport parameter (#5158) --- internal/wire/transport_parameter_test.go | 20 ++++++++++++-- internal/wire/transport_parameters.go | 32 +++++++++++++++++++---- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/internal/wire/transport_parameter_test.go b/internal/wire/transport_parameter_test.go index 2970cdbc8..040937a43 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) { diff --git a/internal/wire/transport_parameters.go b/internal/wire/transport_parameters.go index d39189a0d..2437892a0 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...) }