Files
vps/.agents/skills/encrypted-dns-skill/internal/edns/dns_test.go
T

305 lines
11 KiB
Go

package edns
import (
"encoding/binary"
"reflect"
"strings"
"testing"
"golang.org/x/net/dns/dnsmessage"
)
func TestBuildAndParseResponse(t *testing.T) {
queryWire, query, transactionID, err := BuildQuery("Example.COM.", "A")
if err != nil {
t.Fatalf("build query: %v", err)
}
if query.Name != "example.com" || query.Type != "A" {
t.Fatalf("canonical query = %#v", query)
}
var request dnsmessage.Message
if err := request.Unpack(queryWire); err != nil {
t.Fatalf("unpack query: %v", err)
}
response := dnsmessage.Message{
Header: dnsmessage.Header{
ID: transactionID,
Response: true,
RecursionDesired: true,
RecursionAvailable: true,
AuthenticData: true,
},
Questions: request.Questions,
Answers: []dnsmessage.Resource{{
Header: dnsmessage.ResourceHeader{Name: request.Questions[0].Name, Class: dnsmessage.ClassINET, TTL: 60},
Body: &dnsmessage.AResource{A: [4]byte{192, 0, 2, 1}},
}},
}
responseWire, err := response.Pack()
if err != nil {
t.Fatalf("pack response: %v", err)
}
dnsResult, err := ParseResponse(responseWire, transactionID, query)
if err != nil {
t.Fatalf("parse response: %v", err)
}
if dnsResult.RCode != "NOERROR" || !dnsResult.ResolverReportsDNSSECAuthenticated {
t.Fatalf("unexpected DNS result: %#v", dnsResult)
}
if got := dnsResult.Answers[0]["address"]; got != "192.0.2.1" {
t.Fatalf("address = %v, want 192.0.2.1", got)
}
}
func TestBuildQueryIDNAAndBlockedNames(t *testing.T) {
_, query, _, err := BuildQuery("bücher.example", "AAAA")
if err != nil {
t.Fatalf("build IDNA query: %v", err)
}
if query.Name != "xn--bcher-kva.example" {
t.Fatalf("IDNA name = %q", query.Name)
}
blocked := []string{"localhost", "router.local", "service.internal", "host.lan", "1.0.0.127.in-addr.arpa", "127.0.0.1"}
for _, name := range blocked {
if _, _, _, err := BuildQuery(name, "A"); err == nil {
t.Errorf("BuildQuery(%q) succeeded, want policy error", name)
}
}
}
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, 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)
}
}
func TestParseResponseRejectsTransactionMismatch(t *testing.T) {
name := dnsmessage.MustNewName("example.com.")
message := dnsmessage.Message{
Header: dnsmessage.Header{ID: 2, Response: true},
Questions: []dnsmessage.Question{{Name: name, Type: dnsmessage.TypeA, Class: dnsmessage.ClassINET}},
}
wire, err := message.Pack()
if err != nil {
t.Fatalf("pack response: %v", err)
}
if _, err := ParseResponse(wire, 1, QueryInfo{Name: "example.com", Type: "A"}); err == nil {
t.Fatal("transaction mismatch was accepted")
}
if binary.BigEndian.Uint16(wire[:2]) != 2 {
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)
}
}