From d011c71c7737286d20be7e64b5def8c28fb2f38e Mon Sep 17 00:00:00 2001 From: Ryan Fowler Date: Mon, 26 Jan 2026 12:01:36 -0800 Subject: [PATCH] Improve extra verbose logging with HTTP/3 --- internal/client/client.go | 116 +++++++++++++++++++++++--------------- internal/client/dns.go | 20 +++++++ 2 files changed, 90 insertions(+), 46 deletions(-) diff --git a/internal/client/client.go b/internal/client/client.go index 54aa032..4cab7fa 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -210,70 +210,94 @@ func getHTTP3Transport(dnsServer *url.URL, tlsConfig *tls.Config) http.RoundTrip DisableCompression: true, TLSClientConfig: tlsConfig, } - if dnsServer != nil { - rt.Dial = func(ctx context.Context, addr string, tlsCfg *tls.Config, qcfg *quic.Config) (*quic.Conn, error) { - // Resolve the address to IPs. - var ips []net.IPAddr - var portStr string - var err error + + // Always set custom Dial to ensure trace hooks work. + rt.Dial = func(ctx context.Context, addr string, tlsCfg *tls.Config, qcfg *quic.Config) (*quic.Conn, error) { + host, portStr, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + + // Resolve DNS with trace hooks. + var ips []net.IPAddr + if dnsServer != nil { if dnsServer.Scheme == "" { - var host string - host, portStr, err = net.SplitHostPort(addr) - if err != nil { - return nil, err - } resolver := udpResolver(dnsServer.Host) ips, err = resolver.LookupIPAddr(ctx, host) } else { ips, portStr, err = resolveDOH(ctx, dnsServer, addr) } + } else { + // Use system resolver with trace hooks. + ips, err = resolveWithTrace(ctx, host) + } + if err != nil { + return nil, err + } + if len(ips) == 0 { + return nil, fmt.Errorf("lookup %s: no addresses found", addr) + } + + port, err := net.LookupPort("udp", portStr) + if err != nil { + return nil, err + } + + // Establish quic connection. + trace := httptrace.ContextClientTrace(ctx) + for _, ip := range ips { + udpAddr := &net.UDPAddr{IP: ip.IP, Port: port} + var lc net.ListenConfig + var packetConn net.PacketConn + packetConn, err = lc.ListenPacket(ctx, "udp", ":0") if err != nil { - return nil, err + continue } - if len(ips) == 0 { - return nil, fmt.Errorf("lookup %s: no addresses found", addr) + + if trace != nil && trace.TLSHandshakeStart != nil { + trace.TLSHandshakeStart() } - port, err := net.LookupPort("udp", portStr) + var conn *quic.Conn + conn, err = quic.DialEarly(ctx, packetConn, udpAddr, tlsCfg, qcfg) + if trace != nil && trace.TLSHandshakeDone != nil { + var state tls.ConnectionState + if conn != nil { + state = conn.ConnectionState().TLS + } + trace.TLSHandshakeDone(state, err) + } if err != nil { - return nil, err + packetConn.Close() + continue } + return conn, nil + } - // Establish quic connection. - trace := httptrace.ContextClientTrace(ctx) - for _, ip := range ips { - udpAddr := &net.UDPAddr{IP: ip.IP, Port: port} - var lc net.ListenConfig - var packetConn net.PacketConn - packetConn, err = lc.ListenPacket(ctx, "udp", ":0") - if err != nil { - continue - } + return nil, err + } - if trace != nil && trace.TLSHandshakeStart != nil { - trace.TLSHandshakeStart() - } + return &http3TimingTransport{rt: rt} +} - var conn *quic.Conn - conn, err = quic.DialEarly(ctx, packetConn, udpAddr, tlsCfg, qcfg) - if trace != nil && trace.TLSHandshakeDone != nil { - var state tls.ConnectionState - if conn != nil { - state = conn.ConnectionState().TLS - } - trace.TLSHandshakeDone(state, err) - } - if err != nil { - packetConn.Close() - continue - } - return conn, nil - } +// http3TimingTransport wraps http3.Transport to provide TTFB trace hooks. +type http3TimingTransport struct { + rt *http3.Transport +} - return nil, err +func (t *http3TimingTransport) RoundTrip(req *http.Request) (*http.Response, error) { + resp, err := t.rt.RoundTrip(req) + + // Call GotFirstResponseByte when response headers arrive. + if err == nil { + if trace := httptrace.ContextClientTrace(req.Context()); trace != nil { + if trace.GotFirstResponseByte != nil { + trace.GotFirstResponseByte() + } } } - return rt + + return resp, err } // RequestConfig represents the configuration for creating an HTTP request. diff --git a/internal/client/dns.go b/internal/client/dns.go index e4647aa..1dabc79 100644 --- a/internal/client/dns.go +++ b/internal/client/dns.go @@ -157,6 +157,26 @@ func lookupDOH(ctx context.Context, serverURL *url.URL, host, dnsType string) ([ return addrs, nil } +// resolveWithTrace performs DNS lookup using system resolver with trace hooks. +func resolveWithTrace(ctx context.Context, host string) ([]net.IPAddr, error) { + trace := httptrace.ContextClientTrace(ctx) + if trace != nil && trace.DNSStart != nil { + trace.DNSStart(httptrace.DNSStartInfo{Host: host}) + } + + ips, err := net.DefaultResolver.LookupIPAddr(ctx, host) + + if trace != nil && trace.DNSDone != nil { + info := httptrace.DNSDoneInfo{Err: err} + if err == nil { + info.Addrs = ips + } + trace.DNSDone(info) + } + + return ips, err +} + // rcodeName returns the text for the provided rcode integer. func rcodeName(n int) string { switch n {