From 7f0a821576afe53254d6457171f6f2715bae19f9 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Mon, 22 Aug 2022 15:57:33 +0200 Subject: [PATCH] feat(netxlite): support extracting the CNAME Useful for https://github.com/ooni/probe/issues/1516 --- internal/model/mocks/dnsresponse.go | 5 + internal/model/mocks/dnsresponse_test.go | 16 ++++ internal/model/netx.go | 3 + internal/netxlite/dnsdecoder.go | 14 +++ internal/netxlite/dnsdecoder_test.go | 112 +++++++++++++++++++++-- internal/netxlite/dnsovergetaddrinfo.go | 8 +- 6 files changed, 147 insertions(+), 11 deletions(-) diff --git a/internal/model/mocks/dnsresponse.go b/internal/model/mocks/dnsresponse.go index 7752c42b71..d2dcf96292 100644 --- a/internal/model/mocks/dnsresponse.go +++ b/internal/model/mocks/dnsresponse.go @@ -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{} @@ -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() +} diff --git a/internal/model/mocks/dnsresponse_test.go b/internal/model/mocks/dnsresponse_test.go index e362544226..68e12d26c0 100644 --- a/internal/model/mocks/dnsresponse_test.go +++ b/internal/model/mocks/dnsresponse_test.go @@ -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") + } + }) } diff --git a/internal/model/netx.go b/internal/model/netx.go index 65e1ff4e52..0c8d77233b 100644 --- a/internal/model/netx.go +++ b/internal/model/netx.go @@ -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. diff --git a/internal/netxlite/dnsdecoder.go b/internal/netxlite/dnsdecoder.go index ef7b46eb93..a2935b191e 100644 --- a/internal/netxlite/dnsdecoder.go +++ b/internal/netxlite/dnsdecoder.go @@ -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{} diff --git a/internal/netxlite/dnsdecoder_test.go b/internal/netxlite/dnsdecoder_test.go index 839f203af2..92c4220bca 100644 --- a/internal/netxlite/dnsdecoder_test.go +++ b/internal/netxlite/dnsdecoder_test.go @@ -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 @@ -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 @@ -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 @@ -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{} @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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) + } + }) + }) }) } @@ -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") @@ -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 diff --git a/internal/netxlite/dnsovergetaddrinfo.go b/internal/netxlite/dnsovergetaddrinfo.go index 85dcf56a8d..fa0cfde24f 100644 --- a/internal/netxlite/dnsovergetaddrinfo.go +++ b/internal/netxlite/dnsovergetaddrinfo.go @@ -27,12 +27,13 @@ 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 @@ -40,6 +41,7 @@ func (txp *dnsOverGetaddrinfoTransport) RoundTrip( type dnsOverGetaddrinfoResponse struct { addrs []string + cname string query model.DNSQuery } @@ -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 +}