feat(ednsdiag): add DoQ/DoH3/DNSCrypt transports, proxy support, probe & compare
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
@@ -24,20 +25,31 @@ var recordTypes = map[string]dnsmessage.Type{
|
||||
"SRV": dnsmessage.TypeSRV,
|
||||
"SVCB": dnsmessage.TypeSVCB,
|
||||
"HTTPS": dnsmessage.TypeHTTPS,
|
||||
"PTR": dnsmessage.TypePTR,
|
||||
}
|
||||
|
||||
func BuildQuery(name, recordType string) ([]byte, QueryInfo, uint16, error) {
|
||||
canonical, err := canonicalName(name)
|
||||
if err != nil {
|
||||
return nil, QueryInfo{}, 0, err
|
||||
}
|
||||
|
||||
typeName := strings.ToUpper(recordType)
|
||||
qtype, ok := recordTypes[typeName]
|
||||
if !ok {
|
||||
return nil, QueryInfo{}, 0, fmt.Errorf("unsupported record type %q", recordType)
|
||||
}
|
||||
|
||||
var canonical string
|
||||
var err error
|
||||
if typeName == "PTR" {
|
||||
address, parseErr := netip.ParseAddr(strings.TrimSpace(name))
|
||||
if parseErr != nil {
|
||||
return nil, QueryInfo{}, 0, fmt.Errorf("PTR queries require an IPv4 or IPv6 address")
|
||||
}
|
||||
canonical = reverseName(address.Unmap())
|
||||
} else {
|
||||
canonical, err = canonicalName(name)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, QueryInfo{}, 0, err
|
||||
}
|
||||
|
||||
dnsName, err := dnsmessage.NewName(canonical + ".")
|
||||
if err != nil {
|
||||
return nil, QueryInfo{}, 0, fmt.Errorf("encode domain name: %w", err)
|
||||
@@ -63,6 +75,22 @@ func BuildQuery(name, recordType string) ([]byte, QueryInfo, uint16, error) {
|
||||
return wire, QueryInfo{Name: canonical, Type: typeName}, id, nil
|
||||
}
|
||||
|
||||
func reverseName(address netip.Addr) string {
|
||||
if address.Is4() {
|
||||
bytes := address.As4()
|
||||
return fmt.Sprintf("%d.%d.%d.%d.in-addr.arpa", bytes[3], bytes[2], bytes[1], bytes[0])
|
||||
}
|
||||
|
||||
bytes := address.As16()
|
||||
var builder strings.Builder
|
||||
// Each IPv6 nibble is emitted from least to most significant per RFC 3596.
|
||||
for index := len(bytes) - 1; index >= 0; index-- {
|
||||
fmt.Fprintf(&builder, "%x.%x.", bytes[index]&0x0f, bytes[index]>>4)
|
||||
}
|
||||
builder.WriteString("ip6.arpa")
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func ParseResponse(wire []byte, expectedID uint16, query QueryInfo) (DNSInfo, error) {
|
||||
var message dnsmessage.Message
|
||||
if err := message.Unpack(wire); err != nil {
|
||||
@@ -71,6 +99,12 @@ func ParseResponse(wire []byte, expectedID uint16, query QueryInfo) (DNSInfo, er
|
||||
if !message.Header.Response {
|
||||
return DNSInfo{}, fmt.Errorf("received a DNS query instead of a response")
|
||||
}
|
||||
if message.Header.OpCode != 0 {
|
||||
return DNSInfo{}, fmt.Errorf("DNS response uses unexpected opcode %d", message.Header.OpCode)
|
||||
}
|
||||
if message.Header.Truncated {
|
||||
return DNSInfo{}, fmt.Errorf("DNS response is truncated")
|
||||
}
|
||||
if message.Header.ID != expectedID {
|
||||
return DNSInfo{}, fmt.Errorf("DNS transaction ID mismatch")
|
||||
}
|
||||
@@ -79,13 +113,20 @@ func ParseResponse(wire []byte, expectedID uint16, query QueryInfo) (DNSInfo, er
|
||||
}
|
||||
wantType := recordTypes[query.Type]
|
||||
question := message.Questions[0]
|
||||
if trimRoot(question.Name.String()) != query.Name || question.Type != wantType {
|
||||
if trimRoot(question.Name.String()) != query.Name || question.Type != wantType || question.Class != dnsmessage.ClassINET {
|
||||
return DNSInfo{}, fmt.Errorf("DNS response question does not match request")
|
||||
}
|
||||
|
||||
answers := make([]AnswerRecord, 0, len(message.Answers))
|
||||
for _, resource := range message.Answers {
|
||||
answers = append(answers, normalizeAnswer(resource))
|
||||
if resource.Header.Class != dnsmessage.ClassINET {
|
||||
return DNSInfo{}, fmt.Errorf("DNS answer %q uses unsupported class %d", trimRoot(resource.Header.Name.String()), resource.Header.Class)
|
||||
}
|
||||
answer, err := normalizeAnswer(resource)
|
||||
if err != nil {
|
||||
return DNSInfo{}, err
|
||||
}
|
||||
answers = append(answers, answer)
|
||||
}
|
||||
|
||||
return DNSInfo{
|
||||
@@ -129,7 +170,7 @@ func canonicalName(input string) (string, error) {
|
||||
return ascii, nil
|
||||
}
|
||||
|
||||
func normalizeAnswer(resource dnsmessage.Resource) AnswerRecord {
|
||||
func normalizeAnswer(resource dnsmessage.Resource) (AnswerRecord, error) {
|
||||
record := AnswerRecord{
|
||||
"name": trimRoot(resource.Header.Name.String()),
|
||||
"type": typeName(resource.Header.Type),
|
||||
@@ -177,13 +218,32 @@ func normalizeAnswer(resource dnsmessage.Resource) AnswerRecord {
|
||||
record["tag"] = string(body.Data[2 : 2+tagLength])
|
||||
record["value"] = string(body.Data[2+tagLength:])
|
||||
} else {
|
||||
record["rdata_base64"] = base64.StdEncoding.EncodeToString(body.Data)
|
||||
return nil, fmt.Errorf("CAA answer contains a truncated tag")
|
||||
}
|
||||
} else {
|
||||
record["rdata_base64"] = base64.StdEncoding.EncodeToString(body.Data)
|
||||
return nil, fmt.Errorf("DNS answer type %s cannot be represented by result-v1", typeName(resource.Header.Type))
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("DNS answer type %s has an unexpected wire representation", typeName(resource.Header.Type))
|
||||
}
|
||||
return record, nil
|
||||
}
|
||||
|
||||
func applyHTTPAge(info *DNSInfo, ageSeconds int64) {
|
||||
if ageSeconds <= 0 {
|
||||
return
|
||||
}
|
||||
for _, answer := range info.Answers {
|
||||
ttl, ok := answer["ttl"].(uint32)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if ageSeconds >= int64(ttl) {
|
||||
answer["ttl"] = uint32(0)
|
||||
} else {
|
||||
answer["ttl"] = ttl - uint32(ageSeconds)
|
||||
}
|
||||
}
|
||||
return record
|
||||
}
|
||||
|
||||
func addSVCBFields(record AnswerRecord, priority uint16, target dnsmessage.Name, params []dnsmessage.SVCParam) {
|
||||
|
||||
Reference in New Issue
Block a user