Skip to content

Commit

Permalink
feat(webconnectivityqa): add test for android_dns_cache_no_data (ooni…
Browse files Browse the repository at this point in the history
…#1210)

Part of ooni/probe#2029. The general idea is
to modify v0.4 in a subsequent PR to make it WAI for this test case.
Currently v0.4 does not correctly support this case, which is why we're
doing this work. (See also ooni/probe#2499).
  • Loading branch information
bassosimone authored and Murphy-OrangeMud committed Feb 13, 2024
1 parent 5affe05 commit b735b3d
Show file tree
Hide file tree
Showing 13 changed files with 132 additions and 26 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ require (
github.com/mitchellh/go-wordwrap v1.0.1
github.com/montanaflynn/stats v0.7.1
github.com/ooni/go-libtor v1.1.8
github.com/ooni/netem v0.0.0-20230824170255-db1220e00a4b
github.com/ooni/netem v0.0.0-20230824211724-219d252971fc
github.com/ooni/oocrypto v0.5.3
github.com/ooni/oohttp v0.6.3
github.com/ooni/probe-assets v0.18.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -483,8 +483,8 @@ github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAl
github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU=
github.com/ooni/go-libtor v1.1.8 h1:Wo3V3DVTxl5vZdxtQakqYP+DAHx7pPtAFSl1bnAa08w=
github.com/ooni/go-libtor v1.1.8/go.mod h1:q1YyLwRD9GeMyeerVvwc0vJ2YgwDLTp2bdVcrh/JXyI=
github.com/ooni/netem v0.0.0-20230824170255-db1220e00a4b h1:bO3uWwIFe1ViVGQG5tIPfI6uVWNQozN0lxzS+VyG9F4=
github.com/ooni/netem v0.0.0-20230824170255-db1220e00a4b/go.mod h1:3LJOzTIu2O4ADDJN2ILG4ViJOqyH/u9fKY8QT2Rma8Y=
github.com/ooni/netem v0.0.0-20230824211724-219d252971fc h1:zHLh+al/LOYOha6lyIqW2TOhGvwtTupJ157Yh0AVS0o=
github.com/ooni/netem v0.0.0-20230824211724-219d252971fc/go.mod h1:3LJOzTIu2O4ADDJN2ILG4ViJOqyH/u9fKY8QT2Rma8Y=
github.com/ooni/oocrypto v0.5.3 h1:CAb0Ze6q/EWD1PRGl9KqpzMfkut4O3XMaiKYsyxrWOs=
github.com/ooni/oocrypto v0.5.3/go.mod h1:HjEQ5pQBl6btcWgAsKKq1tFo8CfBrZu63C/vPAUGIDk=
github.com/ooni/oohttp v0.6.3 h1:MHydpeAPU/LSDSI/hIFJwZm4afBhd2Yo+rNxxFdeMCY=
Expand Down
3 changes: 3 additions & 0 deletions internal/experiment/webconnectivity/qa_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import (
func TestQA(t *testing.T) {
for _, tc := range webconnectivityqa.AllTestCases() {
t.Run(tc.Name, func(t *testing.T) {
if (tc.Flags & webconnectivityqa.TestCaseFlagNoV04) != 0 {
t.Skip("this nettest cannot run on Web Connectivity v0.4")
}
measurer := NewExperimentMeasurer(Config{})
if err := webconnectivityqa.RunTestCase(measurer, tc); err != nil {
t.Fatal(err)
Expand Down
25 changes: 25 additions & 0 deletions internal/experiment/webconnectivityqa/dnsblocking.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package webconnectivityqa

import (
"github.com/ooni/probe-cli/v3/internal/netemx"
)

// dnsBlockingAndroidDNSCacheNoData is the case where we're on Android and the getaddrinfo
// resolver returns the android_dns_cache_no_data error.
func dnsBlockingAndroidDNSCacheNoData() *TestCase {
return &TestCase{
Name: "measuring https://www.example.com/ with getaddrinfo errors and android_dns_cache_no_data",
Flags: TestCaseFlagNoV04,
Input: "https://www.example.com/",
Configure: func(env *netemx.QAEnv) {
// make sure the env knows we want to emulate our getaddrinfo wrapper behavior
env.EmulateAndroidGetaddrinfo(true)

// remove the record so that the DNS query returns NXDOMAIN, which is then
// converted into android_dns_cache_no_data by the emulation layer
env.ISPResolverConfig().RemoveRecord("www.example.com")
},
ExpectErr: false,
ExpectTestKeys: &testKeys{Accessible: false, Blocking: "dns"},
}
}
28 changes: 28 additions & 0 deletions internal/experiment/webconnectivityqa/dnsblocking_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package webconnectivityqa

import (
"context"
"errors"
"testing"

"github.com/apex/log"
"github.com/ooni/probe-cli/v3/internal/netemx"
"github.com/ooni/probe-cli/v3/internal/netxlite"
)

func TestDNSBlockingAndroidDNSCacheNoData(t *testing.T) {
env := netemx.MustNewScenario(netemx.InternetScenario)
tc := dnsBlockingAndroidDNSCacheNoData()
tc.Configure(env)

env.Do(func() {
reso := netxlite.NewStdlibResolver(log.Log)
addrs, err := reso.LookupHost(context.Background(), "www.example.com")
if !errors.Is(err, netxlite.ErrAndroidDNSCacheNoData) {
t.Fatal("unexpected error", err)
}
if len(addrs) != 0 {
t.Fatal("expected to see no addresses")
}
})
}
10 changes: 10 additions & 0 deletions internal/experiment/webconnectivityqa/testcase.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,19 @@ package webconnectivityqa

import "github.com/ooni/probe-cli/v3/internal/netemx"

const (
// TestCaseFlagNoV04 means that this test case should not be run by v0.4
TestCaseFlagNoV04 = 1 << iota
)

// TestCase is a test case we could run with this package.
type TestCase struct {
// Name is the test case name
Name string

// Flags contains binary flags describing this test case.
Flags int64

// Input is the input URL
Input string

Expand All @@ -23,6 +31,8 @@ type TestCase struct {
// AllTestCases returns all the defined test cases.
func AllTestCases() []*TestCase {
return []*TestCase{
dnsBlockingAndroidDNSCacheNoData(),

tlsBlockingConnectionReset(),

sucessWithHTTP(),
Expand Down
23 changes: 23 additions & 0 deletions internal/netemx/android.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package netemx

import (
"context"

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

// androidStack wraps [netem.UnderlyingNetwork] to simulate what our getaddrinfo
// wrapper does on Android when it sees the EAI_NODATA return value.
type androidStack struct {
netem.UnderlyingNetwork
}

// GetaddrinfoLookupANY implements [netem.UnderlyingNetwork]
func (as *androidStack) GetaddrinfoLookupANY(ctx context.Context, domain string) ([]string, string, error) {
addrs, cname, err := as.UnderlyingNetwork.GetaddrinfoLookupANY(ctx, domain)
if err != nil {
err = netxlite.NewErrGetaddrinfo(0, netxlite.ErrAndroidDNSCacheNoData)
}
return addrs, cname, err
}
35 changes: 26 additions & 9 deletions internal/netemx/qaenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net"
"net/http"
"sync"
"sync/atomic"
"time"

"github.com/ooni/netem"
Expand Down Expand Up @@ -162,6 +163,10 @@ type QAEnv struct {
// closables contains all entities where we have to take care of closing.
closables []io.Closer

// emulateAndroidGetaddrinfo controls whether to emulate the behavior of our wrapper for
// the android implementation of getaddrinfo returning android_dns_cache_no_data
emulateAndroidGetaddrinfo *atomic.Bool

// ispResolverConfig is the DNS config used by the ISP resolver.
ispResolverConfig *netem.DNSConfig

Expand Down Expand Up @@ -199,14 +204,15 @@ func MustNewQAEnv(options ...QAEnvOption) *QAEnv {

// create an empty QAEnv
env := &QAEnv{
clientNICWrapper: config.clientNICWrapper,
clientStack: nil,
closables: []io.Closer{},
ispResolverConfig: netem.NewDNSConfig(),
dpi: netem.NewDPIEngine(config.logger),
once: sync.Once{},
otherResolversConfig: netem.NewDNSConfig(),
topology: runtimex.Try1(netem.NewStarTopology(config.logger)),
clientNICWrapper: config.clientNICWrapper,
clientStack: nil,
closables: []io.Closer{},
emulateAndroidGetaddrinfo: &atomic.Bool{},
ispResolverConfig: netem.NewDNSConfig(),
dpi: netem.NewDPIEngine(config.logger),
once: sync.Once{},
otherResolversConfig: netem.NewDNSConfig(),
topology: runtimex.Try1(netem.NewStarTopology(config.logger)),
}

// create all the required internals
Expand Down Expand Up @@ -406,10 +412,21 @@ func (env *QAEnv) DPIEngine() *netem.DPIEngine {
return env.dpi
}

// EmulateAndroidGetaddrinfo configures [QAEnv] such that the Do method wraps
// the underlying client stack to return android_dns_cache_no_data on any error
// that occurs. This method can be safely called by multiple goroutines.
func (env *QAEnv) EmulateAndroidGetaddrinfo(value bool) {
env.emulateAndroidGetaddrinfo.Store(value)
}

// Do executes the given function such that [netxlite] code uses the
// underlying clientStack rather than ordinary networking code.
func (env *QAEnv) Do(function func()) {
WithCustomTProxy(env.clientStack, function)
var stack netem.UnderlyingNetwork = env.clientStack
if env.emulateAndroidGetaddrinfo.Load() {
stack = &androidStack{stack}
}
WithCustomTProxy(stack, function)
}

// Close closes all the resources used by [QAEnv].
Expand Down
4 changes: 2 additions & 2 deletions internal/netxlite/getaddrinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ type ErrGetaddrinfo struct {
Code int64
}

// newErrGetaddrinfo creates a new instance of the ErrGetaddrinfo type.
func newErrGetaddrinfo(code int64, err error) *ErrGetaddrinfo {
// NewErrGetaddrinfo creates a new instance of the ErrGetaddrinfo type.
func NewErrGetaddrinfo(code int64, err error) *ErrGetaddrinfo {
return &ErrGetaddrinfo{
Underlying: err,
Code: code,
Expand Down
6 changes: 3 additions & 3 deletions internal/netxlite/getaddrinfo_bsd.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@ func (state *getaddrinfoState) toError(code int64, err error, goos string) error
// comes up again. golang.org/issue/6232.
err = syscall.EMFILE
}
return newErrGetaddrinfo(code, err)
return NewErrGetaddrinfo(code, err)
case C.EAI_NONAME:
err = ErrOODNSNoSuchHost // so it becomes FailureDNSNXDOMAIN
return newErrGetaddrinfo(code, err)
return NewErrGetaddrinfo(code, err)
default:
err = ErrOODNSMisbehaving // so it becomes FailureDNSServerMisbehaving
return newErrGetaddrinfo(code, err)
return NewErrGetaddrinfo(code, err)
}
}
10 changes: 5 additions & 5 deletions internal/netxlite/getaddrinfo_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,13 @@ func (state *getaddrinfoState) toError(code int64, err error, goos string) error
// comes up again. golang.org/issue/6232.
err = syscall.EMFILE
}
return newErrGetaddrinfo(code, err)
return NewErrGetaddrinfo(code, err)
case C.EAI_NONAME:
return newErrGetaddrinfo(code, ErrOODNSNoSuchHost)
return NewErrGetaddrinfo(code, ErrOODNSNoSuchHost)
case C.EAI_NODATA:
return state.toErrorNODATA(err, goos)
default:
return newErrGetaddrinfo(code, ErrOODNSMisbehaving)
return NewErrGetaddrinfo(code, ErrOODNSMisbehaving)
}
}

Expand Down Expand Up @@ -154,8 +154,8 @@ func (state *getaddrinfoState) toError(code int64, err error, goos string) error
func (state *getaddrinfoState) toErrorNODATA(err error, goos string) error {
switch goos {
case "android":
return newErrGetaddrinfo(C.EAI_NODATA, ErrAndroidDNSCacheNoData)
return NewErrGetaddrinfo(C.EAI_NODATA, ErrAndroidDNSCacheNoData)
default:
return newErrGetaddrinfo(C.EAI_NODATA, ErrOODNSNoAnswer)
return NewErrGetaddrinfo(C.EAI_NODATA, ErrOODNSNoAnswer)
}
}
6 changes: 3 additions & 3 deletions internal/netxlite/getaddrinfo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func TestErrorToGetaddrinfoRetval(t *testing.T) {
}{{
name: "with valid getaddrinfo error",
args: args{
newErrGetaddrinfo(144, nil),
NewErrGetaddrinfo(144, nil),
},
want: 144,
}, {
Expand All @@ -49,7 +49,7 @@ func TestErrorToGetaddrinfoRetval(t *testing.T) {
}
}

func Test_newErrGetaddrinfo(t *testing.T) {
func TestNewErrGetaddrinfo(t *testing.T) {
type args struct {
code int64
err error
Expand All @@ -66,7 +66,7 @@ func Test_newErrGetaddrinfo(t *testing.T) {
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := newErrGetaddrinfo(tt.args.code, tt.args.err)
err := NewErrGetaddrinfo(tt.args.code, tt.args.err)
if err == nil {
t.Fatal("expected non-nil error")
}
Expand Down
2 changes: 1 addition & 1 deletion internal/netxlite/getaddrinfo_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,5 @@ func (state *getaddrinfoState) toError(code int64, err error, goos string) error
// is no other error, just cast code to a syscall err.
err = syscall.Errno(code)
}
return newErrGetaddrinfo(int64(code), err)
return NewErrGetaddrinfo(int64(code), err)
}

0 comments on commit b735b3d

Please sign in to comment.