feat(ednsdiag): add DoQ/DoH3/DNSCrypt transports, proxy support, probe & compare
This commit is contained in:
@@ -2,6 +2,8 @@ package edns
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
@@ -68,13 +70,87 @@ func TestBuildQueryIDNAAndBlockedNames(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPTRQueryFromIPAddress(t *testing.T) {
|
||||
_, ipv4, _, err := BuildQuery("192.0.2.1", "PTR")
|
||||
if err != nil {
|
||||
t.Fatalf("build IPv4 PTR query: %v", err)
|
||||
}
|
||||
if ipv4.Name != "1.2.0.192.in-addr.arpa" || ipv4.Type != "PTR" {
|
||||
t.Fatalf("unexpected IPv4 PTR query: %#v", ipv4)
|
||||
}
|
||||
_, ipv6, _, err := BuildQuery("2001:db8::1", "PTR")
|
||||
if err != nil {
|
||||
t.Fatalf("build IPv6 PTR query: %v", err)
|
||||
}
|
||||
if ipv6.Name != "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa" {
|
||||
t.Fatalf("unexpected IPv6 PTR name: %q", ipv6.Name)
|
||||
}
|
||||
_, mappedIPv4, _, err := BuildQuery("::ffff:192.0.2.1", "PTR")
|
||||
if err != nil {
|
||||
t.Fatalf("build IPv4-mapped PTR query: %v", err)
|
||||
}
|
||||
if mappedIPv4.Name != ipv4.Name {
|
||||
t.Fatalf("IPv4-mapped PTR name = %q, want %q", mappedIPv4.Name, ipv4.Name)
|
||||
}
|
||||
if _, _, _, err := BuildQuery("example.com", "PTR"); err == nil {
|
||||
t.Fatal("PTR query accepted a non-IP input")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeSupportedAnswerTypes(t *testing.T) {
|
||||
name := dnsmessage.MustNewName("example.com.")
|
||||
target := dnsmessage.MustNewName("target.example.")
|
||||
resources := []dnsmessage.Resource{
|
||||
{Header: dnsmessage.ResourceHeader{Name: name, Type: dnsmessage.TypeAAAA, TTL: 60}, Body: &dnsmessage.AAAAResource{AAAA: [16]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}}},
|
||||
{Header: dnsmessage.ResourceHeader{Name: name, Type: dnsmessage.TypeCNAME, TTL: 60}, Body: &dnsmessage.CNAMEResource{CNAME: target}},
|
||||
{Header: dnsmessage.ResourceHeader{Name: name, Type: dnsmessage.TypeMX, TTL: 60}, Body: &dnsmessage.MXResource{Pref: 10, MX: target}},
|
||||
{Header: dnsmessage.ResourceHeader{Name: name, Type: dnsmessage.TypeTXT, TTL: 60}, Body: &dnsmessage.TXTResource{TXT: []string{"one", "two"}}},
|
||||
{Header: dnsmessage.ResourceHeader{Name: name, Type: dnsmessage.TypeNS, TTL: 60}, Body: &dnsmessage.NSResource{NS: target}},
|
||||
{Header: dnsmessage.ResourceHeader{Name: name, Type: dnsmessage.TypeSOA, TTL: 60}, Body: &dnsmessage.SOAResource{NS: target, MBox: target, Serial: 1}},
|
||||
{Header: dnsmessage.ResourceHeader{Name: name, Type: dnsmessage.TypeSRV, TTL: 60}, Body: &dnsmessage.SRVResource{Priority: 1, Weight: 2, Port: 443, Target: target}},
|
||||
{Header: dnsmessage.ResourceHeader{Name: name, Type: dnsmessage.TypePTR, TTL: 60}, Body: &dnsmessage.PTRResource{PTR: target}},
|
||||
{Header: dnsmessage.ResourceHeader{Name: name, Type: dnsmessage.TypeSVCB, TTL: 60}, Body: &dnsmessage.SVCBResource{Priority: 1, Target: target}},
|
||||
{Header: dnsmessage.ResourceHeader{Name: name, Type: dnsmessage.TypeHTTPS, TTL: 60}, Body: &dnsmessage.HTTPSResource{SVCBResource: dnsmessage.SVCBResource{Priority: 1, Target: target}}},
|
||||
}
|
||||
wantTypes := []string{"AAAA", "CNAME", "MX", "TXT", "NS", "SOA", "SRV", "PTR", "SVCB", "HTTPS"}
|
||||
wantFields := []map[string]any{
|
||||
{"address": "2001:db8::1"},
|
||||
{"target": "target.example"},
|
||||
{"priority": uint16(10), "exchange": "target.example"},
|
||||
{"strings": []string{"one", "two"}},
|
||||
{"host": "target.example"},
|
||||
{"primary_ns": "target.example", "responsible_mailbox": "target.example", "serial": uint32(1)},
|
||||
{"priority": uint16(1), "weight": uint16(2), "port": uint16(443), "target": "target.example"},
|
||||
{"target": "target.example"},
|
||||
{"priority": uint16(1), "target": "target.example", "params": []map[string]any{}},
|
||||
{"priority": uint16(1), "target": "target.example", "params": []map[string]any{}},
|
||||
}
|
||||
for index, resource := range resources {
|
||||
record, err := normalizeAnswer(resource)
|
||||
if err != nil {
|
||||
t.Fatalf("normalize %s: %v", wantTypes[index], err)
|
||||
}
|
||||
if record["type"] != wantTypes[index] || record["name"] != "example.com" || record["ttl"] != uint32(60) {
|
||||
t.Fatalf("unexpected %s normalization: %#v", wantTypes[index], record)
|
||||
}
|
||||
for field, want := range wantFields[index] {
|
||||
if got := record[field]; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("%s field %s = %#v, want %#v", wantTypes[index], field, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeCAA(t *testing.T) {
|
||||
name := dnsmessage.MustNewName("example.com.")
|
||||
data := append([]byte{0, 5}, []byte("issueletsencrypt.org")...)
|
||||
record := normalizeAnswer(dnsmessage.Resource{
|
||||
record, err := normalizeAnswer(dnsmessage.Resource{
|
||||
Header: dnsmessage.ResourceHeader{Name: name, Type: dnsmessage.Type(257), Class: dnsmessage.ClassINET, TTL: 300},
|
||||
Body: &dnsmessage.UnknownResource{Type: dnsmessage.Type(257), Data: data},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("normalize CAA: %v", err)
|
||||
}
|
||||
if record["tag"] != "issue" || record["value"] != "letsencrypt.org" {
|
||||
t.Fatalf("unexpected CAA normalization: %#v", record)
|
||||
}
|
||||
@@ -98,3 +174,131 @@ func TestParseResponseRejectsTransactionMismatch(t *testing.T) {
|
||||
t.Fatal("test response ID was not encoded")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseResponseRejectsIncompleteOrNonStandardMessages(t *testing.T) {
|
||||
queryWire, query, transactionID, err := BuildQuery("example.com", "A")
|
||||
if err != nil {
|
||||
t.Fatalf("build query: %v", err)
|
||||
}
|
||||
var request dnsmessage.Message
|
||||
if err := request.Unpack(queryWire); err != nil {
|
||||
t.Fatalf("unpack query: %v", err)
|
||||
}
|
||||
validAnswer := dnsmessage.Resource{
|
||||
Header: dnsmessage.ResourceHeader{Name: request.Questions[0].Name, Type: dnsmessage.TypeA, Class: dnsmessage.ClassINET, TTL: 60},
|
||||
Body: &dnsmessage.AResource{A: [4]byte{192, 0, 2, 1}},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
message dnsmessage.Message
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "truncated",
|
||||
message: dnsmessage.Message{Header: dnsmessage.Header{ID: transactionID, Response: true, Truncated: true},
|
||||
Questions: request.Questions},
|
||||
want: "truncated",
|
||||
},
|
||||
{
|
||||
name: "unexpected opcode",
|
||||
message: dnsmessage.Message{Header: dnsmessage.Header{ID: transactionID, Response: true, OpCode: 1},
|
||||
Questions: request.Questions},
|
||||
want: "opcode",
|
||||
},
|
||||
{
|
||||
name: "non-IN question",
|
||||
message: dnsmessage.Message{Header: dnsmessage.Header{ID: transactionID, Response: true}, Questions: []dnsmessage.Question{{
|
||||
Name: request.Questions[0].Name, Type: dnsmessage.TypeA, Class: dnsmessage.ClassCHAOS,
|
||||
}}},
|
||||
want: "question does not match",
|
||||
},
|
||||
{
|
||||
name: "non-IN answer",
|
||||
message: dnsmessage.Message{Header: dnsmessage.Header{ID: transactionID, Response: true}, Questions: request.Questions, Answers: []dnsmessage.Resource{{
|
||||
Header: dnsmessage.ResourceHeader{Name: request.Questions[0].Name, Type: dnsmessage.TypeA, Class: dnsmessage.ClassCHAOS, TTL: 60},
|
||||
Body: &dnsmessage.AResource{A: [4]byte{192, 0, 2, 1}},
|
||||
}}},
|
||||
want: "unsupported class",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
wire, err := test.message.Pack()
|
||||
if err != nil {
|
||||
t.Fatalf("pack response: %v", err)
|
||||
}
|
||||
if _, err := ParseResponse(wire, transactionID, query); err == nil || !strings.Contains(err.Error(), test.want) {
|
||||
t.Fatalf("error = %v, want substring %q", err, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
valid := dnsmessage.Message{Header: dnsmessage.Header{ID: transactionID, Response: true}, Questions: request.Questions, Answers: []dnsmessage.Resource{validAnswer}}
|
||||
if _, err := valid.Pack(); err != nil {
|
||||
t.Fatalf("valid fixture does not pack: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseResponseRejectsNonRepresentableAnswers(t *testing.T) {
|
||||
queryWire, query, transactionID, err := BuildQuery("example.com", "CAA")
|
||||
if err != nil {
|
||||
t.Fatalf("build query: %v", err)
|
||||
}
|
||||
var request dnsmessage.Message
|
||||
if err := request.Unpack(queryWire); err != nil {
|
||||
t.Fatalf("unpack query: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
record dnsmessage.Resource
|
||||
contains string
|
||||
}{
|
||||
{
|
||||
name: "truncated CAA tag",
|
||||
record: dnsmessage.Resource{
|
||||
Header: dnsmessage.ResourceHeader{Name: request.Questions[0].Name, Type: dnsmessage.Type(257), Class: dnsmessage.ClassINET, TTL: 60},
|
||||
Body: &dnsmessage.UnknownResource{Type: dnsmessage.Type(257), Data: []byte{0, 5, 'i'}},
|
||||
},
|
||||
contains: "truncated tag",
|
||||
},
|
||||
{
|
||||
name: "unknown answer type",
|
||||
record: dnsmessage.Resource{
|
||||
Header: dnsmessage.ResourceHeader{Name: request.Questions[0].Name, Type: dnsmessage.Type(99), Class: dnsmessage.ClassINET, TTL: 60},
|
||||
Body: &dnsmessage.UnknownResource{Type: dnsmessage.Type(99), Data: []byte{1, 2}},
|
||||
},
|
||||
contains: "cannot be represented",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
message := dnsmessage.Message{
|
||||
Header: dnsmessage.Header{ID: transactionID, Response: true},
|
||||
Questions: request.Questions,
|
||||
Answers: []dnsmessage.Resource{test.record},
|
||||
}
|
||||
wire, err := message.Pack()
|
||||
if err != nil {
|
||||
t.Fatalf("pack response: %v", err)
|
||||
}
|
||||
if _, err := ParseResponse(wire, transactionID, query); err == nil || !strings.Contains(err.Error(), test.contains) {
|
||||
t.Fatalf("error = %v, want substring %q", err, test.contains)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyHTTPAgeClampsAnswerTTL(t *testing.T) {
|
||||
info := DNSInfo{Answers: []AnswerRecord{
|
||||
{"ttl": uint32(120)},
|
||||
{"ttl": uint32(30)},
|
||||
}}
|
||||
applyHTTPAge(&info, 45)
|
||||
if info.Answers[0]["ttl"] != uint32(75) || info.Answers[1]["ttl"] != uint32(0) {
|
||||
t.Fatalf("unexpected aged TTLs: %#v", info.Answers)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user