c9943dff38
This pull request started as a draft to enable users to see CNAME answers. It contained several patches which we merged separately (see https://github.com/ooni/probe-cli/pull/873#issuecomment-1222406732 and 2301a30630...60b7d1f87b
for details on what has actually changed, which is based on patches originally part of this PR). In its final form, however, this PR only deals with exposing more low-level DNS fields to the archival data format.
Closes: https://github.com/ooni/probe/issues/2228
Related PR spec: https://github.com/ooni/spec/pull/256
283 lines
8.6 KiB
Go
283 lines
8.6 KiB
Go
package measurexlite
|
|
|
|
//
|
|
// DNS Lookup with tracing
|
|
//
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log"
|
|
"net"
|
|
"time"
|
|
|
|
"github.com/miekg/dns"
|
|
"github.com/ooni/probe-cli/v3/internal/engine/geolocate"
|
|
"github.com/ooni/probe-cli/v3/internal/model"
|
|
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
|
"github.com/ooni/probe-cli/v3/internal/tracex"
|
|
)
|
|
|
|
// wrapResolver resolver wraps the passed resolver to save data into the trace
|
|
func (tx *Trace) wrapResolver(resolver model.Resolver) model.Resolver {
|
|
return &resolverTrace{
|
|
r: resolver,
|
|
tx: tx,
|
|
}
|
|
}
|
|
|
|
// resolverTrace is a trace-aware resolver
|
|
type resolverTrace struct {
|
|
r model.Resolver
|
|
tx *Trace
|
|
}
|
|
|
|
var _ model.Resolver = &resolverTrace{}
|
|
|
|
// Address implements model.Resolver.Address
|
|
func (r *resolverTrace) Address() string {
|
|
return r.r.Address()
|
|
}
|
|
|
|
// Network implements model.Resolver.Network
|
|
func (r *resolverTrace) Network() string {
|
|
return r.r.Network()
|
|
}
|
|
|
|
// CloseIdleConnections implements model.Resolver.CloseIdleConnections
|
|
func (r *resolverTrace) CloseIdleConnections() {
|
|
r.r.CloseIdleConnections()
|
|
}
|
|
|
|
// LookupHost implements model.Resolver.LookupHost
|
|
func (r *resolverTrace) LookupHost(ctx context.Context, hostname string) ([]string, error) {
|
|
return r.r.LookupHost(netxlite.ContextWithTrace(ctx, r.tx), hostname)
|
|
}
|
|
|
|
// LookupHTTPS implements model.Resolver.LookupHTTPS
|
|
func (r *resolverTrace) LookupHTTPS(ctx context.Context, domain string) (*model.HTTPSSvc, error) {
|
|
return r.r.LookupHTTPS(netxlite.ContextWithTrace(ctx, r.tx), domain)
|
|
}
|
|
|
|
// LookupNS implements model.Resolver.LookupNS
|
|
func (r *resolverTrace) LookupNS(ctx context.Context, domain string) ([]*net.NS, error) {
|
|
return r.r.LookupNS(netxlite.ContextWithTrace(ctx, r.tx), domain)
|
|
}
|
|
|
|
// NewStdlibResolver returns a trace-ware system resolver
|
|
func (tx *Trace) NewStdlibResolver(logger model.Logger) model.Resolver {
|
|
return tx.wrapResolver(tx.newStdlibResolver(logger))
|
|
}
|
|
|
|
// NewParallelUDPResolver returns a trace-ware parallel UDP resolver
|
|
func (tx *Trace) NewParallelUDPResolver(logger model.Logger, dialer model.Dialer, address string) model.Resolver {
|
|
return tx.wrapResolver(tx.newParallelUDPResolver(logger, dialer, address))
|
|
}
|
|
|
|
// NewParallelDNSOverHTTPSResolver returns a trace-aware parallel DoH resolver
|
|
func (tx *Trace) NewParallelDNSOverHTTPSResolver(logger model.Logger, URL string) model.Resolver {
|
|
return tx.wrapResolver(tx.newParallelDNSOverHTTPSResolver(logger, URL))
|
|
}
|
|
|
|
// OnDNSRoundTripForLookupHost implements model.Trace.OnDNSRoundTripForLookupHost
|
|
func (tx *Trace) OnDNSRoundTripForLookupHost(started time.Time, reso model.Resolver, query model.DNSQuery,
|
|
response model.DNSResponse, addrs []string, err error, finished time.Time) {
|
|
t := finished.Sub(tx.ZeroTime)
|
|
select {
|
|
case tx.dnsLookup <- NewArchivalDNSLookupResultFromRoundTrip(
|
|
tx.Index,
|
|
started.Sub(tx.ZeroTime),
|
|
reso,
|
|
query,
|
|
response,
|
|
addrs,
|
|
err,
|
|
t,
|
|
):
|
|
default:
|
|
}
|
|
}
|
|
|
|
// DNSNetworkAddresser is the type of something we just used to perform a DNS
|
|
// round trip (e.g., model.DNSTransport, model.Resolver) that allows us to get
|
|
// the network and the address of the underlying resolver/transport.
|
|
type DNSNetworkAddresser interface {
|
|
// Address is like model.DNSTransport.Address
|
|
Address() string
|
|
|
|
// Network is like model.DNSTransport.Network
|
|
Network() string
|
|
}
|
|
|
|
// NewArchivalDNSLookupResultFromRoundTrip generates a model.ArchivalDNSLookupResultFromRoundTrip
|
|
// from the available information right after the DNS RoundTrip
|
|
func NewArchivalDNSLookupResultFromRoundTrip(index int64, started time.Duration, reso DNSNetworkAddresser, query model.DNSQuery,
|
|
response model.DNSResponse, addrs []string, err error, finished time.Duration) *model.ArchivalDNSLookupResult {
|
|
return &model.ArchivalDNSLookupResult{
|
|
Answers: newArchivalDNSAnswers(addrs, response),
|
|
Engine: reso.Network(),
|
|
Failure: tracex.NewFailure(err),
|
|
GetaddrinfoError: netxlite.ErrorToGetaddrinfoRetvalOrZero(err),
|
|
Hostname: query.Domain(),
|
|
QueryType: dns.TypeToString[query.Type()],
|
|
RawResponse: maybeRawResponse(response),
|
|
Rcode: maybeResponseRcode(response),
|
|
ResolverHostname: nil,
|
|
ResolverPort: nil,
|
|
ResolverAddress: reso.Address(),
|
|
T0: started.Seconds(),
|
|
T: finished.Seconds(),
|
|
}
|
|
}
|
|
|
|
// maybeResponseRcode returns the response rcode (when available)
|
|
func maybeResponseRcode(resp model.DNSResponse) (out int64) {
|
|
if resp != nil {
|
|
out = int64(resp.Rcode())
|
|
}
|
|
return
|
|
}
|
|
|
|
// maybeRawResponse returns either the raw response (when available) or nil.
|
|
func maybeRawResponse(resp model.DNSResponse) (out []byte) {
|
|
if resp != nil {
|
|
out = resp.Bytes()
|
|
}
|
|
return
|
|
}
|
|
|
|
// newArchivalDNSAnswers generates []model.ArchivalDNSAnswer from [addrs] and [resp].
|
|
func newArchivalDNSAnswers(addrs []string, resp model.DNSResponse) (out []model.ArchivalDNSAnswer) {
|
|
// Design note: in principle we might want to extract everything from the
|
|
// response but, when we're called by netxlite, netxlite has already extracted
|
|
// the addresses to return them to the caller, so I think it's fine to keep
|
|
// this extraction code as such rather than suppressing passing the addrs from
|
|
// netxlite. Also, a wrong IP address is a bug because netxlite should not
|
|
// return invalid IP addresses from its resolvers, so we want to know about that.
|
|
|
|
// Include IP addresses extracted by netxlite
|
|
for _, addr := range addrs {
|
|
ipv6, err := netxlite.IsIPv6(addr)
|
|
if err != nil {
|
|
log.Printf("BUG: NewArchivalDNSLookupResult: invalid IP address: %s", addr)
|
|
continue
|
|
}
|
|
asn, org, _ := geolocate.LookupASN(addr)
|
|
switch ipv6 {
|
|
case false:
|
|
out = append(out, model.ArchivalDNSAnswer{
|
|
ASN: int64(asn),
|
|
ASOrgName: org,
|
|
AnswerType: "A",
|
|
Hostname: "",
|
|
IPv4: addr,
|
|
IPv6: "",
|
|
TTL: nil,
|
|
})
|
|
case true:
|
|
out = append(out, model.ArchivalDNSAnswer{
|
|
ASN: int64(asn),
|
|
ASOrgName: org,
|
|
AnswerType: "AAAA",
|
|
Hostname: "",
|
|
IPv4: "",
|
|
IPv6: addr,
|
|
TTL: nil,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Include additional answer types when a response is available
|
|
if resp != nil {
|
|
|
|
// Include CNAME if available
|
|
if cname, err := resp.DecodeCNAME(); err == nil && cname != "" {
|
|
out = append(out, model.ArchivalDNSAnswer{
|
|
ASN: 0,
|
|
ASOrgName: "",
|
|
AnswerType: "CNAME",
|
|
Hostname: cname,
|
|
IPv4: "",
|
|
IPv6: "",
|
|
TTL: nil,
|
|
})
|
|
}
|
|
|
|
// TODO(bassosimone): what other fields generally present inside A/AAAA replies
|
|
// would it be useful to extract here? Perhaps, the SoA field?
|
|
}
|
|
return
|
|
}
|
|
|
|
// DNSLookupsFromRoundTrip drains the network events buffered inside the DNSLookup channel
|
|
func (tx *Trace) DNSLookupsFromRoundTrip() (out []*model.ArchivalDNSLookupResult) {
|
|
for {
|
|
select {
|
|
case ev := <-tx.dnsLookup:
|
|
out = append(out, ev)
|
|
default:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// FirstDNSLookupOrNil drains the network events buffered inside the DNSLookup channel
|
|
// and returns the first DNSLookup, if any. Otherwise, it returns nil.
|
|
func (tx *Trace) FirstDNSLookup() *model.ArchivalDNSLookupResult {
|
|
ev := tx.DNSLookupsFromRoundTrip()
|
|
if len(ev) < 1 {
|
|
return nil
|
|
}
|
|
return ev[0]
|
|
}
|
|
|
|
// ErrDelayedDNSResponseBufferFull indicates that the delayedDNSResponse buffer is full.
|
|
var ErrDelayedDNSResponseBufferFull = errors.New("buffer full")
|
|
|
|
// OnDelayedDNSResponse implements model.Trace.OnDelayedDNSResponse
|
|
func (tx *Trace) OnDelayedDNSResponse(started time.Time, txp model.DNSTransport, query model.DNSQuery,
|
|
response model.DNSResponse, addrs []string, err error, finished time.Time) error {
|
|
t := finished.Sub(tx.ZeroTime)
|
|
select {
|
|
case tx.delayedDNSResponse <- NewArchivalDNSLookupResultFromRoundTrip(
|
|
tx.Index,
|
|
started.Sub(tx.ZeroTime),
|
|
txp,
|
|
query,
|
|
response,
|
|
addrs,
|
|
err,
|
|
t,
|
|
):
|
|
return nil
|
|
default:
|
|
return ErrDelayedDNSResponseBufferFull
|
|
}
|
|
}
|
|
|
|
// DelayedDNSResponseWithTimeout drains the network events buffered inside
|
|
// the delayedDNSResponse channel. We construct a child context based on [ctx]
|
|
// and the given [timeout] and we stop reading when original [ctx] has been
|
|
// cancelled or the given [timeout] expires, whatever happens first. Once the
|
|
// timeout expired, we drain the chan as much as possible before returning.
|
|
func (tx *Trace) DelayedDNSResponseWithTimeout(ctx context.Context,
|
|
timeout time.Duration) (out []*model.ArchivalDNSLookupResult) {
|
|
ctx, cancel := context.WithTimeout(ctx, timeout)
|
|
defer cancel()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
for { // once the context is done enter in channel draining mode
|
|
select {
|
|
case ev := <-tx.delayedDNSResponse:
|
|
out = append(out, ev)
|
|
default:
|
|
return
|
|
}
|
|
}
|
|
case ev := <-tx.delayedDNSResponse:
|
|
out = append(out, ev)
|
|
}
|
|
}
|
|
}
|