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

@@ -23,6 +23,7 @@ import (
"net"
"net/http"
"os"
"strings"
"sync"
"time"
@@ -30,6 +31,7 @@ import (
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/quic-go/quic-go/http3"
"go.uber.org/zap"
"golang.org/x/net/http2/hpack"
)
@@ -580,8 +582,35 @@ func (h *FPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
"ts": time.Now().UTC().Format(time.RFC3339),
})
// For h3, emit best-effort http_request_headers (map order, degraded).
// For h3: emit h3_settings (once per connection, deduped by remote_addr),
// then emit best-effort http_request_headers (map order — QPACK frame order
// is not available without a stream tap; degraded but usable for JA4H).
if r.ProtoMajor == 3 {
if settingser, ok := w.(http3.Settingser); ok {
select {
case <-settingser.ReceivedSettings():
s := settingser.Settings()
settingsMap := make(map[string]interface{})
if s.EnableDatagrams {
settingsMap["H3_DATAGRAM"] = uint64(1)
}
if s.EnableExtendedConnect {
settingsMap["ENABLE_CONNECT_PROTOCOL"] = uint64(1)
}
for id, val := range s.Other {
settingsMap[h3SettingName(id)] = val
}
go sendFP(map[string]interface{}{
"kind": "h3_settings",
"remote_addr": r.RemoteAddr,
"settings": settingsMap,
"ts": time.Now().UTC().Format(time.RFC3339),
})
default:
// Settings not yet received — skip; the access_log record is sufficient.
}
}
ordered := make([][]string, 0, len(r.Header))
var cookie, acceptLang string
for name, vals := range r.Header {
@@ -589,7 +618,7 @@ func (h *FPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
if len(vals) > 0 {
v = vals[0]
}
ordered = append(ordered, []string{name, v})
ordered = append(ordered, []string{strings.ToLower(name), v})
switch http.CanonicalHeaderKey(name) {
case "Cookie":
cookie = v