Skip to content

Commit

Permalink
feat(netxlite): support extracting the CNAME
Browse files Browse the repository at this point in the history
Useful for ooni/probe#1516
  • Loading branch information
bassosimone committed Aug 22, 2022
1 parent 4cd8a79 commit 7f0a821
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 11 deletions.
5 changes: 5 additions & 0 deletions internal/model/mocks/dnsresponse.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type DNSResponse struct {
MockDecodeHTTPS func() (*model.HTTPSSvc, error)
MockDecodeLookupHost func() ([]string, error)
MockDecodeNS func() ([]*net.NS, error)
MockDecodeCNAME func() (string, error)
}

var _ model.DNSResponse = &DNSResponse{}
Expand Down Expand Up @@ -45,3 +46,7 @@ func (r *DNSResponse) DecodeLookupHost() ([]string, error) {
func (r *DNSResponse) DecodeNS() ([]*net.NS, error) {
return r.MockDecodeNS()
}

func (r *DNSResponse) DecodeCNAME() (string, error) {
return r.MockDecodeCNAME()
}
16 changes: 16 additions & 0 deletions internal/model/mocks/dnsresponse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,20 @@ func TestDNSResponse(t *testing.T) {
t.Fatal("unexpected out")
}
})

t.Run("DecodeCNAME", func(t *testing.T) {
expected := errors.New("mocked error")
r := &DNSResponse{
MockDecodeCNAME: func() (string, error) {
return "", expected
},
}
out, err := r.DecodeCNAME()
if !errors.Is(err, expected) {
t.Fatal("unexpected err", err)
}
if out != "" {
t.Fatal("unexpected out")
}
})
}
3 changes: 3 additions & 0 deletions internal/model/netx.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ type DNSResponse interface {

// DecodeNS returns all the NS entries in this response.
DecodeNS() ([]*net.NS, error)

// DecodeCNAME returns the first CNAME entry in this response.
DecodeCNAME() (string, error)
}

// The DNSDecoder decodes DNS responses.
Expand Down
14 changes: 14 additions & 0 deletions internal/netxlite/dnsdecoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,5 +166,19 @@ func (r *dnsResponse) DecodeNS() ([]*net.NS, error) {
return out, nil
}

// DecodeCNAME implements model.DNSResponse.DecodeCNAME.
func (r *dnsResponse) DecodeCNAME() (string, error) {
if err := r.rcodeToError(); err != nil {
return "", err
}
for _, answer := range r.msg.Answer {
switch avalue := answer.(type) {
case *dns.CNAME:
return avalue.Target, nil
}
}
return "", ErrOODNSNoAnswer
}

var _ model.DNSDecoder = &DNSDecoderMiekg{}
var _ model.DNSResponse = &dnsResponse{}
112 changes: 102 additions & 10 deletions internal/netxlite/dnsdecoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func TestDNSDecoderMiekg(t *testing.T) {
queryID = 17
unrelatedID = 14
)
reply := dnsGenLookupHostReplySuccess(dnsGenQuery(dns.TypeA, queryID))
reply := dnsGenLookupHostReplySuccess(dnsGenQuery(dns.TypeA, queryID), nil)
resp, err := d.DecodeResponse(reply, &mocks.DNSQuery{
MockID: func() uint16 {
return unrelatedID
Expand All @@ -62,7 +62,7 @@ func TestDNSDecoderMiekg(t *testing.T) {
d := &DNSDecoderMiekg{}
queryID := dns.Id()
rawQuery := dnsGenQuery(dns.TypeA, queryID)
rawResponse := dnsGenLookupHostReplySuccess(rawQuery)
rawResponse := dnsGenLookupHostReplySuccess(rawQuery, nil)
query := &mocks.DNSQuery{
MockID: func() uint16 {
return queryID
Expand All @@ -81,7 +81,7 @@ func TestDNSDecoderMiekg(t *testing.T) {
d := &DNSDecoderMiekg{}
queryID := dns.Id()
rawQuery := dnsGenQuery(dns.TypeA, queryID)
rawResponse := dnsGenLookupHostReplySuccess(rawQuery)
rawResponse := dnsGenLookupHostReplySuccess(rawQuery, nil)
query := &mocks.DNSQuery{
MockID: func() uint16 {
return queryID
Expand Down Expand Up @@ -323,7 +323,7 @@ func TestDNSDecoderMiekg(t *testing.T) {
})
})

t.Run("dnsResponse.LookupHost", func(t *testing.T) {
t.Run("dnsResponse.DecodeLookupHost", func(t *testing.T) {
t.Run("with failure", func(t *testing.T) {
// Ensure that we're not trying to decode if rcode != 0
d := &DNSDecoderMiekg{}
Expand Down Expand Up @@ -352,7 +352,7 @@ func TestDNSDecoderMiekg(t *testing.T) {
d := &DNSDecoderMiekg{}
queryID := dns.Id()
rawQuery := dnsGenQuery(dns.TypeA, queryID)
rawResponse := dnsGenLookupHostReplySuccess(rawQuery)
rawResponse := dnsGenLookupHostReplySuccess(rawQuery, nil)
query := &mocks.DNSQuery{
MockID: func() uint16 {
return queryID
Expand All @@ -375,7 +375,7 @@ func TestDNSDecoderMiekg(t *testing.T) {
d := &DNSDecoderMiekg{}
queryID := dns.Id()
rawQuery := dnsGenQuery(dns.TypeA, queryID)
rawResponse := dnsGenLookupHostReplySuccess(rawQuery, "1.1.1.1", "8.8.8.8")
rawResponse := dnsGenLookupHostReplySuccess(rawQuery, nil, "1.1.1.1", "8.8.8.8")
query := &mocks.DNSQuery{
MockID: func() uint16 {
return queryID
Expand Down Expand Up @@ -407,7 +407,7 @@ func TestDNSDecoderMiekg(t *testing.T) {
d := &DNSDecoderMiekg{}
queryID := dns.Id()
rawQuery := dnsGenQuery(dns.TypeAAAA, queryID)
rawResponse := dnsGenLookupHostReplySuccess(rawQuery, "::1", "fe80::1")
rawResponse := dnsGenLookupHostReplySuccess(rawQuery, nil, "::1", "fe80::1")
query := &mocks.DNSQuery{
MockID: func() uint16 {
return queryID
Expand Down Expand Up @@ -439,7 +439,7 @@ func TestDNSDecoderMiekg(t *testing.T) {
d := &DNSDecoderMiekg{}
queryID := dns.Id()
rawQuery := dnsGenQuery(dns.TypeAAAA, queryID)
rawResponse := dnsGenLookupHostReplySuccess(rawQuery, "1.1.1.1", "8.8.8.8")
rawResponse := dnsGenLookupHostReplySuccess(rawQuery, nil, "1.1.1.1", "8.8.8.8")
query := &mocks.DNSQuery{
MockID: func() uint16 {
return queryID
Expand All @@ -465,7 +465,7 @@ func TestDNSDecoderMiekg(t *testing.T) {
d := &DNSDecoderMiekg{}
queryID := dns.Id()
rawQuery := dnsGenQuery(dns.TypeA, queryID)
rawResponse := dnsGenLookupHostReplySuccess(rawQuery, "::1", "fe80::1")
rawResponse := dnsGenLookupHostReplySuccess(rawQuery, nil, "::1", "fe80::1")
query := &mocks.DNSQuery{
MockID: func() uint16 {
return queryID
Expand All @@ -487,6 +487,82 @@ func TestDNSDecoderMiekg(t *testing.T) {
}
})
})

t.Run("dnsResponse.DecodeCNAME", func(t *testing.T) {
t.Run("with failure", func(t *testing.T) {
// Ensure that we're not trying to decode if rcode != 0
d := &DNSDecoderMiekg{}
queryID := dns.Id()
rawQuery := dnsGenQuery(dns.TypeA, queryID)
rawResponse := dnsGenReplyWithError(rawQuery, dns.RcodeRefused)
query := &mocks.DNSQuery{
MockID: func() uint16 {
return queryID
},
}
resp, err := d.DecodeResponse(rawResponse, query)
if err != nil {
t.Fatal(err)
}
cname, err := resp.DecodeCNAME()
if !errors.Is(err, ErrOODNSRefused) {
t.Fatal("unexpected err", err)
}
if cname != "" {
t.Fatal("expected empty cname result")
}
})

t.Run("with empty answer", func(t *testing.T) {
d := &DNSDecoderMiekg{}
queryID := dns.Id()
rawQuery := dnsGenQuery(dns.TypeA, queryID)
var expectedCNAME *dnsCNAME = nil // explicity not set
rawResponse := dnsGenLookupHostReplySuccess(rawQuery, expectedCNAME, "8.8.8.8")
query := &mocks.DNSQuery{
MockID: func() uint16 {
return queryID
},
}
resp, err := d.DecodeResponse(rawResponse, query)
if err != nil {
t.Fatal(err)
}
cname, err := resp.DecodeCNAME()
if !errors.Is(err, ErrOODNSNoAnswer) {
t.Fatal("unexpected err", err)
}
if cname != "" {
t.Fatal("expected empty cname result")
}
})

t.Run("with full answer", func(t *testing.T) {
expectedCNAME := &dnsCNAME{
CNAME: "dns.google.",
}
d := &DNSDecoderMiekg{}
queryID := dns.Id()
rawQuery := dnsGenQuery(dns.TypeA, queryID)
rawResponse := dnsGenLookupHostReplySuccess(rawQuery, expectedCNAME, "8.8.8.8")
query := &mocks.DNSQuery{
MockID: func() uint16 {
return queryID
},
}
resp, err := d.DecodeResponse(rawResponse, query)
if err != nil {
t.Fatal(err)
}
cname, err := resp.DecodeCNAME()
if err != nil {
t.Fatal(err)
}
if cname != expectedCNAME.CNAME {
t.Fatal("unexpected cname", cname)
}
})
})
})
}

Expand Down Expand Up @@ -522,9 +598,14 @@ func dnsGenReplyWithError(rawQuery []byte, code int) []byte {
return data
}

// dnsCNAME is the DNS cname to include into a response.
type dnsCNAME struct {
CNAME string
}

// dnsGenLookupHostReplySuccess generates a successful DNS reply containing the given ips...
// in the answers where each answer's type depends on the IP's type (A/AAAA).
func dnsGenLookupHostReplySuccess(rawQuery []byte, ips ...string) []byte {
func dnsGenLookupHostReplySuccess(rawQuery []byte, cname *dnsCNAME, ips ...string) []byte {
query := new(dns.Msg)
err := query.Unpack(rawQuery)
runtimex.PanicOnError(err, "query.Unpack failed")
Expand Down Expand Up @@ -562,6 +643,17 @@ func dnsGenLookupHostReplySuccess(rawQuery []byte, ips ...string) []byte {
})
}
}
if cname != nil {
reply.Answer = append(reply.Answer, &dns.CNAME{
Hdr: dns.RR_Header{
Name: question.Name,
Rrtype: dns.TypeCNAME,
Class: dns.ClassINET,
Ttl: 0,
},
Target: cname.CNAME,
})
}
data, err := reply.Pack()
runtimex.PanicOnError(err, "reply.Pack failed")
return data
Expand Down
8 changes: 7 additions & 1 deletion internal/netxlite/dnsovergetaddrinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,21 @@ func (txp *dnsOverGetaddrinfoTransport) RoundTrip(
if query.Type() != dns.TypeANY {
return nil, ErrNoDNSTransport
}
addrs, _, err := txp.lookup(ctx, query.Domain())
addrs, cname, err := txp.lookup(ctx, query.Domain())
if err != nil {
return nil, err
}
resp := &dnsOverGetaddrinfoResponse{
addrs: addrs,
cname: cname,
query: query,
}
return resp, nil
}

type dnsOverGetaddrinfoResponse struct {
addrs []string
cname string
query model.DNSQuery
}

Expand Down Expand Up @@ -131,3 +133,7 @@ func (r *dnsOverGetaddrinfoResponse) DecodeLookupHost() ([]string, error) {
func (r *dnsOverGetaddrinfoResponse) DecodeNS() ([]*net.NS, error) {
return nil, ErrNoDNSTransport
}

func (r *dnsOverGetaddrinfoResponse) DecodeCNAME() (string, error) {
return r.cname, nil
}

0 comments on commit 7f0a821

Please sign in to comment.