From 991b0a6120f55fdb438ac895db7a9b32fb771bb4 Mon Sep 17 00:00:00 2001 From: cyBerta Date: Tue, 30 Mar 2021 12:02:51 +0200 Subject: [PATCH] riseupvpn: reduce false positives (#233) * fetch RiseupVPN CA cert with MultiGetter. It allows us to write better tests and ensures this test step is added in the logs * Implement TransportStatus for RiseupVPN tests. It indicates if a whole transport is blocked, which is considered as a test anomaly * Redesign unit tests for RiseupVPN. Instead of a real backend, mocked server responses are used. Tests for invalid CA certs and for TransportStatus are added. * Update internal/engine/experiment/riseupvpn/riseupvpn.go Co-authored-by: Simone Basso --- .../engine/experiment/riseupvpn/riseupvpn.go | 99 ++- .../experiment/riseupvpn/riseupvpn_test.go | 754 ++++++++++++------ 2 files changed, 589 insertions(+), 264 deletions(-) diff --git a/internal/engine/experiment/riseupvpn/riseupvpn.go b/internal/engine/experiment/riseupvpn/riseupvpn.go index 8295dbb..91f8b20 100644 --- a/internal/engine/experiment/riseupvpn/riseupvpn.go +++ b/internal/engine/experiment/riseupvpn/riseupvpn.go @@ -9,7 +9,6 @@ import ( "errors" "time" - "github.com/apex/log" "github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter" "github.com/ooni/probe-cli/v3/internal/engine/model" "github.com/ooni/probe-cli/v3/internal/engine/netx" @@ -18,7 +17,7 @@ import ( const ( testName = "riseupvpn" - testVersion = "0.1.0" + testVersion = "0.2.0" eipServiceURL = "https://api.black.riseup.net:443/3/config/eip-service.json" providerURL = "https://riseup.net/provider.json" geoServiceURL = "https://api.black.riseup.net:9001/json" @@ -66,6 +65,7 @@ type TestKeys struct { APIStatus string `json:"api_status"` CACertStatus bool `json:"ca_cert_status"` FailingGateways []GatewayConnection `json:"failing_gateways"` + TransportStatus map[string]string `json:"transport_status"` } // NewTestKeys creates new riseupvpn TestKeys. @@ -75,6 +75,7 @@ func NewTestKeys() *TestKeys { APIStatus: "ok", CACertStatus: true, FailingGateways: nil, + TransportStatus: nil, } } @@ -96,6 +97,7 @@ func (tk *TestKeys) UpdateProviderAPITestKeys(v urlgetter.MultiOutput) { } // AddGatewayConnectTestKeys updates the TestKeys using the given MultiOutput result of gateway connectivity testing. +// Sets TransportStatus to "ok" if any successful TCP connection could be made func (tk *TestKeys) AddGatewayConnectTestKeys(v urlgetter.MultiOutput, transportType string) { tk.NetworkEvents = append(tk.NetworkEvents, v.TestKeys.NetworkEvents...) tk.TCPConnect = append(tk.TCPConnect, v.TestKeys.TCPConnect...) @@ -108,6 +110,29 @@ func (tk *TestKeys) AddGatewayConnectTestKeys(v urlgetter.MultiOutput, transport return } +func (tk *TestKeys) updateTransportStatus(openvpnGatewayCount int, obfs4GatewayCount int) { + failingOpenvpnGateways, failingObfs4Gateways := 0, 0 + for _, gw := range tk.FailingGateways { + if gw.TransportType == "openvpn" { + failingOpenvpnGateways++ + } else if gw.TransportType == "obfs4" { + failingObfs4Gateways++ + } + } + + if failingOpenvpnGateways < openvpnGatewayCount { + tk.TransportStatus["openvpn"] = "ok" + } else { + tk.TransportStatus["openvpn"] = "blocked" + } + + if failingObfs4Gateways < obfs4GatewayCount { + tk.TransportStatus["obfs4"] = "ok" + } else { + tk.TransportStatus["obfs4"] = "blocked" + } +} + func newGatewayConnection(tcpConnect archival.TCPConnectEntry, transportType string) *GatewayConnection { return &GatewayConnection{ IP: tcpConnect.IP, @@ -160,30 +185,32 @@ func (m Measurer) Run(ctx context.Context, sess model.ExperimentSession, urlgetter.RegisterExtensions(measurement) caTarget := "https://black.riseup.net/ca.crt" - caGetter := urlgetter.Getter{ - Config: m.Config.Config, - Session: sess, - Target: caTarget, - } - log.Info("Getting CA certificate; please be patient...") - tk, err := caGetter.Get(ctx) - testkeys.AddCACertFetchTestKeys(tk) - - if err != nil { - log.Error("Getting CA certificate failed. Aborting test.") - return nil - } - certPool := netx.NewDefaultCertPool() - if ok := certPool.AppendCertsFromPEM([]byte(tk.HTTPResponseBody)); !ok { - testkeys.CACertStatus = false - testkeys.APIStatus = "blocked" - errorValue := "invalid_ca" - testkeys.APIFailure = &errorValue - return nil + + multi := urlgetter.Multi{Begin: measurement.MeasurementStartTimeSaved, Getter: m.Getter, Session: sess} + inputs := []urlgetter.MultiInput{ + {Target: caTarget, Config: urlgetter.Config{ + Method: "GET", + FailOnHTTPError: true, + }}, + } + for entry := range multi.CollectOverall(ctx, inputs, 0, 50, "riseupvpn", callbacks) { + tk := entry.TestKeys + testkeys.AddCACertFetchTestKeys(tk) + if tk.Failure != nil { + return nil + } + + if ok := certPool.AppendCertsFromPEM([]byte(tk.HTTPResponseBody)); !ok { + testkeys.CACertStatus = false + testkeys.APIStatus = "blocked" + errorValue := "invalid_ca" + testkeys.APIFailure = &errorValue + return nil + } } - inputs := []urlgetter.MultiInput{ + inputs = []urlgetter.MultiInput{ // Here we need to provide the method explicitly. See // https://github.com/ooni/probe-engine/issues/827. @@ -203,30 +230,34 @@ func (m Measurer) Run(ctx context.Context, sess model.ExperimentSession, FailOnHTTPError: true, }}, } - multi := urlgetter.Multi{Begin: measurement.MeasurementStartTimeSaved, Getter: m.Getter, Session: sess} + multi = urlgetter.Multi{Begin: measurement.MeasurementStartTimeSaved, Getter: m.Getter, Session: sess} - for entry := range multi.CollectOverall(ctx, inputs, 0, 50, "riseupvpn", callbacks) { + for entry := range multi.CollectOverall(ctx, inputs, 1, 50, "riseupvpn", callbacks) { testkeys.UpdateProviderAPITestKeys(entry) } // test gateways now + testkeys.TransportStatus = map[string]string{} gateways := parseGateways(testkeys) openvpnEndpoints := generateMultiInputs(gateways, "openvpn") obfs4Endpoints := generateMultiInputs(gateways, "obfs4") - overallCount := len(inputs) + len(openvpnEndpoints) + len(obfs4Endpoints) + overallCount := 1 + len(inputs) + len(openvpnEndpoints) + len(obfs4Endpoints) // measure openvpn in parallel multi = urlgetter.Multi{Begin: measurement.MeasurementStartTimeSaved, Getter: m.Getter, Session: sess} - for entry := range multi.CollectOverall(ctx, openvpnEndpoints, len(inputs), overallCount, "riseupvpn", callbacks) { + for entry := range multi.CollectOverall(ctx, openvpnEndpoints, 1+len(inputs), overallCount, "riseupvpn", callbacks) { testkeys.AddGatewayConnectTestKeys(entry, "openvpn") } // measure obfs4 in parallel multi = urlgetter.Multi{Begin: measurement.MeasurementStartTimeSaved, Getter: m.Getter, Session: sess} - for entry := range multi.CollectOverall(ctx, obfs4Endpoints, len(inputs)+len(openvpnEndpoints), overallCount, "riseupvpn", callbacks) { + for entry := range multi.CollectOverall(ctx, obfs4Endpoints, 1+len(inputs)+len(openvpnEndpoints), overallCount, "riseupvpn", callbacks) { testkeys.AddGatewayConnectTestKeys(entry, "obfs4") } + // set transport status based on gateway test results + testkeys.updateTransportStatus(len(openvpnEndpoints), len(obfs4Endpoints)) + return nil } @@ -287,10 +318,11 @@ func NewExperimentMeasurer(config Config) model.ExperimentMeasurer { // Note that this structure is part of the ABI contract with probe-cli // therefore we should be careful when changing it. type SummaryKeys struct { - APIBlocked bool `json:"api_blocked"` - ValidCACert bool `json:"valid_ca_cert"` - FailingGateways int `json:"failing_gateways"` - IsAnomaly bool `json:"-"` + APIBlocked bool `json:"api_blocked"` + ValidCACert bool `json:"valid_ca_cert"` + FailingGateways int `json:"failing_gateways"` + TransportStatus map[string]string `json:"transport_status"` + IsAnomaly bool `json:"-"` } // GetSummaryKeys implements model.ExperimentMeasurer.GetSummaryKeys. @@ -303,7 +335,8 @@ func (m Measurer) GetSummaryKeys(measurement *model.Measurement) (interface{}, e sk.APIBlocked = tk.APIStatus != "ok" sk.ValidCACert = tk.CACertStatus sk.FailingGateways = len(tk.FailingGateways) + sk.TransportStatus = tk.TransportStatus sk.IsAnomaly = (sk.APIBlocked == true || tk.CACertStatus == false || - sk.FailingGateways != 0) + tk.TransportStatus["openvpn"] == "blocked" || tk.TransportStatus["obfs4"] == "blocked") return sk, nil } diff --git a/internal/engine/experiment/riseupvpn/riseupvpn_test.go b/internal/engine/experiment/riseupvpn/riseupvpn_test.go index 3798d5d..1be8aa4 100644 --- a/internal/engine/experiment/riseupvpn/riseupvpn_test.go +++ b/internal/engine/experiment/riseupvpn/riseupvpn_test.go @@ -2,15 +2,14 @@ package riseupvpn_test import ( "context" - "crypto/tls" - "crypto/x509" - "errors" + "encoding/json" "fmt" - "io/ioutil" - "math/rand" - "net/http" + "io" + "strconv" + "strings" "testing" - "time" + + "github.com/ooni/probe-cli/v3/internal/engine/netx/archival" "github.com/apex/log" "github.com/google/go-cmp/cmp" @@ -19,36 +18,204 @@ import ( "github.com/ooni/probe-cli/v3/internal/engine/internal/mockable" "github.com/ooni/probe-cli/v3/internal/engine/model" "github.com/ooni/probe-cli/v3/internal/engine/netx/errorx" - "github.com/ooni/probe-cli/v3/internal/engine/netx/selfcensor" ) +const ( + provider = `{ + "api_uri": "https://api.black.riseup.net:443", + "api_version": "3", + "ca_cert_fingerprint": "SHA256: a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494", + "ca_cert_uri": "https://black.riseup.net/ca.crt", + "default_language": "en", + "description": { + "en": "Riseup is a non-profit collective in Seattle that provides online communication tools for people and groups working toward liberatory social change." + }, + "domain": "riseup.net", + "enrollment_policy": "closed", + "languages": [ + "en" + ], + "name": { + "en": "Riseup Networks" + }, + "service": { + "allow_anonymous": true, + "allow_free": true, + "allow_limited_bandwidth": false, + "allow_paid": false, + "allow_registration": false, + "allow_unlimited_bandwidth": true, + "bandwidth_limit": 102400, + "default_service_level": 1, + "levels": { + "1": { + "description": "Please donate.", + "name": "free" + } + } + }, + "services": [ + "openvpn" + ] + }` + eipservice = `{ + "gateways": [ + { + "capabilities": { + "adblock": false, + "filter_dns": false, + "limited": false, + "transport":[ + { + "type":"openvpn", + "protocols":[ + "tcp" + ], + "ports":[ + "443" + ] + } + ], + "user_ips": false + }, + "host": "test1.riseup.net", + "ip_address": "123.456.123.456", + "location": "paris" + }, + { + "capabilities": { + "adblock": false, + "filter_dns": false, + "limited": false, + "transport":[ + { + "type":"obfs4", + "protocols":[ + "tcp" + ], + "ports":[ + "23042" + ], + "options": { + "cert": "XXXXXXXXXXXXXXXXXXXXXXXXX", + "iatMode": "0" + } + }, + { + "type":"openvpn", + "protocols":[ + "tcp" + ], + "ports":[ + "443" + ] + } + ], + "user_ips": false + }, + "host": "test2.riseup.net", + "ip_address": "234.345.234.345", + "location": "seattle" + } + ], + "locations": { + "paris": { + "country_code": "FR", + "hemisphere": "N", + "name": "Paris", + "timezone": "+2" + }, + "seattle": { + "country_code": "US", + "hemisphere": "N", + "name": "Seattle", + "timezone": "-7" + } + }, + "openvpn_configuration": { + "auth": "SHA1", + "cipher": "AES-128-CBC", + "keepalive": "10 30", + "tls-cipher": "DHE-RSA-AES128-SHA", + "tun-ipv6": true + }, + "serial": 3, + "version": 3 + }` + geoservice = `{"ip":"51.15.0.88","cc":"NL","city":"Haarlem","lat":52.381,"lon":4.6275,"gateways":["test1.riseup.net","test2.riseup.net"]}` + cacert = `-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIBATANBgkqhkiG9w0BAQ0FADBZMRgwFgYDVQQKDA9SaXNl +dXAgTmV0d29ya3MxGzAZBgNVBAsMEmh0dHBzOi8vcmlzZXVwLm5ldDEgMB4GA1UE +AwwXUmlzZXVwIE5ldHdvcmtzIFJvb3QgQ0EwHhcNMTQwNDI4MDAwMDAwWhcNMjQw +NDI4MDAwMDAwWjBZMRgwFgYDVQQKDA9SaXNldXAgTmV0d29ya3MxGzAZBgNVBAsM +Emh0dHBzOi8vcmlzZXVwLm5ldDEgMB4GA1UEAwwXUmlzZXVwIE5ldHdvcmtzIFJv +b3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC76J4ciMJ8Sg0m +TP7DF2DT9zNe0Csk4myoMFC57rfJeqsAlJCv1XMzBmXrw8wq/9z7XHv6n/0sWU7a +7cF2hLR33ktjwODlx7vorU39/lXLndo492ZBhXQtG1INMShyv+nlmzO6GT7ESfNE +LliFitEzwIegpMqxCIHXFuobGSCWF4N0qLHkq/SYUMoOJ96O3hmPSl1kFDRMtWXY +iw1SEKjUvpyDJpVs3NGxeLCaA7bAWhDY5s5Yb2fA1o8ICAqhowurowJpW7n5ZuLK +5VNTlNy6nZpkjt1QycYvNycffyPOFm/Q/RKDlvnorJIrihPkyniV3YY5cGgP+Qkx +HUOT0uLA6LHtzfiyaOqkXwc4b0ZcQD5Vbf6Prd20Ppt6ei0zazkUPwxld3hgyw58 +m/4UIjG3PInWTNf293GngK2Bnz8Qx9e/6TueMSAn/3JBLem56E0WtmbLVjvko+LF +PM5xA+m0BmuSJtrD1MUCXMhqYTtiOvgLBlUm5zkNxALzG+cXB28k6XikXt6MRG7q +hzIPG38zwkooM55yy5i1YfcIi5NjMH6A+t4IJxxwb67MSb6UFOwg5kFokdONZcwj +shczHdG9gLKSBIvrKa03Nd3W2dF9hMbRu//STcQxOailDBQCnXXfAATj9pYzdY4k +ha8VCAREGAKTDAex9oXf1yRuktES4QIDAQABo2AwXjAdBgNVHQ4EFgQUC4tdmLVu +f9hwfK4AGliaet5KkcgwDgYDVR0PAQH/BAQDAgIEMAwGA1UdEwQFMAMBAf8wHwYD +VR0jBBgwFoAUC4tdmLVuf9hwfK4AGliaet5KkcgwDQYJKoZIhvcNAQENBQADggIB +AGzL+GRnYu99zFoy0bXJKOGCF5XUXP/3gIXPRDqQf5g7Cu/jYMID9dB3No4Zmf7v +qHjiSXiS8jx1j/6/Luk6PpFbT7QYm4QLs1f4BlfZOti2KE8r7KRDPIecUsUXW6P/ +3GJAVYH/+7OjA39za9AieM7+H5BELGccGrM5wfl7JeEz8in+V2ZWDzHQO4hMkiTQ +4ZckuaL201F68YpiItBNnJ9N5nHr1MRiGyApHmLXY/wvlrOpclh95qn+lG6/2jk7 +3AmihLOKYMlPwPakJg4PYczm3icFLgTpjV5sq2md9bRyAg3oPGfAuWHmKj2Ikqch +Td5CHKGxEEWbGUWEMP0s1A/JHWiCbDigc4Cfxhy56CWG4q0tYtnc2GMw8OAUO6Wf +Xu5pYKNkzKSEtT/MrNJt44tTZWbKV/Pi/N2Fx36my7TgTUj7g3xcE9eF4JV2H/sg +tsK3pwE0FEqGnT4qMFbixQmc8bGyuakr23wjMvfO7eZUxBuWYR2SkcP26sozF9PF +tGhbZHQVGZUTVPyvwahMUEhbPGVerOW0IYpxkm0x/eaWdTc4vPpf/rIlgbAjarnJ +UN9SaWRlWKSdP4haujnzCoJbM7dU9bjvlGZNyXEekgeT0W2qFeGGp+yyUWw8tNsp +0BuC1b7uW/bBn/xKm319wXVDvBgZgcktMolak39V7DVO +-----END CERTIFICATE-----` + + eipserviceurl = "https://api.black.riseup.net:443/3/config/eip-service.json" + providerurl = "https://riseup.net/provider.json" + geoserviceurl = "https://api.black.riseup.net:9001/json" + cacerturl = "https://black.riseup.net/ca.crt" + openvpnurl1 = "tcpconnect://234.345.234.345:443" + openvpnurl2 = "tcpconnect://123.456.123.456:443" + obfs4url1 = "tcpconnect://234.345.234.345:23042" +) + +var RequestResponse = map[string]string{ + eipserviceurl: eipservice, + providerurl: provider, + geoserviceurl: geoservice, + cacerturl: cacert, + openvpnurl1: "", + openvpnurl2: "", + obfs4url1: "", +} + func TestNewExperimentMeasurer(t *testing.T) { measurer := riseupvpn.NewExperimentMeasurer(riseupvpn.Config{}) if measurer.ExperimentName() != "riseupvpn" { t.Fatal("unexpected name") } - if measurer.ExperimentVersion() != "0.1.0" { + if measurer.ExperimentVersion() != "0.2.0" { t.Fatal("unexpected version") } } func TestGood(t *testing.T) { - if testing.Short() { - t.Skip("skip test in short mode") - } - measurer := riseupvpn.NewExperimentMeasurer(riseupvpn.Config{}) - measurement := new(model.Measurement) - err := measurer.Run( - context.Background(), - &mockable.Session{ - MockableLogger: log.Log, - }, - measurement, - model.NewPrinterCallbacks(log.Log), - ) - if err != nil { - t.Fatal(err) - } + measurement := runDefaultMockTest(t, generateDefaultMockGetter(map[string]bool{ + cacerturl: true, + eipserviceurl: true, + providerurl: true, + geoserviceurl: true, + openvpnurl1: true, + openvpnurl2: true, + obfs4url1: true, + })) + tk := measurement.TestKeys.(*riseupvpn.TestKeys) if tk.Agent != "" { t.Fatal("unexpected Agent: " + tk.Agent) @@ -59,21 +226,6 @@ func TestGood(t *testing.T) { if tk.Failure != nil { t.Fatal("unexpected Failure") } - if len(tk.NetworkEvents) <= 0 { - t.Fatal("no NetworkEvents?!") - } - if len(tk.Queries) <= 0 { - t.Fatal("no Queries?!") - } - if len(tk.Requests) <= 0 { - t.Fatal("no Requests?!") - } - if len(tk.TCPConnect) <= 0 { - t.Fatal("no TCPConnect?!") - } - if len(tk.TLSHandshakes) <= 0 { - t.Fatal("no TLSHandshakes?!") - } if tk.APIFailure != nil { t.Fatal("unexpected ApiFailure") } @@ -86,6 +238,12 @@ func TestGood(t *testing.T) { if tk.FailingGateways != nil { t.Fatal("unexpected FailingGateways value") } + if tk.TransportStatus == nil { + t.Fatal("unexpected nil TransportStatus struct ") + } + if tk.TransportStatus["openvpn"] != "ok" { + t.Fatal("unexpected openvpn transport status") + } } // TestUpdateWithMixedResults tests if one operation failed @@ -132,13 +290,39 @@ func TestUpdateWithMixedResults(t *testing.T) { if *tk.APIFailure != errorx.FailureEOFError { t.Fatal("invalid ApiFailure") } + if tk.FailingGateways != nil { + t.Fatal("invalid FailingGateways") + } + if tk.TransportStatus != nil { + t.Fatal("invalid TransportStatus") + } } -func TestFailureCaCertFetch(t *testing.T) { - measurer := riseupvpn.NewExperimentMeasurer(riseupvpn.Config{}) +func TestInvalidCaCert(t *testing.T) { + requestResponseMap := map[string]string{ + eipserviceurl: eipservice, + providerurl: provider, + geoserviceurl: geoservice, + cacerturl: "invalid", + openvpnurl1: "", + openvpnurl2: "", + obfs4url1: "", + } + measurer := riseupvpn.Measurer{ + Config: riseupvpn.Config{}, + Getter: generateMockGetter(requestResponseMap, map[string]bool{ + cacerturl: true, + eipserviceurl: true, + providerurl: true, + geoserviceurl: true, + openvpnurl1: false, + openvpnurl2: true, + obfs4url1: true, + }), + } + ctx, cancel := context.WithCancel(context.Background()) - // we're cancelling immediately so that the CA Cert fetch fails - cancel() + defer cancel() sess := &mockable.Session{MockableLogger: log.Log} measurement := new(model.Measurement) @@ -147,6 +331,26 @@ func TestFailureCaCertFetch(t *testing.T) { if err != nil { t.Fatal(err) } + tk := measurement.TestKeys.(*riseupvpn.TestKeys) + if tk.CACertStatus == true { + t.Fatal("unexpected CaCertStatus") + } + if tk.APIStatus != "blocked" { + t.Fatal("ApiStatus should be blocked") + } +} + +func TestFailureCaCertFetch(t *testing.T) { + measurement := runDefaultMockTest(t, generateDefaultMockGetter(map[string]bool{ + cacerturl: false, + eipserviceurl: true, + providerurl: true, + geoserviceurl: true, + openvpnurl1: true, + openvpnurl2: true, + obfs4url1: true, + })) + tk := measurement.TestKeys.(*riseupvpn.TestKeys) if tk.CACertStatus != false { t.Fatal("invalid CACertStatus ") @@ -164,21 +368,15 @@ func TestFailureCaCertFetch(t *testing.T) { } func TestFailureEipServiceBlocked(t *testing.T) { - if testing.Short() { - t.Skip("skip test in short mode") - } - measurer := riseupvpn.NewExperimentMeasurer(riseupvpn.Config{}) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - selfcensor.Enable(`{"PoisonSystemDNS":{"api.black.riseup.net":["NXDOMAIN"]}}`) - - sess := &mockable.Session{MockableLogger: log.Log} - measurement := new(model.Measurement) - callbacks := model.NewPrinterCallbacks(log.Log) - err := measurer.Run(ctx, sess, measurement, callbacks) - if err != nil { - t.Fatal(err) - } + measurement := runDefaultMockTest(t, generateDefaultMockGetter(map[string]bool{ + cacerturl: true, + eipserviceurl: false, + providerurl: true, + geoserviceurl: true, + openvpnurl1: true, + openvpnurl2: true, + obfs4url1: true, + })) tk := measurement.TestKeys.(*riseupvpn.TestKeys) if tk.CACertStatus != true { t.Fatal("invalid CACertStatus ") @@ -202,21 +400,15 @@ func TestFailureEipServiceBlocked(t *testing.T) { } func TestFailureProviderUrlBlocked(t *testing.T) { - if testing.Short() { - t.Skip("skip test in short mode") - } - measurer := riseupvpn.NewExperimentMeasurer(riseupvpn.Config{}) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - selfcensor.Enable(`{"BlockedEndpoints":{"198.252.153.70:443":"REJECT"}}`) - - sess := &mockable.Session{MockableLogger: log.Log} - measurement := new(model.Measurement) - callbacks := model.NewPrinterCallbacks(log.Log) - err := measurer.Run(ctx, sess, measurement, callbacks) - if err != nil { - t.Fatal(err) - } + measurement := runDefaultMockTest(t, generateDefaultMockGetter(map[string]bool{ + cacerturl: true, + eipserviceurl: true, + providerurl: false, + geoserviceurl: true, + openvpnurl1: true, + openvpnurl2: true, + obfs4url1: true, + })) tk := measurement.TestKeys.(*riseupvpn.TestKeys) for _, entry := range tk.Requests { @@ -240,21 +432,15 @@ func TestFailureProviderUrlBlocked(t *testing.T) { } func TestFailureGeoIpServiceBlocked(t *testing.T) { - if testing.Short() { - t.Skip("skip test in short mode") - } - measurer := riseupvpn.NewExperimentMeasurer(riseupvpn.Config{}) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - selfcensor.Enable(`{"BlockedEndpoints":{"198.252.153.107:9001":"REJECT"}}`) - - sess := &mockable.Session{MockableLogger: log.Log} - measurement := new(model.Measurement) - callbacks := model.NewPrinterCallbacks(log.Log) - err := measurer.Run(ctx, sess, measurement, callbacks) - if err != nil { - t.Fatal(err) - } + measurement := runDefaultMockTest(t, generateDefaultMockGetter(map[string]bool{ + cacerturl: true, + eipserviceurl: true, + providerurl: true, + geoserviceurl: false, + openvpnurl1: true, + openvpnurl2: true, + obfs4url1: true, + })) tk := measurement.TestKeys.(*riseupvpn.TestKeys) if tk.CACertStatus != true { t.Fatal("invalid CACertStatus ") @@ -277,138 +463,16 @@ func TestFailureGeoIpServiceBlocked(t *testing.T) { } } -func TestFailureGateway(t *testing.T) { - if testing.Short() { - t.Skip("skip test in short mode") - } - var testCases = [...]string{"openvpn", "obfs4"} - eipService, err := fetchEipService() - if err != nil { - t.Log("Preconditions for the test are not met. Skipping due to: " + err.Error()) - t.SkipNow() - } - for _, tc := range testCases { - t.Run(fmt.Sprintf("testing censored transport %s", tc), func(t *testing.T) { - censoredGateway, err := selfCensorRandomGateway(eipService, tc) - if err == nil { - censorString := `{"BlockedEndpoints":{"` + censoredGateway.IP + `:` + censoredGateway.Port + `":"REJECT"}}` - selfcensor.Enable(censorString) - } else { - t.Log("Preconditions for the test are not met. Skipping due to: " + err.Error()) - t.SkipNow() - } - - // - run measurement - runGatewayTest(t, censoredGateway) - }) - } -} - -type SelfCensoredGateway struct { - IP string - Port string -} - -func fetchEipService() (*riseupvpn.EipService, error) { - // - fetch client cert and add to certpool - caFetchClient := &http.Client{ - Timeout: time.Second * 30, - } - - caCertResponse, err := caFetchClient.Get("https://black.riseup.net/ca.crt") - if err != nil { - return nil, err - } - - var bodyString string - - if caCertResponse.StatusCode != http.StatusOK { - return nil, errors.New("unexpected HTTP response code") - } - bodyBytes, err := ioutil.ReadAll(caCertResponse.Body) - defer caCertResponse.Body.Close() - - if err != nil { - return nil, err - } - bodyString = string(bodyBytes) - - certs := x509.NewCertPool() - certs.AppendCertsFromPEM([]byte(bodyString)) - - // - fetch and parse eip-service.json - client := &http.Client{ - Timeout: time.Second * 30, - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{ - RootCAs: certs, - }, - }, - } - - eipResponse, err := client.Get("https://api.black.riseup.net/3/config/eip-service.json") - if err != nil { - return nil, err - } - if eipResponse.StatusCode != http.StatusOK { - return nil, errors.New("Unexpected HTTP response code") - } - - bodyBytes, err = ioutil.ReadAll(eipResponse.Body) - defer eipResponse.Body.Close() - if err != nil { - return nil, err - } - bodyString = string(bodyBytes) - - eipService, err := riseupvpn.DecodeEIP3(bodyString) - if err != nil { - return nil, err - } - return eipService, nil -} - -func selfCensorRandomGateway(eipService *riseupvpn.EipService, transportType string) (*SelfCensoredGateway, error) { - - // - self censor random gateway - gateways := eipService.Gateways - if gateways == nil || len(gateways) == 0 { - return nil, errors.New("No gateways found") - } - - var selfcensoredGateways []SelfCensoredGateway - for _, gateway := range gateways { - for _, transport := range gateway.Capabilities.Transport { - if transport.Type == transportType { - selfcensoredGateways = append(selfcensoredGateways, SelfCensoredGateway{IP: gateway.IPAddress, Port: transport.Ports[0]}) - } - } - } - - if len(selfcensoredGateways) == 0 { - return nil, errors.New("transport " + transportType + " doesn't seem to be supported.") - } - - rnd := rand.New(rand.NewSource(time.Now().UnixNano())) - min := 0 - max := len(selfcensoredGateways) - 1 - randomIndex := rnd.Intn(max-min+1) + min - return &selfcensoredGateways[randomIndex], nil - -} - -func runGatewayTest(t *testing.T, censoredGateway *SelfCensoredGateway) { - measurer := riseupvpn.NewExperimentMeasurer(riseupvpn.Config{}) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - sess := &mockable.Session{MockableLogger: log.Log} - measurement := new(model.Measurement) - callbacks := model.NewPrinterCallbacks(log.Log) - err := measurer.Run(ctx, sess, measurement, callbacks) - if err != nil { - t.Fatal(err) - } +func TestFailureGateway1(t *testing.T) { + measurement := runDefaultMockTest(t, generateDefaultMockGetter(map[string]bool{ + cacerturl: true, + eipserviceurl: true, + providerurl: true, + geoserviceurl: true, + openvpnurl1: false, + openvpnurl2: true, + obfs4url1: true, + })) tk := measurement.TestKeys.(*riseupvpn.TestKeys) if tk.CACertStatus != true { t.Fatal("invalid CACertStatus ") @@ -418,18 +482,122 @@ func runGatewayTest(t *testing.T, censoredGateway *SelfCensoredGateway) { t.Fatal("unexpected amount of failing gateways") } - entry := tk.FailingGateways[0] - if entry.IP != censoredGateway.IP || fmt.Sprint(entry.Port) != censoredGateway.Port { - t.Fatal("unexpected failed gateway configuration") + gw := tk.FailingGateways[0] + if gw.IP != "234.345.234.345" { + t.Fatal("invalid failed gateway ip: " + fmt.Sprint(gw.IP)) + } + if gw.Port != 443 { + t.Fatal("invalid failed gateway port: " + fmt.Sprint(gw.Port)) + } + if gw.TransportType != "openvpn" { + t.Fatal("invalid failed transport type: " + fmt.Sprint(gw.TransportType)) } if tk.APIStatus == "blocked" { - t.Fatal("invalid ApiStatus", tk.APIStatus) + t.Fatal("invalid ApiStatus") } if tk.APIFailure != nil { t.Fatal("ApiFailure should be null") } + + if tk.TransportStatus == nil || tk.TransportStatus["openvpn"] == "blocked" { + t.Fatal("invalid TransportStatus: " + fmt.Sprint(tk.TransportStatus)) + } + + if tk.TransportStatus == nil || tk.TransportStatus["obfs4"] == "blocked" { + t.Fatal("invalid TransportStatus: " + fmt.Sprint(tk.TransportStatus)) + } +} + +func TestFailureTransport(t *testing.T) { + measurement := runDefaultMockTest(t, generateDefaultMockGetter(map[string]bool{ + cacerturl: true, + eipserviceurl: true, + providerurl: true, + geoserviceurl: true, + openvpnurl1: false, + openvpnurl2: false, + obfs4url1: false, + })) + tk := measurement.TestKeys.(*riseupvpn.TestKeys) + + if tk.TransportStatus == nil || tk.TransportStatus["openvpn"] != "blocked" { + t.Fatal("invalid TransportStatus: " + fmt.Sprint(tk.TransportStatus)) + } + + if tk.TransportStatus == nil || tk.TransportStatus["obfs4"] != "blocked" { + t.Fatal("invalid TransportStatus: " + fmt.Sprint(tk.TransportStatus)) + } +} + +func TestMissingTransport(t *testing.T) { + eipService, err := riseupvpn.DecodeEIP3(eipservice) + if err != nil { + t.Fatal("Preconditions for the test are not met.") + } + + //remove obfs4 capability from 2. gateway so that our + //mock provider supports only openvpn + index := -1 + transports := eipService.Gateways[1].Capabilities.Transport + for i, transport := range transports { + if transport.Type == "obfs4" { + index = i + break + } + } + if index == -1 { + t.Fatal("Preconditions for the test are not met. Default eipservice string should contain obfs4 transport.") + } + + transports[index] = transports[len(transports)-1] + transports = transports[:len(transports)-1] + eipService.Gateways[1].Capabilities.Transport = transports + eipservicejson, err := json.Marshal(eipservice) + + requestResponseMap := map[string]string{ + eipserviceurl: string(eipservicejson), + providerurl: provider, + geoserviceurl: geoservice, + cacerturl: cacert, + openvpnurl1: "", + openvpnurl2: "", + obfs4url1: "", + } + + measurer := riseupvpn.Measurer{ + Config: riseupvpn.Config{}, + Getter: generateMockGetter(requestResponseMap, map[string]bool{ + cacerturl: true, + eipserviceurl: true, + providerurl: true, + geoserviceurl: true, + openvpnurl1: true, + openvpnurl2: true, + obfs4url1: false, + }), + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + sess := &mockable.Session{MockableLogger: log.Log} + measurement := new(model.Measurement) + callbacks := model.NewPrinterCallbacks(log.Log) + err = measurer.Run(ctx, sess, measurement, callbacks) + if err != nil { + t.Fatal(err) + } + tk := measurement.TestKeys.(*riseupvpn.TestKeys) + if tk.TransportStatus == nil || tk.TransportStatus["openvpn"] != "blocked" { + t.Fatal("invalid TransportStatus: " + fmt.Sprint(tk.TransportStatus)) + } + + if _, found := tk.TransportStatus["obfs"]; found { + t.Fatal("invalid TransportStatus: " + fmt.Sprint(tk.TransportStatus)) + } + } func TestSummaryKeysInvalidType(t *testing.T) { @@ -450,21 +618,27 @@ func TestSummaryKeysWorksAsIntended(t *testing.T) { APIStatus: "blocked", CACertStatus: true, FailingGateways: nil, + TransportStatus: nil, }, sk: riseupvpn.SummaryKeys{ - APIBlocked: true, - ValidCACert: true, - IsAnomaly: true, + APIBlocked: true, + ValidCACert: true, + IsAnomaly: true, + TransportStatus: nil, + FailingGateways: 0, }, }, { tk: riseupvpn.TestKeys{ APIStatus: "ok", CACertStatus: false, FailingGateways: nil, + TransportStatus: nil, }, sk: riseupvpn.SummaryKeys{ - ValidCACert: false, - IsAnomaly: true, + ValidCACert: false, + IsAnomaly: true, + FailingGateways: 0, + TransportStatus: nil, }, }, { tk: riseupvpn.TestKeys{ @@ -475,13 +649,39 @@ func TestSummaryKeysWorksAsIntended(t *testing.T) { Port: 443, TransportType: "obfs4", }}, + TransportStatus: map[string]string{ + "obfs4": "blocked", + "openvpn": "ok", + }, }, sk: riseupvpn.SummaryKeys{ FailingGateways: 1, IsAnomaly: true, ValidCACert: true, + TransportStatus: map[string]string{ + "obfs4": "blocked", + "openvpn": "ok", + }, }, - }} + }, { + tk: riseupvpn.TestKeys{ + APIStatus: "ok", + CACertStatus: true, + FailingGateways: nil, + TransportStatus: map[string]string{ + "openvpn": "ok", + }, + }, + sk: riseupvpn.SummaryKeys{ + ValidCACert: true, + IsAnomaly: false, + FailingGateways: 0, + TransportStatus: map[string]string{ + "openvpn": "ok", + }, + }, + }, + } for idx, tt := range tests { t.Run(fmt.Sprintf("%d", idx), func(t *testing.T) { m := &riseupvpn.Measurer{} @@ -498,3 +698,95 @@ func TestSummaryKeysWorksAsIntended(t *testing.T) { }) } } + +func generateMockGetter(requestResponse map[string]string, responseStatus map[string]bool) urlgetter.MultiGetter { + return func(ctx context.Context, g urlgetter.Getter) (urlgetter.TestKeys, error) { + url := g.Target + responseBody, foundRequest := requestResponse[url] + isSuccessStatus, foundStatus := responseStatus[url] + if !foundRequest || !foundStatus { + return urlgetter.DefaultMultiGetter(ctx, g) + } + + var failure *string + var failedOperation *string + isBlocked := !isSuccessStatus + var responseStatus int64 = 200 + + if isBlocked { + responseBody = "" + eofError := io.EOF.Error() + failure = &eofError + connectOperation := errorx.ConnectOperation + failedOperation = &connectOperation + responseStatus = 0 + } + + tcpConnect := archival.TCPConnectEntry{ + // use some dummy IP/Port combination for URLs, we don't do DNS resolution + IP: "123.456.234.123", + Port: 443, + Status: archival.TCPConnectStatus{ + Success: isSuccessStatus, + Blocked: &isBlocked, + Failure: failure, + }, + } + if strings.Contains(url, "tcpconnect://") { + ipPort := strings.Split(strings.Split(url, "//")[1], ":") + port, err := strconv.ParseInt(ipPort[1], 10, 32) + if err == nil { + tcpConnect.IP = ipPort[0] + tcpConnect.Port = int(port) + } + } + + tk := urlgetter.TestKeys{ + Failure: failure, + FailedOperation: failedOperation, + HTTPResponseStatus: responseStatus, + HTTPResponseBody: responseBody, + Requests: []archival.RequestEntry{archival.RequestEntry{ + Failure: failure, + Request: archival.HTTPRequest{ + URL: url, + Body: archival.MaybeBinaryValue{}, + BodyIsTruncated: false, + }, + Response: archival.HTTPResponse{ + Body: archival.HTTPBody{ + Value: responseBody, + }, + BodyIsTruncated: false, + }}, + }, + TCPConnect: []archival.TCPConnectEntry{tcpConnect}, + } + return tk, nil + } +} +func generateDefaultMockGetter(responseStatuses map[string]bool) urlgetter.MultiGetter { + return generateMockGetter(RequestResponse, responseStatuses) +} + +func runDefaultMockTest(t *testing.T, multiGetter urlgetter.MultiGetter) *model.Measurement { + measurer := riseupvpn.Measurer{ + Config: riseupvpn.Config{}, + Getter: multiGetter, + } + + measurement := new(model.Measurement) + err := measurer.Run( + context.Background(), + &mockable.Session{ + MockableLogger: log.Log, + }, + measurement, + model.NewPrinterCallbacks(log.Log), + ) + + if err != nil { + t.Fatal(err) + } + return measurement +}