diff --git a/internal/aghtest/upstream.go b/internal/aghtest/upstream.go index 77a2ae1d..be6bbdbe 100644 --- a/internal/aghtest/upstream.go +++ b/internal/aghtest/upstream.go @@ -5,13 +5,12 @@ import ( "encoding/hex" "fmt" "net" + "net/netip" "strings" - "testing" "github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/golibs/errors" "github.com/miekg/dns" - "github.com/stretchr/testify/require" ) // Additional Upstream Testing Utilities @@ -26,51 +25,10 @@ type Upstream struct { IPv4 map[string][]net.IP // IPv6 is a map of hostname to IPv6. IPv6 map[string][]net.IP - // Reverse is a map of address to domain name. - Reverse map[string][]string - // Addr is the address for Address method. - Addr string } var _ upstream.Upstream = (*Upstream)(nil) -// RespondTo returns a response with answer if req has class cl, question type -// qt, and target targ. -func RespondTo(t testing.TB, req *dns.Msg, cl, qt uint16, targ, answer string) (resp *dns.Msg) { - t.Helper() - - require.NotNil(t, req) - require.Len(t, req.Question, 1) - - q := req.Question[0] - targ = dns.Fqdn(targ) - if q.Qclass != cl || q.Qtype != qt || q.Name != targ { - return nil - } - - respHdr := dns.RR_Header{ - Name: targ, - Rrtype: qt, - Class: cl, - Ttl: 60, - } - - resp = new(dns.Msg).SetReply(req) - switch qt { - case dns.TypePTR: - resp.Answer = []dns.RR{ - &dns.PTR{ - Hdr: respHdr, - Ptr: answer, - }, - } - default: - t.Fatalf("unsupported question type: %s", dns.Type(qt)) - } - - return resp -} - // Exchange implements the [upstream.Upstream] interface for *Upstream. // // TODO(a.garipov): Split further into handlers. @@ -105,10 +63,6 @@ func (u *Upstream) Exchange(m *dns.Msg) (resp *dns.Msg, err error) { for _, ip := range u.IPv6[name] { resp.Answer = append(resp.Answer, &dns.AAAA{Hdr: hdr, AAAA: ip}) } - case dns.TypePTR: - for _, name := range u.Reverse[name] { - resp.Answer = append(resp.Answer, &dns.PTR{Hdr: hdr, Ptr: name}) - } } if len(resp.Answer) == 0 { resp.SetRcode(m, dns.RcodeNameError) @@ -119,7 +73,7 @@ func (u *Upstream) Exchange(m *dns.Msg) (resp *dns.Msg, err error) { // Address implements [upstream.Upstream] interface for *Upstream. func (u *Upstream) Address() string { - return u.Addr + return "todo.upstream.example" } // Close implements [upstream.Upstream] interface for *Upstream. @@ -127,6 +81,98 @@ 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 @@ -152,9 +198,7 @@ func NewBlockUpstream(hostname string, shouldBlock bool) (u *UpstreamMock) { } return &UpstreamMock{ - OnAddress: func() (addr string) { - return "sbpc.upstream.example" - }, + OnAddress: func() (addr string) { return "sbpc.upstream.example" }, OnExchange: func(req *dns.Msg) (resp *dns.Msg, err error) { resp = respTmpl.Copy() resp.SetReply(req) @@ -162,6 +206,7 @@ func NewBlockUpstream(hostname string, shouldBlock bool) (u *UpstreamMock) { return resp, nil }, + OnClose: func() (err error) { return nil }, } } @@ -173,11 +218,10 @@ const ErrUpstream errors.Error = "test upstream error" // its Exchange method. func NewErrorUpstream() (u *UpstreamMock) { return &UpstreamMock{ - OnAddress: func() (addr string) { - return "error.upstream.example" - }, + 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 }, } } diff --git a/internal/dnsforward/dns_test.go b/internal/dnsforward/dns_test.go index acaf5f55..1c558744 100644 --- a/internal/dnsforward/dns_test.go +++ b/internal/dnsforward/dns_test.go @@ -457,19 +457,13 @@ func TestServer_ProcessRestrictLocal(t *testing.T) { intPTRAnswer = "some.local-client." ) - ups := &aghtest.UpstreamMock{ - OnAddress: func() (addr string) { return "upstream.example" }, - OnExchange: func(req *dns.Msg) (resp *dns.Msg, err error) { - resp = aghalg.Coalesce( - aghtest.RespondTo(t, req, dns.ClassINET, dns.TypePTR, extPTRQuestion, extPTRAnswer), - aghtest.RespondTo(t, req, dns.ClassINET, dns.TypePTR, intPTRQuestion, intPTRAnswer), - new(dns.Msg).SetRcode(req, dns.RcodeNameError), - ) - - return resp, nil - }, - OnClose: func() (err error) { return nil }, - } + ups := aghtest.NewUpstreamMock(func(req *dns.Msg) (resp *dns.Msg, err error) { + return aghalg.Coalesce( + aghtest.MatchedResponse(req, dns.TypePTR, extPTRQuestion, extPTRAnswer), + aghtest.MatchedResponse(req, dns.TypePTR, intPTRQuestion, intPTRAnswer), + new(dns.Msg).SetRcode(req, dns.RcodeNameError), + ), nil + }) s := createTestServer(t, &filtering.Config{}, ServerConfig{ UDPListenAddrs: []*net.UDPAddr{{}}, @@ -547,18 +541,12 @@ func TestServer_ProcessLocalPTR_usingResolvers(t *testing.T) { UDPListenAddrs: []*net.UDPAddr{{}}, TCPListenAddrs: []*net.TCPAddr{{}}, }, - &aghtest.UpstreamMock{ - OnAddress: func() (addr string) { return "upstream.example" }, - OnExchange: func(req *dns.Msg) (resp *dns.Msg, err error) { - resp = aghalg.Coalesce( - aghtest.RespondTo(t, req, dns.ClassINET, dns.TypePTR, reqAddr, locDomain), - new(dns.Msg).SetRcode(req, dns.RcodeNameError), - ) - - return resp, nil - }, - OnClose: func() (err error) { return nil }, - }, + aghtest.NewUpstreamMock(func(req *dns.Msg) (resp *dns.Msg, err error) { + return aghalg.Coalesce( + aghtest.MatchedResponse(req, dns.TypePTR, reqAddr, locDomain), + new(dns.Msg).SetRcode(req, dns.RcodeNameError), + ), nil + }), ) var proxyCtx *proxy.DNSContext diff --git a/internal/dnsforward/dnsforward_test.go b/internal/dnsforward/dnsforward_test.go index 06dc7260..56c21516 100644 --- a/internal/dnsforward/dnsforward_test.go +++ b/internal/dnsforward/dnsforward_test.go @@ -161,8 +161,23 @@ func createTestTLS(t *testing.T, tlsConf TLSConfig) (s *Server, certPem []byte) return s, certPem } +const googleDomainName = "google-public-dns-a.google.com." + func createGoogleATestMessage() *dns.Msg { - return createTestMessage("google-public-dns-a.google.com.") + return createTestMessage(googleDomainName) +} + +func newGoogleUpstream() (u upstream.Upstream) { + return &aghtest.UpstreamMock{ + OnAddress: func() (addr string) { return "google.upstream.example" }, + OnExchange: func(req *dns.Msg) (resp *dns.Msg, err error) { + return aghalg.Coalesce( + aghtest.MatchedResponse(req, dns.TypeA, googleDomainName, "8.8.8.8"), + new(dns.Msg).SetRcode(req, dns.RcodeNameError), + ), nil + }, + OnClose: func() (err error) { return nil }, + } } func createTestMessage(host string) *dns.Msg { @@ -247,13 +262,7 @@ func TestServer(t *testing.T) { UDPListenAddrs: []*net.UDPAddr{{}}, TCPListenAddrs: []*net.TCPAddr{{}}, }, nil) - s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{ - &aghtest.Upstream{ - IPv4: map[string][]net.IP{ - "google-public-dns-a.google.com.": {{8, 8, 8, 8}}, - }, - }, - } + s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{newGoogleUpstream()} startDeferStop(t, s) testCases := []struct { @@ -320,13 +329,7 @@ func TestServerWithProtectionDisabled(t *testing.T) { UDPListenAddrs: []*net.UDPAddr{{}}, TCPListenAddrs: []*net.TCPAddr{{}}, }, nil) - s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{ - &aghtest.Upstream{ - IPv4: map[string][]net.IP{ - "google-public-dns-a.google.com.": {{8, 8, 8, 8}}, - }, - }, - } + s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{newGoogleUpstream()} startDeferStop(t, s) // Message over UDP. @@ -343,13 +346,7 @@ func TestDoTServer(t *testing.T) { s, certPem := createTestTLS(t, TLSConfig{ TLSListenAddrs: []*net.TCPAddr{{}}, }) - s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{ - &aghtest.Upstream{ - IPv4: map[string][]net.IP{ - "google-public-dns-a.google.com.": {{8, 8, 8, 8}}, - }, - }, - } + s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{newGoogleUpstream()} startDeferStop(t, s) // Add our self-signed generated config to roots. @@ -373,13 +370,7 @@ func TestDoQServer(t *testing.T) { s, _ := createTestTLS(t, TLSConfig{ QUICListenAddrs: []*net.UDPAddr{{IP: net.IP{127, 0, 0, 1}}}, }) - s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{ - &aghtest.Upstream{ - IPv4: map[string][]net.IP{ - "google-public-dns-a.google.com.": {{8, 8, 8, 8}}, - }, - }, - } + s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{newGoogleUpstream()} startDeferStop(t, s) // Create a DNS-over-QUIC upstream. @@ -417,13 +408,7 @@ func TestServerRace(t *testing.T) { ConfigModified: func() {}, } s := createTestServer(t, filterConf, forwardConf, nil) - s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{ - &aghtest.Upstream{ - IPv4: map[string][]net.IP{ - "google-public-dns-a.google.com.": {{8, 8, 8, 8}}, - }, - }, - } + s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{newGoogleUpstream()} startDeferStop(t, s) // Message over UDP. @@ -557,11 +542,12 @@ func TestServerCustomClientUpstream(t *testing.T) { } s := createTestServer(t, &filtering.Config{}, forwardConf, nil) s.conf.GetCustomUpstreamByClient = func(_ string) (conf *proxy.UpstreamConfig, err error) { - ups := &aghtest.Upstream{ - IPv4: map[string][]net.IP{ - "host.": {{192, 168, 0, 1}}, - }, - } + ups := aghtest.NewUpstreamMock(func(req *dns.Msg) (resp *dns.Msg, err error) { + return aghalg.Coalesce( + aghtest.MatchedResponse(req, dns.TypeA, "host", "192.168.0.1"), + new(dns.Msg).SetRcode(req, dns.RcodeNameError), + ), nil + }) return &proxy.UpstreamConfig{ Upstreams: []upstream.Upstream{ups}, @@ -604,7 +590,6 @@ func TestBlockCNAMEProtectionEnabled(t *testing.T) { testUpstm := &aghtest.Upstream{ CName: testCNAMEs, IPv4: testIPv4, - IPv6: nil, } s.conf.ProtectionEnabled = false s.dnsProxy.UpstreamConfig = &proxy.UpstreamConfig{ @@ -931,16 +916,13 @@ func TestRewrite(t *testing.T) { }, })) - s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{ - &aghtest.Upstream{ - CName: map[string][]string{ - "example.org": {"somename"}, - }, - IPv4: map[string][]net.IP{ - "example.org.": {{4, 3, 2, 1}}, - }, - }, - } + ups := aghtest.NewUpstreamMock(func(req *dns.Msg) (resp *dns.Msg, err error) { + return aghalg.Coalesce( + aghtest.MatchedResponse(req, dns.TypeA, "example.org", "4.3.2.1"), + new(dns.Msg).SetRcode(req, dns.RcodeNameError), + ), nil + }) + s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{ups} startDeferStop(t, s) addr := s.dnsProxy.Addr(proxy.ProtoUDP) @@ -1212,12 +1194,10 @@ func TestServer_Exchange(t *testing.T) { extUpstream := &aghtest.UpstreamMock{ OnAddress: func() (addr string) { return "external.upstream.example" }, OnExchange: func(req *dns.Msg) (resp *dns.Msg, err error) { - resp = aghalg.Coalesce( - aghtest.RespondTo(t, req, dns.ClassINET, dns.TypePTR, revExtIPv4, onesHost), + return aghalg.Coalesce( + aghtest.MatchedResponse(req, dns.TypePTR, revExtIPv4, onesHost), new(dns.Msg).SetRcode(req, dns.RcodeNameError), - ) - - return resp, nil + ), nil }, } @@ -1227,12 +1207,10 @@ func TestServer_Exchange(t *testing.T) { locUpstream := &aghtest.UpstreamMock{ OnAddress: func() (addr string) { return "local.upstream.example" }, OnExchange: func(req *dns.Msg) (resp *dns.Msg, err error) { - resp = aghalg.Coalesce( - aghtest.RespondTo(t, req, dns.ClassINET, dns.TypePTR, revLocIPv4, localDomainHost), + return aghalg.Coalesce( + aghtest.MatchedResponse(req, dns.TypePTR, revLocIPv4, localDomainHost), new(dns.Msg).SetRcode(req, dns.RcodeNameError), - ) - - return resp, nil + ), nil }, } diff --git a/internal/home/rdns_test.go b/internal/home/rdns_test.go index e8d28e61..9f90ce5a 100644 --- a/internal/home/rdns_test.go +++ b/internal/home/rdns_test.go @@ -189,13 +189,11 @@ func TestRDNS_WorkerLoop(t *testing.T) { locUpstream := &aghtest.UpstreamMock{ OnAddress: func() (addr string) { return "local.upstream.example" }, OnExchange: func(req *dns.Msg) (resp *dns.Msg, err error) { - resp = aghalg.Coalesce( - aghtest.RespondTo(t, req, dns.ClassINET, dns.TypePTR, revIPv4, "local.domain"), - aghtest.RespondTo(t, req, dns.ClassINET, dns.TypePTR, revIPv6, "ipv6.domain"), + return aghalg.Coalesce( + aghtest.MatchedResponse(req, dns.TypePTR, revIPv4, "local.domain"), + aghtest.MatchedResponse(req, dns.TypePTR, revIPv6, "ipv6.domain"), new(dns.Msg).SetRcode(req, dns.RcodeNameError), - ) - - return resp, nil + ), nil }, }