refactor: DNSTransport I/Os DNS messages (#760)

This diff refactors the DNSTransport model to receive in input a DNSQuery and return in output a DNSResponse.

The design of DNSQuery and DNSResponse takes into account the use case of a transport using getaddrinfo, meaning that we don't need to serialize and deserialize messages when using getaddrinfo.

The current codebase does not use a getaddrinfo transport, but I wrote one such a transport in the Websteps Winter 2021 prototype (https://github.com/bassosimone/websteps-illustrated/).

The design conversation that lead to producing this diff is https://github.com/ooni/probe/issues/2099
This commit is contained in:
Simone Basso
2022-05-25 17:03:58 +02:00
committed by GitHub
parent 7a0a156aec
commit 01a513a496
35 changed files with 1731 additions and 1076 deletions
+9 -25
View File
@@ -1,36 +1,20 @@
package mocks
import (
"net"
//
// Mocks for model.DNSDecoder
//
"github.com/miekg/dns"
import (
"github.com/ooni/probe-cli/v3/internal/model"
)
// DNSDecoder allows mocking dnsx.DNSDecoder.
// DNSDecoder allows mocking model.DNSDecoder.
type DNSDecoder struct {
MockDecodeLookupHost func(qtype uint16, reply []byte, queryID uint16) ([]string, error)
MockDecodeHTTPS func(reply []byte, queryID uint16) (*model.HTTPSSvc, error)
MockDecodeNS func(reply []byte, queryID uint16) ([]*net.NS, error)
MockDecodeReply func(reply []byte) (*dns.Msg, error)
MockDecodeResponse func(data []byte, query model.DNSQuery) (model.DNSResponse, error)
}
// DecodeLookupHost calls MockDecodeLookupHost.
func (e *DNSDecoder) DecodeLookupHost(qtype uint16, reply []byte, queryID uint16) ([]string, error) {
return e.MockDecodeLookupHost(qtype, reply, queryID)
}
var _ model.DNSDecoder = &DNSDecoder{}
// DecodeHTTPS calls MockDecodeHTTPS.
func (e *DNSDecoder) DecodeHTTPS(reply []byte, queryID uint16) (*model.HTTPSSvc, error) {
return e.MockDecodeHTTPS(reply, queryID)
}
// DecodeNS calls MockDecodeNS.
func (e *DNSDecoder) DecodeNS(reply []byte, queryID uint16) ([]*net.NS, error) {
return e.MockDecodeNS(reply, queryID)
}
// DecodeReply calls MockDecodeReply.
func (e *DNSDecoder) DecodeReply(reply []byte) (*dns.Msg, error) {
return e.MockDecodeReply(reply)
func (e *DNSDecoder) DecodeResponse(data []byte, query model.DNSQuery) (model.DNSResponse, error) {
return e.MockDecodeResponse(data, query)
}
+3 -53
View File
@@ -2,70 +2,20 @@ package mocks
import (
"errors"
"net"
"testing"
"github.com/miekg/dns"
"github.com/ooni/probe-cli/v3/internal/model"
)
func TestDNSDecoder(t *testing.T) {
t.Run("DecodeLookupHost", func(t *testing.T) {
t.Run("DecodeResponse", func(t *testing.T) {
expected := errors.New("mocked error")
e := &DNSDecoder{
MockDecodeLookupHost: func(qtype uint16, reply []byte, queryID uint16) ([]string, error) {
MockDecodeResponse: func(reply []byte, query model.DNSQuery) (model.DNSResponse, error) {
return nil, expected
},
}
out, err := e.DecodeLookupHost(dns.TypeA, make([]byte, 17), dns.Id())
if !errors.Is(err, expected) {
t.Fatal("unexpected err", err)
}
if out != nil {
t.Fatal("unexpected out")
}
})
t.Run("DecodeHTTPS", func(t *testing.T) {
expected := errors.New("mocked error")
e := &DNSDecoder{
MockDecodeHTTPS: func(reply []byte, queryID uint16) (*model.HTTPSSvc, error) {
return nil, expected
},
}
out, err := e.DecodeHTTPS(make([]byte, 17), dns.Id())
if !errors.Is(err, expected) {
t.Fatal("unexpected err", err)
}
if out != nil {
t.Fatal("unexpected out")
}
})
t.Run("DecodeNS", func(t *testing.T) {
expected := errors.New("mocked error")
e := &DNSDecoder{
MockDecodeNS: func(reply []byte, queryID uint16) ([]*net.NS, error) {
return nil, expected
},
}
out, err := e.DecodeNS(make([]byte, 17), dns.Id())
if !errors.Is(err, expected) {
t.Fatal("unexpected err", err)
}
if out != nil {
t.Fatal("unexpected out")
}
})
t.Run("DecodeReply", func(t *testing.T) {
expected := errors.New("mocked error")
e := &DNSDecoder{
MockDecodeReply: func(reply []byte) (*dns.Msg, error) {
return nil, expected
},
}
out, err := e.DecodeReply(make([]byte, 17))
out, err := e.DecodeResponse(make([]byte, 17), &DNSQuery{})
if !errors.Is(err, expected) {
t.Fatal("unexpected err", err)
}
+11 -3
View File
@@ -1,11 +1,19 @@
package mocks
// DNSEncoder allows mocking dnsx.DNSEncoder.
//
// Mocks for model.DNSEncoder.
//
import "github.com/ooni/probe-cli/v3/internal/model"
// DNSEncoder allows mocking model.DNSEncoder.
type DNSEncoder struct {
MockEncode func(domain string, qtype uint16, padding bool) ([]byte, uint16, error)
MockEncode func(domain string, qtype uint16, padding bool) model.DNSQuery
}
var _ model.DNSEncoder = &DNSEncoder{}
// Encode calls MockEncode.
func (e *DNSEncoder) Encode(domain string, qtype uint16, padding bool) ([]byte, uint16, error) {
func (e *DNSEncoder) Encode(domain string, qtype uint16, padding bool) model.DNSQuery {
return e.MockEncode(domain, qtype, padding)
}
+26 -4
View File
@@ -5,24 +5,46 @@ import (
"testing"
"github.com/miekg/dns"
"github.com/ooni/probe-cli/v3/internal/model"
)
func TestDNSEncoder(t *testing.T) {
t.Run("Encode", func(t *testing.T) {
expected := errors.New("mocked error")
queryID := dns.Id()
e := &DNSEncoder{
MockEncode: func(domain string, qtype uint16, padding bool) ([]byte, uint16, error) {
return nil, 0, expected
MockEncode: func(domain string, qtype uint16, padding bool) model.DNSQuery {
return &DNSQuery{
MockDomain: func() string {
return dns.Fqdn(domain) // do what an implementation MUST do
},
MockType: func() uint16 {
return qtype
},
MockBytes: func() ([]byte, error) {
return nil, expected
},
MockID: func() uint16 {
return queryID
},
}
},
}
out, queryID, err := e.Encode("dns.google", dns.TypeA, true)
query := e.Encode("dns.google", dns.TypeA, true)
if query.Domain() != "dns.google." {
t.Fatal("invalid domain")
}
if query.Type() != dns.TypeA {
t.Fatal("invalid type")
}
out, err := query.Bytes()
if !errors.Is(err, expected) {
t.Fatal("unexpected err", err)
}
if out != nil {
t.Fatal("unexpected out")
}
if queryID != 0 {
if query.ID() != queryID {
t.Fatal("unexpected queryID")
}
})
+33
View File
@@ -0,0 +1,33 @@
package mocks
//
// Mocks for model.DNSQuery.
//
import "github.com/ooni/probe-cli/v3/internal/model"
// DNSQuery allocks mocking model.DNSQuery.
type DNSQuery struct {
MockDomain func() string
MockType func() uint16
MockBytes func() ([]byte, error)
MockID func() uint16
}
func (q *DNSQuery) Domain() string {
return q.MockDomain()
}
func (q *DNSQuery) Type() uint16 {
return q.MockType()
}
func (q *DNSQuery) Bytes() ([]byte, error) {
return q.MockBytes()
}
func (q *DNSQuery) ID() uint16 {
return q.MockID()
}
var _ model.DNSQuery = &DNSQuery{}
+62
View File
@@ -0,0 +1,62 @@
package mocks
import (
"bytes"
"testing"
"github.com/miekg/dns"
)
func TestDNSQuery(t *testing.T) {
t.Run("Domain", func(t *testing.T) {
expected := "dns.google."
q := &DNSQuery{
MockDomain: func() string {
return expected
},
}
if q.Domain() != expected {
t.Fatal("invalid domain")
}
})
t.Run("Type", func(t *testing.T) {
expected := dns.TypeAAAA
q := &DNSQuery{
MockType: func() uint16 {
return expected
},
}
if q.Type() != expected {
t.Fatal("invalid type")
}
})
t.Run("Bytes", func(t *testing.T) {
expected := []byte{0xde, 0xea, 0xad, 0xbe, 0xef}
q := &DNSQuery{
MockBytes: func() ([]byte, error) {
return expected, nil
},
}
out, err := q.Bytes()
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(expected, out) {
t.Fatal("invalid bytes")
}
})
t.Run("ID", func(t *testing.T) {
expected := dns.Id()
q := &DNSQuery{
MockID: func() uint16 {
return expected
},
}
if q.ID() != expected {
t.Fatal("invalid id")
}
})
}
+47
View File
@@ -0,0 +1,47 @@
package mocks
//
// Mocks for model.DNSResponse
//
import (
"net"
"github.com/ooni/probe-cli/v3/internal/model"
)
// DNSResponse allows mocking model.DNSResponse.
type DNSResponse struct {
MockQuery func() model.DNSQuery
MockBytes func() []byte
MockRcode func() int
MockDecodeHTTPS func() (*model.HTTPSSvc, error)
MockDecodeLookupHost func() ([]string, error)
MockDecodeNS func() ([]*net.NS, error)
}
var _ model.DNSResponse = &DNSResponse{}
func (r *DNSResponse) Query() model.DNSQuery {
return r.MockQuery()
}
func (r *DNSResponse) Bytes() []byte {
return r.MockBytes()
}
func (r *DNSResponse) Rcode() int {
return r.MockRcode()
}
func (r *DNSResponse) DecodeHTTPS() (*model.HTTPSSvc, error) {
return r.MockDecodeHTTPS()
}
func (r *DNSResponse) DecodeLookupHost() ([]string, error) {
return r.MockDecodeLookupHost()
}
func (r *DNSResponse) DecodeNS() ([]*net.NS, error) {
return r.MockDecodeNS()
}
+105
View File
@@ -0,0 +1,105 @@
package mocks
import (
"bytes"
"errors"
"net"
"testing"
"github.com/miekg/dns"
"github.com/ooni/probe-cli/v3/internal/model"
)
func TestDNSResponse(t *testing.T) {
t.Run("Query", func(t *testing.T) {
qid := dns.Id()
query := &DNSQuery{
MockID: func() uint16 {
return qid
},
}
resp := &DNSResponse{
MockQuery: func() model.DNSQuery {
return query
},
}
out := resp.Query()
if out.ID() != query.ID() {
t.Fatal("invalid query")
}
})
t.Run("Bytes", func(t *testing.T) {
expected := []byte{0xde, 0xea, 0xad, 0xbe, 0xef}
resp := &DNSResponse{
MockBytes: func() []byte {
return expected
},
}
out := resp.Bytes()
if !bytes.Equal(expected, out) {
t.Fatal("invalid bytes")
}
})
t.Run("Rcode", func(t *testing.T) {
expected := dns.RcodeBadAlg
resp := &DNSResponse{
MockRcode: func() int {
return expected
},
}
out := resp.Rcode()
if out != expected {
t.Fatal("invalid rcode")
}
})
t.Run("DecodeLookupHost", func(t *testing.T) {
expected := errors.New("mocked error")
r := &DNSResponse{
MockDecodeLookupHost: func() ([]string, error) {
return nil, expected
},
}
out, err := r.DecodeLookupHost()
if !errors.Is(err, expected) {
t.Fatal("unexpected err", err)
}
if out != nil {
t.Fatal("unexpected out")
}
})
t.Run("DecodeHTTPS", func(t *testing.T) {
expected := errors.New("mocked error")
r := &DNSResponse{
MockDecodeHTTPS: func() (*model.HTTPSSvc, error) {
return nil, expected
},
}
out, err := r.DecodeHTTPS()
if !errors.Is(err, expected) {
t.Fatal("unexpected err", err)
}
if out != nil {
t.Fatal("unexpected out")
}
})
t.Run("DecodeNS", func(t *testing.T) {
expected := errors.New("mocked error")
r := &DNSResponse{
MockDecodeNS: func() ([]*net.NS, error) {
return nil, expected
},
}
out, err := r.DecodeNS()
if !errors.Is(err, expected) {
t.Fatal("unexpected err", err)
}
if out != nil {
t.Fatal("unexpected out")
}
})
}
+9 -3
View File
@@ -1,10 +1,14 @@
package mocks
import "context"
import (
"context"
"github.com/ooni/probe-cli/v3/internal/model"
)
// DNSTransport allows mocking dnsx.DNSTransport.
type DNSTransport struct {
MockRoundTrip func(ctx context.Context, query []byte) ([]byte, error)
MockRoundTrip func(ctx context.Context, query model.DNSQuery) (model.DNSResponse, error)
MockRequiresPadding func() bool
@@ -15,8 +19,10 @@ type DNSTransport struct {
MockCloseIdleConnections func()
}
var _ model.DNSTransport = &DNSTransport{}
// RoundTrip calls MockRoundTrip.
func (txp *DNSTransport) RoundTrip(ctx context.Context, query []byte) ([]byte, error) {
func (txp *DNSTransport) RoundTrip(ctx context.Context, query model.DNSQuery) (model.DNSResponse, error) {
return txp.MockRoundTrip(ctx, query)
}
+3 -2
View File
@@ -6,17 +6,18 @@ import (
"testing"
"github.com/ooni/probe-cli/v3/internal/atomicx"
"github.com/ooni/probe-cli/v3/internal/model"
)
func TestDNSTransport(t *testing.T) {
t.Run("RoundTrip", func(t *testing.T) {
expected := errors.New("mocked error")
txp := &DNSTransport{
MockRoundTrip: func(ctx context.Context, query []byte) ([]byte, error) {
MockRoundTrip: func(ctx context.Context, query model.DNSQuery) (model.DNSResponse, error) {
return nil, expected
},
}
resp, err := txp.RoundTrip(context.Background(), make([]byte, 16))
resp, err := txp.RoundTrip(context.Background(), &DNSQuery{})
if !errors.Is(err, expected) {
t.Fatal("not the error we expected", err)
}