diff --git a/internal/experiment/riseupvpn/riseupvpn.go b/internal/experiment/riseupvpn/riseupvpn.go index 37061826cd..d1184e5299 100644 --- a/internal/experiment/riseupvpn/riseupvpn.go +++ b/internal/experiment/riseupvpn/riseupvpn.go @@ -17,7 +17,7 @@ import ( 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" @@ -29,13 +29,17 @@ type EipService struct { Gateways []GatewayV3 } +// Capabilities is a list of transports a gateway supports +type Capabilities struct { + Transport []TransportV3 +} + // GatewayV3 describes a gateway. type GatewayV3 struct { - Capabilities struct { - Transport []TransportV3 - } - Host string - IPAddress string `json:"ip_address"` + Capabilities Capabilities + Host string + IPAddress string `json:"ip_address"` + Location string `json:"location"` } // TransportV3 describes a transport. @@ -53,6 +57,24 @@ type GatewayConnection struct { 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. type Config struct { urlgetter.Config @@ -61,7 +83,7 @@ type Config struct { // TestKeys contains riseupvpn test keys. type TestKeys struct { urlgetter.TestKeys - APIFailure *string `json:"api_failure"` + APIFailure []string `json:"api_failure"` APIStatus string `json:"api_status"` CACertStatus bool `json:"ca_cert_status"` FailingGateways []GatewayConnection `json:"failing_gateways"` @@ -86,12 +108,13 @@ 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 + for _, request := range v.TestKeys.Requests { + if request.Request.URL == eipServiceURL && request.Failure != nil { + tk.APIStatus = "blocked" + } + } + tk.APIFailure = append(tk.APIFailure, *v.TestKeys.Failure) return } } @@ -147,11 +170,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 +222,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,22 +244,35 @@ 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.APIStatus == "blocked" { + for _, input := range inputs { + input.Config.Tunnel = "torsf" + } + for entry := range multi.CollectOverall(ctx, inputs, 1, 20, "riseupvpn", callbacks) { + testkeys.UpdateProviderAPITestKeys(entry) + } + } + // test gateways now testkeys.TransportStatus = map[string]string{} gateways := parseGateways(testkeys) @@ -299,18 +326,117 @@ func generateMultiInputs(gateways []GatewayV3, transportType string) []urlgetter } func parseGateways(testKeys *TestKeys) []GatewayV3 { + var eipService *EipService = nil + var geoService *GeoService = nil 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 + 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.Failure == nil { + var err error = nil + geoService, err = DecodeGeoService(requestEntry.Response.Body.Value) + if err != nil { + testKeys.APIFailure = append(testKeys.APIFailure, "invalid_geoservice_response") } } } - return nil + return filterGateways(eipService, geoService) +} + +// filterGateways selects a subset of available gateways supporting obfs4 +func filterGateways(eipService *EipService, 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 *EipService, 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 @@ -323,6 +449,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} diff --git a/internal/experiment/riseupvpn/riseupvpn_test.go b/internal/experiment/riseupvpn/riseupvpn_test.go index 9af91c9699..023303bdf9 100644 --- a/internal/experiment/riseupvpn/riseupvpn_test.go +++ b/internal/experiment/riseupvpn/riseupvpn_test.go @@ -16,6 +16,7 @@ import ( "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/model" + "github.com/ooni/probe-cli/v3/internal/model/mocks" "github.com/ooni/probe-cli/v3/internal/netxlite" "github.com/ooni/probe-cli/v3/internal/tracex" ) @@ -142,8 +143,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 +186,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 +200,7 @@ var RequestResponse = map[string]string{ openvpnurl1: "", openvpnurl2: "", obfs4url1: "", + obfs4url2: "", } func TestNewExperimentMeasurer(t *testing.T) { @@ -204,20 +208,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) @@ -257,7 +263,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 +272,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 @@ -291,7 +305,7 @@ func TestUpdateWithMixedResults(t *testing.T) { 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 { @@ -311,6 +325,7 @@ func TestInvalidCaCert(t *testing.T) { openvpnurl1: "", openvpnurl2: "", obfs4url1: "", + obfs4url2: "", } measurer := riseupvpn.Measurer{ Config: riseupvpn.Config{}, @@ -319,13 +334,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,14 +360,16 @@ 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.APIStatus != "ok" { + t.Fatal("ApiStatus should be ok") } + if tk.FailingGateways != nil { t.Fatal("invalid FailingGateways") } - if tk.TransportStatus != nil { - t.Fatal("invalid TransportStatus") + + if tk.TransportStatus == nil || tk.TransportStatus["openvpn"] != "ok" || tk.TransportStatus["obfs4"] != "ok" { + t.Fatal("invalid TransportStatus: " + fmt.Sprint(tk.TransportStatus)) } } @@ -367,21 +388,21 @@ func TestFailureCaCertFetch(t *testing.T) { if tk.CACertStatus != false { t.Fatal("invalid CACertStatus ") } - if tk.APIStatus != "blocked" { + if tk.APIStatus != "ok" { 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") + if tk.TransportStatus == nil || tk.TransportStatus["openvpn"] != "ok" || tk.TransportStatus["obfs4"] != "ok" { + t.Fatal("invalid TransportStatus: " + fmt.Sprint(tk.TransportStatus)) } } @@ -440,7 +461,7 @@ func TestFailureProviderUrlBlocked(t *testing.T) { if tk.CACertStatus != true { t.Fatal("invalid CACertStatus ") } - if tk.APIStatus != "blocked" { + if tk.APIStatus != "ok" { t.Fatal("invalid ApiStatus") } @@ -472,7 +493,7 @@ func TestFailureGeoIpServiceBlocked(t *testing.T) { } } - if tk.APIStatus != "blocked" { + if tk.APIStatus != "ok" { t.Fatal("invalid ApiStatus") } @@ -481,15 +502,89 @@ func TestFailureGeoIpServiceBlocked(t *testing.T) { } } -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, + 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") + } + + 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 TestFailureGateway1TransportOK(t *testing.T) { + eipService, err := riseupvpn.DecodeEIP3(eipservice) + if err != nil { + t.Fatal("Preconditions for the test are not met.") + } + + //add obfs4 capability to 1. gateway + addObfs4Capability(&eipService.Gateways[0]) + + eipservicejson, err := json.Marshal(eipService) + if err != nil { + t.Fatal(err) + } + t.Log(string(eipservicejson)) + + requestResponseMap := map[string]string{ + eipserviceurl: string(eipservicejson), + providerurl: provider, + geoserviceurl: geoservice, + cacerturl: cacert, + openvpnurl1: "", + openvpnurl2: "", + obfs4url1: "", + obfs4url2: "", + } + + measurement := runDefaultMockTest(t, generateMockGetter(requestResponseMap, map[string]bool{ + cacerturl: true, + eipserviceurl: true, + providerurl: true, + geoserviceurl: true, + openvpnurl1: false, // failed gateway openvpnurl2: true, obfs4url1: true, + obfs4url2: true, })) tk := measurement.TestKeys.(*riseupvpn.TestKeys) if tk.CACertStatus != true { @@ -623,6 +718,208 @@ func TestMissingTransport(t *testing.T) { } } +func TestIgnoreOverloadedGateways(t *testing.T) { + eipService, err := riseupvpn.DecodeEIP3(eipservice) + if err != nil { + t.Fatal("Preconditions for the test are not met.") + } + + //add obfs4 capability for 1. gateway + addObfs4Capability(&eipService.Gateways[0]) + 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: "", + } + + measurement := runDefaultMockTest(t, generateMockGetter(requestResponseMap, map[string]bool{ + cacerturl: true, + eipserviceurl: true, + providerurl: true, + geoserviceurl: true, + 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" || tk.TransportStatus["obfs"] == "blocked" { + t.Fatal("invalid TransportStatus: " + fmt.Sprint(tk.TransportStatus)) + } + + if tk.FailingGateways != nil { + t.Fatal("unexpected amount of failing gateways. Overloaded gateways shouldn't be tested. " + fmt.Sprint(tk.FailingGateways)) + } +} + +func TestIgnoreLocationsWithFewObfs4Bridges(t *testing.T) { + eipService, err := riseupvpn.DecodeEIP3(eipservice) + if err != nil { + t.Fatal("Preconditions for the test are not met.") + } + + 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) + + 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 TestIgnoreGatewaysNotIncludedInGeoAPIResponse(t *testing.T) { + eipService, err := riseupvpn.DecodeEIP3(eipservice) + if err != nil { + t.Fatal("Preconditions for the test are not met.") + } + + 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) + + 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.") + } + + //add obfs4 capability for 1. gateway + addObfs4Capability(&eipService.Gateways[0]) + eipservicejson, err := json.Marshal(eipService) + if err != nil { + t.Fatal(err) + } + + requestResponseMap := map[string]string{ + eipserviceurl: string(eipservicejson), + providerurl: provider, + geoserviceurl: "invalid", + cacerturl: cacert, + openvpnurl1: "", + openvpnurl2: "", + obfs4url1: "", + obfs4url2: "", + } + + 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 + })) + + tk := measurement.TestKeys.(*riseupvpn.TestKeys) + + 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 !foundFailure { + t.Fatal("expected API Failure invalid_geoservice_response is missing: " + fmt.Sprint(tk.APIFailure)) + } +} + func TestSummaryKeysInvalidType(t *testing.T) { measurement := new(model.Measurement) m := &riseupvpn.Measurer{} @@ -814,3 +1111,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.EipService, host string, ipAddress string, location string) { + gateway := riseupvpn.GatewayV3{ + Capabilities: riseupvpn.Capabilities{}, + Host: host, + IPAddress: ipAddress, + Location: location, + } + addObfs4Capability(&gateway) + service.Gateways = append(service.Gateways, gateway) +}