http3: send SETTINGS_MAX_FIELD_SECTION_SIZE in the SETTINGS frame (#5431)

* http3/qlog: implement qlogging of SETTINGS_MAX_FIELD_SECTION_SIZE

* http3: send SETTINGS_MAX_FIELD_SECTION_SIZE in the SETTINGS frame
This commit is contained in:
Marten Seemann
2025-11-16 21:45:27 +08:00
committed by GitHub
parent e46470d68f
commit be2a6229c4
8 changed files with 141 additions and 29 deletions

View File

@@ -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)

View File

@@ -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{}),

View File

@@ -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{}),

View File

@@ -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)

View File

@@ -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)

View File

@@ -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"))

View File

@@ -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{{

View File

@@ -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)