diff --git a/allexperiments.go b/allexperiments.go index 893520bf164b4b0185e3b101895a466b93a78869..ce276db190728dc792698d8d8e2f8646d17a6ae9 100644 --- a/allexperiments.go +++ b/allexperiments.go @@ -11,6 +11,7 @@ import ( "github.com/ooni/probe-engine/experiment/hirl" "github.com/ooni/probe-engine/experiment/ndt7" "github.com/ooni/probe-engine/experiment/psiphon" + "github.com/ooni/probe-engine/experiment/riseupvpn" "github.com/ooni/probe-engine/experiment/sniblocking" "github.com/ooni/probe-engine/experiment/stunreachability" "github.com/ooni/probe-engine/experiment/telegram" @@ -202,6 +203,18 @@ var experimentsByName = map[string]func(*Session) *ExperimentBuilder{ } }, + "riseupvpn": func(session *Session) *ExperimentBuilder { + return &ExperimentBuilder{ + build: func(config interface{}) *Experiment { + return NewExperiment(session, riseupvpn.NewExperimentMeasurer( + *config.(*riseupvpn.Config), + )) + }, + config: &riseupvpn.Config{}, + inputPolicy: InputNone, + } + }, + "telegram": func(session *Session) *ExperimentBuilder { return &ExperimentBuilder{ build: func(config interface{}) *Experiment { diff --git a/experiment/riseupvpn/riseupvpn.go b/experiment/riseupvpn/riseupvpn.go new file mode 100644 index 0000000000000000000000000000000000000000..61162a3ecd52411b5f023ef11b894aa347e9d6cd --- /dev/null +++ b/experiment/riseupvpn/riseupvpn.go @@ -0,0 +1,280 @@ +// Package riseupvpn contains the RiseupVPN network experiment. +// API testing based on telegram experiment +// TODO: write spec +package riseupvpn + +import ( + "context" + "encoding/json" + "time" + + "github.com/apex/log" + "github.com/ooni/probe-engine/experiment/urlgetter" + "github.com/ooni/probe-engine/model" + "github.com/ooni/probe-engine/netx" + "github.com/ooni/probe-engine/netx/archival" +) + +const ( + testName = "riseupvpn" + testVersion = "0.0.2" + 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 main json object of eip-service.json +type EipService struct { + Gateways []GatewayV3 +} + +//GatewayV3 json obj Version 3 +type GatewayV3 struct { + Capabilities struct { + Transport []TransportV3 + } + Host string + IPAddress string `json:"ip_address"` +} + +//TransportV3 json obj Version 3 +type TransportV3 struct { + Type string + Protocols []string + Ports []string + Options map[string]string +} + +type gatewayConnection struct { + IP string + Port int + TransportType string `json:"transport_type"` +} + +// Config contains the riseupvpn experiment config. +type Config struct { + urlgetter.Config +} + +// TestKeys contains riseupvpn test keys. +type TestKeys struct { + urlgetter.TestKeys + RiseupVPNApiFailure *string `json:"riseupvpn_api_failure"` + RiseupVPNApiStatus string `json:"riseupvpn_api_status"` + RiseupVPNCACertStatus bool `json:"riseupvpn_ca_cert_status"` + RiseupVPNFailingGateways []gatewayConnection `json:"riseupvpn_failing_gateways"` +} + +// NewTestKeys creates new riseupvpn TestKeys. +func NewTestKeys() *TestKeys { + return &TestKeys{ + RiseupVPNApiFailure: nil, + RiseupVPNApiStatus: "ok", + RiseupVPNCACertStatus: true, + RiseupVPNFailingGateways: nil, + } +} + +// Update updates the TestKeys using the given MultiOutput result. +func (tk *TestKeys) Update(v urlgetter.MultiOutput) { + tk.NetworkEvents = append(tk.NetworkEvents, v.TestKeys.NetworkEvents...) + tk.Queries = append(tk.Queries, v.TestKeys.Queries...) + 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.RiseupVPNApiStatus != "ok" { + return // we already flipped the state + } + if v.TestKeys.Failure != nil { + tk.RiseupVPNApiStatus = "blocked" + tk.RiseupVPNApiFailure = v.TestKeys.Failure + return + } +} + +// AddGatewayConnectTestKeys updates the TestKeys using the given MultiOutput result of gateway connectivity testing. +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.RiseupVPNFailingGateways = append(tk.RiseupVPNFailingGateways, *gatewayConnection) + } + } + return +} + +func newGatewayConnection(tcpConnect archival.TCPConnectEntry, transportType string) *gatewayConnection { + return &gatewayConnection{ + IP: tcpConnect.IP, + Port: tcpConnect.Port, + TransportType: transportType, + } +} + +// AddCACertFetchTestKeys Adding generic ctx.Get() testKeys to riseupvpn specific test keys +func (tk *TestKeys) AddCACertFetchTestKeys(testKeys urlgetter.TestKeys) { + tk.NetworkEvents = append(tk.NetworkEvents, testKeys.NetworkEvents...) + tk.Queries = append(tk.Queries, testKeys.Queries...) + 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.RiseupVPNApiStatus = "blocked" + tk.RiseupVPNApiFailure = tk.Failure + tk.RiseupVPNCACertStatus = false + } +} + +// Measurer performs the measurement +type Measurer struct { + // Config contains the experiment settings. If empty we + // will be using default settings. + Config Config + + // Getter is an optional getter to be used for testing. + Getter urlgetter.MultiGetter +} + +// ExperimentName implements ExperimentMeasurer.ExperimentName +func (m Measurer) ExperimentName() string { + return testName +} + +// ExperimentVersion implements ExperimentMeasurer.ExperimentVersion +func (m Measurer) ExperimentVersion() string { + return testVersion +} + +// Run implements ExperimentMeasurer.Run +func (m Measurer) Run(ctx context.Context, sess model.ExperimentSession, + measurement *model.Measurement, callbacks model.ExperimentCallbacks) error { + ctx, cancel := context.WithTimeout(ctx, 90*time.Second) + defer cancel() + testkeys := NewTestKeys() + measurement.TestKeys = testkeys + + caTarget := "https://black.riseup.net/ca.crt" + caGetter := urlgetter.Getter{ + Config: m.Config.Config, + Session: sess, + Target: caTarget, + } + log.Info("Getting CA cerificate; please be patient...") + tk, err := caGetter.Get(ctx) + + if err != nil { + log.Error("Getting CA cerificate failed. Aborting test.") + testkeys.AddCACertFetchTestKeys(tk) + measurement.TestKeys = testkeys + return nil + } + + ok := netx.CertPool.AppendCertsFromPEM([]byte(tk.HTTPResponseBody)) + if !ok { + testkeys.RiseupVPNCACertStatus = false + testkeys.RiseupVPNApiStatus = "blocked" + errorValue := "invalid_ca" + testkeys.RiseupVPNApiFailure = &errorValue + return nil + } + + urlgetter.RegisterExtensions(measurement) + inputs := []urlgetter.MultiInput{ + + // Here we need to provide the method explicitly. See + // https://github.com/ooni/probe-engine/issues/827. + {Target: providerURL, Config: urlgetter.Config{ + Method: "GET", + FailOnHTTPError: true, + }}, + {Target: eipServiceURL, Config: urlgetter.Config{ + Method: "GET", + FailOnHTTPError: true, + }}, + {Target: geoServiceURL, Config: urlgetter.Config{ + Method: "GET", + FailOnHTTPError: true, + }}, + } + multi := urlgetter.Multi{Begin: time.Now(), Getter: m.Getter, Session: sess} + + for entry := range multi.Collect(ctx, inputs, "riseupvpn", callbacks) { + testkeys.Update(entry) + } + + //test gateways now + gateways := parseGateways(testkeys) + openvpnEndpoints := generateMultiInputs(gateways, "openvpn") + obfs4Endpoints := generateMultiInputs(gateways, "obfs4") + + // measure openvpn in parallel + multi = urlgetter.Multi{Begin: time.Now(), Getter: m.Getter, Session: sess} + for entry := range multi.Collect(ctx, openvpnEndpoints, "riseupvpn", callbacks) { + testkeys.AddGatewayConnectTestKeys(entry, "openvpn") + } + + // measure obfs4 in parallel + multi = urlgetter.Multi{Begin: time.Now(), Getter: m.Getter, Session: sess} + for entry := range multi.Collect(ctx, obfs4Endpoints, "riseupvpn", callbacks) { + testkeys.AddGatewayConnectTestKeys(entry, "obfs4") + } + + return nil +} + +func generateMultiInputs(gateways []GatewayV3, transportType string) []urlgetter.MultiInput { + var gatewayInputs []urlgetter.MultiInput + for _, gateway := range gateways { + for _, transport := range gateway.Capabilities.Transport { + if transport.Type != transportType { + continue + } + supportsTCP := false + for _, protocol := range transport.Protocols { + if protocol == "tcp" { + supportsTCP = true + } + } + if !supportsTCP { + continue + } + for _, port := range transport.Ports { + tcpConnection := tcpConnect + gateway.IPAddress + ":" + port + gatewayInputs = append(gatewayInputs, urlgetter.MultiInput{Target: tcpConnection}) + } + } + } + return gatewayInputs +} + +func parseGateways(testKeys *TestKeys) []GatewayV3 { + for _, requestEntry := range testKeys.Requests { + if requestEntry.Request.URL == eipServiceURL && requestEntry.Failure == nil { + eipService, err := DecodeEIP3(requestEntry.Response.Body.Value) + if err == nil { + return eipService.Gateways + } + } + } + + return nil +} + +//DecodeEIP3 decodes eip-service.json version 3 +func DecodeEIP3(body string) (*EipService, error) { + var eip EipService + err := json.Unmarshal([]byte(body), &eip) + if err != nil { + log.Error(err.Error()) + } + + return &eip, err +} + +// NewExperimentMeasurer creates a new ExperimentMeasurer. +func NewExperimentMeasurer(config Config) model.ExperimentMeasurer { + return Measurer{Config: config} +} diff --git a/experiment/riseupvpn/riseupvpn_test.go b/experiment/riseupvpn/riseupvpn_test.go new file mode 100644 index 0000000000000000000000000000000000000000..5d5f1e965145394887135a56a901fcd0362ea0b6 --- /dev/null +++ b/experiment/riseupvpn/riseupvpn_test.go @@ -0,0 +1,480 @@ +package riseupvpn_test + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" + "math/rand" + "net/http" + "strconv" + "strings" + "testing" + "time" + + "github.com/apex/log" + "github.com/ooni/probe-engine/experiment/riseupvpn" + "github.com/ooni/probe-engine/experiment/urlgetter" + "github.com/ooni/probe-engine/internal/mockable" + "github.com/ooni/probe-engine/model" + "github.com/ooni/probe-engine/netx/errorx" + "github.com/ooni/probe-engine/netx/selfcensor" +) + +func TestNewExperimentMeasurer(t *testing.T) { + measurer := riseupvpn.NewExperimentMeasurer(riseupvpn.Config{}) + if measurer.ExperimentName() != "riseupvpn" { + t.Fatal("unexpected name") + } + if measurer.ExperimentVersion() != "0.0.2" { + t.Fatal("unexpected version") + } +} + +func TestIntegration(t *testing.T) { + measurer := riseupvpn.NewExperimentMeasurer(riseupvpn.Config{}) + measurement := new(model.Measurement) + err := measurer.Run( + context.Background(), + &mockable.Session{ + MockableLogger: log.Log, + }, + measurement, + model.NewPrinterCallbacks(log.Log), + ) + if err != nil { + t.Fatal(err) + } + tk := measurement.TestKeys.(*riseupvpn.TestKeys) + if tk.Agent != "" { + t.Fatal("unexpected Agent: " + tk.Agent) + } + if tk.FailedOperation != nil { + t.Fatal("unexpected FailedOperation") + } + if tk.Failure != nil { + t.Fatal("unexpected Failure") + } + if len(tk.NetworkEvents) <= 0 { + t.Fatal("no NetworkEvents?!") + } + if len(tk.Queries) <= 0 { + t.Fatal("no Queries?!") + } + if len(tk.Requests) <= 0 { + t.Fatal("no Requests?!") + } + if len(tk.TCPConnect) <= 0 { + t.Fatal("no TCPConnect?!") + } + if len(tk.TLSHandshakes) <= 0 { + t.Fatal("no TLSHandshakes?!") + } + if tk.RiseupVPNApiFailure != nil { + t.Fatal("unexpected RiseupVPNApiFailure") + } + if tk.RiseupVPNApiStatus != "ok" { + t.Fatal("unexpected RiseupvpnStatus") + } + if tk.RiseupVPNCACertStatus != true { + t.Fatal("unexpected RiseupvPNCaCertStatus") + } + if tk.RiseupVPNFailingGateways != nil { + t.Fatal("unexpected RiseupVPNFailingGateways value") + } +} + +// TestUpdateWithMixedResults tests if one operation failed +// RiseupVPNApiStatus is considered as blocked +func TestUpdateWithMixedResults(t *testing.T) { + tk := riseupvpn.NewTestKeys() + tk.Update(urlgetter.MultiOutput{ + Input: urlgetter.MultiInput{ + Config: urlgetter.Config{Method: "GET"}, + Target: "https://api.black.riseup.net:443/3/config/eip-service.json", + }, + TestKeys: urlgetter.TestKeys{ + HTTPResponseStatus: 200, + }, + }) + tk.Update(urlgetter.MultiOutput{ + Input: urlgetter.MultiInput{ + Config: urlgetter.Config{Method: "GET"}, + Target: "https://riseup.net/provider.json", + }, + TestKeys: urlgetter.TestKeys{ + FailedOperation: (func() *string { + s := errorx.HTTPRoundTripOperation + return &s + })(), + Failure: (func() *string { + s := errorx.FailureEOFError + return &s + })(), + }, + }) + tk.Update(urlgetter.MultiOutput{ + Input: urlgetter.MultiInput{ + Config: urlgetter.Config{Method: "GET"}, + Target: "https://api.black.riseup.net:9001/json", + }, + TestKeys: urlgetter.TestKeys{ + HTTPResponseStatus: 200, + }, + }) + if tk.RiseupVPNApiStatus != "blocked" { + t.Fatal("RiseupVPNApiStatus should be blocked") + } + if *tk.RiseupVPNApiFailure != errorx.FailureEOFError { + t.Fatal("invalid RiseupVPNApiFailure") + } +} + +func TestIntegrationFailureCaCertFetch(t *testing.T) { + measurer := riseupvpn.NewExperimentMeasurer(riseupvpn.Config{}) + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + sess := &mockable.Session{MockableLogger: log.Log} + measurement := new(model.Measurement) + callbacks := model.NewPrinterCallbacks(log.Log) + err := measurer.Run(ctx, sess, measurement, callbacks) + if err != nil { + t.Fatal(err) + } + tk := measurement.TestKeys.(*riseupvpn.TestKeys) + if tk.RiseupVPNCACertStatus != false { + t.Fatal("invalid RiseupVPNCACertStatus ") + } + if tk.RiseupVPNApiStatus != "blocked" { + t.Fatal("invalid RiseupVPNApiStatus") + } + + if tk.RiseupVPNApiFailure != nil { + t.Fatal("RiseupVPNApiFailure should be null") + } + if len(tk.Requests) > 1 { + t.Fatal("Unexpected requests") + } + +} + +func TestIntegrationFailureEipServiceBlocked(t *testing.T) { + measurer := riseupvpn.NewExperimentMeasurer(riseupvpn.Config{}) + ctx, cancel := context.WithCancel(context.Background()) + selfcensor.Enable(`{"PoisonSystemDNS":{"api.black.riseup.net":["NXDOMAIN"]}}`) + + sess := &mockable.Session{MockableLogger: log.Log} + measurement := new(model.Measurement) + callbacks := model.NewPrinterCallbacks(log.Log) + err := measurer.Run(ctx, sess, measurement, callbacks) + if err != nil { + t.Fatal(err) + } + tk := measurement.TestKeys.(*riseupvpn.TestKeys) + if tk.RiseupVPNCACertStatus != true { + t.Fatal("invalid RiseupVPNCACertStatus ") + } + + for _, entry := range tk.Requests { + if entry.Request.URL == "https://api.black.riseup.net:443/3/config/eip-service.json" { + if entry.Failure == nil { + t.Fatal("Failure for " + entry.Request.URL + " should not be null") + } + } + } + + if tk.RiseupVPNApiStatus != "blocked" { + t.Fatal("invalid RiseupVPNApiStatus") + } + + if tk.RiseupVPNApiFailure == nil { + t.Fatal("RiseupVPNApiFailure should not be null") + } + + cancel() +} + +func TestIntegrationFailureProviderUrlBlocked(t *testing.T) { + measurer := riseupvpn.NewExperimentMeasurer(riseupvpn.Config{}) + ctx, cancel := context.WithCancel(context.Background()) + selfcensor.Enable(`{"BlockedEndpoints":{"198.252.153.70:443":"REJECT"}}`) + + sess := &mockable.Session{MockableLogger: log.Log} + measurement := new(model.Measurement) + callbacks := model.NewPrinterCallbacks(log.Log) + err := measurer.Run(ctx, sess, measurement, callbacks) + if err != nil { + t.Fatal(err) + } + tk := measurement.TestKeys.(*riseupvpn.TestKeys) + + for _, entry := range tk.Requests { + if entry.Request.URL == "https://riseup.net/provider.json" { + if entry.Failure == nil { + t.Fatal("Failure for " + entry.Request.URL + " should not be null") + } + } + } + + if tk.RiseupVPNCACertStatus != true { + t.Fatal("invalid RiseupVPNCACertStatus ") + } + if tk.RiseupVPNApiStatus != "blocked" { + t.Fatal("invalid RiseupVPNApiStatus") + } + + if tk.RiseupVPNApiFailure == nil { + t.Fatal("RiseupVPNApiFailure should not be null") + } + cancel() +} + +func TestIntegrationFailureGeoIpServiceBlocked(t *testing.T) { + measurer := riseupvpn.NewExperimentMeasurer(riseupvpn.Config{}) + ctx, cancel := context.WithCancel(context.Background()) + selfcensor.Enable(`{"BlockedEndpoints":{"198.252.153.107:9001":"REJECT"}}`) + + sess := &mockable.Session{MockableLogger: log.Log} + measurement := new(model.Measurement) + callbacks := model.NewPrinterCallbacks(log.Log) + err := measurer.Run(ctx, sess, measurement, callbacks) + if err != nil { + t.Fatal(err) + } + tk := measurement.TestKeys.(*riseupvpn.TestKeys) + if tk.RiseupVPNCACertStatus != true { + t.Fatal("invalid RiseupVPNCACertStatus ") + } + + for _, entry := range tk.Requests { + if entry.Request.URL == "https://api.black.riseup.net:9001/json" { + if entry.Failure == nil { + t.Fatal("Failure for " + entry.Request.URL + " should not be null") + } + } + } + + if tk.RiseupVPNApiStatus != "blocked" { + t.Fatal("invalid RiseupVPNApiStatus") + } + + if tk.RiseupVPNApiFailure == nil { + t.Fatal("RiseupVPNApiFailure should not be null") + } + + cancel() +} + +func TestIntegrationFailureOpenvpnGateway(t *testing.T) { + // - fetch client cert and add to certpool + caFetchClient := &http.Client{ + Timeout: time.Second * 30, + } + + caCertResponse, err := caFetchClient.Get("https://black.riseup.net/ca.crt") + if err != nil { + t.SkipNow() + } + defer caCertResponse.Body.Close() + + var bodyString string + if caCertResponse.StatusCode == http.StatusOK { + bodyBytes, err := ioutil.ReadAll(caCertResponse.Body) + if err != nil { + t.SkipNow() + } + bodyString = string(bodyBytes) + } + + certs := x509.NewCertPool() + certs.AppendCertsFromPEM([]byte(bodyString)) + + // - fetch and parse eip-service.json + client := &http.Client{ + Timeout: time.Second * 30, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: certs, + }, + }, + } + + eipResponse, err := client.Get("https://api.black.riseup.net/3/config/eip-service.json") + if err != nil { + t.SkipNow() + } + defer eipResponse.Body.Close() + + if eipResponse.StatusCode == http.StatusOK { + bodyBytes, err := ioutil.ReadAll(eipResponse.Body) + if err != nil { + return + } + bodyString = string(bodyBytes) + } + + eipService, err := riseupvpn.DecodeEIP3(bodyString) + + // - self censor random gateway + gateways := eipService.Gateways + if gateways == nil || len(gateways) == 0 { + t.SkipNow() + } + rnd := rand.New(rand.NewSource(time.Now().UnixNano())) + min := 0 + max := len(gateways) - 1 + randomIndex := rnd.Intn(max-min+1) + min + + IP := gateways[randomIndex].IPAddress + port := gateways[randomIndex].Capabilities.Transport[0].Ports[0] + selfcensor.Enable(`{"BlockedEndpoints":{"` + IP + `:` + port + `":"REJECT"}}`) + + // - run measurement + measurer := riseupvpn.NewExperimentMeasurer(riseupvpn.Config{}) + ctx, cancel := context.WithCancel(context.Background()) + + sess := &mockable.Session{MockableLogger: log.Log} + measurement := new(model.Measurement) + callbacks := model.NewPrinterCallbacks(log.Log) + err = measurer.Run(ctx, sess, measurement, callbacks) + if err != nil { + t.Fatal(err) + } + tk := measurement.TestKeys.(*riseupvpn.TestKeys) + if tk.RiseupVPNCACertStatus != true { + t.Fatal("invalid RiseupVPNCACertStatus ") + } + + if tk.RiseupVPNFailingGateways == nil || len(tk.RiseupVPNFailingGateways) != 1 { + t.Fatal("unexpected amount of failing gateways") + } + + entry := tk.RiseupVPNFailingGateways[0] + if entry.IP != IP || fmt.Sprint(entry.Port) != port { + t.Fatal("unexpected failed gateway configuration") + } + + if tk.RiseupVPNApiStatus == "blocked" { + t.Fatal("invalid RiseupVPNApiStatus") + } + + if tk.RiseupVPNApiFailure != nil { + t.Fatal("RiseupVPNApiFailure should be null") + } + + cancel() +} + +func TestIntegrationFailureObfs4Gateway(t *testing.T) { + // - fetch client cert and add to certpool + caFetchClient := &http.Client{ + Timeout: time.Second * 30, + } + + caCertResponse, err := caFetchClient.Get("https://black.riseup.net/ca.crt") + if err != nil { + t.SkipNow() + } + defer caCertResponse.Body.Close() + + var bodyString string + if caCertResponse.StatusCode == http.StatusOK { + bodyBytes, err := ioutil.ReadAll(caCertResponse.Body) + if err != nil { + t.SkipNow() + } + bodyString = string(bodyBytes) + } + + certs := x509.NewCertPool() + certs.AppendCertsFromPEM([]byte(bodyString)) + + // - fetch and parse eip-service.json + client := &http.Client{ + Timeout: time.Second * 30, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: certs, + }, + }, + } + + eipResponse, err := client.Get("https://api.black.riseup.net/3/config/eip-service.json") + if err != nil { + t.SkipNow() + } + defer eipResponse.Body.Close() + + if eipResponse.StatusCode == http.StatusOK { + bodyBytes, err := ioutil.ReadAll(eipResponse.Body) + if err != nil { + return + } + bodyString = string(bodyBytes) + } + + eipService, err := riseupvpn.DecodeEIP3(bodyString) + + // - self censor random gateway + gateways := eipService.Gateways + if gateways == nil || len(gateways) == 0 { + t.SkipNow() + } + + var selfcensoredGateways []string + for _, gateway := range gateways { + for _, transport := range gateway.Capabilities.Transport { + if transport.Type == "obfs4" { + selfcensoredGateways = append(selfcensoredGateways, `{"BlockedEndpoints":{"`+gateway.IPAddress+`:`+transport.Ports[0]+`":"REJECT"}}`) + } + } + } + + if len(selfcensoredGateways) == 0 { + t.SkipNow() + } + + rnd := rand.New(rand.NewSource(time.Now().UnixNano())) + min := 0 + max := len(selfcensoredGateways) - 1 + randomIndex := rnd.Intn(max-min+1) + min + + selfcensor.Enable(selfcensoredGateways[randomIndex]) + + // - run measurement + measurer := riseupvpn.NewExperimentMeasurer(riseupvpn.Config{}) + ctx, cancel := context.WithCancel(context.Background()) + + sess := &mockable.Session{MockableLogger: log.Log} + measurement := new(model.Measurement) + callbacks := model.NewPrinterCallbacks(log.Log) + err = measurer.Run(ctx, sess, measurement, callbacks) + if err != nil { + t.Fatal(err) + } + tk := measurement.TestKeys.(*riseupvpn.TestKeys) + if tk.RiseupVPNCACertStatus != true { + t.Fatal("invalid RiseupVPNCACertStatus ") + } + + if tk.RiseupVPNFailingGateways == nil || len(tk.RiseupVPNFailingGateways) != 1 { + t.Fatal("unexpected amount of failing gateways") + } + + entry := tk.RiseupVPNFailingGateways[0] + if !strings.Contains(selfcensoredGateways[randomIndex], entry.IP) || !strings.Contains(selfcensoredGateways[randomIndex], strconv.Itoa(entry.Port)) { + t.Fatal("unexpected failed gateway configuration") + } + + if tk.RiseupVPNApiStatus == "blocked" { + t.Fatal("invalid RiseupVPNApiStatus") + } + + if tk.RiseupVPNApiFailure != nil { + t.Fatal("RiseupVPNApiFailure should be null") + } + + cancel() +}