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
@@ -3,22 +3,25 @@ package edns
import (
"context"
"crypto/tls"
"encoding/binary"
"fmt"
"io"
"net"
"net/url"
"time"
)
func exchangeDoT(ctx context.Context, provider Provider, wire []byte) ([]byte, TransportInfo, error) {
return exchangeDoTWithTLSConfig(ctx, provider, wire, &tls.Config{
func exchangeDoT(ctx context.Context, provider Provider, wire []byte, explicitProxy string) ([]byte, TransportInfo, error) {
return exchangeDoTWithTLSConfigAndProxy(ctx, provider, wire, &tls.Config{
ServerName: provider.DoTName,
MinVersion: tls.VersionTLS12,
NextProtos: []string{"dot"},
})
}, explicitProxy)
}
func exchangeDoTWithTLSConfig(ctx context.Context, provider Provider, wire []byte, tlsConfig *tls.Config) ([]byte, TransportInfo, error) {
return exchangeDoTWithTLSConfigAndProxy(ctx, provider, wire, tlsConfig, "")
}
func exchangeDoTWithTLSConfigAndProxy(ctx context.Context, provider Provider, wire []byte, tlsConfig *tls.Config, explicitProxy string) ([]byte, TransportInfo, error) {
started := time.Now()
info := TransportInfo{
Protocol: "dot",
@@ -26,7 +29,13 @@ func exchangeDoTWithTLSConfig(ctx context.Context, provider Provider, wire []byt
Bootstrap: "system_resolver",
}
rawConnection, err := (&net.Dialer{}).DialContext(ctx, "tcp", provider.DoTAddr)
endpoint := &url.URL{Scheme: "https", Host: provider.DoTAddr}
proxyURL, err := resolveProxy(endpoint, explicitProxy)
if err != nil {
return nil, info, fmt.Errorf("select DoT proxy: %w", err)
}
info.Proxy = proxyDisplayURL(proxyURL)
rawConnection, err := dialTCP(ctx, provider.DoTAddr, proxyURL)
if err != nil {
info.ElapsedMS = time.Since(started).Milliseconds()
return nil, info, fmt.Errorf("connect to DoT server: %w", err)
@@ -66,29 +75,10 @@ func exchangeDoTWithTLSConfig(ctx context.Context, provider Provider, wire []byt
}
func exchangeTCPFrame(connection io.ReadWriter, wire []byte) ([]byte, error) {
if len(wire) == 0 || len(wire) > maxDNSMessageSize {
return nil, fmt.Errorf("invalid DNS message length %d", len(wire))
}
frame := make([]byte, 2+len(wire))
binary.BigEndian.PutUint16(frame[:2], uint16(len(wire)))
copy(frame[2:], wire)
if err := writeAll(connection, frame); err != nil {
if err := writeDNSFrame(connection, wire); err != nil {
return nil, fmt.Errorf("write framed DNS query: %w", err)
}
var lengthBytes [2]byte
if _, err := io.ReadFull(connection, lengthBytes[:]); err != nil {
return nil, fmt.Errorf("read DNS response length: %w", err)
}
length := int(binary.BigEndian.Uint16(lengthBytes[:]))
if length == 0 {
return nil, fmt.Errorf("DoT server returned an empty DNS message")
}
response := make([]byte, length)
if _, err := io.ReadFull(connection, response); err != nil {
return nil, fmt.Errorf("read DNS response: %w", err)
}
return response, nil
return readDNSFrame(connection)
}
func writeAll(writer io.Writer, payload []byte) error {