diff --git a/internal/experiment/riseupvpn/riseupvpn.go b/internal/experiment/riseupvpn/riseupvpn.go index ff662cebb1..19b0d58b6d 100644 --- a/internal/experiment/riseupvpn/riseupvpn.go +++ b/internal/experiment/riseupvpn/riseupvpn.go @@ -6,36 +6,38 @@ package riseupvpn import ( "context" "encoding/json" - "errors" "time" "github.com/ooni/probe-cli/v3/internal/experiment/urlgetter" "github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/netxlite" - "github.com/ooni/probe-cli/v3/internal/tracex" ) const ( testName = "riseupvpn" - testVersion = "0.2.0" + testVersion = "0.3.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" tcpConnect = "tcpconnect://" ) -// EipService is the main JSON object of eip-service.json. -type EipService struct { +// EipServiceV3 is the main JSON object of eip-service.json. +type EipServiceV3 struct { Gateways []GatewayV3 } +// CapabilitiesV3 is a list of transports a gateway supports +type CapabilitiesV3 struct { + Transport []TransportV3 +} + // GatewayV3 describes a gateway. type GatewayV3 struct { - Capabilities struct { - Transport []TransportV3 - } - Host string - IPAddress string `json:"ip_address"` + Capabilities CapabilitiesV3 + Host string + IPAddress string `json:"ip_address"` + Location string `json:"location"` } // TransportV3 describes a transport. @@ -46,11 +48,22 @@ type TransportV3 struct { Options map[string]string } -// GatewayConnection describes the connection to a riseupvpn gateway. -type GatewayConnection struct { - IP string `json:"ip"` - Port int `json:"port"` - TransportType string `json:"transport_type"` +// GatewayLoad describes the load of a single Gateway. +type GatewayLoad struct { + Host string `json:"host"` + Fullness float64 `json:"fullness"` + Overload bool `json:"overload"` +} + +// GeoService represents the geoService API (also known as menshen) json response +type GeoService struct { + IPAddress string `json:"ip"` + Country string `json:"cc"` + City string `json:"city"` + Latitude float64 `json:"lat"` + Longitude float64 `json:"lon"` + Gateways []string `json:"gateways"` + SortedGateways []GatewayLoad `json:"sortedGateways"` } // Config contains the riseupvpn experiment config. @@ -61,21 +74,15 @@ type Config struct { // TestKeys contains riseupvpn test keys. type TestKeys struct { urlgetter.TestKeys - APIFailure *string `json:"api_failure"` - APIStatus string `json:"api_status"` - CACertStatus bool `json:"ca_cert_status"` - FailingGateways []GatewayConnection `json:"failing_gateways"` - TransportStatus map[string]string `json:"transport_status"` + APIFailure []string `json:"api_failure"` + CACertStatus bool `json:"ca_cert_status"` } // NewTestKeys creates new riseupvpn TestKeys. func NewTestKeys() *TestKeys { return &TestKeys{ - APIFailure: nil, - APIStatus: "ok", - CACertStatus: true, - FailingGateways: nil, - TransportStatus: nil, + APIFailure: nil, + CACertStatus: true, } } @@ -86,12 +93,8 @@ func (tk *TestKeys) UpdateProviderAPITestKeys(v urlgetter.MultiOutput) { tk.Requests = append(tk.Requests, v.TestKeys.Requests...) tk.TCPConnect = append(tk.TCPConnect, v.TestKeys.TCPConnect...) tk.TLSHandshakes = append(tk.TLSHandshakes, v.TestKeys.TLSHandshakes...) - if tk.APIStatus != "ok" { - return // we already flipped the state - } if v.TestKeys.Failure != nil { - tk.APIStatus = "blocked" - tk.APIFailure = v.TestKeys.Failure + tk.APIFailure = append(tk.APIFailure, *v.TestKeys.Failure) return } } @@ -102,42 +105,6 @@ func (tk *TestKeys) UpdateProviderAPITestKeys(v urlgetter.MultiOutput) { 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...) - for _, tcpConnect := range v.TestKeys.TCPConnect { - if !tcpConnect.Status.Success { - gatewayConnection := newGatewayConnection(tcpConnect, transportType) - tk.FailingGateways = append(tk.FailingGateways, *gatewayConnection) - } - } -} - -func (tk *TestKeys) updateTransportStatus(openvpnGatewayCount, 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 tracex.TCPConnectEntry, transportType string) *GatewayConnection { - return &GatewayConnection{ - IP: tcpConnect.IP, - Port: tcpConnect.Port, - TransportType: transportType, - } } // AddCACertFetchTestKeys adds generic urlgetter.Get() testKeys to riseupvpn specific test keys @@ -147,11 +114,6 @@ func (tk *TestKeys) AddCACertFetchTestKeys(testKeys urlgetter.TestKeys) { tk.Requests = append(tk.Requests, testKeys.Requests...) tk.TCPConnect = append(tk.TCPConnect, testKeys.TCPConnect...) tk.TLSHandshakes = append(tk.TLSHandshakes, testKeys.TLSHandshakes...) - if testKeys.Failure != nil { - tk.APIStatus = "blocked" - tk.APIFailure = tk.Failure - tk.CACertStatus = false - } } // Measurer performs the measurement. @@ -204,21 +166,17 @@ func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { FailOnHTTPError: true, }}, } - for entry := range multi.CollectOverall(ctx, inputs, 0, 50, "riseupvpn", callbacks) { + for entry := range multi.CollectOverall(ctx, inputs, 0, 20, "riseupvpn", callbacks) { tk := entry.TestKeys testkeys.AddCACertFetchTestKeys(tk) if tk.Failure != nil { - // TODO(bassosimone,cyberta): should we update the testkeys - // in this case (e.g., APIFailure?) - // See https://github.com/ooni/probe/issues/1432. - return nil - } - if ok := certPool.AppendCertsFromPEM([]byte(tk.HTTPResponseBody)); !ok { testkeys.CACertStatus = false - testkeys.APIStatus = "blocked" - errorValue := "invalid_ca" - testkeys.APIFailure = &errorValue - return nil + testkeys.APIFailure = append(testkeys.APIFailure, *tk.Failure) + certPool = nil + } else if ok := certPool.AppendCertsFromPEM([]byte(tk.HTTPResponseBody)); !ok { + testkeys.CACertStatus = false + testkeys.APIFailure = append(testkeys.APIFailure, "invalid_ca") + certPool = nil } } @@ -230,46 +188,62 @@ func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { CertPool: certPool, Method: "GET", FailOnHTTPError: true, + NoTLSVerify: !testkeys.CACertStatus, }}, {Target: eipServiceURL, Config: urlgetter.Config{ CertPool: certPool, Method: "GET", FailOnHTTPError: true, + NoTLSVerify: !testkeys.CACertStatus, }}, {Target: geoServiceURL, Config: urlgetter.Config{ CertPool: certPool, Method: "GET", FailOnHTTPError: true, + NoTLSVerify: !testkeys.CACertStatus, }}, } - for entry := range multi.CollectOverall(ctx, inputs, 1, 50, "riseupvpn", callbacks) { + + for entry := range multi.CollectOverall(ctx, inputs, 1, 20, "riseupvpn", callbacks) { testkeys.UpdateProviderAPITestKeys(entry) } + if testkeys.APIFailure != nil { + // scrub reponse bodys before early returning + var scrubbedRequests = []model.ArchivalHTTPRequestResult{} + for _, requestEntry := range testkeys.Requests { + requestEntry.Response.Body = model.ArchivalHTTPBody{Value: "[scrubbed]"} + scrubbedRequests = append(scrubbedRequests, requestEntry) + } + testkeys.Requests = scrubbedRequests + return nil + } + // test gateways now - testkeys.TransportStatus = map[string]string{} gateways := parseGateways(testkeys) openvpnEndpoints := generateMultiInputs(gateways, "openvpn") obfs4Endpoints := generateMultiInputs(gateways, "obfs4") overallCount := 1 + len(inputs) + len(openvpnEndpoints) + len(obfs4Endpoints) + startCount := 1 + len(inputs) // measure openvpn in parallel for entry := range multi.CollectOverall( - ctx, openvpnEndpoints, 1+len(inputs), overallCount, "riseupvpn", callbacks) { + ctx, openvpnEndpoints, startCount, overallCount, "riseupvpn", callbacks) { testkeys.AddGatewayConnectTestKeys(entry, "openvpn") } + startCount += len(openvpnEndpoints) // measure obfs4 in parallel // TODO(bassosimone): when urlgetter is able to do obfs4 handshakes, here // can possibly also test for the obfs4 handshake. // See https://github.com/ooni/probe/issues/1463. for entry := range multi.CollectOverall( - ctx, obfs4Endpoints, 1+len(inputs)+len(openvpnEndpoints), overallCount, "riseupvpn", callbacks) { + ctx, obfs4Endpoints, startCount, overallCount, "riseupvpn", callbacks) { testkeys.AddGatewayConnectTestKeys(entry, "obfs4") } // set transport status based on gateway test results - testkeys.updateTransportStatus(len(openvpnEndpoints), len(obfs4Endpoints)) + //testkeys.updateTransportStatus(len(openvpnEndpoints), len(obfs4Endpoints)) return nil } @@ -299,23 +273,126 @@ func generateMultiInputs(gateways []GatewayV3, transportType string) []urlgetter } func parseGateways(testKeys *TestKeys) []GatewayV3 { + var eipService *EipServiceV3 = nil + var geoService *GeoService = nil + var scrubbedRequests = []model.ArchivalHTTPRequestResult{} for _, requestEntry := range testKeys.Requests { - if requestEntry.Request.URL == eipServiceURL && requestEntry.Failure == nil { - // TODO(bassosimone,cyberta): is it reasonable that we discard - // the error when the JSON we fetched cannot be parsed? - // See https://github.com/ooni/probe/issues/1432 - eipService, err := DecodeEIP3(requestEntry.Response.Body.Value) - if err == nil { - return eipService.Gateways + if requestEntry.Request.URL == eipServiceURL && requestEntry.Response.Code == 200 { + var err error = nil + eipService, err = DecodeEIP3(requestEntry.Response.Body.Value) + if err != nil { + testKeys.APIFailure = append(testKeys.APIFailure, "invalid_eipservice_response") + return nil + } + } else if requestEntry.Request.URL == geoServiceURL && requestEntry.Response.Code == 200 { + var err error = nil + geoService, err = DecodeGeoService(requestEntry.Response.Body.Value) + if err != nil { + testKeys.APIFailure = append(testKeys.APIFailure, "invalid_geoservice_response") } } + requestEntry.Response.Body = model.ArchivalHTTPBody{Value: "[scrubbed]"} + scrubbedRequests = append(scrubbedRequests, requestEntry) } - return nil + testKeys.Requests = scrubbedRequests + return filterGateways(eipService, geoService) +} + +// filterGateways selects a subset of available gateways supporting obfs4 +func filterGateways(eipService *EipServiceV3, geoService *GeoService) []GatewayV3 { + var result []GatewayV3 = nil + if eipService != nil { + locations := getLocationsUnderTest(eipService, geoService) + for _, gateway := range eipService.Gateways { + if !gateway.hasTransport("obfs4") || + !gateway.isLocationUnderTest(locations) || + geoService != nil && !geoService.isHealthyGateway(gateway) { + continue + } + result = append(result, gateway) + if len(result) == 3 { + return result + } + } + } + return result +} + +// getLocationsUnderTest parses all gateways supporting obfs4 and returns the two locations having most obfs4 bridges +func getLocationsUnderTest(eipService *EipServiceV3, geoService *GeoService) []string { + var result []string = nil + if eipService != nil { + locationMap := map[string]int{} + locations := []string{} + for _, gateway := range eipService.Gateways { + if !gateway.hasTransport("obfs4") { + continue + } + if _, ok := locationMap[gateway.Location]; !ok { + locations = append(locations, gateway.Location) + } + locationMap[gateway.Location] += 1 + } + + location1 := "" + location2 := "" + for _, location := range locations { + if locationMap[location] > locationMap[location1] { + location2 = location1 + location1 = location + } else if locationMap[location] > locationMap[location2] { + location2 = location + } + } + if location1 != "" { + result = append(result, location1) + } + if location2 != "" { + result = append(result, location2) + } + } + + return result +} + +func (gateway *GatewayV3) hasTransport(s string) bool { + for _, transport := range gateway.Capabilities.Transport { + if s == transport.Type { + return true + } + } + return false +} + +func (gateway *GatewayV3) isLocationUnderTest(locations []string) bool { + for _, location := range locations { + if location == gateway.Location { + return true + } + } + return false +} + +func (geoService *GeoService) isHealthyGateway(gateway GatewayV3) bool { + if geoService.SortedGateways == nil { + // Earlier versions of the geoservice don't include the sorted gateway list containing the load info, + // so we can't say anything about the load of a gateway in that case. + // We assume it's an healthy location. Riseup will switch to the updated API soon *fingers crossed* + return true + } + for _, gatewayLoad := range geoService.SortedGateways { + if gatewayLoad.Host == gateway.Host { + return !gatewayLoad.Overload + } + } + + // gateways that are not included in the geoservice should be considered unusable + return false } // DecodeEIP3 decodes eip-service.json version 3 -func DecodeEIP3(body string) (*EipService, error) { - var eip EipService +func DecodeEIP3(body string) (*EipServiceV3, error) { + var eip EipServiceV3 err := json.Unmarshal([]byte(body), &eip) if err != nil { return nil, err @@ -323,6 +400,16 @@ func DecodeEIP3(body string) (*EipService, error) { return &eip, nil } +// DecodeGeoService decodes geoService json +func DecodeGeoService(body string) (*GeoService, error) { + var gs GeoService + err := json.Unmarshal([]byte(body), &gs) + if err != nil { + return nil, err + } + return &gs, nil +} + // NewExperimentMeasurer creates a new ExperimentMeasurer. func NewExperimentMeasurer(config Config) model.ExperimentMeasurer { return Measurer{Config: config} @@ -333,28 +420,11 @@ func NewExperimentMeasurer(config Config) model.ExperimentMeasurer { // Note that this structure is part of the ABI contract with ooniprobe // 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"` - TransportStatus map[string]string `json:"transport_status"` - IsAnomaly bool `json:"-"` + IsAnomaly bool `json:"-"` } // GetSummaryKeys implements model.ExperimentMeasurer.GetSummaryKeys. func (m Measurer) GetSummaryKeys(measurement *model.Measurement) (interface{}, error) { sk := SummaryKeys{IsAnomaly: false} - tk, ok := measurement.TestKeys.(*TestKeys) - if !ok { - return sk, errors.New("invalid test keys type") - } - sk.APIBlocked = tk.APIStatus != "ok" - sk.ValidCACert = tk.CACertStatus - sk.FailingGateways = len(tk.FailingGateways) - sk.TransportStatus = tk.TransportStatus - // Note: the order in the following OR chains matter: TransportStatus - // is nil if APIBlocked or !CACertStatus - sk.IsAnomaly = (sk.APIBlocked || !tk.CACertStatus || - tk.TransportStatus["openvpn"] == "blocked" || - tk.TransportStatus["obfs4"] == "blocked") return sk, nil } diff --git a/internal/experiment/riseupvpn/riseupvpn_test.go b/internal/experiment/riseupvpn/riseupvpn_test.go index 9af91c9699..d094dd9abe 100644 --- a/internal/experiment/riseupvpn/riseupvpn_test.go +++ b/internal/experiment/riseupvpn/riseupvpn_test.go @@ -11,10 +11,9 @@ import ( "testing" "github.com/apex/log" - "github.com/google/go-cmp/cmp" "github.com/ooni/probe-cli/v3/internal/experiment/riseupvpn" "github.com/ooni/probe-cli/v3/internal/experiment/urlgetter" - "github.com/ooni/probe-cli/v3/internal/legacy/mockable" + "github.com/ooni/probe-cli/v3/internal/mocks" "github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/netxlite" "github.com/ooni/probe-cli/v3/internal/tracex" @@ -142,8 +141,9 @@ const ( "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----- + geoservice = `{"ip":"51.15.0.88","cc":"NL","city":"Haarlem","lat":52.381,"lon":4.6275,"gateways":["test1.riseup.net","test2.riseup.net"]}` + geoService_update = `{"ip":"51.15.0.88","cc":"NL","city":"Haarlem","lat":52.381,"lon":4.6275,"gateways":["test1.riseup.net","test2.riseup.net"], "sortedGateways": [{ "host": "test1.riseup.net", "fullness": 0.2, "overload": false }, { "host": "test2.riseup.net", "fullness": 0.9, "overload": true }]}` + cacert = `-----BEGIN CERTIFICATE----- MIIFjTCCA3WgAwIBAgIBATANBgkqhkiG9w0BAQ0FADBZMRgwFgYDVQQKDA9SaXNl dXAgTmV0d29ya3MxGzAZBgNVBAsMEmh0dHBzOi8vcmlzZXVwLm5ldDEgMB4GA1UE AwwXUmlzZXVwIE5ldHdvcmtzIFJvb3QgQ0EwHhcNMTQwNDI4MDAwMDAwWhcNMjQw @@ -184,9 +184,10 @@ UN9SaWRlWKSdP4haujnzCoJbM7dU9bjvlGZNyXEekgeT0W2qFeGGp+yyUWw8tNsp 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" + openvpnurl1 = "tcpconnect://234.345.234.345:443" // "Seattle" + openvpnurl2 = "tcpconnect://123.456.123.456:443" // "Paris" obfs4url1 = "tcpconnect://234.345.234.345:23042" + obfs4url2 = "tcpconnect://123.456.123.456:444" ) var RequestResponse = map[string]string{ @@ -197,6 +198,7 @@ var RequestResponse = map[string]string{ openvpnurl1: "", openvpnurl2: "", obfs4url1: "", + obfs4url2: "", } func TestNewExperimentMeasurer(t *testing.T) { @@ -204,20 +206,22 @@ func TestNewExperimentMeasurer(t *testing.T) { if measurer.ExperimentName() != "riseupvpn" { t.Fatal("unexpected name") } - if measurer.ExperimentVersion() != "0.2.0" { + if measurer.ExperimentVersion() != "0.3.0" { t.Fatal("unexpected version") } } func TestGood(t *testing.T) { + // the gateaway openvpnurl2 is filtered out, since it doesn't support additionally obfs4 measurement := runDefaultMockTest(t, generateDefaultMockGetter(map[string]bool{ cacerturl: true, eipserviceurl: true, providerurl: true, geoserviceurl: true, openvpnurl1: true, - openvpnurl2: true, + openvpnurl2: false, obfs4url1: true, + obfs4url2: false, })) tk := measurement.TestKeys.(*riseupvpn.TestKeys) @@ -233,20 +237,18 @@ func TestGood(t *testing.T) { if tk.APIFailure != nil { t.Fatal("unexpected ApiFailure") } - if tk.APIStatus != "ok" { - t.Fatal("unexpected ApiStatus") - } if tk.CACertStatus != true { t.Fatal("unexpected CaCertStatus") } - if tk.FailingGateways != nil { - t.Fatal("unexpected FailingGateways value") - } - if tk.TransportStatus == nil { - t.Fatal("unexpected nil TransportStatus struct ") + + hasOpenvpn1 := false + for _, tcpConnect := range tk.TCPConnect { + if tcpConnect.IP == "234.345.234.345" { + hasOpenvpn1 = true + } } - if tk.TransportStatus["openvpn"] != "ok" { - t.Fatal("unexpected openvpn transport status") + if !hasOpenvpn1 { + t.Fatalf("Gateway tests should run %t", hasOpenvpn1) } } @@ -257,7 +259,7 @@ func TestUpdateWithMixedResults(t *testing.T) { tk.UpdateProviderAPITestKeys(urlgetter.MultiOutput{ Input: urlgetter.MultiInput{ Config: urlgetter.Config{Method: "GET"}, - Target: "https://api.black.riseup.net:443/3/config/eip-service.json", + Target: "https://riseup.net/provider.json", }, TestKeys: urlgetter.TestKeys{ HTTPResponseStatus: 200, @@ -266,9 +268,17 @@ func TestUpdateWithMixedResults(t *testing.T) { tk.UpdateProviderAPITestKeys(urlgetter.MultiOutput{ Input: urlgetter.MultiInput{ Config: urlgetter.Config{Method: "GET"}, - Target: "https://riseup.net/provider.json", + Target: "https://api.black.riseup.net:443/3/config/eip-service.json", }, TestKeys: urlgetter.TestKeys{ + Requests: []model.ArchivalHTTPRequestResult{ + { + Request: model.ArchivalHTTPRequest{URL: "https://api.black.riseup.net:443/3/config/eip-service.json"}, + Failure: (func() *string { + s := "eof" + return &s + })(), + }}, FailedOperation: (func() *string { s := netxlite.HTTPRoundTripOperation return &s @@ -288,18 +298,11 @@ func TestUpdateWithMixedResults(t *testing.T) { HTTPResponseStatus: 200, }, }) - if tk.APIStatus != "blocked" { - t.Fatal("ApiStatus should be blocked") - } - if *tk.APIFailure != netxlite.FailureEOFError { + + if len(tk.APIFailure) > 0 && tk.APIFailure[0] != netxlite.FailureEOFError { t.Fatal("invalid ApiFailure") } - if tk.FailingGateways != nil { - t.Fatal("invalid FailingGateways") - } - if tk.TransportStatus != nil { - t.Fatal("invalid TransportStatus") - } + } func TestInvalidCaCert(t *testing.T) { @@ -311,6 +314,7 @@ func TestInvalidCaCert(t *testing.T) { openvpnurl1: "", openvpnurl2: "", obfs4url1: "", + obfs4url2: "", } measurer := riseupvpn.Measurer{ Config: riseupvpn.Config{}, @@ -319,13 +323,17 @@ func TestInvalidCaCert(t *testing.T) { eipserviceurl: true, providerurl: true, geoserviceurl: true, - openvpnurl1: false, - openvpnurl2: true, + openvpnurl1: true, + openvpnurl2: false, // filtered out, no obfs4 support obfs4url1: true, + obfs4url2: false, // filtered out }), } ctx := context.Background() - sess := &mockable.Session{MockableLogger: log.Log} + sess := &mocks.Session{MockLogger: func() model.Logger { + return model.DiscardLogger + }} + measurement := new(model.Measurement) callbacks := model.NewPrinterCallbacks(log.Log) args := &model.ExperimentArgs{ @@ -341,15 +349,6 @@ func TestInvalidCaCert(t *testing.T) { if tk.CACertStatus == true { t.Fatal("unexpected CaCertStatus") } - if tk.APIStatus != "blocked" { - t.Fatal("ApiStatus should be blocked") - } - if tk.FailingGateways != nil { - t.Fatal("invalid FailingGateways") - } - if tk.TransportStatus != nil { - t.Fatal("invalid TransportStatus") - } } func TestFailureCaCertFetch(t *testing.T) { @@ -367,21 +366,17 @@ func TestFailureCaCertFetch(t *testing.T) { if tk.CACertStatus != false { t.Fatal("invalid CACertStatus ") } - if tk.APIStatus != "blocked" { - t.Fatal("invalid ApiStatus") - } - if tk.APIFailure != nil { - t.Fatal("ApiFailure should be null") + if tk.APIFailure == nil || len(tk.APIFailure) != 1 || tk.APIFailure[0] != io.EOF.Error() { + t.Fatal("ApiFailure should not be null" + fmt.Sprint(tk.APIFailure)) } - if len(tk.Requests) > 1 { - t.Fatal("Unexpected requests") + if len(tk.Requests) == 1 { + t.Fatal("Too less requests, expected to run all API requests") } - if tk.FailingGateways != nil { - t.Fatal("invalid FailingGateways") - } - if tk.TransportStatus != nil { - t.Fatal("invalid TransportStatus") + for _, tcpConnect := range tk.TCPConnect { + if tcpConnect.IP == openvpnurl1 || tcpConnect.IP == openvpnurl2 || tcpConnect.IP == obfs4url1 || tcpConnect.IP == obfs4url2 { + t.Fatal("No gateaway tests should be run if API fails") + } } } @@ -408,10 +403,6 @@ func TestFailureEipServiceBlocked(t *testing.T) { } } - if tk.APIStatus != "blocked" { - t.Fatal("invalid ApiStatus") - } - if tk.APIFailure == nil { t.Fatal("ApiFailure should not be null") } @@ -440,9 +431,6 @@ func TestFailureProviderUrlBlocked(t *testing.T) { if tk.CACertStatus != true { t.Fatal("invalid CACertStatus ") } - if tk.APIStatus != "blocked" { - t.Fatal("invalid ApiStatus") - } if tk.APIFailure == nil { t.Fatal("ApiFailure should not be null") @@ -472,107 +460,211 @@ func TestFailureGeoIpServiceBlocked(t *testing.T) { } } - if tk.APIStatus != "blocked" { - t.Fatal("invalid ApiStatus") - } - if tk.APIFailure == nil { t.Fatal("ApiFailure should not be null") } } -func TestFailureGateway1(t *testing.T) { +func TestFailureGateway1TransportNOK(t *testing.T) { measurement := runDefaultMockTest(t, generateDefaultMockGetter(map[string]bool{ cacerturl: true, eipserviceurl: true, providerurl: true, geoserviceurl: true, - openvpnurl1: false, - openvpnurl2: true, + openvpnurl1: false, // failed gateway + openvpnurl2: false, // filtered out obfs4url1: true, + obfs4url2: false, })) tk := measurement.TestKeys.(*riseupvpn.TestKeys) if tk.CACertStatus != true { t.Fatal("invalid CACertStatus ") } - if tk.FailingGateways == nil || len(tk.FailingGateways) != 1 { - t.Fatal("unexpected amount of failing gateways") - } - - 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") + for _, tcpConnect := range tk.TCPConnect { + if !tcpConnect.Status.Success { + if tcpConnect.IP != "234.345.234.345" { + t.Fatal("invalid failed gateway ip: " + fmt.Sprint(tcpConnect.IP)) + } + if tcpConnect.Port != 443 { + t.Fatal("invalid failed gateway port: " + fmt.Sprint(tcpConnect.Port)) + } + } } 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)) +func TestIgnoreOverloadedGateways(t *testing.T) { + eipService, err := riseupvpn.DecodeEIP3(eipservice) + if err != nil { + t.Fatal("Preconditions for the test are not met.") } - if tk.TransportStatus == nil || tk.TransportStatus["obfs4"] == "blocked" { - t.Fatal("invalid TransportStatus: " + fmt.Sprint(tk.TransportStatus)) + //add obfs4 capability for 1. gateway + addObfs4Capability(&eipService.Gateways[0]) + eipservicejson, err := json.Marshal(eipService) + if err != nil { + t.Fatal(err) } -} -func TestFailureTransport(t *testing.T) { - measurement := runDefaultMockTest(t, generateDefaultMockGetter(map[string]bool{ + requestResponseMap := map[string]string{ + eipserviceurl: string(eipservicejson), + providerurl: provider, + geoserviceurl: geoService_update, + cacerturl: cacert, + openvpnurl1: "", + openvpnurl2: "", + obfs4url1: "", + obfs4url2: "", + } + + measurement := runDefaultMockTest(t, generateMockGetter(requestResponseMap, map[string]bool{ cacerturl: true, eipserviceurl: true, providerurl: true, geoserviceurl: true, - openvpnurl1: false, - openvpnurl2: false, - obfs4url1: false, + openvpnurl1: false, // should be filtered out, since overloaded + openvpnurl2: true, + obfs4url1: false, // should be filtered out, since overloaded + obfs4url2: true, })) + tk := measurement.TestKeys.(*riseupvpn.TestKeys) - if tk.TransportStatus == nil || tk.TransportStatus["openvpn"] != "blocked" { - t.Fatal("invalid TransportStatus: " + fmt.Sprint(tk.TransportStatus)) + for _, tcpConnect := range tk.TCPConnect { + if !tcpConnect.Status.Success { + t.Fatal("unexpected failing tcpconnect. Overloaded gateways shouldn't be tested. " + fmt.Sprint(tcpConnect)) + } + } +} + +func TestIgnoreLocationsWithFewObfs4Bridges(t *testing.T) { + eipService, err := riseupvpn.DecodeEIP3(eipservice) + if err != nil { + t.Fatal("Preconditions for the test are not met.") } - if tk.TransportStatus == nil || tk.TransportStatus["obfs4"] != "blocked" { - t.Fatal("invalid TransportStatus: " + fmt.Sprint(tk.TransportStatus)) + addObfs4Capability(&eipService.Gateways[0]) + addGateway(eipService, "vpn1.test", "123.12.123.11", "tokio") + addGateway(eipService, "vpn2.test", "123.12.123.12", "tokio") + addGateway(eipService, "vpn3.test", "123.12.123.13", "paris") + + eipservicejson, err := json.Marshal(eipService) + if err != nil { + t.Fatal(err) + } + + requestResponseMap := map[string]string{ + eipserviceurl: string(eipservicejson), + providerurl: provider, + geoserviceurl: geoservice, + cacerturl: cacert, + openvpnurl1: "", + openvpnurl2: "", + obfs4url1: "", + obfs4url2: "", + "tcpconnect://123.12.123.11:444": "", + "tcpconnect://123.12.123.12:444": "", + "tcpconnect://123.12.123.13:444": "", + } + + measurement := runDefaultMockTest(t, generateMockGetter(requestResponseMap, map[string]bool{ + cacerturl: true, + eipserviceurl: true, + providerurl: true, + geoserviceurl: false, + openvpnurl1: false, // should be filtered out, b/c its's location is not under test + openvpnurl2: true, + obfs4url1: false, // should be filtered out, b/c its's location is not under test + obfs4url2: true, + "tcpconnect://123.12.123.11:444": true, + "tcpconnect://123.12.123.12:444": true, + "tcpconnect://123.12.123.13:444": true, + })) + + tk := measurement.TestKeys.(*riseupvpn.TestKeys) + + for _, tcpConnect := range tk.TCPConnect { + if tcpConnect.IP == "234.345.234.345" { // Seattle + t.Fatal("Locations with few gateways should be ignored") + } } + /* + if tk.TransportStatus == nil || tk.TransportStatus["openvpn"] == "blocked" || tk.TransportStatus["obfs"] == "blocked" { + t.Fatal("invalid TransportStatus: " + fmt.Sprint(tk.TransportStatus)) + } + + if tk.FailingGateways != nil { + t.Fatal("unexpected amount of failing gateways. Only locations under test should be evaluated." + fmt.Sprint(tk.FailingGateways)) + } + */ } -func TestMissingTransport(t *testing.T) { +func TestIgnoreGatewaysNotIncludedInGeoAPIResponse(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 + addGateway(eipService, "vpn1.test", "123.12.123.11", "tokio") + addGateway(eipService, "vpn2.test", "123.12.123.12", "tokio") + eipservicejson, err := json.Marshal(eipService) + if err != nil { + t.Fatal(err) + } + + requestResponseMap := map[string]string{ + eipserviceurl: string(eipservicejson), + providerurl: provider, + geoserviceurl: geoService_update, + cacerturl: cacert, + openvpnurl1: "", + openvpnurl2: "", + obfs4url1: "", + obfs4url2: "", + "tcpconnect://123.12.123.11:444": "", + "tcpconnect://123.12.123.12:444": "", + } + + measurement := runDefaultMockTest(t, generateMockGetter(requestResponseMap, map[string]bool{ + cacerturl: true, + eipserviceurl: true, + providerurl: true, + geoserviceurl: true, + openvpnurl1: true, + openvpnurl2: true, + obfs4url1: true, + obfs4url2: true, + "tcpconnect://123.12.123.11:444": false, // filtered out since they don't appear in the *valid* geoservice response + "tcpconnect://123.12.123.12:444": false, + })) + + tk := measurement.TestKeys.(*riseupvpn.TestKeys) + + for _, tcpConnect := range tk.TCPConnect { + if tcpConnect.IP == "123.12.123.11" { // Seattle + t.Fatal("Locations with that are not part of the geoip response should be ignored") } } - if index == -1 { - t.Fatal("Preconditions for the test are not met. Default eipservice string should contain obfs4 transport.") + /* + if tk.FailingGateways != nil { + t.Fatal("unexpected amount of failing gateways. " + fmt.Sprint(tk.FailingGateways)) + } + */ +} + +func TestHandleInvalidGeoAPIResponse(t *testing.T) { + eipService, err := riseupvpn.DecodeEIP3(eipservice) + if err != nil { + t.Fatal("Preconditions for the test are not met.") } - transports[index] = transports[len(transports)-1] - transports = transports[:len(transports)-1] - eipService.Gateways[1].Capabilities.Transport = transports - eipservicejson, err := json.Marshal(eipservice) + //add obfs4 capability for 1. gateway + addObfs4Capability(&eipService.Gateways[0]) + eipservicejson, err := json.Marshal(eipService) if err != nil { t.Fatal(err) } @@ -580,145 +672,53 @@ func TestMissingTransport(t *testing.T) { requestResponseMap := map[string]string{ eipserviceurl: string(eipservicejson), providerurl: provider, - geoserviceurl: geoservice, + geoserviceurl: "invalid", cacerturl: cacert, openvpnurl1: "", openvpnurl2: "", obfs4url1: "", + obfs4url2: "", } - 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, - }), - } + measurement := runDefaultMockTest(t, generateMockGetter(requestResponseMap, map[string]bool{ + cacerturl: true, + eipserviceurl: true, + providerurl: true, + geoserviceurl: true, + openvpnurl1: false, // all gateways are assumed to be healthy + openvpnurl2: true, // and aren't filtered out + obfs4url1: false, // because the geoservice reply is misconfigured + obfs4url2: true, // and hence it's impossible to read the overload status + })) - ctx := context.Background() - sess := &mockable.Session{MockableLogger: log.Log} - measurement := new(model.Measurement) - callbacks := model.NewPrinterCallbacks(log.Log) - args := &model.ExperimentArgs{ - Callbacks: callbacks, - Measurement: measurement, - Session: sess, - } - err = measurer.Run(ctx, args) - 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 tk.FailingGateways == nil || len(tk.FailingGateways) != 2 { + t.Fatal("unexpected amount of failing gateways. " + fmt.Sprint(tk.FailingGateways)) + } + */ + foundFailure := false + for _, failure := range tk.APIFailure { + if failure == "invalid_geoservice_response" { + foundFailure = true + break + } } - if _, found := tk.TransportStatus["obfs"]; found { - t.Fatal("invalid TransportStatus: " + fmt.Sprint(tk.TransportStatus)) + if !foundFailure { + t.Fatal("expected API Failure invalid_geoservice_response is missing: " + fmt.Sprint(tk.APIFailure)) } } -func TestSummaryKeysInvalidType(t *testing.T) { +func TestSummaryKeysAlwaysReturnIsAnomalyFalse(t *testing.T) { measurement := new(model.Measurement) m := &riseupvpn.Measurer{} - _, err := m.GetSummaryKeys(measurement) - if err.Error() != "invalid test keys type" { - t.Fatal("not the error we expected") + result, err := m.GetSummaryKeys(measurement) + if err != nil { + t.Fatal("GetSummaryKeys should never return an error") } -} - -func TestSummaryKeysWorksAsIntended(t *testing.T) { - tests := []struct { - tk riseupvpn.TestKeys - sk riseupvpn.SummaryKeys - }{{ - tk: riseupvpn.TestKeys{ - APIStatus: "blocked", - CACertStatus: true, - FailingGateways: nil, - TransportStatus: nil, - }, - sk: riseupvpn.SummaryKeys{ - 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, - FailingGateways: 0, - TransportStatus: nil, - }, - }, { - tk: riseupvpn.TestKeys{ - APIStatus: "ok", - CACertStatus: true, - FailingGateways: []riseupvpn.GatewayConnection{{ - IP: "1.1.1.1", - 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{} - measurement := &model.Measurement{TestKeys: &tt.tk} - got, err := m.GetSummaryKeys(measurement) - if err != nil { - t.Fatal(err) - return - } - sk := got.(riseupvpn.SummaryKeys) - if diff := cmp.Diff(tt.sk, sk); diff != "" { - t.Fatal(diff) - } - }) + if result.(riseupvpn.SummaryKeys).IsAnomaly { + t.Fatal("GetSummaryKeys should never return IsAnomaly true") } } @@ -781,6 +781,7 @@ func generateMockGetter(requestResponse map[string]string, responseStatus map[st Value: responseBody, }, BodyIsTruncated: false, + Code: responseStatus, }}, }, TCPConnect: []tracex.TCPConnectEntry{tcpConnect}, @@ -803,9 +804,7 @@ func runDefaultMockTest(t *testing.T, multiGetter urlgetter.MultiGetter) *model. args := &model.ExperimentArgs{ Callbacks: model.NewPrinterCallbacks(log.Log), Measurement: measurement, - Session: &mockable.Session{ - MockableLogger: log.Log, - }, + Session: &mocks.Session{MockLogger: func() model.Logger { return log.Log }}, } err := measurer.Run(context.Background(), args) @@ -814,3 +813,30 @@ func runDefaultMockTest(t *testing.T, multiGetter urlgetter.MultiGetter) *model. } return measurement } + +func addObfs4Capability(gateway *riseupvpn.GatewayV3) { + transports := gateway.Capabilities.Transport + transport := riseupvpn.TransportV3{ + Type: "obfs4", + Protocols: []string{"tcp"}, + Ports: []string{"444"}, + Options: map[string]string{ + "cert": "XXXXXXXXXXXXXXXXXXXXXXXXX", + "iatMode": "0", + }, + } + + transports = append(transports, transport) + gateway.Capabilities.Transport = transports +} + +func addGateway(service *riseupvpn.EipServiceV3, host string, ipAddress string, location string) { + gateway := riseupvpn.GatewayV3{ + Capabilities: riseupvpn.CapabilitiesV3{}, + Host: host, + IPAddress: ipAddress, + Location: location, + } + addObfs4Capability(&gateway) + service.Gateways = append(service.Gateways, gateway) +}