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.
This commit is contained in:
2026-05-10 03:43:34 -04:00
parent 5675dd8ebc
commit 6a6f5807aa
17 changed files with 1268 additions and 2185 deletions

View File

@@ -1,80 +1,13 @@
package decnetfp
import (
"bytes"
"context"
"io"
"sync"
"time"
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/logging"
)
// newH3SettingsTracer is the quic.Config.Tracer factory. We don't use
// quic-go's logging.ConnectionTracer for SETTINGS (its ReceivedStreamFrame
// hook gives only metadata, not payload bytes). The actual h3 SETTINGS
// capture happens in h3TappingUniStream by wrapping AcceptUniStream.
// This function returns nil (no-op tracer) so quic-go uses its default path.
func newH3SettingsTracer(_ context.Context, _ logging.Perspective, _ quic.ConnectionID) *logging.ConnectionTracer {
return nil
}
// ── QUIC connection wrapper ───────────────────────────────────────────────────
// h3SettingsTappingConn wraps quic.Connection and intercepts AcceptUniStream
// so the first bytes of each client-initiated unidirectional stream can be
// inspected for h3 control stream SETTINGS before being replayed to the
// http3.Server.
type h3SettingsTappingConn struct {
quic.Connection
remoteAddr string
}
func (c *h3SettingsTappingConn) AcceptUniStream(ctx context.Context) (quic.ReceiveStream, error) {
stream, err := c.Connection.AcceptUniStream(ctx)
if err != nil {
return stream, err
}
return &h3TappingUniStream{ReceiveStream: stream, remoteAddr: c.remoteAddr}, nil
}
// ── QUIC receive-stream wrapper ───────────────────────────────────────────────
// h3TappingUniStream peeks at the first bytes of a unidirectional stream to
// identify the h3 control stream (stream type 0x00, RFC 9114 §6.2.1) and
// extract its first SETTINGS frame, then replays all bytes to the caller.
type h3TappingUniStream struct {
quic.ReceiveStream
once sync.Once
buf bytes.Buffer
reader io.Reader
remoteAddr string
}
// maxH3ControlPeek is enough to cover the stream-type varint + SETTINGS
// frame type varint + frame-length varint + a typical SETTINGS frame body
// (6 settings × 8 bytes each = 48 bytes, plus 3 varint headers ≈ 64 bytes).
const maxH3ControlPeek = 256
func (s *h3TappingUniStream) Read(p []byte) (int, error) {
s.once.Do(func() {
scratch := make([]byte, maxH3ControlPeek)
n, _ := s.ReceiveStream.Read(scratch)
s.buf.Write(scratch[:n])
go tryParseH3ControlStream(s.remoteAddr, s.buf.Bytes())
s.reader = io.MultiReader(&s.buf, s.ReceiveStream)
})
if s.reader != nil {
return s.reader.Read(p)
}
return s.ReceiveStream.Read(p)
}
// tryParseH3ControlStream examines the peeked bytes. If the stream opens
// with stream-type 0x00 (h3 control stream) and the first frame is SETTINGS
// (type 0x04), it emits an h3_settings fp record. All errors are silent —
// this is a best-effort tap.
// 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 {
@@ -99,7 +32,7 @@ func tryParseH3ControlStream(remoteAddr string, data []byte) {
return
}
if uint64(len(data)) < frameLen {
return // need more bytes — we only peeked 256
return // truncated
}
body := data[:frameLen]