package webconnectivity // // DNSResolvers // // Generated by `boilerplate' using the multi-resolver template. // import ( "context" "math/rand" "net" "net/http" "net/url" "sync" "time" "github.com/apex/log" "github.com/ooni/probe-cli/v3/internal/atomicx" "github.com/ooni/probe-cli/v3/internal/measurexlite" "github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/netxlite" ) // Resolves the URL's domain using several resolvers. // // The zero value of this structure IS NOT valid and you MUST initialize // all the fields marked as MANDATORY before using this structure. type DNSResolvers struct { // DNSCache is the MANDATORY DNS cache. DNSCache *DNSCache // Domain is the MANDATORY domain to resolve. Domain string // IDGenerator is the MANDATORY atomic int64 to generate task IDs. IDGenerator *atomicx.Int64 // Logger is the MANDATORY logger to use. Logger model.Logger // NumRedirects it the MANDATORY counter of the number of redirects. NumRedirects *NumRedirects // TestKeys is MANDATORY and contains the TestKeys. TestKeys *TestKeys // URL is the MANDATORY URL we're measuring. URL *url.URL // ZeroTime is the MANDATORY zero time of the measurement. ZeroTime time.Time // WaitGroup is the MANDATORY wait group this task belongs to. WaitGroup *sync.WaitGroup // CookieJar contains the OPTIONAL cookie jar, used for redirects. CookieJar http.CookieJar // Referer contains the OPTIONAL referer, used for redirects. Referer string // Session is the OPTIONAL session. If the session is set, we will use // it to start the task that issues the control request. This request must // only be sent during the first iteration. It would be pointless to // issue such a request for subsequent redirects, because the TH will // always follow the redirect chain caused by the provided URL. Session model.ExperimentSession // THAddr is the OPTIONAL test helper address. THAddr string // UDPAddress is the OPTIONAL address of the UDP resolver to use. If this // field is not set we use a default one (e.g., `8.8.8.8:53`). UDPAddress string } // Start starts this task in a background goroutine. func (t *DNSResolvers) Start(ctx context.Context) { t.WaitGroup.Add(1) go func() { defer t.WaitGroup.Done() // synchronize with the parent t.Run(ctx) }() } // run performs a DNS lookup and returns the looked up addrs func (t *DNSResolvers) run(parentCtx context.Context) []DNSEntry { // create output channels for the lookup systemOut := make(chan []string) udpOut := make(chan []string) httpsOut := make(chan []string) whoamiSystemV4Out := make(chan []DNSWhoamiInfoEntry) whoamiUDPv4Out := make(chan []DNSWhoamiInfoEntry) // TODO(bassosimone): add opportunistic support for detecting // whether DNS queries are answered regardless of dest addr by // sending a few queries to root DNS servers udpAddress := t.udpAddress() // start asynchronous lookups go t.lookupHostSystem(parentCtx, systemOut) go t.lookupHostUDP(parentCtx, udpAddress, udpOut) go t.lookupHostDNSOverHTTPS(parentCtx, httpsOut) go t.whoamiSystemV4(parentCtx, whoamiSystemV4Out) go t.whoamiUDPv4(parentCtx, udpAddress, whoamiUDPv4Out) // collect resulting IP addresses (which may be nil/empty lists) systemAddrs := <-systemOut udpAddrs := <-udpOut httpsAddrs := <-httpsOut // collect whoami results (which also may be nil/empty) whoamiSystemV4 := <-whoamiSystemV4Out whoamiUDPv4 := <-whoamiUDPv4Out t.TestKeys.WithDNSWhoami(func(di *DNSWhoamiInfo) { di.SystemV4 = whoamiSystemV4 di.UDPv4[udpAddress] = whoamiUDPv4 }) // merge the resolved IP addresses merged := map[string]*DNSEntry{} for _, addr := range systemAddrs { if _, found := merged[addr]; !found { merged[addr] = &DNSEntry{} } merged[addr].Addr = addr merged[addr].Flags |= DNSAddrFlagSystemResolver } for _, addr := range udpAddrs { if _, found := merged[addr]; !found { merged[addr] = &DNSEntry{} } merged[addr].Addr = addr merged[addr].Flags |= DNSAddrFlagUDP } for _, addr := range httpsAddrs { if _, found := merged[addr]; !found { merged[addr] = &DNSEntry{} } merged[addr].Addr = addr merged[addr].Flags |= DNSAddrFlagHTTPS } // implementation note: we don't remove bogons because accessing // them can lead us to discover block pages var entries []DNSEntry for _, entry := range merged { entries = append(entries, *entry) } return entries } // Run runs this task in the current goroutine. func (t *DNSResolvers) Run(parentCtx context.Context) { var ( addresses []DNSEntry found bool ) // attempt to use the dns cache addresses, found = t.DNSCache.Get(t.Domain) if !found { // fall back to performing a real dns lookup addresses = t.run(parentCtx) // insert the addresses we just looked us into the cache t.DNSCache.Set(t.Domain, addresses) log.Infof("using resolved addrs: %+v", addresses) } else { log.Infof("using previously-cached addrs: %+v", addresses) } // create priority selector ps := newPrioritySelector(parentCtx, t.ZeroTime, t.TestKeys, t.Logger, addresses) // fan out a number of child async tasks to use the IP addrs t.startCleartextFlows(parentCtx, ps, addresses) t.startSecureFlows(parentCtx, ps, addresses) t.maybeStartControlFlow(parentCtx, ps, addresses) } // whoamiSystemV4 performs a DNS whoami lookup for the system resolver. This function must // always emit an ouput on the [out] channel to synchronize with the caller func. func (t *DNSResolvers) whoamiSystemV4(parentCtx context.Context, out chan<- []DNSWhoamiInfoEntry) { value, _ := DNSWhoamiSingleton.SystemV4(parentCtx) t.Logger.Infof("DNS whoami for system resolver: %+v", value) out <- value } // whoamiUDPv4 performs a DNS whoami lookup for the given UDP resolver. This function must // always emit an ouput on the [out] channel to synchronize with the caller func. func (t *DNSResolvers) whoamiUDPv4(parentCtx context.Context, udpAddress string, out chan<- []DNSWhoamiInfoEntry) { value, _ := DNSWhoamiSingleton.UDPv4(parentCtx, udpAddress) t.Logger.Infof("DNS whoami for %s/udp resolver: %+v", udpAddress, value) out <- value } // lookupHostSystem performs a DNS lookup using the system resolver. This function must // always emit an ouput on the [out] channel to synchronize with the caller func. func (t *DNSResolvers) lookupHostSystem(parentCtx context.Context, out chan<- []string) { // create context with attached a timeout const timeout = 4 * time.Second lookupCtx, lookpCancel := context.WithTimeout(parentCtx, timeout) defer lookpCancel() // create trace's index index := t.IDGenerator.Add(1) // create trace trace := measurexlite.NewTrace(index, t.ZeroTime) // start the operation logger ol := measurexlite.NewOperationLogger( t.Logger, "[#%d] lookup %s using system", index, t.Domain, ) // runs the lookup reso := trace.NewStdlibResolver(t.Logger) addrs, err := reso.LookupHost(lookupCtx, t.Domain) t.TestKeys.AppendQueries(trace.DNSLookupsFromRoundTrip()...) ol.Stop(err) out <- addrs } // lookupHostUDP performs a DNS lookup using an UDP resolver. This function must always // emit an ouput on the [out] channel to synchronize with the caller func. func (t *DNSResolvers) lookupHostUDP(parentCtx context.Context, udpAddress string, out chan<- []string) { // create context with attached a timeout const timeout = 4 * time.Second lookupCtx, lookpCancel := context.WithTimeout(parentCtx, timeout) defer lookpCancel() // create trace's index index := t.IDGenerator.Add(1) // create trace trace := measurexlite.NewTrace(index, t.ZeroTime) // start the operation logger ol := measurexlite.NewOperationLogger( t.Logger, "[#%d] lookup %s using %s", index, t.Domain, udpAddress, ) // runs the lookup dialer := netxlite.NewDialerWithoutResolver(t.Logger) reso := trace.NewParallelUDPResolver(t.Logger, dialer, udpAddress) addrs, err := reso.LookupHost(lookupCtx, t.Domain) // saves the results making sure we split Do53 queries from other queries do53, other := t.do53SplitQueries(trace.DNSLookupsFromRoundTrip()) t.TestKeys.AppendQueries(do53...) t.TestKeys.WithTestKeysDo53(func(tkd *TestKeysDo53) { tkd.Queries = append(tkd.Queries, other...) tkd.NetworkEvents = append(tkd.NetworkEvents, trace.NetworkEvents()...) }) ol.Stop(err) out <- addrs // wait for late DNS replies t.WaitGroup.Add(1) go t.waitForLateReplies(parentCtx, trace) } // Waits for late DNS replies. func (t *DNSResolvers) waitForLateReplies(parentCtx context.Context, trace *measurexlite.Trace) { defer t.WaitGroup.Done() const lateTimeout = 500 * time.Millisecond events := trace.DelayedDNSResponseWithTimeout(parentCtx, lateTimeout) t.TestKeys.AppendDNSLateReplies(events...) } // Divides queries generated by Do53 in Do53-proper queries and other queries. func (t *DNSResolvers) do53SplitQueries( input []*model.ArchivalDNSLookupResult) (do53, other []*model.ArchivalDNSLookupResult) { for _, query := range input { switch query.Engine { case "udp", "tcp": do53 = append(do53, query) default: other = append(other, query) } } return } // TODO(bassosimone): maybe cycle through a bunch of well known addresses // Returns the UDP resolver we should be using by default. func (t *DNSResolvers) udpAddress() string { if t.UDPAddress != "" { return t.UDPAddress } return "8.8.4.4:53" } // OpportunisticDNSOverHTTPS allows to perform opportunistic DNS-over-HTTPS // measurements as part of Web Connectivity. type OpportunisticDNSOverHTTPS struct { // interval is the next interval after which to measure. interval time.Duration // mu provides mutual exclusion mu *sync.Mutex // rnd is the random number generator to use. rnd *rand.Rand // t is when we last run an opportunistic measurement. t time.Time // urls contains the urls of known DoH services. urls []string } // MaybeNextURL returns the next URL to measure, if any. Our aim is to perform // periodic, opportunistic DoH measurements as part of Web Connectivity. func (o *OpportunisticDNSOverHTTPS) MaybeNextURL() (string, bool) { now := time.Now() o.mu.Lock() defer o.mu.Unlock() if o.t.IsZero() || now.Sub(o.t) > o.interval { o.rnd.Shuffle(len(o.urls), func(i, j int) { o.urls[i], o.urls[j] = o.urls[j], o.urls[i] }) o.t = now o.interval = time.Duration(20+o.rnd.Uint32()%20) * time.Second return o.urls[0], true } return "", false } // TODO(bassosimone): consider whether factoring out this code // and storing the state on disk instead of using memory // TODO(bassosimone): consider unifying somehow this code and // the systemresolver code (or maybe just the list of resolvers) // OpportunisticDNSOverHTTPSSingleton is the singleton used to keep // track of the opportunistic DNS-over-HTTPS measurements state. var OpportunisticDNSOverHTTPSSingleton = &OpportunisticDNSOverHTTPS{ interval: 0, mu: &sync.Mutex{}, rnd: rand.New(rand.NewSource(time.Now().UnixNano())), t: time.Time{}, urls: []string{ "https://mozilla.cloudflare-dns.com/dns-query", "https://dns.nextdns.io/dns-query", "https://dns.google/dns-query", "https://dns.quad9.net/dns-query", }, } // lookupHostDNSOverHTTPS performs a DNS lookup using a DoH resolver. This function must // always emit an ouput on the [out] channel to synchronize with the caller func. func (t *DNSResolvers) lookupHostDNSOverHTTPS(parentCtx context.Context, out chan<- []string) { // obtain an opportunistic DoH URL URL, good := OpportunisticDNSOverHTTPSSingleton.MaybeNextURL() if !good { // no need to perform opportunistic DoH at this time but we still // need to fake out a lookup to please our caller out <- []string{} return } // create context with attached a timeout const timeout = 4 * time.Second lookupCtx, lookpCancel := context.WithTimeout(parentCtx, timeout) defer lookpCancel() // create trace's index index := t.IDGenerator.Add(1) // create trace trace := measurexlite.NewTrace(index, t.ZeroTime) // start the operation logger ol := measurexlite.NewOperationLogger( t.Logger, "[#%d] lookup %s using %s", index, t.Domain, URL, ) // runs the lookup reso := trace.NewParallelDNSOverHTTPSResolver(t.Logger, URL) addrs, err := reso.LookupHost(lookupCtx, t.Domain) reso.CloseIdleConnections() // save results making sure we properly split DoH queries from other queries doh, other := t.dohSplitQueries(trace.DNSLookupsFromRoundTrip()) t.TestKeys.AppendQueries(doh...) t.TestKeys.WithTestKeysDoH(func(tkdh *TestKeysDoH) { tkdh.Queries = append(tkdh.Queries, other...) tkdh.NetworkEvents = append(tkdh.NetworkEvents, trace.NetworkEvents()...) tkdh.TCPConnect = append(tkdh.TCPConnect, trace.TCPConnects()...) tkdh.TLSHandshakes = append(tkdh.TLSHandshakes, trace.TLSHandshakes()...) }) ol.Stop(err) out <- addrs } // Divides queries generated by DoH in DoH-proper queries and other queries. func (t *DNSResolvers) dohSplitQueries( input []*model.ArchivalDNSLookupResult) (doh, other []*model.ArchivalDNSLookupResult) { for _, query := range input { switch query.Engine { case "doh": doh = append(doh, query) default: other = append(other, query) } } return } // startCleartextFlows starts a TCP measurement flow for each IP addr. func (t *DNSResolvers) startCleartextFlows( ctx context.Context, ps *prioritySelector, addresses []DNSEntry, ) { if t.URL.Scheme != "http" { // Do not bother with measuring HTTP when the user // has asked us to measure an HTTPS URL. return } port := "80" if urlPort := t.URL.Port(); urlPort != "" { port = urlPort } for _, addr := range addresses { task := &CleartextFlow{ Address: net.JoinHostPort(addr.Addr, port), DNSCache: t.DNSCache, IDGenerator: t.IDGenerator, Logger: t.Logger, NumRedirects: t.NumRedirects, TestKeys: t.TestKeys, ZeroTime: t.ZeroTime, WaitGroup: t.WaitGroup, CookieJar: t.CookieJar, FollowRedirects: t.URL.Scheme == "http", HostHeader: t.URL.Host, PrioSelector: ps, Referer: t.Referer, UDPAddress: t.UDPAddress, URLPath: t.URL.Path, URLRawQuery: t.URL.RawQuery, } task.Start(ctx) } } // startSecureFlows starts a TCP+TLS measurement flow for each IP addr. func (t *DNSResolvers) startSecureFlows( ctx context.Context, ps *prioritySelector, addresses []DNSEntry, ) { if t.URL.Scheme != "https" { // When the scheme is not HTTPS we fetch using HTTP ps = nil } port := "443" if urlPort := t.URL.Port(); urlPort != "" { if t.URL.Scheme != "https" { // If the URL is like http://example.com:8080/, we don't know // which would be the correct port where to use HTTPS. return } port = urlPort } for _, addr := range addresses { task := &SecureFlow{ Address: net.JoinHostPort(addr.Addr, port), DNSCache: t.DNSCache, IDGenerator: t.IDGenerator, Logger: t.Logger, NumRedirects: t.NumRedirects, TestKeys: t.TestKeys, ZeroTime: t.ZeroTime, WaitGroup: t.WaitGroup, ALPN: []string{"h2", "http/1.1"}, CookieJar: t.CookieJar, FollowRedirects: t.URL.Scheme == "https", SNI: t.URL.Hostname(), HostHeader: t.URL.Host, PrioSelector: ps, Referer: t.Referer, UDPAddress: t.UDPAddress, URLPath: t.URL.Path, URLRawQuery: t.URL.RawQuery, } task.Start(ctx) } } // maybeStartControlFlow starts the control flow iff .Session and .THAddr are set. func (t *DNSResolvers) maybeStartControlFlow( ctx context.Context, ps *prioritySelector, addresses []DNSEntry, ) { // note: for subsequent requests we don't set .Session and .THAddr hence // we are not going to query the test helper more than once if t.Session != nil && t.THAddr != "" { var addrs []string for _, addr := range addresses { addrs = append(addrs, addr.Addr) } ctrl := &Control{ Addresses: addrs, ExtraMeasurementsStarter: t, // allows starting follow-up measurement flows Logger: t.Logger, PrioSelector: ps, TestKeys: t.TestKeys, Session: t.Session, THAddr: t.THAddr, URL: t.URL, WaitGroup: t.WaitGroup, } ctrl.Start(ctx) } }