Skip to content

Commit

Permalink
refactor: move webconnectivitylte algos to webconnectivityalgo (#1496)
Browse files Browse the repository at this point in the history
This diff refactors webconnectivitylte by moving some algorithms inside
the new webconnectivityalgo package.

In subsequent commits, we'll seize the opportunity of adding tests for
these algorithms, refactor the code, and add specific tests.

Part of ooni/probe#2669.

While there, recognize that the webconnectivityqa package does not
belong to internal/experiment but to internal.
  • Loading branch information
bassosimone authored Feb 8, 2024
1 parent 37db467 commit a6f3ca4
Show file tree
Hide file tree
Showing 44 changed files with 190 additions and 148 deletions.
2 changes: 1 addition & 1 deletion internal/cmd/qatool/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import (
"regexp"

"github.com/ooni/probe-cli/v3/internal/experiment/webconnectivitylte"
"github.com/ooni/probe-cli/v3/internal/experiment/webconnectivityqa"
"github.com/ooni/probe-cli/v3/internal/geoipx"
"github.com/ooni/probe-cli/v3/internal/minipipeline"
"github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/must"
"github.com/ooni/probe-cli/v3/internal/runtimex"
"github.com/ooni/probe-cli/v3/internal/webconnectivityqa"
)

var (
Expand Down
2 changes: 1 addition & 1 deletion internal/experiment/webconnectivity/qa_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package webconnectivity
import (
"testing"

"github.com/ooni/probe-cli/v3/internal/experiment/webconnectivityqa"
"github.com/ooni/probe-cli/v3/internal/webconnectivityqa"
)

func TestQA(t *testing.T) {
Expand Down
70 changes: 11 additions & 59 deletions internal/experiment/webconnectivitylte/dnsresolvers.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ package webconnectivitylte
import (
"context"
"fmt"
"math/rand"
"net"
"net/http"
"net/url"
Expand All @@ -21,6 +20,7 @@ import (
"github.com/ooni/probe-cli/v3/internal/measurexlite"
"github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/netxlite"
"github.com/ooni/probe-cli/v3/internal/webconnectivityalgo"
)

// Resolves the URL's domain using several resolvers.
Expand Down Expand Up @@ -95,8 +95,8 @@ func (t *DNSResolvers) run(parentCtx context.Context) []DNSEntry {
systemOut := make(chan []string)
udpOut := make(chan []string)
httpsOut := make(chan []string)
whoamiSystemV4Out := make(chan []DNSWhoamiInfoEntry)
whoamiUDPv4Out := make(chan []DNSWhoamiInfoEntry)
whoamiSystemV4Out := make(chan []webconnectivityalgo.DNSWhoamiInfoEntry)
whoamiUDPv4Out := make(chan []webconnectivityalgo.DNSWhoamiInfoEntry)

// TODO(bassosimone): add opportunistic support for detecting
// whether DNS queries are answered regardless of dest addr by
Expand Down Expand Up @@ -188,15 +188,15 @@ func (t *DNSResolvers) Run(parentCtx context.Context) {

// 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) {
func (t *DNSResolvers) whoamiSystemV4(parentCtx context.Context, out chan<- []webconnectivityalgo.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) {
func (t *DNSResolvers) whoamiUDPv4(parentCtx context.Context, udpAddress string, out chan<- []webconnectivityalgo.DNSWhoamiInfoEntry) {
value, _ := DNSWhoamiSingleton.UDPv4(parentCtx, udpAddress)
t.Logger.Infof("DNS whoami for %s/udp resolver: %+v", udpAddress, value)
out <- value
Expand Down Expand Up @@ -302,62 +302,14 @@ func (t *DNSResolvers) udpAddress() string {
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",
},
}
var OpportunisticDNSOverHTTPSSingleton = webconnectivityalgo.NewOpportunisticDNSOverHTTPSURLProvider(
"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.
Expand Down
77 changes: 2 additions & 75 deletions internal/experiment/webconnectivitylte/dnswhoami.go
Original file line number Diff line number Diff line change
@@ -1,81 +1,8 @@
package webconnectivitylte

import (
"context"
"sync"
"time"

"github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/netxlite"
"github.com/ooni/probe-cli/v3/internal/webconnectivityalgo"
)

// TODO(bassosimone): this code needs refining before we can merge it inside
// master. For one, we already have systemv4 info. Additionally, it would
// be neat to avoid additional AAAA queries. Furthermore, we should also see
// to implement support for IPv6 only clients as well.

// DNSWhoamiService is a service that performs DNS whoami lookups.
type DNSWhoamiService struct {
// mu provides mutual exclusion
mu *sync.Mutex

// systemv4 contains systemv4 results
systemv4 []DNSWhoamiInfoEntry

// udpv4 contains udpv4 results
udpv4 map[string][]DNSWhoamiInfoEntry
}

// SystemV4 returns the results of querying using the system resolver and IPv4.
func (svc *DNSWhoamiService) SystemV4(ctx context.Context) ([]DNSWhoamiInfoEntry, bool) {
svc.mu.Lock()
defer svc.mu.Unlock()
if len(svc.systemv4) <= 0 {
ctx, cancel := context.WithTimeout(ctx, 4*time.Second)
defer cancel()
netx := &netxlite.Netx{}
reso := netx.NewStdlibResolver(model.DiscardLogger)
addrs, err := reso.LookupHost(ctx, "whoami.v4.powerdns.org")
if err != nil || len(addrs) < 1 {
return nil, false
}
svc.systemv4 = []DNSWhoamiInfoEntry{{
Address: addrs[0],
}}
}
return svc.systemv4, len(svc.systemv4) > 0
}

// UDPv4 returns the results of querying a given UDP resolver and IPv4.
func (svc *DNSWhoamiService) UDPv4(ctx context.Context, address string) ([]DNSWhoamiInfoEntry, bool) {
svc.mu.Lock()
defer svc.mu.Unlock()
if len(svc.udpv4[address]) <= 0 {
ctx, cancel := context.WithTimeout(ctx, 4*time.Second)
defer cancel()
netx := &netxlite.Netx{}
dialer := netxlite.NewDialerWithStdlibResolver(model.DiscardLogger)
reso := netx.NewParallelUDPResolver(model.DiscardLogger, dialer, address)
// TODO(bassosimone): this should actually only send an A query. Sending an AAAA
// query is _way_ unnecessary since we know that only A is going to work.
addrs, err := reso.LookupHost(ctx, "whoami.v4.powerdns.org")
if err != nil || len(addrs) < 1 {
return nil, false
}
svc.udpv4[address] = []DNSWhoamiInfoEntry{{
Address: addrs[0],
}}
}
value := svc.udpv4[address]
return value, len(value) > 0
}

// TODO(bassosimone): consider factoring this code and keeping state
// on disk rather than on memory.

// DNSWhoamiSingleton is the DNSWhoamiService singleton.
var DNSWhoamiSingleton = &DNSWhoamiService{
mu: &sync.Mutex{},
systemv4: []DNSWhoamiInfoEntry{},
udpv4: map[string][]DNSWhoamiInfoEntry{},
}
var DNSWhoamiSingleton = webconnectivityalgo.NewDNSWhoamiService()
2 changes: 1 addition & 1 deletion internal/experiment/webconnectivitylte/qa_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package webconnectivitylte
import (
"testing"

"github.com/ooni/probe-cli/v3/internal/experiment/webconnectivityqa"
"github.com/ooni/probe-cli/v3/internal/webconnectivityqa"
)

func TestQA(t *testing.T) {
Expand Down
17 changes: 6 additions & 11 deletions internal/experiment/webconnectivitylte/testkeys.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/ooni/probe-cli/v3/internal/legacy/tracex"
"github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/optional"
"github.com/ooni/probe-cli/v3/internal/webconnectivityalgo"
)

// TestKeys contains the results produced by web_connectivity.
Expand Down Expand Up @@ -153,19 +154,13 @@ type ConnPriorityLogEntry struct {
T float64 `json:"t"`
}

// DNSWhoamiInfoEntry contains an entry for DNSWhoamiInfo.
type DNSWhoamiInfoEntry struct {
// Address is the IP address
Address string `json:"address"`
}

// DNSWhoamiInfo contains info about DNS whoami.
// DNSWhoamiInfo contains information about a DNS whoami lookup.
type DNSWhoamiInfo struct {
// SystemV4 contains results related to the system resolver using IPv4.
SystemV4 []DNSWhoamiInfoEntry `json:"system_v4"`
SystemV4 []webconnectivityalgo.DNSWhoamiInfoEntry `json:"system_v4"`

// UDPv4 contains results related to an UDP resolver using IPv4.
UDPv4 map[string][]DNSWhoamiInfoEntry `json:"udp_v4"`
UDPv4 map[string][]webconnectivityalgo.DNSWhoamiInfoEntry `json:"udp_v4"`
}

// TestKeysDoH contains ancillary observations collected using DoH (e.g., the
Expand Down Expand Up @@ -334,8 +329,8 @@ func NewTestKeys() *TestKeys {
SOCKSProxy: nil,
NetworkEvents: []*model.ArchivalNetworkEvent{},
DNSWoami: &DNSWhoamiInfo{
SystemV4: []DNSWhoamiInfoEntry{},
UDPv4: map[string][]DNSWhoamiInfoEntry{},
SystemV4: []webconnectivityalgo.DNSWhoamiInfoEntry{},
UDPv4: map[string][]webconnectivityalgo.DNSWhoamiInfoEntry{},
},
DoH: &TestKeysDoH{
NetworkEvents: []*model.ArchivalNetworkEvent{},
Expand Down
67 changes: 67 additions & 0 deletions internal/webconnectivityalgo/dnsoverhttps.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package webconnectivityalgo

//
// DNS-over-HTTPS
//
// Code to manage DNS-over-HTTPS testing.
//

import (
"math/rand"
"sync"
"time"
)

// 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)

// OpportunisticDNSOverHTTPSURLProvider allows to perform opportunistic DNS-over-HTTPS
// measurements as part of Web Connectivity LTE. The zero value of this struct is not valid,
// please use [NewOpportunisticDNSOverHTTPSURLProvider] to construct.
type OpportunisticDNSOverHTTPSURLProvider 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
}

// NewOpportunisticDNSOverHTTPSURLProvider creates a new [*OpportunisticDNSOverHTTPSURLProvider].
func NewOpportunisticDNSOverHTTPSURLProvider(urls ...string) *OpportunisticDNSOverHTTPSURLProvider {
return &OpportunisticDNSOverHTTPSURLProvider{
interval: 0,
mu: &sync.Mutex{},
rnd: rand.New(rand.NewSource(time.Now().UnixNano())),
t: time.Time{},
urls: urls,
}
}

// 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 *OpportunisticDNSOverHTTPSURLProvider) 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
}
Loading

0 comments on commit a6f3ca4

Please sign in to comment.