Skip to content

Commit

Permalink
sniblocking: classify failures to produce a result (#391)
Browse files Browse the repository at this point in the history
Since in OONI we typically use a three class system (i.e. accessible,
anomaly, or blocking), I am using these three classes.

I am also adding further information that help in understanding what
is the real root cause of the failure.

Part of #309.
  • Loading branch information
bassosimone authored Mar 7, 2020
1 parent 030e792 commit 30ee2a6
Show file tree
Hide file tree
Showing 11 changed files with 216 additions and 54 deletions.
59 changes: 57 additions & 2 deletions experiment/sniblocking/sniblocking.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import (

const (
testName = "sni_blocking"
testVersion = "0.0.3"
testVersion = "0.0.4"
)

// Config contains the experiment config.
Expand Down Expand Up @@ -53,9 +53,57 @@ type Subresult struct {
// TestKeys contains sniblocking test keys.
type TestKeys struct {
Control Subresult `json:"control"`
Result string `json:"result"`
Target Subresult `json:"target"`
}

const (
classAccessibleInvalidHostname = "accessible_invalid_hostname"
classAccessibleValidHostname = "accessible_valid_hostname"
classAnomalySSLError = "anomaly_ssl_error"
classAnomalyTestHelperBlocked = "anomaly_test_helper_blocked"
classAnomalyTimeout = "anomaly_timeout"
classAnomalyUnexpectedFailure = "anomaly_unexpected_failure"
classBlockedTCPIPError = "blocked_tcpip_error"
)

func (tk *TestKeys) classify() string {
// This implementation of classify is loosely modeled after
// https://github.com/ooni/spec/pull/159#discussion_r373754706
if tk.Target.Failure == nil {
return classAccessibleValidHostname
}
// TODO(bassosimone): we should write jafar tests to understand
// what error is returned in the case of MITM and make sure we
// can reliably detect and distinguish this case from other cases
// of TLS error. For now, the following is coded such that the
// MITM will result in classAnomalySSLErrror.
//
// See https://github.com/ooni/probe-engine/issues/393.
switch *tk.Target.Failure {
case modelx.FailureConnectionRefused:
return classAnomalyTestHelperBlocked
case modelx.FailureDNSNXDOMAINError:
return classAnomalyTestHelperBlocked
case modelx.FailureConnectionReset:
return classBlockedTCPIPError
case modelx.FailureEOFError:
return classBlockedTCPIPError
case modelx.FailureSSLInvalidHostname:
return classAccessibleInvalidHostname
case modelx.FailureSSLUnknownAuthority:
return classAnomalySSLError
case modelx.FailureSSLInvalidCertificate:
return classAnomalySSLError
case modelx.FailureGenericTimeoutError:
if tk.Control.Failure != nil {
return classAnomalyTestHelperBlocked
}
return classAnomalyTimeout
}
return classAnomalyUnexpectedFailure
}

type measurer struct {
cache map[string]Subresult
config Config
Expand Down Expand Up @@ -83,7 +131,7 @@ func (m *measurer) measureone(
select {
case <-time.After(sleeptime):
case <-ctx.Done():
s := "generic_timeout_error"
s := modelx.FailureGenericTimeoutError
return Subresult{
Failure: &s,
SNI: sni,
Expand Down Expand Up @@ -188,6 +236,8 @@ func processall(
break
}
}
testkeys.Result = testkeys.classify()
sess.Logger().Infof("sni_blocking: result: %s", testkeys.Result)
callbacks.OnDataUsage(
float64(receivedBytes)/1024.0, // downloaded
float64(sentBytes)/1024.0, // uploaded
Expand Down Expand Up @@ -230,6 +280,11 @@ func (m *measurer) Run(
m.config.ControlSNI, "443",
)
}
// TODO(bassosimone): if the user has configured DoT or DoH, here we
// probably want to perform the name resolution before the measurements
// or to make sure that the classify logic is robust to that.
//
// See https://github.com/ooni/probe-engine/issues/392.
maybeParsed, err := maybeURLToSNI(measurement.Input)
if err != nil {
return err
Expand Down
92 changes: 88 additions & 4 deletions experiment/sniblocking/sniblocking_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,103 @@ import (
"github.com/ooni/probe-engine/internal/mockable"
"github.com/ooni/probe-engine/internal/netxlogger"
"github.com/ooni/probe-engine/model"
"github.com/ooni/probe-engine/netx/modelx"
)

const (
softwareName = "ooniprobe-example"
softwareVersion = "0.0.1"
)

func TestUnitTestKeysClassify(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() != classAccessibleValidHostname {
t.Fatal("unexpected result")
}
})
t.Run("with tk.Target.Failure == connection_refused", func(t *testing.T) {
tk := new(TestKeys)
tk.Target.Failure = asStringPtr(modelx.FailureConnectionRefused)
if tk.classify() != classAnomalyTestHelperBlocked {
t.Fatal("unexpected result")
}
})
t.Run("with tk.Target.Failure == dns_nxdomain_error", func(t *testing.T) {
tk := new(TestKeys)
tk.Target.Failure = asStringPtr(modelx.FailureDNSNXDOMAINError)
if tk.classify() != classAnomalyTestHelperBlocked {
t.Fatal("unexpected result")
}
})
t.Run("with tk.Target.Failure == connection_reset", func(t *testing.T) {
tk := new(TestKeys)
tk.Target.Failure = asStringPtr(modelx.FailureConnectionReset)
if tk.classify() != classBlockedTCPIPError {
t.Fatal("unexpected result")
}
})
t.Run("with tk.Target.Failure == eof_error", func(t *testing.T) {
tk := new(TestKeys)
tk.Target.Failure = asStringPtr(modelx.FailureEOFError)
if tk.classify() != classBlockedTCPIPError {
t.Fatal("unexpected result")
}
})
t.Run("with tk.Target.Failure == ssl_invalid_hostname", func(t *testing.T) {
tk := new(TestKeys)
tk.Target.Failure = asStringPtr(modelx.FailureSSLInvalidHostname)
if tk.classify() != classAccessibleInvalidHostname {
t.Fatal("unexpected result")
}
})
t.Run("with tk.Target.Failure == ssl_unknown_authority", func(t *testing.T) {
tk := new(TestKeys)
tk.Target.Failure = asStringPtr(modelx.FailureSSLUnknownAuthority)
if tk.classify() != classAnomalySSLError {
t.Fatal("unexpected result")
}
})
t.Run("with tk.Target.Failure == ssl_invalid_certificate", func(t *testing.T) {
tk := new(TestKeys)
tk.Target.Failure = asStringPtr(modelx.FailureSSLInvalidCertificate)
if tk.classify() != classAnomalySSLError {
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(modelx.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(modelx.FailureGenericTimeoutError)
tk.Control.Failure = asStringPtr(modelx.FailureGenericTimeoutError)
if tk.classify() != classAnomalyTestHelperBlocked {
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 TestUnitNewExperimentMeasurer(t *testing.T) {
measurer := NewExperimentMeasurer(Config{})
if measurer.ExperimentName() != "sni_blocking" {
t.Fatal("unexpected name")
}
if measurer.ExperimentVersion() != "0.0.3" {
if measurer.ExperimentVersion() != "0.0.4" {
t.Fatal("unexpected version")
}
}
Expand Down Expand Up @@ -105,7 +189,7 @@ func TestUnitMeasureoneCancelledContext(t *testing.T) {
"kernel.org",
"example.com:443",
)
if *result.Failure != "generic_timeout_error" {
if *result.Failure != modelx.FailureGenericTimeoutError {
t.Fatal("unexpected failure")
}
if result.SNI != "kernel.org" {
Expand All @@ -127,7 +211,7 @@ func TestUnitMeasureoneSuccess(t *testing.T) {
"kernel.org",
"example.com:443",
)
if *result.Failure != "ssl_invalid_hostname" {
if *result.Failure != modelx.FailureSSLInvalidHostname {
t.Fatal("unexpected failure")
}
if result.SNI != "kernel.org" {
Expand Down Expand Up @@ -159,7 +243,7 @@ func TestUnitMeasureonewithcacheWorks(t *testing.T) {
if result.Cached != expected {
t.Fatal("unexpected cached")
}
if *result.Failure != "ssl_invalid_hostname" {
if *result.Failure != modelx.FailureSSLInvalidHostname {
t.Fatal("unexpected failure")
}
if result.SNI != "kernel.org" {
Expand Down
12 changes: 6 additions & 6 deletions internal/oonidatamodel/oonidatamodel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func TestUnitNewTCPConnectListFailure(t *testing.T) {
Connects: []*modelx.ConnectEvent{
&modelx.ConnectEvent{
RemoteAddress: "8.8.8.8:53",
Error: errors.New("connection_reset"),
Error: errors.New(modelx.FailureConnectionReset),
},
},
})
Expand All @@ -79,7 +79,7 @@ func TestUnitNewTCPConnectListFailure(t *testing.T) {
if out[0].Port != 53 {
t.Fatal("unexpected out[0].Port")
}
if *out[0].Status.Failure != "connection_reset" {
if *out[0].Status.Failure != modelx.FailureConnectionReset {
t.Fatal("unexpected out[0].Failure")
}
if out[0].Status.Success != false {
Expand All @@ -92,7 +92,7 @@ func TestUnitNewTCPConnectListInvalidInput(t *testing.T) {
Connects: []*modelx.ConnectEvent{
&modelx.ConnectEvent{
RemoteAddress: "8.8.8.8",
Error: errors.New("connection_reset"),
Error: errors.New(modelx.FailureConnectionReset),
},
},
})
Expand All @@ -105,7 +105,7 @@ func TestUnitNewTCPConnectListInvalidInput(t *testing.T) {
if out[0].Port != 0 {
t.Fatal("unexpected out[0].Port")
}
if *out[0].Status.Failure != "connection_reset" {
if *out[0].Status.Failure != modelx.FailureConnectionReset {
t.Fatal("unexpected out[0].Failure")
}
if out[0].Status.Success != false {
Expand Down Expand Up @@ -648,7 +648,7 @@ func TestUnitNewDNSQueriesListSuccess(t *testing.T) {
TransportNetwork: "system",
},
&modelx.ResolveDoneEvent{
Error: errors.New("dns_nxdomain_error"),
Error: errors.New(modelx.FailureDNSNXDOMAINError),
Hostname: "dns.googlex",
TransportNetwork: "system",
},
Expand Down Expand Up @@ -767,7 +767,7 @@ func dnscheckbad(e DNSQueryEntry) error {
if e.Engine != "system" {
return errors.New("invalid engine")
}
if *e.Failure != "dns_nxdomain_error" {
if *e.Failure != modelx.FailureDNSNXDOMAINError {
return errors.New("invalid failure")
}
if e.Hostname != "dns.googlex" {
Expand Down
5 changes: 3 additions & 2 deletions internal/oonitemplates/oonitemplates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"time"

goptlib "git.torproject.org/pluggable-transports/goptlib.git"
"github.com/ooni/probe-engine/netx/modelx"
"gitlab.com/yawning/obfs4.git/transports"
obfs4base "gitlab.com/yawning/obfs4.git/transports/base"
)
Expand Down Expand Up @@ -37,7 +38,7 @@ func TestIntegrationDNSLookupCancellation(t *testing.T) {
if results.Error == nil {
t.Fatal("expected an error here")
}
if results.Error.Error() != "generic_timeout_error" {
if results.Error.Error() != modelx.FailureGenericTimeoutError {
t.Fatal("not the error we expected")
}
if len(results.Addresses) > 0 {
Expand Down Expand Up @@ -152,7 +153,7 @@ func TestIntegrationTLSConnectCancellation(t *testing.T) {
if results.Error == nil {
t.Fatal("expected an error here")
}
if results.Error.Error() != "generic_timeout_error" {
if results.Error.Error() != modelx.FailureGenericTimeoutError {
t.Fatal("not the error we expected")
}
}
Expand Down
3 changes: 2 additions & 1 deletion netx/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"time"

"github.com/ooni/probe-engine/netx"
"github.com/ooni/probe-engine/netx/modelx"
)

func dowithclient(t *testing.T, client *netx.HTTPClient) {
Expand Down Expand Up @@ -147,7 +148,7 @@ func TestIntegrationHTTPTransportTimeout(t *testing.T) {
if err == nil {
t.Fatal("expected an error here")
}
if !strings.HasSuffix(err.Error(), "generic_timeout_error") {
if !strings.HasSuffix(err.Error(), modelx.FailureGenericTimeoutError) {
t.Fatal("not the error we expected")
}
if resp != nil {
Expand Down
4 changes: 2 additions & 2 deletions netx/internal/dialer/dnsdialer/dnsdialer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,11 @@ func TestReduceErrors(t *testing.T) {
Failure: "unknown_error: antani",
}
err3 := &modelx.ErrWrapper{
Failure: "connection_refused",
Failure: modelx.FailureConnectionRefused,
}
err4 := errors.New("mocked error #3")
result := reduceErrors([]error{err1, err2, err3, err4})
if result.Error() != "connection_refused" {
if result.Error() != modelx.FailureConnectionRefused {
t.Fatal("wrong result")
}
})
Expand Down
Loading

0 comments on commit 30ee2a6

Please sign in to comment.