diff --git a/internal/archival/archival.go b/internal/archival/archival.go index 821b787..0765c2f 100644 --- a/internal/archival/archival.go +++ b/internal/archival/archival.go @@ -206,7 +206,7 @@ func (ev *FlatDNSLookupEvent) ToArchival(begin time.Time) []model.ArchivalDNSLoo case DNSLookupTypeNS: return ev.toArchivalNS(begin) default: - log.Printf("ToArchivalDNSLookupResultList: unhandled record: %+v", ev) + log.Printf("[BUG] ToArchivalDNSLookupResultList: unhandled record: %+v", ev) return []model.ArchivalDNSLookupResult{} } } @@ -504,8 +504,10 @@ func (ev *FlatDNSRoundTripEvent) fillHostnameAndQueryType(out *model.ArchivalDNS out.QueryType = "CNAME" case dns.TypeANY: out.QueryType = "ANY" + case dns.TypePTR: + out.QueryType = "PTR" default: - // nothing + log.Printf("[BUG] fillHostnameAndQueryType: unhandled query type: %d", q0.Qtype) } } @@ -570,8 +572,20 @@ func (ev *FlatDNSRoundTripEvent) fillAnswers(out *model.ArchivalDNSLookupResult) NS: "", TTL: ev.ttl(v.Hdr.Ttl), }) + case *dns.PTR: + out.Answers = append(out.Answers, model.ArchivalDNSAnswer{ + ALPN: "", + ASN: 0, + ASOrgName: "", + AnswerType: "PTR", + Hostname: v.Ptr, // ooniprobe-legacy probably did this + IPv4: "", + IPv6: "", + NS: "", + TTL: ev.ttl(v.Hdr.Ttl), + }) default: - // nothing + log.Printf("[BUG] fillAnswers: unhandled record type %T", answer) } } } diff --git a/internal/archival/flat.go b/internal/archival/flat.go index 3c25f84..55dad98 100644 --- a/internal/archival/flat.go +++ b/internal/archival/flat.go @@ -27,6 +27,7 @@ type FlatDNSLookupEvent struct { Finished time.Time LookupType DNSLookupType NS []string `json:",omitempty"` + PTRs []string `json:",omitempty"` ResolverAddress string `json:",omitempty"` ResolverNetwork NetworkType Started time.Time @@ -44,7 +45,8 @@ func NewFakeFlatDNSLookupEvent(resolverNetwork NetworkType, resolverAddress stri Failure: "", Finished: now, LookupType: lookupType, - NS: []string{}, + NS: nil, + PTRs: nil, ResolverAddress: resolverAddress, ResolverNetwork: resolverNetwork, Started: now, diff --git a/internal/archival/resolver.go b/internal/archival/resolver.go index 81987fb..51da3a6 100644 --- a/internal/archival/resolver.go +++ b/internal/archival/resolver.go @@ -27,13 +27,16 @@ var ( // DNSLookupTypeNS indicates we're performing a NS lookup type. DNSLookupTypeNS = DNSLookupType("ns") + + // DNSLookupTypeReverse indicates we're performing a reverse lookup. + DNSLookupTypeReverse = DNSLookupType("reverse") ) // WrapResolver wraps a resolver to use the saver. func (s *Saver) WrapResolver(reso model.Resolver) model.Resolver { return &resolverSaver{ - Resolver: reso, - s: s, + r: reso, + s: s, } } @@ -46,20 +49,38 @@ func (s *Saver) WrapDNSTransport(txp model.DNSTransport) model.DNSTransport { } type resolverSaver struct { - model.Resolver + r model.Resolver s *Saver } +var _ model.Resolver = &resolverSaver{} + func (r *resolverSaver) LookupHost(ctx context.Context, domain string) ([]string, error) { - return r.s.lookupHost(ctx, r.Resolver, domain) + return r.s.lookupHost(ctx, r.r, domain) } func (r *resolverSaver) LookupHTTPS(ctx context.Context, domain string) (*model.HTTPSSvc, error) { - return r.s.lookupHTTPS(ctx, r.Resolver, domain) + return r.s.lookupHTTPS(ctx, r.r, domain) } func (r *resolverSaver) LookupNS(ctx context.Context, domain string) ([]*net.NS, error) { - return r.s.lookupNS(ctx, r.Resolver, domain) + return r.s.lookupNS(ctx, r.r, domain) +} + +func (r *resolverSaver) LookupPTR(ctx context.Context, domain string) ([]string, error) { + return r.s.lookupPTR(ctx, r.r, domain) +} + +func (r *resolverSaver) Address() string { + return r.r.Address() +} + +func (r *resolverSaver) Network() string { + return r.r.Network() +} + +func (r *resolverSaver) CloseIdleConnections() { + r.r.CloseIdleConnections() } func (s *Saver) lookupHost(ctx context.Context, reso model.Resolver, domain string) ([]string, error) { @@ -74,6 +95,7 @@ func (s *Saver) lookupHost(ctx context.Context, reso model.Resolver, domain stri Finished: time.Now(), LookupType: DNSLookupTypeGetaddrinfo, NS: nil, + PTRs: nil, ResolverAddress: reso.Address(), ResolverNetwork: NetworkType(reso.Network()), Started: started, @@ -99,6 +121,7 @@ func (s *Saver) lookupHTTPS(ctx context.Context, reso model.Resolver, domain str Finished: time.Now(), LookupType: DNSLookupTypeHTTPS, NS: nil, + PTRs: nil, ResolverAddress: reso.Address(), ResolverNetwork: NetworkType(reso.Network()), Started: started, @@ -133,6 +156,7 @@ func (s *Saver) lookupNS(ctx context.Context, reso model.Resolver, domain string Finished: time.Now(), LookupType: DNSLookupTypeNS, NS: s.ns(ns), + PTRs: nil, ResolverAddress: reso.Address(), ResolverNetwork: NetworkType(reso.Network()), Started: started, @@ -147,6 +171,26 @@ func (s *Saver) ns(ns []*net.NS) (out []string) { return } +func (s *Saver) lookupPTR(ctx context.Context, reso model.Resolver, domain string) ([]string, error) { + started := time.Now() + domains, err := reso.LookupPTR(ctx, domain) + s.appendDNSLookupEvent(&FlatDNSLookupEvent{ + ALPNs: nil, + Addresses: nil, + CNAME: "", + Domain: domain, + Failure: NewFlatFailure(err), + Finished: time.Now(), + LookupType: DNSLookupTypeReverse, + NS: []string{}, + PTRs: domains, + ResolverAddress: reso.Address(), + ResolverNetwork: NetworkType(reso.Network()), + Started: started, + }) + return domains, err +} + type dnsTransportSaver struct { model.DNSTransport s *Saver diff --git a/internal/dnsping/urladdress.go b/internal/dnsping/urladdress.go index 3e43219..4c20fe2 100644 --- a/internal/dnsping/urladdress.go +++ b/internal/dnsping/urladdress.go @@ -48,6 +48,7 @@ func (spr *SinglePingResult) DNSLookupMeasurementList( out = append(out, &measurex.DNSLookupMeasurement{ ID: entry.ID, URLMeasurementID: urlMeasurementID, + ReverseAddress: "", Lookup: &archival.FlatDNSLookupEvent{ ALPNs: entry.ALPNs, Addresses: entry.Addresses, diff --git a/internal/engine/experiment/websteps/cache.go b/internal/engine/experiment/websteps/cache.go index 7a88e08..16c6764 100644 --- a/internal/engine/experiment/websteps/cache.go +++ b/internal/engine/experiment/websteps/cache.go @@ -99,6 +99,7 @@ func (sc *stepsCache) dnsLookup(mx measurex.AbstractMeasurer, o := &measurex.DNSLookupMeasurement{ ID: mx.NextID(), URLMeasurementID: urlMeasurementID, + ReverseAddress: "", Lookup: &archival.FlatDNSLookupEvent{ ALPNs: alpns, Addresses: addrs, @@ -108,6 +109,7 @@ func (sc *stepsCache) dnsLookup(mx measurex.AbstractMeasurer, Finished: now, LookupType: archival.DNSLookupTypeHTTPS, NS: []string{}, + PTRs: []string{}, ResolverAddress: "dnscache", ResolverNetwork: "", Started: now, diff --git a/internal/engine/experiment/websteps/client.go b/internal/engine/experiment/websteps/client.go index 7a451ef..7c5f05b 100644 --- a/internal/engine/experiment/websteps/client.go +++ b/internal/engine/experiment/websteps/client.go @@ -398,6 +398,7 @@ func (c *Client) importTHMeasurement(mx measurex.AbstractMeasurer, in *THRespons dns := &measurex.DNSLookupMeasurement{ ID: mx.NextID(), URLMeasurementID: cur.ID, + ReverseAddress: e.ReverseAddress, Lookup: &archival.FlatDNSLookupEvent{ ALPNs: e.ALPNs(), Addresses: e.Addresses(), @@ -407,6 +408,7 @@ func (c *Client) importTHMeasurement(mx measurex.AbstractMeasurer, in *THRespons Finished: now, LookupType: e.LookupType(), NS: e.NS(), + PTRs: e.PTRs(), ResolverAddress: e.ResolverAddress(), ResolverNetwork: e.ResolverNetwork(), Started: now, diff --git a/internal/engine/experiment/websteps/th.go b/internal/engine/experiment/websteps/th.go index 85c9d51..f89d0a1 100644 --- a/internal/engine/experiment/websteps/th.go +++ b/internal/engine/experiment/websteps/th.go @@ -549,6 +549,7 @@ func (thr *THRequestHandler) step( um.DNS = append(um.DNS, m) } probeAddrs := thr.addProbeDNS(mx, um, req.Plan) + revch := thr.reverseDNSLookupAsync(ctx, mx, um, probeAddrs) // Implementation note: of course it doesn't make sense here for the // test helper to follow bogons discovered by the client :^) epplan, _ := um.NewEndpointPlan(thr.logger(), measurex.EndpointPlanningExcludeBogons) @@ -562,10 +563,45 @@ func (thr *THRequestHandler) step( for m := range mx.MeasureEndpoints(ctx, epplan...) { um.Endpoint = append(um.Endpoint, m) } - thr.saver().Save(um) // allows saving the measurement for analysis + um.DNS = append(um.DNS, <-revch...) // merge async results of the reverse lookup + thr.saver().Save(um) // allows saving the measurement for analysis return thr.serialize(um), nil } +// reverseDNSLookupAsync performs a reverse DNS lookup for all the IP addresses we know. +func (thr *THRequestHandler) reverseDNSLookupAsync(ctx context.Context, mx measurex.AbstractMeasurer, + um *measurex.URLMeasurement, probeAddrs []string) <-chan []*measurex.DNSLookupMeasurement { + out := make(chan []*measurex.DNSLookupMeasurement) + go func() { + // 0. close channel when done, we'll return a nil list in the worst case + defer close(out) + // 1. build a list of unique IP addresses to reverse lookup + uniqm := map[string]int{} + for _, dns := range um.DNS { + for _, addr := range dns.Addresses() { + uniqm[addr]++ + } + } + for _, addr := range probeAddrs { + uniqm[addr]++ + } + uniq := []string{} + for addr := range uniqm { + uniq = append(uniq, addr) + } + // 2. generate a reverse lookup plan + plan := um.NewDNSReverseLookupPlans(uniq, thr.resolvers()...) + // 3. collect results + v := []*measurex.DNSLookupMeasurement{} + for m := range mx.DNSLookups(ctx, plan...) { + v = append(v, m) + } + // 4. return results to the caller. + out <- v + }() + return out +} + // patchEndpointPlan returns a modified endpoint plan where: // // 1. we include cookies from the probe (if any); @@ -684,6 +720,7 @@ func (thr *THRequestHandler) simplifyDNS( out = append(out, &measurex.DNSLookupMeasurement{ ID: 0, URLMeasurementID: 0, + ReverseAddress: entry.ReverseAddress, Lookup: &archival.FlatDNSLookupEvent{ ALPNs: entry.ALPNs(), Addresses: entry.Addresses(), @@ -693,6 +730,7 @@ func (thr *THRequestHandler) simplifyDNS( Finished: thhResponseTime, LookupType: entry.LookupType(), NS: entry.NS(), + PTRs: entry.PTRs(), ResolverAddress: entry.ResolverAddress(), ResolverNetwork: entry.ResolverNetwork(), Started: thhResponseTime, diff --git a/internal/measurex/archival.go b/internal/measurex/archival.go index d1ee068..fa38a41 100644 --- a/internal/measurex/archival.go +++ b/internal/measurex/archival.go @@ -21,6 +21,10 @@ type ArchivalDNSLookupMeasurement struct { // Domain is the domain this lookup refers to. Domain string `json:"domain"` + // ReverseAddress is a convenience field to help analysis that is only + // set when we're performing a reverse DNS lookup. + ReverseAddress string `json:"reverse_address,omitempty"` + // ResolverNetwork is the network used by this resolver. ResolverNetwork string `json:"resolver_network"` @@ -42,6 +46,7 @@ func (m *DNSLookupMeasurement) ToArchival(begin time.Time) ArchivalDNSLookupMeas return ArchivalDNSLookupMeasurement{ ID: m.ID, Domain: m.Domain(), + ReverseAddress: m.ReverseAddress, ResolverNetwork: string(m.ResolverNetwork()), ResolverAddress: m.ResolverAddress(), Failure: m.Failure().ToArchivalFailure(), diff --git a/internal/measurex/dns.go b/internal/measurex/dns.go index e2637ec..334d950 100644 --- a/internal/measurex/dns.go +++ b/internal/measurex/dns.go @@ -18,6 +18,7 @@ import ( "github.com/bassosimone/websteps-illustrated/internal/archival" "github.com/bassosimone/websteps-illustrated/internal/model" + "github.com/miekg/dns" ) // DNSResolverInfo contains info about a DNS resolver. @@ -78,6 +79,11 @@ type DNSLookupPlan struct { // Domain is the domain to measure. Domain string + // ReverseAddress is a convenience field holding the addr for + // which we issued a reverse lookup, which only makes sense when + // we're actually performing a reverse lookup. + ReverseAddress string `json:",omitempty"` + // LookupType is the type of lookup to perform. LookupType archival.DNSLookupType @@ -143,6 +149,7 @@ func newDNSLookupPlans(urlMeasurementID int64, domain string, basePlan := &DNSLookupPlan{ URLMeasurementID: urlMeasurementID, Domain: domain, + ReverseAddress: "", LookupType: archival.DNSLookupTypeGetaddrinfo, Options: options, Resolver: r, @@ -164,6 +171,7 @@ func (dlp *DNSLookupPlan) Clone() (out *DNSLookupPlan) { out = &DNSLookupPlan{ URLMeasurementID: dlp.URLMeasurementID, Domain: dlp.Domain, + ReverseAddress: dlp.ReverseAddress, LookupType: dlp.LookupType, Options: dlp.Options.Flatten(), Resolver: dlp.Resolver.Clone(), @@ -218,6 +226,11 @@ type DNSLookupMeasurement struct { // handy to know it when we're processing measurements. URLMeasurementID int64 `json:"-"` + // ReverseAddress is a convenience field holding the addr for + // which we issued a reverse lookup, which only makes sense when + // we're actually performing a reverse lookup. + ReverseAddress string `json:",omitempty"` + // Lookup contains the DNS lookup event. This field contains a summary // of the information discovered during this lookup. We recommend using // this structure for processing the results. @@ -230,6 +243,31 @@ type DNSLookupMeasurement struct { RoundTrip []*archival.FlatDNSRoundTripEvent `json:",omitempty"` } +// NewDNSReverseLookupPlans generates a []*DNSLookupPlan for performing +// a reverse lookup for the given list of addresses and the given resolvers. +func (um *URLMeasurement) NewDNSReverseLookupPlans( + addrs []string, ri ...*DNSResolverInfo) []*DNSLookupPlan { + out := []*DNSLookupPlan{} + for _, addr := range addrs { + reverseAddr, err := dns.ReverseAddr(addr) + if err != nil { + // TODO(bassosimone): should we emit a warning about this? + continue + } + for _, r := range ri { + out = append(out, &DNSLookupPlan{ + URLMeasurementID: um.ID, + Domain: reverseAddr, + ReverseAddress: addr, + LookupType: archival.DNSLookupTypeReverse, + Options: um.Options, + Resolver: r, + }) + } + } + return out +} + // Summary returns a string representing the DNS measurement's plan. Two // measurements are ~same if they have the same summary. // @@ -261,15 +299,17 @@ func (dlm *DNSLookupMeasurement) Summary() string { // // The following table shows when two lookup types are weakly compatible: // -// +-------------+-------------+-------+--------+ -// | | getaddrinfo | https | ns | -// +-------------+-------------+-------+--------+ -// | getaddrinfo | yes | yes | no | -// +-------------+-------------+-------+--------+ -// | https | yes | yes | no | -// +-------------+-------------+-------+--------+ -// | ns | no | no | yes | -// +-------------+-------------+-------+--------+ +// +-------------+-------------+-------+--------+---------+ +// | | getaddrinfo | https | ns | reverse | +// +-------------+-------------+-------+--------+---------+ +// | getaddrinfo | yes | yes | no | no | +// +-------------+-------------+-------+--------+---------+ +// | https | yes | yes | no | no | +// +-------------+-------------+-------+--------+---------+ +// | ns | no | no | yes | no | +// +-------------+-------------+-------+--------+---------+ +// | reverse | no | no | no | yes | +// +-------------+-------------+-------+--------+---------+ // // In addition, two lookup types are _always_ weakly compatible when they're the // same, even if they are not listed in the above table. @@ -317,14 +357,11 @@ func (dlm *DNSLookupMeasurement) IsAnotherInstanceOf(o *DNSLookupMeasurement) bo // UsingResolverIPv6 returns whether this DNS lookups used an IPv6 resolver. func (dlm *DNSLookupMeasurement) UsingResolverIPv6() (usingIPv6 bool) { if dlm.Lookup != nil { - switch dlm.Lookup.ResolverNetwork { + switch v := dlm.Lookup.ResolverNetwork; v { case "tcp", "udp", "dot": usingIPv6 = isEndpointIPv6(dlm.ResolverAddress()) - case "doh": - // TODO(bassosimone): implement this case - log.Printf("[BUG] UsingResolverIPv6: doh case is not implemented") default: - // nothing + log.Printf("[BUG] UsingResolverIPv6: case %s: not implemented", v) } } return @@ -351,6 +388,14 @@ func (dlm *DNSLookupMeasurement) Addresses() []string { return nil } +// PTRs returns the PTRs we discovered during the lookup. +func (dlm *DNSLookupMeasurement) PTRs() []string { + if dlm.Lookup != nil { + return dlm.Lookup.PTRs + } + return nil +} + // CNAME returns the CNAME we discovered during the lookup. func (dlm *DNSLookupMeasurement) CNAME() string { if dlm.Lookup != nil { @@ -419,7 +464,7 @@ func (dlm *DNSLookupMeasurement) ResolverNetwork() archival.NetworkType { // DoH resolvers, we just return the URL. For all the other resolvers, we use the // network as the scheme and the address as the URL host. func (dlm *DNSLookupMeasurement) ResolverURL() string { - switch dlm.ResolverNetwork() { + switch v := dlm.ResolverNetwork(); v { case archival.NetworkTypeUDP: return fmt.Sprintf("udp://%s", dlm.ResolverAddress()) case archival.NetworkTypeTCP: @@ -431,6 +476,7 @@ func (dlm *DNSLookupMeasurement) ResolverURL() string { case "system": return "system:///" default: + log.Printf("[BUG] ResolverURL not implemented for: %s", v) return "" } } @@ -500,6 +546,8 @@ func (mx *Measurer) dnsLookup(ctx context.Context, output <- mx.lookupHTTPSSvcUDP(ctx, t) case archival.DNSLookupTypeNS: output <- mx.lookupNSUDP(ctx, t) + default: + log.Printf("[BUG] asked the UDP resolver for %s lookup type", t.LookupType) } case archival.NetworkTypeDoH, archival.NetworkTypeDoH3: switch t.LookupType { @@ -509,6 +557,10 @@ func (mx *Measurer) dnsLookup(ctx context.Context, output <- mx.lookupHTTPSSvcDoH(ctx, t) case archival.DNSLookupTypeNS: output <- mx.lookupNSDoH(ctx, t) + case archival.DNSLookupTypeReverse: + output <- mx.lookupReverseDoH(ctx, t) + default: + log.Printf("[BUG] asked the HTTPS resolver for %s lookup type", t.LookupType) } } } @@ -610,6 +662,20 @@ func (mx *Measurer) lookupNSDoH( return mx.newDNSLookupMeasurement(id, t, saver.MoveOutTrace()) } +// lookupReverseDoH performs a reverse lookup using a DoH resolver. +func (mx *Measurer) lookupReverseDoH( + ctx context.Context, t *DNSLookupPlan) *DNSLookupMeasurement { + saver := archival.NewSaver() + hc := mx.httpClientForDNSLookupTarget(t) + r := mx.Library.NewResolverDoH( + saver, hc, string(t.ResolverNetwork()), t.ResolverAddress()) + // Note: no close idle connections because actually we'd like to keep + // open connections with the server. + id := mx.NextID() + _, _ = mx.doLookupReverse(ctx, t.Domain, r, t, id) + return mx.newDNSLookupMeasurement(id, t, saver.MoveOutTrace()) +} + // doLookupHost is the worker function to perform an A and AAAA lookup. func (mx *Measurer) doLookupHost(ctx context.Context, domain string, r model.Resolver, t *DNSLookupPlan, id int64) ([]string, error) { @@ -649,12 +715,26 @@ func (mx *Measurer) doLookupNS(ctx context.Context, domain string, return ns, err } +// doLookupReverse is the worker function to perform a reverse lookup. +func (mx *Measurer) doLookupReverse(ctx context.Context, domain string, + r model.Resolver, t *DNSLookupPlan, id int64) ([]string, error) { + ol := NewOperationLogger(mx.Logger, "[#%d] LookupReverse %s with %s resolver %s", + id, domain, r.Network(), r.Address()) + timeout := t.Options.dnsLookupTimeout() + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + ptrs, err := r.LookupPTR(ctx, domain) + ol.Stop(err) + return ptrs, err +} + // newDNSLookupMeasurement is the internal factory for creating a DNSLookupMeasurement. func (mx *Measurer) newDNSLookupMeasurement(id int64, t *DNSLookupPlan, trace *archival.Trace) *DNSLookupMeasurement { out := &DNSLookupMeasurement{ ID: id, URLMeasurementID: t.URLMeasurementID, + ReverseAddress: t.ReverseAddress, Lookup: nil, RoundTrip: nil, } @@ -689,6 +769,7 @@ func newFakeHTTPSSvcDNSLookupMeasurement(urlMeasurementID int64, mx AbstractMeas return &DNSLookupMeasurement{ ID: mx.NextID(), URLMeasurementID: urlMeasurementID, + ReverseAddress: "", Lookup: archival.NewFakeFlatDNSLookupEvent( resolverNetwork, resolverAddress, archival.DNSLookupTypeHTTPS, domain, alpns, addresses, diff --git a/internal/model/netx.go b/internal/model/netx.go index c80e603..229830e 100644 --- a/internal/model/netx.go +++ b/internal/model/netx.go @@ -61,6 +61,9 @@ type DNSDecoder interface { // DecodeLookupNS is like DecodeLookupHTTPS but for NS queries. DecodeLookupNS(data []byte, queryID uint16) ([]*net.NS, error) + // DecodeLookupPTR is like DecodeLookupNS but for PTR queries. + DecodeLookupPTR(data []byte, queryID uint16) ([]string, error) + // ParseReply parses a reply without decoding it. This function // will ONLY return error if data is not a valid DNS message. In // particular, it WILL NOT return any error that depends on the @@ -96,6 +99,9 @@ type DNSDecoder interface { // DecodeReplyLookupNS is like DecodeReplyLookupHTTPS but for NS queries. DecodeReplyLookupNS(reply *dns.Msg) ([]*net.NS, error) + + // DecodeReplyLookupPTR is like DecodeReplyLookupNS but for PTR queries. + DecodeReplyLookupPTR(reply *dns.Msg) ([]string, error) } // The DNSEncoder encodes DNS queries to bytes @@ -230,6 +236,10 @@ type Resolver interface { // LookupNS issues a NS query for a domain. LookupNS(ctx context.Context, domain string) ([]*net.NS, error) + + // LookupPTR issues a PTR query for a domain. To perform a reverse DNS lookup + // you need to reverse the IP addr first using miekg/dns.ReverseAddr. + LookupPTR(ctx context.Context, domain string) ([]string, error) } // TLSConn is the kind of TLS conn we use in ooniprobe. diff --git a/internal/netxlite/dnsdecoder.go b/internal/netxlite/dnsdecoder.go index 025aac3..34cadb0 100644 --- a/internal/netxlite/dnsdecoder.go +++ b/internal/netxlite/dnsdecoder.go @@ -176,4 +176,29 @@ func (d *DNSDecoderMiekg) DecodeReplyLookupHost(qtype uint16, reply *dns.Msg) ([ return addrs, nil } +func (d *DNSDecoderMiekg) DecodeLookupPTR(data []byte, queryID uint16) ([]string, error) { + reply, err := d.ParseReplyForQueryID(data, queryID) + if err != nil { + return nil, err + } + return d.DecodeReplyLookupPTR(reply) +} + +func (d *DNSDecoderMiekg) DecodeReplyLookupPTR(reply *dns.Msg) ([]string, error) { + if err := d.rcodeToError(reply); err != nil { + return nil, err + } + var domains []string + for _, answer := range reply.Answer { + switch v := answer.(type) { + case *dns.PTR: + domains = append(domains, v.Ptr) + } + } + if len(domains) <= 0 { + return nil, ErrOODNSNoAnswer + } + return domains, nil +} + var _ model.DNSDecoder = &DNSDecoderMiekg{} diff --git a/internal/netxlite/dnssystem.go b/internal/netxlite/dnssystem.go index 9ec4df8..7f04ad2 100644 --- a/internal/netxlite/dnssystem.go +++ b/internal/netxlite/dnssystem.go @@ -83,6 +83,12 @@ func (r *DNSSystemResolver) LookupNS( return nil, ErrDNSNotImplemented } +// LookupPTR implements DNSResolver.LookupPTR. +func (r *DNSSystemResolver) LookupPTR( + ctx context.Context, domain string) ([]string, error) { + return nil, ErrDNSNotImplemented +} + // DNSSystemTransport is a transport that uses getaddrinfo or // the go standard library to perform a DNS resolution. type DNSSystemTransport struct { diff --git a/internal/netxlite/legacy.go b/internal/netxlite/legacy.go index e59f254..8dfa95b 100644 --- a/internal/netxlite/legacy.go +++ b/internal/netxlite/legacy.go @@ -89,6 +89,11 @@ func (r *resolverSystem) LookupNS( return nil, ErrNoDNSTransport } +func (r *resolverSystem) LookupPTR( + ctx context.Context, domain string) ([]string, error) { + return nil, ErrNoDNSTransport +} + // These vars export internal names to legacy ooni/probe-cli code. // // Deprecated: do not use these names in new code. diff --git a/internal/netxlite/parallelresolver.go b/internal/netxlite/parallelresolver.go index cc15bda..96603b6 100644 --- a/internal/netxlite/parallelresolver.go +++ b/internal/netxlite/parallelresolver.go @@ -144,3 +144,18 @@ func (r *ParallelResolver) LookupNS( } return r.Decoder.DecodeLookupNS(replydata, queryID) } + +// LookupPTR implements Resolver.LookupPTR. +func (r *ParallelResolver) LookupPTR( + ctx context.Context, hostname string) ([]string, error) { + querydata, queryID, err := r.Encoder.EncodeQuery( + hostname, dns.TypePTR, r.Txp.RequiresPadding()) + if err != nil { + return nil, err + } + replydata, err := r.Txp.RoundTrip(ctx, querydata) + if err != nil { + return nil, err + } + return r.Decoder.DecodeLookupPTR(replydata, queryID) +} diff --git a/internal/netxlite/resolver.go b/internal/netxlite/resolver.go index bfbca1e..0d31a78 100644 --- a/internal/netxlite/resolver.go +++ b/internal/netxlite/resolver.go @@ -240,6 +240,11 @@ func (r *nullResolver) LookupNS( return nil, ErrNoResolver } +func (r *nullResolver) LookupPTR( + ctx context.Context, domain string) ([]string, error) { + return nil, ErrNoResolver +} + // resolverErrWrapper is a Resolver that knows about wrapping errors. type resolverErrWrapper struct { model.Resolver diff --git a/internal/netxlite/serialresolver.go b/internal/netxlite/serialresolver.go index a39e2ae..b32dd68 100644 --- a/internal/netxlite/serialresolver.go +++ b/internal/netxlite/serialresolver.go @@ -145,3 +145,18 @@ func (r *SerialResolver) LookupNS( } return r.Decoder.DecodeLookupNS(replydata, queryID) } + +// LookupPTR implements Resolver.LookupPTR. +func (r *SerialResolver) LookupPTR( + ctx context.Context, hostname string) ([]string, error) { + querydata, queryID, err := r.Encoder.EncodeQuery( + hostname, dns.TypePTR, r.Txp.RequiresPadding()) + if err != nil { + return nil, err + } + replydata, err := r.Txp.RoundTrip(ctx, querydata) + if err != nil { + return nil, err + } + return r.Decoder.DecodeLookupPTR(replydata, queryID) +}