- Renames caddy.listeners.decnet_h2fp → decnet_fp; adds h1 raw-byte header capture (plainTappingConn) and h2 continuous HPACK decode loop (parseH2HeadersLoop) so headers_ordered reflects actual wire order, not Go map iteration order. - Adds H3App Caddy module (decnet_h3) that owns UDP/443 via quic-go, wraps accepted QUIC connections with h3SettingsTappingConn to intercept the h3 control stream and extract RFC 9114 SETTINGS in wire order. - Wires access_log emission from FPHandler.ServeHTTP via responseCapture. - Updates syslog_bridge.py (canonical + per-service copies) with inline _compute_ja4h and new fp socket record branches: http_request_headers, h3_settings, access_log. - Fixes ingester proto field alias (bridge emits 'proto', ingester expected 'protocol') and exposes _process_fingerprint_bounties test alias. - Go tests: h1/h2/h3 golden-byte tests all green; h3_tracer_test covers varint parser, GREASE detection, truncated-stream safety. - Python tests: 15/15 green across bridge JA4H hash parity, ingester compat (old + new event shapes), and Caddyfile h3 template assertions.
179 lines
4.2 KiB
Go
179 lines
4.2 KiB
Go
package decnetfp
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/caddyserver/caddy/v2"
|
|
"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"
|
|
"github.com/quic-go/quic-go/http3"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
func init() {
|
|
caddy.RegisterModule(H3App{})
|
|
httpcaddyfile.RegisterGlobalOption("decnet_h3", parseH3AppOption)
|
|
}
|
|
|
|
// parseH3AppOption maps the `decnet_h3` global Caddyfile block to the
|
|
// decnet_h3 app config (empty JSON — all config comes from env).
|
|
func parseH3AppOption(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
|
for d.Next() {
|
|
for d.NextBlock(0) {
|
|
}
|
|
}
|
|
return json.RawMessage(`{}`), nil
|
|
}
|
|
|
|
// H3App is a Caddy app that owns the QUIC/UDP listener on port 443, forwarding
|
|
// accepted h3 connections to the Caddy HTTP app's handler chain. This is the
|
|
// only way to inject a per-connection quic-go Tracer for h3 SETTINGS capture,
|
|
// since Caddy does not expose its QUIC config.
|
|
//
|
|
// Activate with a `decnet_h3` global Caddyfile block AND omit `h3` from the
|
|
// `:443` server's `protocols` list, otherwise both this app and Caddy's HTTP
|
|
// server will fight over UDP/443.
|
|
//
|
|
// The app is a no-op when `HTTP_VERSIONS` env does not contain `"http/3"`.
|
|
type H3App struct {
|
|
caddyCtx caddy.Context
|
|
logger *zap.Logger
|
|
listener *quic.Listener
|
|
transport *quic.Transport
|
|
httpSrv *http3.Server
|
|
cancelLoop context.CancelFunc
|
|
wg *sync.WaitGroup
|
|
}
|
|
|
|
func (H3App) CaddyModule() caddy.ModuleInfo {
|
|
return caddy.ModuleInfo{
|
|
ID: "decnet_h3",
|
|
New: func() caddy.Module { return new(H3App) },
|
|
}
|
|
}
|
|
|
|
func (a *H3App) Provision(ctx caddy.Context) error {
|
|
a.caddyCtx = ctx
|
|
a.logger = ctx.Logger()
|
|
return nil
|
|
}
|
|
|
|
func (a *H3App) Start() error {
|
|
a.wg = &sync.WaitGroup{}
|
|
if !strings.Contains(os.Getenv("HTTP_VERSIONS"), "http/3") {
|
|
return nil
|
|
}
|
|
|
|
cert, err := tls.LoadX509KeyPair("/opt/tls/cert.pem", "/opt/tls/key.pem")
|
|
if err != nil {
|
|
return fmt.Errorf("decnet_h3: load TLS cert: %w", err)
|
|
}
|
|
tlsCfg := &tls.Config{
|
|
Certificates: []tls.Certificate{cert},
|
|
NextProtos: []string{"h3"},
|
|
}
|
|
|
|
udpConn, err := net.ListenPacket("udp", ":443")
|
|
if err != nil {
|
|
return fmt.Errorf("decnet_h3: bind UDP/443: %w", err)
|
|
}
|
|
tr := &quic.Transport{Conn: udpConn.(*net.UDPConn)}
|
|
a.transport = tr
|
|
|
|
ln, err := tr.Listen(tlsCfg, &quic.Config{Tracer: newH3SettingsTracer})
|
|
if err != nil {
|
|
tr.Close()
|
|
return fmt.Errorf("decnet_h3: quic listen: %w", err)
|
|
}
|
|
a.listener = ln
|
|
|
|
handler, err := a.findHTTPHandler()
|
|
if err != nil {
|
|
ln.Close()
|
|
tr.Close()
|
|
return fmt.Errorf("decnet_h3: find HTTP handler: %w", err)
|
|
}
|
|
|
|
a.httpSrv = &http3.Server{Handler: handler}
|
|
|
|
loopCtx, cancel := context.WithCancel(context.Background())
|
|
a.cancelLoop = cancel
|
|
a.wg.Add(1)
|
|
go func() {
|
|
defer a.wg.Done()
|
|
a.acceptLoop(loopCtx)
|
|
}()
|
|
|
|
a.logger.Info("decnet_h3 listening on UDP/443")
|
|
return nil
|
|
}
|
|
|
|
func (a *H3App) acceptLoop(ctx context.Context) {
|
|
for {
|
|
conn, err := a.listener.Accept(ctx)
|
|
if err != nil {
|
|
return
|
|
}
|
|
wrapped := &h3SettingsTappingConn{
|
|
Connection: conn,
|
|
remoteAddr: conn.RemoteAddr().String(),
|
|
}
|
|
go func() {
|
|
a.httpSrv.ServeQUICConn(wrapped) //nolint:errcheck
|
|
}()
|
|
}
|
|
}
|
|
|
|
func (a *H3App) Stop() error {
|
|
if a.cancelLoop != nil {
|
|
a.cancelLoop()
|
|
}
|
|
if a.listener != nil {
|
|
a.listener.Close()
|
|
}
|
|
a.wg.Wait()
|
|
if a.transport != nil {
|
|
a.transport.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// findHTTPHandler returns the http.Handler for Caddy's :443 server.
|
|
func (a *H3App) findHTTPHandler() (http.Handler, error) {
|
|
appIface, err := a.caddyCtx.App("http")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get http app: %w", err)
|
|
}
|
|
httpApp, ok := appIface.(*caddyhttp.App)
|
|
if !ok {
|
|
return nil, fmt.Errorf("unexpected http app type %T", appIface)
|
|
}
|
|
for _, srv := range httpApp.Servers {
|
|
for _, addr := range srv.Listen {
|
|
if strings.Contains(addr, ":443") {
|
|
return srv, nil
|
|
}
|
|
}
|
|
}
|
|
// Fall back to any available server.
|
|
for _, srv := range httpApp.Servers {
|
|
return srv, nil
|
|
}
|
|
return nil, fmt.Errorf("no HTTP servers found in caddy http app")
|
|
}
|
|
|
|
var (
|
|
_ caddy.App = (*H3App)(nil)
|
|
_ caddy.Provisioner = (*H3App)(nil)
|
|
)
|