feat(ednsdiag): add DoQ/DoH3/DNSCrypt transports, proxy support, probe & compare

This commit is contained in:
windyboy
2026-08-14 18:17:26 +08:00
parent 8303d78caf
commit 70aea6cd72
21 changed files with 1665 additions and 236 deletions
@@ -11,28 +11,46 @@ import (
"net"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
const maxDNSMessageSize = 65535
func exchangeDoH(ctx context.Context, provider Provider, wire []byte, method string) ([]byte, TransportInfo, error) {
client := newDoHClient(provider.DoHURL)
return exchangeDoHWithClient(ctx, client, provider.DoHURL, wire, method)
func exchangeDoH(ctx context.Context, provider Provider, wire []byte, method, explicitProxy string) ([]byte, TransportInfo, error) {
client, proxyLabel, err := newDoHClient(provider.DoHURL, explicitProxy)
if err != nil {
return nil, TransportInfo{Protocol: "doh", Encrypted: true, Bootstrap: "system_resolver"}, err
}
response, info, err := exchangeDoHWithClient(ctx, client, provider.DoHURL, wire, method)
info.Proxy = proxyLabel
return response, info, err
}
func newDoHClient(endpoint string) *http.Client {
origin, _ := url.Parse(endpoint)
func newDoHClient(endpoint, explicitProxy string) (*http.Client, string, error) {
return newDoHClientWithTLSConfig(endpoint, explicitProxy, &tls.Config{MinVersion: tls.VersionTLS12})
}
func newDoHClientWithTLSConfig(endpoint, explicitProxy string, tlsConfig *tls.Config) (*http.Client, string, error) {
origin, err := url.Parse(endpoint)
if err != nil {
return nil, "", fmt.Errorf("parse DoH endpoint: %w", err)
}
proxyURL, err := resolveProxy(origin, explicitProxy)
if err != nil {
return nil, "", fmt.Errorf("select DoH proxy: %w", err)
}
transport := &http.Transport{
ForceAttemptHTTP2: true,
DialContext: (&net.Dialer{Timeout: 5 * time.Second, KeepAlive: 30 * time.Second}).DialContext,
TLSClientConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
},
ForceAttemptHTTP2: true,
DialContext: (&net.Dialer{Timeout: 5 * time.Second, KeepAlive: 30 * time.Second}).DialContext,
TLSClientConfig: tlsConfig.Clone(),
TLSHandshakeTimeout: 5 * time.Second,
}
return &http.Client{
if proxyURL != nil {
transport.Proxy = http.ProxyURL(proxyURL)
}
client := &http.Client{
Transport: transport,
CheckRedirect: func(request *http.Request, via []*http.Request) error {
if len(via) >= 3 {
@@ -47,12 +65,17 @@ func newDoHClient(endpoint string) *http.Client {
return nil
},
}
return client, proxyDisplayURL(proxyURL), nil
}
func exchangeDoHWithClient(ctx context.Context, client *http.Client, endpoint string, wire []byte, method string) ([]byte, TransportInfo, error) {
return exchangeHTTPSDNSWithClient(ctx, client, endpoint, wire, method, "doh")
}
func exchangeHTTPSDNSWithClient(ctx context.Context, client *http.Client, endpoint string, wire []byte, method, protocol string) ([]byte, TransportInfo, error) {
started := time.Now()
info := TransportInfo{
Protocol: "doh",
Protocol: protocol,
Encrypted: true,
Bootstrap: "system_resolver",
}
@@ -94,12 +117,27 @@ func exchangeDoHWithClient(ctx context.Context, client *http.Client, endpoint st
defer response.Body.Close()
info.HTTPVersion = response.Proto
if age := response.Header.Get("Age"); age != "" {
parsedAge, err := strconv.ParseInt(age, 10, 64)
if err != nil || parsedAge < 0 {
return nil, info, fmt.Errorf("DoH server returned invalid Age header %q", age)
}
info.HTTPAgeSeconds = parsedAge
}
if response.TLS == nil || len(response.TLS.VerifiedChains) == 0 {
return nil, info, fmt.Errorf("DoH server TLS identity was not verified")
}
info.ServerAuthenticated = true
info.TLSVersion = tlsVersionName(response.TLS.Version)
info.ALPN = response.TLS.NegotiatedProtocol
if protocol == "doh3" {
if response.ProtoMajor != 3 {
return nil, info, fmt.Errorf("DoH3 server used unexpected HTTP version %q", response.Proto)
}
if info.ALPN != "h3" {
return nil, info, fmt.Errorf("DoH3 server negotiated unexpected ALPN protocol %q", info.ALPN)
}
}
if response.StatusCode < 200 || response.StatusCode > 299 {
return nil, info, fmt.Errorf("DoH server returned HTTP status %d", response.StatusCode)