quic-go v0.59.0 (shipped with Caddy v2.11.2) removed quic.Connection as a public interface and quic-go/logging as a public package, breaking H3App's connection-wrapping approach. Resolution: - Remove H3App (h3app.go) entirely; Caddy handles h3 natively when h3 is in the protocols list. - Rewrite h3conn.go to keep only tryParseH3ControlStream + varint/name utilities (tested, useful for future stream-level tapping if the API ever re-exposes it). - FPHandler.ServeHTTP: for h3 requests, type-assert ResponseWriter to http3.Settingser (the public interface exposed by quic-go/http3 v0.59), read the peer's Settings after ReceivedSettings channel closes, emit h3_settings fp record. - https/entrypoint.sh: include h3 in CADDY_PROTOCOLS (Caddy now owns UDP/443); remove DECNET_H3_GLOBAL block. - Update go.mod/go.sum to caddy v2.11.2 + quic-go v0.59.0. - Update test_https_compose_h3_app.py to expect h3 in protocols when http/3 is selected, and assert decnet_h3 block is absent. - All Go tests (9) and Python tests (15) remain green.
118 lines
2.7 KiB
Go
118 lines
2.7 KiB
Go
package decnetfp
|
|
|
|
import (
|
|
"time"
|
|
)
|
|
|
|
// tryParseH3ControlStream examines raw bytes from the beginning of an h3
|
|
// unidirectional stream. If the stream opens with stream-type 0x00 (h3
|
|
// control stream, RFC 9114 §6.2.1) and the first frame is SETTINGS
|
|
// (type 0x04), it emits an h3_settings fp record. All errors are silent.
|
|
func tryParseH3ControlStream(remoteAddr string, data []byte) {
|
|
streamType, c0 := quicVarint(data)
|
|
if c0 == 0 || streamType != 0x00 {
|
|
return // not the h3 control stream
|
|
}
|
|
data = data[c0:]
|
|
|
|
frameType, c1 := quicVarint(data)
|
|
if c1 == 0 {
|
|
return
|
|
}
|
|
data = data[c1:]
|
|
|
|
frameLen, c2 := quicVarint(data)
|
|
if c2 == 0 {
|
|
return
|
|
}
|
|
data = data[c2:]
|
|
|
|
// Per RFC 9114 §7.2.4: the first frame on the control stream MUST be SETTINGS.
|
|
if frameType != 0x04 {
|
|
return
|
|
}
|
|
if uint64(len(data)) < frameLen {
|
|
return // truncated
|
|
}
|
|
body := data[:frameLen]
|
|
|
|
settings := make(map[string]uint64)
|
|
frameOrder := make([]uint64, 0, 8)
|
|
for len(body) > 0 {
|
|
id, ci := quicVarint(body)
|
|
if ci == 0 {
|
|
break
|
|
}
|
|
body = body[ci:]
|
|
val, cv := quicVarint(body)
|
|
if cv == 0 {
|
|
break
|
|
}
|
|
body = body[cv:]
|
|
settings[h3SettingName(id)] = val
|
|
frameOrder = append(frameOrder, id)
|
|
}
|
|
|
|
sendFP(map[string]interface{}{
|
|
"kind": "h3_settings",
|
|
"remote_addr": remoteAddr,
|
|
"settings": settings,
|
|
"frame_order": frameOrder,
|
|
"ts": time.Now().UTC().Format(time.RFC3339),
|
|
})
|
|
}
|
|
|
|
// quicVarint decodes an RFC 9000 §16 variable-length integer.
|
|
// Returns (value, bytes_consumed); bytes_consumed == 0 on failure.
|
|
func quicVarint(b []byte) (uint64, int) {
|
|
if len(b) == 0 {
|
|
return 0, 0
|
|
}
|
|
prefix := b[0] >> 6
|
|
switch prefix {
|
|
case 0:
|
|
return uint64(b[0] & 0x3f), 1
|
|
case 1:
|
|
if len(b) < 2 {
|
|
return 0, 0
|
|
}
|
|
return uint64(b[0]&0x3f)<<8 | uint64(b[1]), 2
|
|
case 2:
|
|
if len(b) < 4 {
|
|
return 0, 0
|
|
}
|
|
return uint64(b[0]&0x3f)<<24 | uint64(b[1])<<16 | uint64(b[2])<<8 | uint64(b[3]), 4
|
|
case 3:
|
|
if len(b) < 8 {
|
|
return 0, 0
|
|
}
|
|
return uint64(b[0]&0x3f)<<56 | uint64(b[1])<<48 | uint64(b[2])<<40 | uint64(b[3])<<32 |
|
|
uint64(b[4])<<24 | uint64(b[5])<<16 | uint64(b[6])<<8 | uint64(b[7]), 8
|
|
}
|
|
return 0, 0
|
|
}
|
|
|
|
// h3SettingName maps RFC 9114 and extension SETTINGS IDs to human-readable names.
|
|
func h3SettingName(id uint64) string {
|
|
switch id {
|
|
case 0x01:
|
|
return "QPACK_MAX_TABLE_CAPACITY"
|
|
case 0x06:
|
|
return "MAX_FIELD_SECTION_SIZE"
|
|
case 0x07:
|
|
return "QPACK_BLOCKED_STREAMS"
|
|
case 0x08:
|
|
return "ENABLE_CONNECT_PROTOCOL"
|
|
case 0x33:
|
|
return "H3_DATAGRAM"
|
|
case 0xc671706a:
|
|
return "ENABLE_WEBTRANSPORT"
|
|
default:
|
|
// GREASE values per RFC 9114 §7.2.8 pattern (0x1f * N + 0x21)
|
|
if id > 0x20 && (id-0x21)%0x1f == 0 {
|
|
return "GREASE"
|
|
}
|
|
return "UNKNOWN"
|
|
}
|
|
}
|