]> git.feebdaed.xyz Git - 0xmirror/quic-go.git/commitdiff
wire: add support for the min_ack_delay transport parameter (#5266)
authorMarten Seemann <martenseemann@gmail.com>
Thu, 24 Jul 2025 13:43:34 +0000 (15:43 +0200)
committerGitHub <noreply@github.com>
Thu, 24 Jul 2025 13:43:34 +0000 (15:43 +0200)
* wire: implement parsing and writing of the min_ack_delay transport parameter

* wire: validate the min_ack_delay transport parameter

fuzzing/transportparameters/fuzz.go
internal/wire/transport_parameter_test.go
internal/wire/transport_parameters.go

index 267979ce3b1d86b95863c2bc514378983f11d779..8bbe431afbd941c04a31af49925f90c7fd46b736 100644 (file)
@@ -93,5 +93,13 @@ func validateTransportParameters(tp *wire.TransportParameters, sentBy protocol.P
        if tp.PreferredAddress != nil && tp.PreferredAddress.ConnectionID.Len() > 20 {
                return fmt.Errorf("invalid preferred_address connection ID length: %s", tp.PreferredAddress.ConnectionID)
        }
+       if tp.MinAckDelay != nil {
+               if *tp.MinAckDelay < 0 {
+                       return fmt.Errorf("negative min_ack_delay: %s", *tp.MinAckDelay)
+               }
+               if *tp.MinAckDelay > tp.MaxAckDelay {
+                       return fmt.Errorf("min_ack_delay (%s) is greater than max_ack_delay (%s)", *tp.MinAckDelay, tp.MaxAckDelay)
+               }
+       }
        return nil
 }
index 040937a43d6b7378c0359a464d5c4d6ccc3057ec..c44561cbf082ceb4971753bdc02634ed76e4591f 100644 (file)
@@ -32,6 +32,7 @@ func appendInitialSourceConnectionID(b []byte) []byte {
 
 func TestTransportParametersStringRepresentation(t *testing.T) {
        rcid := protocol.ParseConnectionID([]byte{0xde, 0xad, 0xc0, 0xde})
+       minAckDelay := 42 * time.Millisecond
        p := &TransportParameters{
                InitialMaxStreamDataBidiLocal:   1234,
                InitialMaxStreamDataBidiRemote:  2345,
@@ -49,8 +50,9 @@ func TestTransportParametersStringRepresentation(t *testing.T) {
                ActiveConnectionIDLimit:         123,
                MaxDatagramFrameSize:            876,
                EnableResetStreamAt:             true,
+               MinAckDelay:                     &minAckDelay,
        }
-       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}"
+       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, MinAckDelay: 42ms}"
        require.Equal(t, expected, p.String())
 }
 
@@ -78,6 +80,7 @@ func TestMarshalAndUnmarshalTransportParameters(t *testing.T) {
        var token protocol.StatelessResetToken
        rand.Read(token[:])
        rcid := protocol.ParseConnectionID([]byte{0xde, 0xad, 0xc0, 0xde})
+       minAckDelay := 42 * time.Millisecond
        params := &TransportParameters{
                InitialMaxStreamDataBidiLocal:   protocol.ByteCount(getRandomValue()),
                InitialMaxStreamDataBidiRemote:  protocol.ByteCount(getRandomValue()),
@@ -97,6 +100,7 @@ func TestMarshalAndUnmarshalTransportParameters(t *testing.T) {
                MaxUDPPayloadSize:               1200 + protocol.ByteCount(getRandomValueUpTo(quicvarint.Max-1200)),
                MaxDatagramFrameSize:            protocol.ByteCount(getRandomValue()),
                EnableResetStreamAt:             getRandomValue()%2 == 0,
+               MinAckDelay:                     &minAckDelay,
        }
        data := params.Marshal(protocol.PerspectiveServer)
 
@@ -120,11 +124,15 @@ func TestMarshalAndUnmarshalTransportParameters(t *testing.T) {
        require.Equal(t, params.MaxUDPPayloadSize, p.MaxUDPPayloadSize)
        require.Equal(t, params.MaxDatagramFrameSize, p.MaxDatagramFrameSize)
        require.Equal(t, params.EnableResetStreamAt, p.EnableResetStreamAt)
+       require.NotNil(t, p.MinAckDelay)
+       require.Equal(t, minAckDelay, *p.MinAckDelay)
 }
 
 func TestMarshalAdditionalTransportParameters(t *testing.T) {
        origAdditionalTransportParametersClient := AdditionalTransportParametersClient
-       t.Cleanup(func() { AdditionalTransportParametersClient = origAdditionalTransportParametersClient })
+       t.Cleanup(func() {
+               AdditionalTransportParametersClient = origAdditionalTransportParametersClient
+       })
        AdditionalTransportParametersClient = map[uint64][]byte{1337: []byte("foobar")}
 
        result := quicvarint.Append([]byte{}, 1337)
@@ -136,24 +144,24 @@ func TestMarshalAdditionalTransportParameters(t *testing.T) {
        require.False(t, bytes.Contains(params.Marshal(protocol.PerspectiveServer), result))
 }
 
-func TestMarshalWithoutRetrySourceConnectionID(t *testing.T) {
+func TestMarshalRetrySourceConnectionID(t *testing.T) {
+       // no retry source connection ID
        data := (&TransportParameters{
                StatelessResetToken:     &protocol.StatelessResetToken{},
                ActiveConnectionIDLimit: 2,
        }).Marshal(protocol.PerspectiveServer)
-       p := &TransportParameters{}
+       var p TransportParameters
        require.NoError(t, p.Unmarshal(data, protocol.PerspectiveServer))
        require.Nil(t, p.RetrySourceConnectionID)
-}
 
-func TestMarshalZeroLengthRetrySourceConnectionID(t *testing.T) {
+       // zero-length retry source connection ID
        rcid := protocol.ParseConnectionID([]byte{})
-       data := (&TransportParameters{
+       data = (&TransportParameters{
                RetrySourceConnectionID: &rcid,
                StatelessResetToken:     &protocol.StatelessResetToken{},
                ActiveConnectionIDLimit: 2,
        }).Marshal(protocol.PerspectiveServer)
-       p := &TransportParameters{}
+       p TransportParameters{}
        require.NoError(t, p.Unmarshal(data, protocol.PerspectiveServer))
        require.NotNil(t, p.RetrySourceConnectionID)
        require.Zero(t, p.RetrySourceConnectionID.Len())
@@ -163,7 +171,7 @@ func TestTransportParameterNoMaxAckDelayIfDefault(t *testing.T) {
        const num = 1000
        var defaultLen, dataLen int
        maxAckDelay := protocol.DefaultMaxAckDelay + time.Millisecond
-       for i := 0; i < num; i++ {
+       for range num {
                dataDefault := (&TransportParameters{
                        MaxAckDelay:         protocol.DefaultMaxAckDelay,
                        StatelessResetToken: &protocol.StatelessResetToken{},
@@ -184,7 +192,7 @@ func TestTransportParameterNoMaxAckDelayIfDefault(t *testing.T) {
 func TestTransportParameterNoAckDelayExponentIfDefault(t *testing.T) {
        const num = 1000
        var defaultLen, dataLen int
-       for i := 0; i < num; i++ {
+       for range num {
                dataDefault := (&TransportParameters{
                        AckDelayExponent:    protocol.DefaultAckDelayExponent,
                        StatelessResetToken: &protocol.StatelessResetToken{},
@@ -383,6 +391,34 @@ func TestTransportParameterErrors(t *testing.T) {
                        perspective:    protocol.PerspectiveClient,
                        expectedErrMsg: "wrong length for reset_stream_at: 1 (expected empty)",
                },
+               {
+                       name: "min ack delay is greater than max ack delay",
+                       data: func() []byte {
+                               b := quicvarint.Append(nil, uint64(minAckDelayParameterID))
+                               b = quicvarint.Append(b, uint64(quicvarint.Len(42001)))
+                               b = quicvarint.Append(b, 42001) // 42001 microseconds
+                               b = quicvarint.Append(b, uint64(maxAckDelayParameterID))
+                               b = quicvarint.Append(b, uint64(quicvarint.Len(42)))
+                               b = quicvarint.Append(b, 42) // 42 microseconds
+                               return appendInitialSourceConnectionID(b)
+                       }(),
+                       perspective:    protocol.PerspectiveClient,
+                       expectedErrMsg: "min_ack_delay (42.001ms) is greater than max_ack_delay (42ms)",
+               },
+               {
+                       name: "huge min ack delay value",
+                       data: func() []byte {
+                               b := quicvarint.Append(nil, uint64(minAckDelayParameterID))
+                               b = quicvarint.Append(b, uint64(quicvarint.Len(quicvarint.Max)))
+                               b = quicvarint.Append(b, quicvarint.Max)
+                               b = quicvarint.Append(b, uint64(maxAckDelayParameterID))
+                               b = quicvarint.Append(b, uint64(quicvarint.Len(42)))
+                               b = quicvarint.Append(b, 42) // 42 microseconds
+                               return appendInitialSourceConnectionID(b)
+                       }(),
+                       perspective:    protocol.PerspectiveClient,
+                       expectedErrMsg: "min_ack_delay (2562047h47m16.854775807s) is greater than max_ack_delay (42ms)",
+               },
        }
 
        for _, tt := range tests {
index 2437892a0d11ae055a33a9c4833fc87c60d67771..ff101b6e751fdda1237ae90428665ac9ca2946e1 100644 (file)
@@ -6,6 +6,7 @@ import (
        "errors"
        "fmt"
        "io"
+       "math"
        "net/netip"
        "slices"
        "time"
@@ -47,6 +48,8 @@ const (
        maxDatagramFrameSizeParameterID transportParameterID = 0x20
        // https://datatracker.ietf.org/doc/draft-ietf-quic-reliable-stream-reset/06/
        resetStreamAtParameterID transportParameterID = 0x17f7586d2cb571
+       // https://datatracker.ietf.org/doc/draft-ietf-quic-ack-frequency/11/
+       minAckDelayParameterID transportParameterID = 0xff04de1b
 )
 
 // PreferredAddress is the value encoding in the preferred_address transport parameter
@@ -86,6 +89,7 @@ type TransportParameters struct {
 
        MaxDatagramFrameSize protocol.ByteCount // RFC 9221
        EnableResetStreamAt  bool               // https://datatracker.ietf.org/doc/draft-ietf-quic-reliable-stream-reset/06/
+       MinAckDelay          *time.Duration
 }
 
 // Unmarshal the transport parameters
@@ -106,12 +110,12 @@ func (p *TransportParameters) unmarshal(b []byte, sentBy protocol.Perspective, f
        var (
                readOriginalDestinationConnectionID bool
                readInitialSourceConnectionID       bool
-               readActiveConnectionIDLimit         bool
        )
 
        p.AckDelayExponent = protocol.DefaultAckDelayExponent
        p.MaxAckDelay = protocol.DefaultMaxAckDelay
        p.MaxDatagramFrameSize = protocol.InvalidByteCount
+       p.ActiveConnectionIDLimit = protocol.DefaultActiveConnectionIDLimit
 
        for len(b) > 0 {
                paramIDInt, l, err := quicvarint.Parse(b)
@@ -130,9 +134,6 @@ func (p *TransportParameters) unmarshal(b []byte, sentBy protocol.Perspective, f
                }
                parameterIDs = append(parameterIDs, paramID)
                switch paramID {
-               case activeConnectionIDLimitParameterID:
-                       readActiveConnectionIDLimit = true
-                       fallthrough
                case maxIdleTimeoutParameterID,
                        maxUDPPayloadSizeParameterID,
                        initialMaxDataParameterID,
@@ -143,7 +144,9 @@ func (p *TransportParameters) unmarshal(b []byte, sentBy protocol.Perspective, f
                        initialMaxStreamsUniParameterID,
                        maxAckDelayParameterID,
                        maxDatagramFrameSizeParameterID,
-                       ackDelayExponentParameterID:
+                       ackDelayExponentParameterID,
+                       activeConnectionIDLimitParameterID,
+                       minAckDelayParameterID:
                        if err := p.readNumericTransportParameter(b, paramID, int(paramLen)); err != nil {
                                return err
                        }
@@ -212,8 +215,9 @@ func (p *TransportParameters) unmarshal(b []byte, sentBy protocol.Perspective, f
                }
        }
 
-       if !readActiveConnectionIDLimit {
-               p.ActiveConnectionIDLimit = protocol.DefaultActiveConnectionIDLimit
+       // min_ack_delay must be less or equal to max_ack_delay
+       if p.MinAckDelay != nil && *p.MinAckDelay > p.MaxAckDelay {
+               return fmt.Errorf("min_ack_delay (%s) is greater than max_ack_delay (%s)", *p.MinAckDelay, p.MaxAckDelay)
        }
        if !fromSessionTicket {
                if sentBy == protocol.PerspectiveServer && !readOriginalDestinationConnectionID {
@@ -334,6 +338,12 @@ func (p *TransportParameters) readNumericTransportParameter(b []byte, paramID tr
                p.ActiveConnectionIDLimit = val
        case maxDatagramFrameSizeParameterID:
                p.MaxDatagramFrameSize = protocol.ByteCount(val)
+       case minAckDelayParameterID:
+               mad := time.Duration(val) * time.Microsecond
+               if mad < 0 {
+                       mad = math.MaxInt64
+               }
+               p.MinAckDelay = &mad
        default:
                return fmt.Errorf("TransportParameter BUG: transport parameter %d not found", paramID)
        }
@@ -445,6 +455,9 @@ func (p *TransportParameters) Marshal(pers protocol.Perspective) []byte {
                b = quicvarint.Append(b, uint64(resetStreamAtParameterID))
                b = quicvarint.Append(b, 0)
        }
+       if p.MinAckDelay != nil {
+               b = p.marshalVarintParam(b, minAckDelayParameterID, uint64(*p.MinAckDelay/time.Microsecond))
+       }
 
        if pers == protocol.PerspectiveClient && len(AdditionalTransportParametersClient) > 0 {
                for k, v := range AdditionalTransportParametersClient {
@@ -561,6 +574,10 @@ func (p *TransportParameters) String() string {
        }
        logString += ", EnableResetStreamAt: %t"
        logParams = append(logParams, p.EnableResetStreamAt)
+       if p.MinAckDelay != nil {
+               logString += ", MinAckDelay: %s"
+               logParams = append(logParams, *p.MinAckDelay)
+       }
        logString += "}"
        return fmt.Sprintf(logString, logParams...)
 }