From 2f480e3e9d5329eb47548138a7ae57578cd897a1 Mon Sep 17 00:00:00 2001 From: cyBerta Date: Sat, 4 Mar 2023 02:17:41 +0100 Subject: [PATCH 01/10] RiseupVPN: Don't do TLS verification if fetching the ca cert failed or the ca was invalid. Instead of early returning the test, we can still gather the necessary configs to run the gateway tests. This change implies that there might occur multiple failures while fetching data from the API, which is why TestKeys.APIFailures becomes a string array. --- internal/experiment/riseupvpn/riseupvpn.go | 31 +++++++++------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/internal/experiment/riseupvpn/riseupvpn.go b/internal/experiment/riseupvpn/riseupvpn.go index 37061826cd..ce2b7ed5a0 100644 --- a/internal/experiment/riseupvpn/riseupvpn.go +++ b/internal/experiment/riseupvpn/riseupvpn.go @@ -61,7 +61,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 +86,9 @@ 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 } } @@ -147,11 +144,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. @@ -208,17 +200,15 @@ func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { 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.APIStatus = "blocked" + testkeys.APIFailure = append(testkeys.APIFailure, "invalid_ca") + certPool = nil } } @@ -230,16 +220,19 @@ 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) { From a35d21ecd1e135eac69abb0e44e3cb6f3990b48b Mon Sep 17 00:00:00 2001 From: cyBerta Date: Mon, 6 Mar 2023 15:19:18 +0100 Subject: [PATCH 02/10] RiseupVPN: re-run fetches from the API using Tor+Snowflake, if the API was considered to be blocked --- internal/experiment/riseupvpn/riseupvpn.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/internal/experiment/riseupvpn/riseupvpn.go b/internal/experiment/riseupvpn/riseupvpn.go index ce2b7ed5a0..12cdb6f46f 100644 --- a/internal/experiment/riseupvpn/riseupvpn.go +++ b/internal/experiment/riseupvpn/riseupvpn.go @@ -235,10 +235,20 @@ func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { NoTLSVerify: !testkeys.CACertStatus, }}, } + for entry := range multi.CollectOverall(ctx, inputs, 1, 50, "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, 50, "riseupvpn", callbacks) { + testkeys.UpdateProviderAPITestKeys(entry) + } + } + // test gateways now testkeys.TransportStatus = map[string]string{} gateways := parseGateways(testkeys) From ef38cec3a46a6db0be298ef2760476089ee492b9 Mon Sep 17 00:00:00 2001 From: cyBerta Date: Mon, 6 Mar 2023 15:20:27 +0100 Subject: [PATCH 03/10] Fixing tests after chaning riseupvpn test logic. Gateway tests are no longer skipped, API config data is fetched even if CA cert is invalid or missing --- .../experiment/riseupvpn/riseupvpn_test.go | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/internal/experiment/riseupvpn/riseupvpn_test.go b/internal/experiment/riseupvpn/riseupvpn_test.go index 9af91c9699..d82b4d2eec 100644 --- a/internal/experiment/riseupvpn/riseupvpn_test.go +++ b/internal/experiment/riseupvpn/riseupvpn_test.go @@ -291,7 +291,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 { @@ -344,11 +344,24 @@ func TestInvalidCaCert(t *testing.T) { if tk.APIStatus != "blocked" { t.Fatal("ApiStatus should be blocked") } - if tk.FailingGateways != nil { - t.Fatal("invalid FailingGateways") + + if tk.FailingGateways == nil || len(tk.FailingGateways) != 1 { + t.Fatal("invalid length of FailingGateways") } - if tk.TransportStatus != nil { - t.Fatal("invalid TransportStatus") + + 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.TransportStatus == nil || tk.TransportStatus["openvpn"] != "ok" { + t.Fatal("invalid TransportStatus: " + fmt.Sprint(tk.TransportStatus)) } } @@ -371,17 +384,17 @@ func TestFailureCaCertFetch(t *testing.T) { 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)) } } From cfa4857e3cc4fe6b44a3aae1513e468f529c1fe9 Mon Sep 17 00:00:00 2001 From: cyBerta Date: Mon, 6 Mar 2023 23:19:45 +0100 Subject: [PATCH 04/10] Filter gateways: just expose a limited set of gateways for network testing, avoid those known to be overloaded. --- internal/experiment/riseupvpn/riseupvpn.go | 103 +++++++++++++++++++-- 1 file changed, 96 insertions(+), 7 deletions(-) diff --git a/internal/experiment/riseupvpn/riseupvpn.go b/internal/experiment/riseupvpn/riseupvpn.go index 12cdb6f46f..34b4598f95 100644 --- a/internal/experiment/riseupvpn/riseupvpn.go +++ b/internal/experiment/riseupvpn/riseupvpn.go @@ -24,6 +24,8 @@ const ( tcpConnect = "tcpconnect://" ) +var locations = []string{"Seattle", "Amsterdam"} + // EipService is the main JSON object of eip-service.json. type EipService struct { Gateways []GatewayV3 @@ -36,6 +38,7 @@ type GatewayV3 struct { } Host string IPAddress string `json:"ip_address"` + Location string `json:"location"` } // TransportV3 describes a transport. @@ -53,6 +56,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 @@ -302,18 +323,76 @@ 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) +} + +func filterGateways(eipService *EipService, geoService *GeoService) []GatewayV3 { + var result []GatewayV3 = nil + for _, gateway := range eipService.Gateways { + if !gateway.hasTransport("obfs4") || + !gateway.isLocationUnderTest() || + geoService != nil && !geoService.isHealthyGateway(gateway) { + continue + } + result = append(result, gateway) + if len(result) == 3 { + return result + } + } + + 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() 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 + } + } + + return false } // DecodeEIP3 decodes eip-service.json version 3 @@ -326,6 +405,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} From 0f273eafdf1c68d660728ad1425801360ed46b75 Mon Sep 17 00:00:00 2001 From: cyBerta Date: Mon, 6 Mar 2023 23:48:29 +0100 Subject: [PATCH 05/10] RiseupVPN: reduce multi-collectors (url-getter) overallCount after reducing the amount of tested gateways --- internal/experiment/riseupvpn/riseupvpn.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/experiment/riseupvpn/riseupvpn.go b/internal/experiment/riseupvpn/riseupvpn.go index 34b4598f95..5adcc243eb 100644 --- a/internal/experiment/riseupvpn/riseupvpn.go +++ b/internal/experiment/riseupvpn/riseupvpn.go @@ -217,7 +217,7 @@ 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 { @@ -257,7 +257,7 @@ func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { }}, } - 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) } @@ -265,7 +265,7 @@ func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { for _, input := range inputs { input.Config.Tunnel = "torsf" } - 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) } } From f6150526cdadd4989b7dba953b00022a32b97725 Mon Sep 17 00:00:00 2001 From: cyBerta Date: Fri, 17 Mar 2023 16:47:33 +0100 Subject: [PATCH 06/10] add null check in gateway filter function --- internal/experiment/riseupvpn/riseupvpn.go | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/internal/experiment/riseupvpn/riseupvpn.go b/internal/experiment/riseupvpn/riseupvpn.go index 5adcc243eb..6c1e605c36 100644 --- a/internal/experiment/riseupvpn/riseupvpn.go +++ b/internal/experiment/riseupvpn/riseupvpn.go @@ -346,15 +346,17 @@ func parseGateways(testKeys *TestKeys) []GatewayV3 { func filterGateways(eipService *EipService, geoService *GeoService) []GatewayV3 { var result []GatewayV3 = nil - for _, gateway := range eipService.Gateways { - if !gateway.hasTransport("obfs4") || - !gateway.isLocationUnderTest() || - geoService != nil && !geoService.isHealthyGateway(gateway) { - continue - } - result = append(result, gateway) - if len(result) == 3 { - return result + if eipService != nil { + for _, gateway := range eipService.Gateways { + if !gateway.hasTransport("obfs4") || + !gateway.isLocationUnderTest() || + geoService != nil && !geoService.isHealthyGateway(gateway) { + continue + } + result = append(result, gateway) + if len(result) == 3 { + return result + } } } From 74239a51e79dd21fbf969adcd0c3a8559c326f99 Mon Sep 17 00:00:00 2001 From: cyBerta Date: Sat, 18 Mar 2023 21:11:10 +0100 Subject: [PATCH 07/10] test only gateways from locations with high redundancy regarding obfs4 bridges, avoid hard-coding locations in the test --- internal/experiment/riseupvpn/riseupvpn.go | 44 ++++++++++++++++++++-- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/internal/experiment/riseupvpn/riseupvpn.go b/internal/experiment/riseupvpn/riseupvpn.go index 6c1e605c36..d6466a3b16 100644 --- a/internal/experiment/riseupvpn/riseupvpn.go +++ b/internal/experiment/riseupvpn/riseupvpn.go @@ -24,8 +24,6 @@ const ( tcpConnect = "tcpconnect://" ) -var locations = []string{"Seattle", "Amsterdam"} - // EipService is the main JSON object of eip-service.json. type EipService struct { Gateways []GatewayV3 @@ -344,12 +342,14 @@ func parseGateways(testKeys *TestKeys) []GatewayV3 { 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() || + !gateway.isLocationUnderTest(locations) || geoService != nil && !geoService.isHealthyGateway(gateway) { continue } @@ -359,6 +359,42 @@ func filterGateways(eipService *EipService, geoService *GeoService) []GatewayV3 } } } + 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 } @@ -372,7 +408,7 @@ func (gateway *GatewayV3) hasTransport(s string) bool { return false } -func (gateway *GatewayV3) isLocationUnderTest() bool { +func (gateway *GatewayV3) isLocationUnderTest(locations []string) bool { for _, location := range locations { if location == gateway.Location { return true From b82111060ddae3950c3bc5816ce027961aa9ca42 Mon Sep 17 00:00:00 2001 From: cyBerta Date: Mon, 20 Mar 2023 15:27:40 +0100 Subject: [PATCH 08/10] update RiseupVPN tests --- internal/experiment/riseupvpn/riseupvpn.go | 16 +- .../experiment/riseupvpn/riseupvpn_test.go | 353 ++++++++++++++++-- 2 files changed, 338 insertions(+), 31 deletions(-) diff --git a/internal/experiment/riseupvpn/riseupvpn.go b/internal/experiment/riseupvpn/riseupvpn.go index d6466a3b16..46866158fb 100644 --- a/internal/experiment/riseupvpn/riseupvpn.go +++ b/internal/experiment/riseupvpn/riseupvpn.go @@ -29,14 +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"` - Location string `json:"location"` + Capabilities Capabilities + Host string + IPAddress string `json:"ip_address"` + Location string `json:"location"` } // TransportV3 describes a transport. @@ -430,6 +433,7 @@ func (geoService *GeoService) isHealthyGateway(gateway GatewayV3) bool { } } + // gateways that are not included in the geoservice should be considered unusable return false } diff --git a/internal/experiment/riseupvpn/riseupvpn_test.go b/internal/experiment/riseupvpn/riseupvpn_test.go index d82b4d2eec..f2591eaab0 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) @@ -311,6 +317,7 @@ func TestInvalidCaCert(t *testing.T) { openvpnurl1: "", openvpnurl2: "", obfs4url1: "", + obfs4url2: "", } measurer := riseupvpn.Measurer{ Config: riseupvpn.Config{}, @@ -319,13 +326,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{ @@ -345,22 +356,11 @@ func TestInvalidCaCert(t *testing.T) { t.Fatal("ApiStatus should be blocked") } - if tk.FailingGateways == nil || len(tk.FailingGateways) != 1 { - t.Fatal("invalid length of FailingGateways") - } - - 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.FailingGateways != nil { + t.Fatal("invalid FailingGateways") } - if tk.TransportStatus == nil || tk.TransportStatus["openvpn"] != "ok" { + if tk.TransportStatus == nil || tk.TransportStatus["openvpn"] != "ok" || tk.TransportStatus["obfs4"] != "ok" { t.Fatal("invalid TransportStatus: " + fmt.Sprint(tk.TransportStatus)) } } @@ -494,15 +494,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 { @@ -636,6 +710,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{} @@ -827,3 +1103,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) +} From 3cdb18f502538607f89fa2ffd0ea80ab032525ca Mon Sep 17 00:00:00 2001 From: cyBerta Date: Mon, 20 Mar 2023 15:28:08 +0100 Subject: [PATCH 09/10] update RiseupVPN test version --- internal/experiment/riseupvpn/riseupvpn.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/experiment/riseupvpn/riseupvpn.go b/internal/experiment/riseupvpn/riseupvpn.go index 46866158fb..faf8c8a7a9 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" From b31586e73185bf7e3418a6133f693340b3b2f25b Mon Sep 17 00:00:00 2001 From: cyBerta Date: Mon, 20 Mar 2023 19:47:41 +0100 Subject: [PATCH 10/10] only take failures of RiseupVPNs main configuration endpoint into consideration for APIStatus blocked --- internal/experiment/riseupvpn/riseupvpn.go | 8 ++++--- .../experiment/riseupvpn/riseupvpn_test.go | 22 +++++++++++++------ 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/internal/experiment/riseupvpn/riseupvpn.go b/internal/experiment/riseupvpn/riseupvpn.go index faf8c8a7a9..d1184e5299 100644 --- a/internal/experiment/riseupvpn/riseupvpn.go +++ b/internal/experiment/riseupvpn/riseupvpn.go @@ -109,7 +109,11 @@ func (tk *TestKeys) UpdateProviderAPITestKeys(v urlgetter.MultiOutput) { tk.TCPConnect = append(tk.TCPConnect, v.TestKeys.TCPConnect...) tk.TLSHandshakes = append(tk.TLSHandshakes, v.TestKeys.TLSHandshakes...) if v.TestKeys.Failure != nil { - tk.APIStatus = "blocked" + 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 } @@ -223,12 +227,10 @@ func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { testkeys.AddCACertFetchTestKeys(tk) if tk.Failure != nil { testkeys.CACertStatus = false - testkeys.APIStatus = "blocked" testkeys.APIFailure = append(testkeys.APIFailure, *tk.Failure) certPool = nil } else if ok := certPool.AppendCertsFromPEM([]byte(tk.HTTPResponseBody)); !ok { testkeys.CACertStatus = false - testkeys.APIStatus = "blocked" testkeys.APIFailure = append(testkeys.APIFailure, "invalid_ca") certPool = nil } diff --git a/internal/experiment/riseupvpn/riseupvpn_test.go b/internal/experiment/riseupvpn/riseupvpn_test.go index f2591eaab0..023303bdf9 100644 --- a/internal/experiment/riseupvpn/riseupvpn_test.go +++ b/internal/experiment/riseupvpn/riseupvpn_test.go @@ -263,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, @@ -272,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 @@ -352,8 +360,8 @@ 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 { @@ -380,7 +388,7 @@ 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") } @@ -453,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") } @@ -485,7 +493,7 @@ func TestFailureGeoIpServiceBlocked(t *testing.T) { } } - if tk.APIStatus != "blocked" { + if tk.APIStatus != "ok" { t.Fatal("invalid ApiStatus") }