Files
anti 6a6f5807aa fix(pr3): adapt to quic-go v0.59.0 API — drop H3App, capture h3 SETTINGS via http3.Settingser
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.
2026-05-10 03:43:34 -04:00

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"
}
}