feat(netxlite): support extracting the CNAME (#875)

* feat(netxlite): support extracting the CNAME

Closes https://github.com/ooni/probe/issues/2225

* fix(netxlite): attempt to increase coverage and improve tests

1. dnsovergetaddrinfo: specify the behavior of a DNSResponse returned
by this file to make it line with normal responses and write unit tests
to make sure we adhere to expectations;

2. dnsoverudp: make sure we wait to deferred responses also w/o a
custom context and post on a private channel and test that;

3. utls: recognize that we can actually write a test for NetConn and
what needs to change when we'll use go1.19 by default will just be
a cast that at that point can be removed.
This commit is contained in:
Simone Basso 2022-08-23 13:04:00 +02:00 committed by GitHub
commit cc24f28b9d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 390 additions and 39 deletions

View file

@ -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 *dnsCNAMEAnswer = 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 := &dnsCNAMEAnswer{
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,18 @@ func dnsGenReplyWithError(rawQuery []byte, code int) []byte {
return data
}
// ImplementationNote: dnsCNAMEAnswer could have been a string but then
// dnsGenLookupHostReplySuccess invocations would have been confusing to read,
// because they would not have had a boundary between CNAME and addrs.
// dnsCNAMEAnswer is the DNS cname answer to include into a response.
type dnsCNAMEAnswer 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 *dnsCNAMEAnswer, ips ...string) []byte {
query := new(dns.Msg)
err := query.Unpack(rawQuery)
runtimex.PanicOnError(err, "query.Unpack failed")
@ -562,6 +647,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