Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 70 additions & 46 deletions internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
20 changes: 20 additions & 0 deletions internal/client/dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down