ooni-probe-cli/internal/engine/experiment/sniblocking/sniblocking_test.go
Simone Basso cf6dbe48e0
netxlite: call getaddrinfo and handle platform-specific oddities (#764)
This commit changes our system resolver to call getaddrinfo directly when CGO is enabled. This change allows us to:

1. obtain the CNAME easily

2. obtain the real getaddrinfo retval

3. handle platform specific oddities such as `EAI_NODATA`
returned on Android devices

See https://github.com/ooni/probe/issues/2029 and https://github.com/ooni/probe/issues/2029#issuecomment-1140258729 in particular.

See https://github.com/ooni/probe/issues/2033 for documentation regarding the desire to see `getaddrinfo`'s retval.

See https://github.com/ooni/probe/issues/2118 for possible follow-up changes.
2022-05-28 15:10:30 +02:00

437 lines
11 KiB
Go

package sniblocking
import (
"context"
"strings"
"testing"
"time"
"github.com/apex/log"
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
"github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/netxlite"
)
func TestTestKeysClassify(t *testing.T) {
asStringPtr := func(s string) *string {
return &s
}
t.Run("with tk.Target.Failure == nil", func(t *testing.T) {
tk := new(TestKeys)
if tk.classify() != classSuccessGotServerHello {
t.Fatal("unexpected result")
}
})
t.Run("with tk.Target.Failure == connection_refused", func(t *testing.T) {
tk := new(TestKeys)
tk.Target.Failure = asStringPtr(netxlite.FailureConnectionRefused)
if tk.classify() != classAnomalyTestHelperUnreachable {
t.Fatal("unexpected result")
}
})
t.Run("with tk.Target.Failure == dns_nxdomain_error", func(t *testing.T) {
tk := new(TestKeys)
tk.Target.Failure = asStringPtr(netxlite.FailureDNSNXDOMAINError)
if tk.classify() != classAnomalyTestHelperUnreachable {
t.Fatal("unexpected result")
}
})
t.Run("with tk.Target.Failure == android_dns_cache_no_data", func(t *testing.T) {
tk := new(TestKeys)
tk.Target.Failure = asStringPtr(netxlite.FailureAndroidDNSCacheNoData)
if tk.classify() != classAnomalyTestHelperUnreachable {
t.Fatal("unexpected result")
}
})
t.Run("with tk.Target.Failure == connection_reset", func(t *testing.T) {
tk := new(TestKeys)
tk.Target.Failure = asStringPtr(netxlite.FailureConnectionReset)
if tk.classify() != classInterferenceReset {
t.Fatal("unexpected result")
}
})
t.Run("with tk.Target.Failure == eof_error", func(t *testing.T) {
tk := new(TestKeys)
tk.Target.Failure = asStringPtr(netxlite.FailureEOFError)
if tk.classify() != classInterferenceClosed {
t.Fatal("unexpected result")
}
})
t.Run("with tk.Target.Failure == ssl_invalid_hostname", func(t *testing.T) {
tk := new(TestKeys)
tk.Target.Failure = asStringPtr(netxlite.FailureSSLInvalidHostname)
if tk.classify() != classSuccessGotServerHello {
t.Fatal("unexpected result")
}
})
t.Run("with tk.Target.Failure == ssl_unknown_authority", func(t *testing.T) {
tk := new(TestKeys)
tk.Target.Failure = asStringPtr(netxlite.FailureSSLUnknownAuthority)
if tk.classify() != classInterferenceUnknownAuthority {
t.Fatal("unexpected result")
}
})
t.Run("with tk.Target.Failure == ssl_invalid_certificate", func(t *testing.T) {
tk := new(TestKeys)
tk.Target.Failure = asStringPtr(netxlite.FailureSSLInvalidCertificate)
if tk.classify() != classInterferenceInvalidCertificate {
t.Fatal("unexpected result")
}
})
t.Run("with tk.Target.Failure == generic_timeout_error #1", func(t *testing.T) {
tk := new(TestKeys)
tk.Target.Failure = asStringPtr(netxlite.FailureGenericTimeoutError)
if tk.classify() != classAnomalyTimeout {
t.Fatal("unexpected result")
}
})
t.Run("with tk.Target.Failure == generic_timeout_error #2", func(t *testing.T) {
tk := new(TestKeys)
tk.Target.Failure = asStringPtr(netxlite.FailureGenericTimeoutError)
tk.Control.Failure = asStringPtr(netxlite.FailureGenericTimeoutError)
if tk.classify() != classAnomalyTestHelperUnreachable {
t.Fatal("unexpected result")
}
})
t.Run("with tk.Target.Failure == unknown_failure", func(t *testing.T) {
tk := new(TestKeys)
tk.Target.Failure = asStringPtr("unknown_failure")
if tk.classify() != classAnomalyUnexpectedFailure {
t.Fatal("unexpected result")
}
})
}
func TestNewExperimentMeasurer(t *testing.T) {
measurer := NewExperimentMeasurer(Config{})
if measurer.ExperimentName() != "sni_blocking" {
t.Fatal("unexpected name")
}
if measurer.ExperimentVersion() != "0.3.0" {
t.Fatal("unexpected version")
}
}
func TestMeasurerMeasureNoMeasurementInput(t *testing.T) {
measurer := NewExperimentMeasurer(Config{
ControlSNI: "example.com",
})
err := measurer.Run(
context.Background(),
newsession(),
new(model.Measurement),
model.NewPrinterCallbacks(log.Log),
)
if err.Error() != "Experiment requires measurement.Input" {
t.Fatal("not the error we expected")
}
}
func TestMeasurerMeasureWithInvalidInput(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel() // immediately cancel the context
measurer := NewExperimentMeasurer(Config{
ControlSNI: "example.com",
})
measurement := &model.Measurement{
Input: "\t",
}
err := measurer.Run(
ctx,
newsession(),
measurement,
model.NewPrinterCallbacks(log.Log),
)
if err == nil {
t.Fatal("expected an error here")
}
}
func TestMeasurerMeasureWithCancelledContext(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel() // immediately cancel the context
measurer := NewExperimentMeasurer(Config{
ControlSNI: "example.com",
})
measurement := &model.Measurement{
Input: "kernel.org",
}
err := measurer.Run(
ctx,
newsession(),
measurement,
model.NewPrinterCallbacks(log.Log),
)
if err != nil {
t.Fatal(err)
}
sk, err := measurer.GetSummaryKeys(measurement)
if err != nil {
t.Fatal(err)
}
if _, ok := sk.(SummaryKeys); !ok {
t.Fatal("invalid type for summary keys")
}
}
func TestMeasureoneCancelledContext(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel() // immediately cancel the context
result := new(Measurer).measureone(
ctx,
&mockable.Session{MockableLogger: log.Log},
time.Now(),
"kernel.org",
"example.com:443",
)
if result.Agent != "" {
t.Fatal("not the expected Agent")
}
if result.BootstrapTime != 0.0 {
t.Fatal("not the expected BootstrapTime")
}
if result.DNSCache != nil {
t.Fatal("not the expected DNSCache")
}
if result.FailedOperation == nil || *result.FailedOperation != netxlite.TopLevelOperation {
t.Fatal("not the expected FailedOperation")
}
if result.Failure == nil || *result.Failure != netxlite.FailureInterrupted {
t.Fatal("not the expected failure")
}
if result.NetworkEvents != nil {
t.Fatal("not the expected NetworkEvents")
}
if result.Queries != nil {
t.Fatal("not the expected Queries")
}
if result.Requests != nil {
t.Fatal("not the expected Requests")
}
if result.SOCKSProxy != "" {
t.Fatal("not the expected SOCKSProxy")
}
if result.TCPConnect != nil {
t.Fatal("not the expected TCPConnect")
}
if result.TLSHandshakes != nil {
t.Fatal("not the expected TLSHandshakes")
}
if result.Tunnel != "" {
t.Fatal("not the expected Tunnel")
}
if result.SNI != "kernel.org" {
t.Fatal("unexpected SNI")
}
if result.THAddress != "example.com:443" {
t.Fatal("unexpected THAddress")
}
}
func TestMeasureoneWithPreMeasurementFailure(t *testing.T) {
result := new(Measurer).measureone(
context.Background(),
&mockable.Session{MockableLogger: log.Log},
time.Now(),
"kernel.org",
"example.com:443\t\t\t", // cause URL parse error
)
if result.Agent != "redirect" {
t.Fatal("not the expected Agent")
}
if result.BootstrapTime != 0.0 {
t.Fatal("not the expected BootstrapTime")
}
if result.DNSCache != nil {
t.Fatal("not the expected DNSCache")
}
if result.FailedOperation == nil || *result.FailedOperation != "top_level" {
t.Fatal("not the expected FailedOperation")
}
if result.Failure == nil || !strings.Contains(*result.Failure, "invalid target URL") {
t.Fatal("not the expected failure")
}
if result.NetworkEvents != nil {
t.Fatal("not the expected NetworkEvents")
}
if result.Queries != nil {
t.Fatal("not the expected Queries")
}
if result.Requests != nil {
t.Fatal("not the expected Requests")
}
if result.SOCKSProxy != "" {
t.Fatal("not the expected SOCKSProxy")
}
if result.TCPConnect != nil {
t.Fatal("not the expected TCPConnect")
}
if result.TLSHandshakes != nil {
t.Fatal("not the expected TLSHandshakes")
}
if result.Tunnel != "" {
t.Fatal("not the expected Tunnel")
}
if result.SNI != "kernel.org" {
t.Fatal("unexpected SNI")
}
if result.THAddress != "example.com:443\t\t\t" {
t.Fatal("unexpected THAddress")
}
}
func TestMeasureoneSuccess(t *testing.T) {
result := new(Measurer).measureone(
context.Background(),
&mockable.Session{MockableLogger: log.Log},
time.Now(),
"kernel.org",
"example.com:443",
)
if result.Agent != "redirect" {
t.Fatal("not the expected Agent")
}
if result.BootstrapTime != 0.0 {
t.Fatal("not the expected BootstrapTime")
}
if result.DNSCache != nil {
t.Fatal("not the expected DNSCache")
}
if result.FailedOperation == nil || *result.FailedOperation != netxlite.TLSHandshakeOperation {
t.Fatal("not the expected FailedOperation")
}
if result.Failure == nil || *result.Failure != netxlite.FailureSSLInvalidHostname {
t.Fatal("unexpected failure")
}
if len(result.NetworkEvents) < 1 {
t.Fatal("not the expected NetworkEvents")
}
if len(result.Queries) < 1 {
t.Fatal("not the expected Queries")
}
if result.Requests != nil {
t.Fatal("not the expected Requests")
}
if result.SOCKSProxy != "" {
t.Fatal("not the expected SOCKSProxy")
}
if len(result.TCPConnect) < 1 {
t.Fatal("not the expected TCPConnect")
}
if len(result.TLSHandshakes) < 1 {
t.Fatal("not the expected TLSHandshakes")
}
if result.Tunnel != "" {
t.Fatal("not the expected Tunnel")
}
if result.SNI != "kernel.org" {
t.Fatal("unexpected SNI")
}
if result.THAddress != "example.com:443" {
t.Fatal("unexpected THAddress")
}
}
func TestMeasureonewithcacheWorks(t *testing.T) {
measurer := &Measurer{cache: make(map[string]Subresult)}
output := make(chan Subresult, 2)
for i := 0; i < 2; i++ {
measurer.measureonewithcache(
context.Background(),
output,
&mockable.Session{MockableLogger: log.Log},
time.Now(),
"kernel.org",
"example.com:443",
)
}
for _, expected := range []bool{false, true} {
result := <-output
if result.Cached != expected {
t.Fatal("unexpected cached")
}
if *result.Failure != netxlite.FailureSSLInvalidHostname {
t.Fatal("unexpected failure")
}
if result.SNI != "kernel.org" {
t.Fatal("unexpected SNI")
}
}
}
func TestProcessallPanicsIfInvalidSNI(t *testing.T) {
defer func() {
panicdata := recover()
if panicdata == nil {
t.Fatal("expected to see panic here")
}
if panicdata.(string) != "unexpected smk.SNI" {
t.Fatal("not the panic we expected")
}
}()
outputs := make(chan Subresult, 1)
measurement := &model.Measurement{
Input: "kernel.org",
}
go func() {
outputs <- Subresult{
SNI: "antani.io",
}
}()
processall(
outputs,
measurement,
model.NewPrinterCallbacks(log.Log),
[]string{"kernel.org", "example.com"},
newsession(),
"example.com",
)
}
func TestMaybeURLToSNI(t *testing.T) {
t.Run("for invalid URL", func(t *testing.T) {
parsed, err := maybeURLToSNI("\t")
if err == nil {
t.Fatal("expected an error here")
}
if parsed != "" {
t.Fatal("expected empty parsed here")
}
})
t.Run("for domain name", func(t *testing.T) {
parsed, err := maybeURLToSNI("kernel.org")
if err != nil {
t.Fatal(err)
}
if parsed != "kernel.org" {
t.Fatal("expected different domain here")
}
})
t.Run("for valid URL", func(t *testing.T) {
parsed, err := maybeURLToSNI("https://kernel.org/robots.txt")
if err != nil {
t.Fatal(err)
}
if parsed != "kernel.org" {
t.Fatal("expected different domain here")
}
})
}
func newsession() model.ExperimentSession {
return &mockable.Session{MockableLogger: log.Log}
}
func TestSummaryKeysGeneric(t *testing.T) {
measurement := &model.Measurement{TestKeys: &TestKeys{}}
m := &Measurer{}
osk, err := m.GetSummaryKeys(measurement)
if err != nil {
t.Fatal(err)
}
sk := osk.(SummaryKeys)
if sk.IsAnomaly {
t.Fatal("invalid isAnomaly")
}
}