mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2024-12-30 18:48:16 +03:00
ac7634da37
Merge in DNS/adguard-home from imp-ups-tests to master
Squashed commit of the following:
commit 60c22e8dfb81c3613b00994ccf01ba96137946f9
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date: Fri Oct 28 19:26:34 2022 +0300
all: imp names, fix aaaa
commit c3bd8a9fa95a86f5af86ea2956565337ee620c99
Merge: 23f82237 746e9df7
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date: Fri Oct 28 19:16:56 2022 +0300
Merge branch 'master' into imp-ups-tests
commit 23f82237a81b0ef22cf418afdc2ec95dc50639a3
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date: Fri Oct 28 18:14:27 2022 +0300
all: imp upstream tests
227 lines
6.1 KiB
Go
227 lines
6.1 KiB
Go
package aghtest
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"strings"
|
|
|
|
"github.com/AdguardTeam/dnsproxy/upstream"
|
|
"github.com/AdguardTeam/golibs/errors"
|
|
"github.com/miekg/dns"
|
|
)
|
|
|
|
// Additional Upstream Testing Utilities
|
|
|
|
// Upstream is a mock implementation of upstream.Upstream.
|
|
//
|
|
// TODO(a.garipov): Replace with UpstreamMock and rename it to just Upstream.
|
|
type Upstream struct {
|
|
// CName is a map of hostname to canonical name.
|
|
CName map[string][]string
|
|
// IPv4 is a map of hostname to IPv4.
|
|
IPv4 map[string][]net.IP
|
|
// IPv6 is a map of hostname to IPv6.
|
|
IPv6 map[string][]net.IP
|
|
}
|
|
|
|
var _ upstream.Upstream = (*Upstream)(nil)
|
|
|
|
// Exchange implements the [upstream.Upstream] interface for *Upstream.
|
|
//
|
|
// TODO(a.garipov): Split further into handlers.
|
|
func (u *Upstream) Exchange(m *dns.Msg) (resp *dns.Msg, err error) {
|
|
resp = new(dns.Msg).SetReply(m)
|
|
|
|
if len(m.Question) == 0 {
|
|
return nil, fmt.Errorf("question should not be empty")
|
|
}
|
|
|
|
q := m.Question[0]
|
|
name := q.Name
|
|
for _, cname := range u.CName[name] {
|
|
resp.Answer = append(resp.Answer, &dns.CNAME{
|
|
Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeCNAME},
|
|
Target: cname,
|
|
})
|
|
}
|
|
|
|
qtype := q.Qtype
|
|
hdr := dns.RR_Header{
|
|
Name: name,
|
|
Rrtype: qtype,
|
|
}
|
|
|
|
switch qtype {
|
|
case dns.TypeA:
|
|
for _, ip := range u.IPv4[name] {
|
|
resp.Answer = append(resp.Answer, &dns.A{Hdr: hdr, A: ip})
|
|
}
|
|
case dns.TypeAAAA:
|
|
for _, ip := range u.IPv6[name] {
|
|
resp.Answer = append(resp.Answer, &dns.AAAA{Hdr: hdr, AAAA: ip})
|
|
}
|
|
}
|
|
if len(resp.Answer) == 0 {
|
|
resp.SetRcode(m, dns.RcodeNameError)
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
// Address implements [upstream.Upstream] interface for *Upstream.
|
|
func (u *Upstream) Address() string {
|
|
return "todo.upstream.example"
|
|
}
|
|
|
|
// Close implements [upstream.Upstream] interface for *Upstream.
|
|
func (u *Upstream) Close() (err error) {
|
|
return nil
|
|
}
|
|
|
|
// MatchedResponse is a test helper that returns a response with answer if req
|
|
// has question type qt, and target targ. Otherwise, it returns nil.
|
|
//
|
|
// req must not be nil and req.Question must have a length of 1. Answer is
|
|
// interpreted in the following ways:
|
|
//
|
|
// - For A and AAAA queries, answer must be an IP address of the corresponding
|
|
// protocol version.
|
|
//
|
|
// - For PTR queries, answer should be a domain name in the response.
|
|
//
|
|
// If the answer does not correspond to the question type, MatchedResponse panics.
|
|
// Panics are used instead of [testing.TB], because the helper is intended to
|
|
// use in [UpstreamMock.OnExchange] callbacks, which are usually called in a
|
|
// separate goroutine.
|
|
//
|
|
// TODO(a.garipov): Consider adding version with DNS class as well.
|
|
func MatchedResponse(req *dns.Msg, qt uint16, targ, answer string) (resp *dns.Msg) {
|
|
if req == nil || len(req.Question) != 1 {
|
|
panic(fmt.Errorf("bad req: %+v", req))
|
|
}
|
|
|
|
q := req.Question[0]
|
|
targ = dns.Fqdn(targ)
|
|
if q.Qclass != dns.ClassINET || q.Qtype != qt || q.Name != targ {
|
|
return nil
|
|
}
|
|
|
|
respHdr := dns.RR_Header{
|
|
Name: targ,
|
|
Rrtype: qt,
|
|
Class: dns.ClassINET,
|
|
Ttl: 60,
|
|
}
|
|
|
|
resp = new(dns.Msg).SetReply(req)
|
|
switch qt {
|
|
case dns.TypeA:
|
|
resp.Answer = mustAnsA(respHdr, answer)
|
|
case dns.TypeAAAA:
|
|
resp.Answer = mustAnsAAAA(respHdr, answer)
|
|
case dns.TypePTR:
|
|
resp.Answer = []dns.RR{&dns.PTR{
|
|
Hdr: respHdr,
|
|
Ptr: answer,
|
|
}}
|
|
default:
|
|
panic(fmt.Errorf("aghtest: bad question type: %s", dns.Type(qt)))
|
|
}
|
|
|
|
return resp
|
|
}
|
|
|
|
// mustAnsA returns valid answer records if s is a valid IPv4 address.
|
|
// Otherwise, mustAnsA panics.
|
|
func mustAnsA(respHdr dns.RR_Header, s string) (ans []dns.RR) {
|
|
ip, err := netip.ParseAddr(s)
|
|
if err != nil || !ip.Is4() {
|
|
panic(fmt.Errorf("aghtest: bad A answer: %+v", s))
|
|
}
|
|
|
|
return []dns.RR{&dns.A{
|
|
Hdr: respHdr,
|
|
A: ip.AsSlice(),
|
|
}}
|
|
}
|
|
|
|
// mustAnsAAAA returns valid answer records if s is a valid IPv6 address.
|
|
// Otherwise, mustAnsAAAA panics.
|
|
func mustAnsAAAA(respHdr dns.RR_Header, s string) (ans []dns.RR) {
|
|
ip, err := netip.ParseAddr(s)
|
|
if err != nil || !ip.Is6() {
|
|
panic(fmt.Errorf("aghtest: bad AAAA answer: %+v", s))
|
|
}
|
|
|
|
return []dns.RR{&dns.AAAA{
|
|
Hdr: respHdr,
|
|
AAAA: ip.AsSlice(),
|
|
}}
|
|
}
|
|
|
|
// NewUpstreamMock returns an [*UpstreamMock], fields OnAddress and OnClose of
|
|
// which are set to stubs that return "upstream.example" and nil respectively.
|
|
// The field OnExchange is set to onExc.
|
|
func NewUpstreamMock(onExc func(req *dns.Msg) (resp *dns.Msg, err error)) (u *UpstreamMock) {
|
|
return &UpstreamMock{
|
|
OnAddress: func() (addr string) { return "upstream.example" },
|
|
OnExchange: onExc,
|
|
OnClose: func() (err error) { return nil },
|
|
}
|
|
}
|
|
|
|
// NewBlockUpstream returns an [*UpstreamMock] that works like an upstream that
|
|
// supports hash-based safe-browsing/adult-blocking feature. If shouldBlock is
|
|
// true, hostname's actual hash is returned, blocking it. Otherwise, it returns
|
|
// a different hash.
|
|
func NewBlockUpstream(hostname string, shouldBlock bool) (u *UpstreamMock) {
|
|
hash := sha256.Sum256([]byte(hostname))
|
|
hashStr := hex.EncodeToString(hash[:])
|
|
if !shouldBlock {
|
|
hashStr = hex.EncodeToString(hash[:])[:2] + strings.Repeat("ab", 28)
|
|
}
|
|
|
|
ans := &dns.TXT{
|
|
Hdr: dns.RR_Header{
|
|
Name: "",
|
|
Rrtype: dns.TypeTXT,
|
|
Class: dns.ClassINET,
|
|
Ttl: 60,
|
|
},
|
|
Txt: []string{hashStr},
|
|
}
|
|
respTmpl := &dns.Msg{
|
|
Answer: []dns.RR{ans},
|
|
}
|
|
|
|
return &UpstreamMock{
|
|
OnAddress: func() (addr string) { return "sbpc.upstream.example" },
|
|
OnExchange: func(req *dns.Msg) (resp *dns.Msg, err error) {
|
|
resp = respTmpl.Copy()
|
|
resp.SetReply(req)
|
|
resp.Answer[0].(*dns.TXT).Hdr.Name = req.Question[0].Name
|
|
|
|
return resp, nil
|
|
},
|
|
OnClose: func() (err error) { return nil },
|
|
}
|
|
}
|
|
|
|
// ErrUpstream is the error returned from the [*UpstreamMock] created by
|
|
// [NewErrorUpstream].
|
|
const ErrUpstream errors.Error = "test upstream error"
|
|
|
|
// NewErrorUpstream returns an [*UpstreamMock] that returns [ErrUpstream] from
|
|
// its Exchange method.
|
|
func NewErrorUpstream() (u *UpstreamMock) {
|
|
return &UpstreamMock{
|
|
OnAddress: func() (addr string) { return "error.upstream.example" },
|
|
OnExchange: func(_ *dns.Msg) (resp *dns.Msg, err error) {
|
|
return nil, errors.Error("test upstream error")
|
|
},
|
|
OnClose: func() (err error) { return nil },
|
|
}
|
|
}
|