2026-08-13 12:52:20 +08:00
|
|
|
package edns
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
2026-08-14 18:17:26 +08:00
|
|
|
"encoding/binary"
|
2026-08-13 12:52:20 +08:00
|
|
|
"fmt"
|
|
|
|
|
)
|
|
|
|
|
|
2026-08-14 18:17:26 +08:00
|
|
|
type queryExchange func(context.Context, Provider, []byte, QueryOptions) ([]byte, TransportInfo, dnsCryptPeerInfo, error)
|
|
|
|
|
|
2026-08-13 12:52:20 +08:00
|
|
|
func Query(ctx context.Context, options QueryOptions) Result {
|
2026-08-14 18:17:26 +08:00
|
|
|
return queryWithExchange(ctx, options, exchangeProtocol)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func queryWithExchange(ctx context.Context, options QueryOptions, exchange queryExchange) Result {
|
2026-08-13 12:52:20 +08:00
|
|
|
wire, query, transactionID, err := BuildQuery(options.Name, options.RecordType)
|
|
|
|
|
result := Result{
|
|
|
|
|
SchemaVersion: 1,
|
|
|
|
|
Operation: "query",
|
|
|
|
|
Query: query,
|
|
|
|
|
Transport: TransportInfo{
|
|
|
|
|
Protocol: options.Protocol,
|
|
|
|
|
Encrypted: true,
|
|
|
|
|
Bootstrap: "system_resolver",
|
|
|
|
|
},
|
|
|
|
|
DNS: DNSInfo{Answers: []AnswerRecord{}},
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
result.Query = QueryInfo{Name: options.Name, Type: options.RecordType}
|
|
|
|
|
result.Error = &ErrorInfo{Class: "input", Message: err.Error()}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
provider, err := FindProvider(options.Provider)
|
|
|
|
|
if err != nil {
|
|
|
|
|
result.Error = &ErrorInfo{Class: "input", Message: err.Error()}
|
|
|
|
|
return result
|
|
|
|
|
}
|
2026-08-14 18:17:26 +08:00
|
|
|
if err := ValidateProxyURL(options.Proxy); err != nil {
|
|
|
|
|
result.Error = &ErrorInfo{Class: "input", Message: err.Error()}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
if options.Proxy != "" && options.Protocol != "doh" && options.Protocol != "dot" {
|
|
|
|
|
result.Error = &ErrorInfo{Class: "unsupported", Message: fmt.Sprintf("proxying is not available for protocol %q", options.Protocol)}
|
|
|
|
|
return result
|
2026-08-13 15:34:54 +08:00
|
|
|
}
|
2026-08-13 12:52:20 +08:00
|
|
|
result.Resolver = ResolverInfo{Provider: provider.ID, Profile: provider.Profile}
|
2026-08-14 18:17:26 +08:00
|
|
|
endpoint, err := provider.Endpoint(options.Protocol)
|
|
|
|
|
if err != nil {
|
|
|
|
|
class := "input"
|
|
|
|
|
if IsUnsupported(err) {
|
|
|
|
|
class = "unsupported"
|
|
|
|
|
}
|
|
|
|
|
result.Error = &ErrorInfo{Class: class, Message: err.Error()}
|
|
|
|
|
return result
|
2026-08-13 12:52:20 +08:00
|
|
|
}
|
2026-08-14 18:17:26 +08:00
|
|
|
result.Resolver.Endpoint = endpoint
|
|
|
|
|
if options.Protocol == "doh" || options.Protocol == "doh3" || options.Protocol == "doq" {
|
|
|
|
|
binary.BigEndian.PutUint16(wire[:2], 0)
|
|
|
|
|
transactionID = 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
response, transport, peer, err := exchange(ctx, provider, wire, options)
|
|
|
|
|
result.Transport = transport
|
2026-08-13 12:52:20 +08:00
|
|
|
if err != nil {
|
|
|
|
|
result.Error = &ErrorInfo{Class: "transport", Message: err.Error()}
|
|
|
|
|
return result
|
|
|
|
|
}
|
2026-08-14 18:17:26 +08:00
|
|
|
if options.Protocol == "dnscrypt" {
|
|
|
|
|
result.Resolver.Endpoint = peer.ServerAddress
|
|
|
|
|
result.Resolver.AuthenticationName = peer.ProviderName
|
|
|
|
|
result.Resolver.CertificateSerial = peer.CertificateSerial
|
|
|
|
|
}
|
2026-08-13 12:52:20 +08:00
|
|
|
|
|
|
|
|
result.DNS, err = ParseResponse(response, transactionID, query)
|
|
|
|
|
if err != nil {
|
|
|
|
|
result.Error = &ErrorInfo{Class: "protocol", Message: err.Error()}
|
|
|
|
|
return result
|
|
|
|
|
}
|
2026-08-14 18:17:26 +08:00
|
|
|
applyHTTPAge(&result.DNS, result.Transport.HTTPAgeSeconds)
|
2026-08-13 12:52:20 +08:00
|
|
|
result.Completed = true
|
|
|
|
|
return result
|
|
|
|
|
}
|
2026-08-14 18:17:26 +08:00
|
|
|
|
|
|
|
|
func exchangeProtocol(ctx context.Context, provider Provider, wire []byte, options QueryOptions) ([]byte, TransportInfo, dnsCryptPeerInfo, error) {
|
|
|
|
|
var response []byte
|
|
|
|
|
var transport TransportInfo
|
|
|
|
|
var peer dnsCryptPeerInfo
|
|
|
|
|
var err error
|
|
|
|
|
switch options.Protocol {
|
|
|
|
|
case "doh":
|
|
|
|
|
response, transport, err = exchangeDoH(ctx, provider, wire, options.Method, options.Proxy)
|
|
|
|
|
case "dot":
|
|
|
|
|
response, transport, err = exchangeDoT(ctx, provider, wire, options.Proxy)
|
|
|
|
|
case "doq":
|
|
|
|
|
response, transport, err = exchangeDoQ(ctx, provider, wire)
|
|
|
|
|
case "doh3":
|
|
|
|
|
response, transport, err = exchangeDoH3(ctx, provider, wire, options.Method)
|
|
|
|
|
case "dnscrypt":
|
|
|
|
|
response, transport, peer, err = exchangeDNSCrypt(ctx, provider.DNSCryptStamp, wire)
|
|
|
|
|
default:
|
|
|
|
|
err = fmt.Errorf("protocol %q is not available; run ednsdiag capabilities", options.Protocol)
|
|
|
|
|
}
|
|
|
|
|
return response, transport, peer, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func Probe(ctx context.Context, options QueryOptions) Result {
|
|
|
|
|
result := Query(ctx, options)
|
|
|
|
|
result.Operation = "probe"
|
|
|
|
|
return result
|
|
|
|
|
}
|