diff --git a/http3/client.go b/http3/client.go index 1a11e0250..5bf477fd4 100644 --- a/http3/client.go +++ b/http3/client.go @@ -137,10 +137,15 @@ func (c *ClientConn) setupConn() error { b := make([]byte, 0, 64) b = quicvarint.Append(b, streamTypeControlStream) // send the SETTINGS frame - b = (&settingsFrame{Datagram: c.enableDatagrams, Other: c.additionalSettings}).Append(b) + b = (&settingsFrame{ + Datagram: c.enableDatagrams, + Other: c.additionalSettings, + MaxFieldSectionSize: int64(c.maxResponseHeaderBytes), + }).Append(b) if c.conn.qlogger != nil { sf := qlog.SettingsFrame{ - Other: maps.Clone(c.additionalSettings), + MaxFieldSectionSize: int64(c.maxResponseHeaderBytes), + Other: maps.Clone(c.additionalSettings), } if c.enableDatagrams { sf.Datagram = pointer(true) diff --git a/http3/client_test.go b/http3/client_test.go index 007d1dbc7..28f5bfd27 100644 --- a/http3/client_test.go +++ b/http3/client_test.go @@ -67,8 +67,14 @@ func testClientSettings(t *testing.T, enableDatagrams bool, other map[uint64]uin []qlogwriter.Event{ qlog.FrameCreated{ StreamID: str.StreamID(), - Raw: qlog.RawInfo{Length: 5}, - Frame: qlog.Frame{Frame: qlog.SettingsFrame{Datagram: datagramValue, Other: other}}, + Raw: qlog.RawInfo{Length: 10}, + Frame: qlog.Frame{ + Frame: qlog.SettingsFrame{ + MaxFieldSectionSize: defaultMaxResponseHeaderBytes, + Datagram: datagramValue, + Other: other, + }, + }, }, }, filterQlogEventsForFrame(eventRecorder.Events(qlog.FrameCreated{}), qlog.SettingsFrame{}), diff --git a/http3/conn_test.go b/http3/conn_test.go index c848f0b10..accc3948d 100644 --- a/http3/conn_test.go +++ b/http3/conn_test.go @@ -30,9 +30,10 @@ func TestConnReceiveSettings(t *testing.T) { ) b := quicvarint.Append(nil, streamTypeControlStream) sf := &settingsFrame{ - Datagram: true, - ExtendedConnect: true, - Other: map[uint64]uint64{1337: 42}, + MaxFieldSectionSize: 1234, + Datagram: true, + ExtendedConnect: true, + Other: map[uint64]uint64{1337: 42}, } b = sf.Append(b) controlStr, err := clientConn.OpenUniStream() @@ -61,7 +62,14 @@ func TestConnReceiveSettings(t *testing.T) { qlog.FrameParsed{ StreamID: controlStr.StreamID(), Raw: qlog.RawInfo{Length: expectedLen, PayloadLength: expectedPayloadLen}, - Frame: qlog.Frame{Frame: qlog.SettingsFrame{Datagram: pointer(true), ExtendedConnect: pointer(true), Other: map[uint64]uint64{1337: 42}}}, + Frame: qlog.Frame{ + Frame: qlog.SettingsFrame{ + MaxFieldSectionSize: 1234, + Datagram: pointer(true), + ExtendedConnect: pointer(true), + Other: map[uint64]uint64{1337: 42}, + }, + }, }, }, filterQlogEventsForFrame(eventRecorder.Events(qlog.FrameParsed{}), qlog.SettingsFrame{}), diff --git a/http3/frames.go b/http3/frames.go index 4b46fb1b0..879a9f1b2 100644 --- a/http3/frames.go +++ b/http3/frames.go @@ -179,6 +179,8 @@ func (f *headersFrame) Append(b []byte) []byte { } const ( + // SETTINGS_MAX_FIELD_SECTION_SIZE + settingMaxFieldSectionSize = 0x6 // Extended CONNECT, RFC 9220 settingExtendedConnect = 0x8 // HTTP Datagrams, RFC 9297 @@ -186,10 +188,11 @@ const ( ) type settingsFrame struct { - Datagram bool // HTTP Datagrams, RFC 9297 - ExtendedConnect bool // Extended CONNECT, RFC 9220 + MaxFieldSectionSize int64 // SETTINGS_MAX_FIELD_SECTION_SIZE, -1 if not set - Other map[uint64]uint64 // all settings that we don't explicitly recognize + Datagram bool // HTTP Datagrams, RFC 9297 + ExtendedConnect bool // Extended CONNECT, RFC 9220 + Other map[uint64]uint64 // all settings that we don't explicitly recognize } func pointer[T any](v T) *T { @@ -207,10 +210,10 @@ func parseSettingsFrame(r *countingByteReader, l uint64, streamID quic.StreamID, } return nil, err } - frame := &settingsFrame{} + frame := &settingsFrame{MaxFieldSectionSize: -1} b := bytes.NewReader(buf) - var settingsFrame qlog.SettingsFrame - var readDatagram, readExtendedConnect bool + settingsFrame := qlog.SettingsFrame{MaxFieldSectionSize: -1} + var readMaxFieldSectionSize, readDatagram, readExtendedConnect bool for b.Len() > 0 { id, err := quicvarint.Read(b) if err != nil { // should not happen. We allocated the whole frame already. @@ -222,6 +225,13 @@ func parseSettingsFrame(r *countingByteReader, l uint64, streamID quic.StreamID, } switch id { + case settingMaxFieldSectionSize: + if readMaxFieldSectionSize { + return nil, fmt.Errorf("duplicate setting: %d", id) + } + readMaxFieldSectionSize = true + frame.MaxFieldSectionSize = int64(val) + settingsFrame.MaxFieldSectionSize = int64(val) case settingExtendedConnect: if readExtendedConnect { return nil, fmt.Errorf("duplicate setting: %d", id) @@ -274,6 +284,9 @@ func parseSettingsFrame(r *countingByteReader, l uint64, streamID quic.StreamID, func (f *settingsFrame) Append(b []byte) []byte { b = quicvarint.Append(b, 0x4) var l int + if f.MaxFieldSectionSize >= 0 { + l += quicvarint.Len(settingMaxFieldSectionSize) + quicvarint.Len(uint64(f.MaxFieldSectionSize)) + } for id, val := range f.Other { l += quicvarint.Len(id) + quicvarint.Len(val) } @@ -284,6 +297,10 @@ func (f *settingsFrame) Append(b []byte) []byte { l += quicvarint.Len(settingExtendedConnect) + quicvarint.Len(1) } b = quicvarint.Append(b, uint64(l)) + if f.MaxFieldSectionSize >= 0 { + b = quicvarint.Append(b, settingMaxFieldSectionSize) + b = quicvarint.Append(b, uint64(f.MaxFieldSectionSize)) + } if f.Datagram { b = quicvarint.Append(b, settingDatagram) b = quicvarint.Append(b, 1) diff --git a/http3/frames_test.go b/http3/frames_test.go index ee7501805..ce5425929 100644 --- a/http3/frames_test.go +++ b/http3/frames_test.go @@ -244,6 +244,11 @@ func TestParserSettingsFrameDuplicateSettings(t *testing.T) { num: settingExtendedConnect, val: 1, }, + { + name: "max field section size", + num: settingMaxFieldSectionSize, + val: 1337, + }, { name: "datagram", num: settingDatagram, @@ -264,6 +269,42 @@ func TestParserSettingsFrameDuplicateSettings(t *testing.T) { } } +func TestParserSettingsFrameMaxFieldSectionSize(t *testing.T) { + t.Run("absent", func(t *testing.T) { + testParserSettingsFrameMaxFieldSectionSize(t, false) + }) + + t.Run("with value", func(t *testing.T) { + testParserSettingsFrameMaxFieldSectionSize(t, true) + }) +} + +func testParserSettingsFrameMaxFieldSectionSize(t *testing.T, present bool) { + var settings []byte + if present { + settings = appendSetting(nil, settingMaxFieldSectionSize, 1337) + } + data := quicvarint.Append(nil, 4) // type byte + data = quicvarint.Append(data, uint64(len(settings))) + data = append(data, settings...) + + fp := frameParser{r: bytes.NewReader(data)} + f, err := fp.ParseNext(nil) + require.NoError(t, err) + require.IsType(t, &settingsFrame{}, f) + sf := f.(*settingsFrame) + if present { + require.EqualValues(t, 1337, sf.MaxFieldSectionSize) + } else { + require.EqualValues(t, -1, sf.MaxFieldSectionSize) + } + + fp = frameParser{r: bytes.NewReader(sf.Append(nil))} + f2, err := fp.ParseNext(nil) + require.NoError(t, err) + require.Equal(t, sf, f2) +} + func TestParserSettingsFrameDatagram(t *testing.T) { t.Run("enabled", func(t *testing.T) { testParserSettingsFrameDatagram(t, true) diff --git a/http3/qlog/frame.go b/http3/qlog/frame.go index 78959a934..b404453da 100644 --- a/http3/qlog/frame.go +++ b/http3/qlog/frame.go @@ -97,9 +97,10 @@ func (f *GoAwayFrame) encode(enc *jsontext.Encoder) error { } type SettingsFrame struct { - Datagram *bool - ExtendedConnect *bool - Other map[uint64]uint64 + MaxFieldSectionSize int64 + Datagram *bool + ExtendedConnect *bool + Other map[uint64]uint64 } func (f *SettingsFrame) encode(enc *jsontext.Encoder) error { @@ -109,6 +110,14 @@ func (f *SettingsFrame) encode(enc *jsontext.Encoder) error { h.WriteToken(jsontext.String("settings")) h.WriteToken(jsontext.String("settings")) h.WriteToken(jsontext.BeginArray) + if f.MaxFieldSectionSize >= 0 { + h.WriteToken(jsontext.BeginObject) + h.WriteToken(jsontext.String("name")) + h.WriteToken(jsontext.String("settings_max_field_section_size")) + h.WriteToken(jsontext.String("value")) + h.WriteToken(jsontext.Uint(uint64(f.MaxFieldSectionSize))) + h.WriteToken(jsontext.EndObject) + } if f.Datagram != nil { h.WriteToken(jsontext.BeginObject) h.WriteToken(jsontext.String("name")) diff --git a/http3/qlog/frame_test.go b/http3/qlog/frame_test.go index ab1bfaa44..a1bafbc0b 100644 --- a/http3/qlog/frame_test.go +++ b/http3/qlog/frame_test.go @@ -91,8 +91,11 @@ func TestSettingsFrame(t *testing.T) { expected map[string]any }{ { - name: "datagram: true", - frame: SettingsFrame{Datagram: pointer(true)}, + name: "datagram: true", + frame: SettingsFrame{ + MaxFieldSectionSize: -1, + Datagram: pointer(true), + }, expected: map[string]any{ "frame_type": "settings", "settings": []map[string]any{{ @@ -102,8 +105,11 @@ func TestSettingsFrame(t *testing.T) { }, }, { - name: "extended_connect: false", - frame: SettingsFrame{ExtendedConnect: pointer(false)}, + name: "extended_connect: false", + frame: SettingsFrame{ + MaxFieldSectionSize: -1, + ExtendedConnect: pointer(false), + }, expected: map[string]any{ "frame_type": "settings", "settings": []map[string]any{{ @@ -113,8 +119,23 @@ func TestSettingsFrame(t *testing.T) { }, }, { - name: "datagram: false, extended_connect: false", - frame: SettingsFrame{Datagram: pointer(false), ExtendedConnect: pointer(false)}, + name: "max_field_section_size", + frame: SettingsFrame{MaxFieldSectionSize: 1337}, + expected: map[string]any{ + "frame_type": "settings", + "settings": []map[string]any{{ + "name": "settings_max_field_section_size", + "value": float64(1337), + }}, + }, + }, + { + name: "datagram: false, extended_connect: false", + frame: SettingsFrame{ + MaxFieldSectionSize: -1, + Datagram: pointer(false), + ExtendedConnect: pointer(false), + }, expected: map[string]any{ "frame_type": "settings", "settings": []map[string]any{ @@ -128,7 +149,10 @@ func TestSettingsFrame(t *testing.T) { // Only test a single unknown setting. // Testing multiple unknown settings doesn't add a lot of value, // and would require us to deal with non-deterministic map iteration order. - frame: SettingsFrame{Other: map[uint64]uint64{0xdead: 0xbeef}}, + frame: SettingsFrame{ + MaxFieldSectionSize: -1, + Other: map[uint64]uint64{0xdead: 0xbeef}, + }, expected: map[string]any{ "frame_type": "settings", "settings": []map[string]any{{ diff --git a/http3/server.go b/http3/server.go index 3d42016ed..b377a4d0f 100644 --- a/http3/server.go +++ b/http3/server.go @@ -454,14 +454,16 @@ func (s *Server) handleConn(conn *quic.Conn) error { b := make([]byte, 0, 64) b = quicvarint.Append(b, streamTypeControlStream) // stream type b = (&settingsFrame{ - Datagram: s.EnableDatagrams, - ExtendedConnect: true, - Other: s.AdditionalSettings, + MaxFieldSectionSize: int64(s.maxHeaderBytes()), + Datagram: s.EnableDatagrams, + ExtendedConnect: true, + Other: s.AdditionalSettings, }).Append(b) if qlogger != nil { sf := qlog.SettingsFrame{ - ExtendedConnect: pointer(true), - Other: maps.Clone(s.AdditionalSettings), + MaxFieldSectionSize: int64(s.maxHeaderBytes()), + ExtendedConnect: pointer(true), + Other: maps.Clone(s.AdditionalSettings), } if s.EnableDatagrams { sf.Datagram = pointer(true)