riseupvpn: reduce false positives (#233)

* fetch RiseupVPN CA cert with MultiGetter. It allows us to write better tests and ensures this test step is added in the logs

* Implement TransportStatus for RiseupVPN tests. It indicates if a whole transport is blocked, which is considered as a test anomaly

* Redesign unit tests for RiseupVPN. Instead of a real backend, mocked server responses are used. Tests for invalid CA certs and for TransportStatus are added.

* Update internal/engine/experiment/riseupvpn/riseupvpn.go

Co-authored-by: Simone Basso <bassosimone@gmail.com>
This commit is contained in:
cyBerta 2021-03-30 12:02:51 +02:00 committed by GitHub
parent dae02ce5b6
commit 991b0a6120
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 589 additions and 264 deletions

View File

@ -9,7 +9,6 @@ import (
"errors"
"time"
"github.com/apex/log"
"github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter"
"github.com/ooni/probe-cli/v3/internal/engine/model"
"github.com/ooni/probe-cli/v3/internal/engine/netx"
@ -18,7 +17,7 @@ import (
const (
testName = "riseupvpn"
testVersion = "0.1.0"
testVersion = "0.2.0"
eipServiceURL = "https://api.black.riseup.net:443/3/config/eip-service.json"
providerURL = "https://riseup.net/provider.json"
geoServiceURL = "https://api.black.riseup.net:9001/json"
@ -66,6 +65,7 @@ type TestKeys struct {
APIStatus string `json:"api_status"`
CACertStatus bool `json:"ca_cert_status"`
FailingGateways []GatewayConnection `json:"failing_gateways"`
TransportStatus map[string]string `json:"transport_status"`
}
// NewTestKeys creates new riseupvpn TestKeys.
@ -75,6 +75,7 @@ func NewTestKeys() *TestKeys {
APIStatus: "ok",
CACertStatus: true,
FailingGateways: nil,
TransportStatus: nil,
}
}
@ -96,6 +97,7 @@ func (tk *TestKeys) UpdateProviderAPITestKeys(v urlgetter.MultiOutput) {
}
// AddGatewayConnectTestKeys updates the TestKeys using the given MultiOutput result of gateway connectivity testing.
// Sets TransportStatus to "ok" if any successful TCP connection could be made
func (tk *TestKeys) AddGatewayConnectTestKeys(v urlgetter.MultiOutput, transportType string) {
tk.NetworkEvents = append(tk.NetworkEvents, v.TestKeys.NetworkEvents...)
tk.TCPConnect = append(tk.TCPConnect, v.TestKeys.TCPConnect...)
@ -108,6 +110,29 @@ func (tk *TestKeys) AddGatewayConnectTestKeys(v urlgetter.MultiOutput, transport
return
}
func (tk *TestKeys) updateTransportStatus(openvpnGatewayCount int, obfs4GatewayCount int) {
failingOpenvpnGateways, failingObfs4Gateways := 0, 0
for _, gw := range tk.FailingGateways {
if gw.TransportType == "openvpn" {
failingOpenvpnGateways++
} else if gw.TransportType == "obfs4" {
failingObfs4Gateways++
}
}
if failingOpenvpnGateways < openvpnGatewayCount {
tk.TransportStatus["openvpn"] = "ok"
} else {
tk.TransportStatus["openvpn"] = "blocked"
}
if failingObfs4Gateways < obfs4GatewayCount {
tk.TransportStatus["obfs4"] = "ok"
} else {
tk.TransportStatus["obfs4"] = "blocked"
}
}
func newGatewayConnection(tcpConnect archival.TCPConnectEntry, transportType string) *GatewayConnection {
return &GatewayConnection{
IP: tcpConnect.IP,
@ -160,30 +185,32 @@ func (m Measurer) Run(ctx context.Context, sess model.ExperimentSession,
urlgetter.RegisterExtensions(measurement)
caTarget := "https://black.riseup.net/ca.crt"
caGetter := urlgetter.Getter{
Config: m.Config.Config,
Session: sess,
Target: caTarget,
}
log.Info("Getting CA certificate; please be patient...")
tk, err := caGetter.Get(ctx)
testkeys.AddCACertFetchTestKeys(tk)
if err != nil {
log.Error("Getting CA certificate failed. Aborting test.")
return nil
}
certPool := netx.NewDefaultCertPool()
if ok := certPool.AppendCertsFromPEM([]byte(tk.HTTPResponseBody)); !ok {
testkeys.CACertStatus = false
testkeys.APIStatus = "blocked"
errorValue := "invalid_ca"
testkeys.APIFailure = &errorValue
return nil
multi := urlgetter.Multi{Begin: measurement.MeasurementStartTimeSaved, Getter: m.Getter, Session: sess}
inputs := []urlgetter.MultiInput{
{Target: caTarget, Config: urlgetter.Config{
Method: "GET",
FailOnHTTPError: true,
}},
}
for entry := range multi.CollectOverall(ctx, inputs, 0, 50, "riseupvpn", callbacks) {
tk := entry.TestKeys
testkeys.AddCACertFetchTestKeys(tk)
if tk.Failure != nil {
return nil
}
if ok := certPool.AppendCertsFromPEM([]byte(tk.HTTPResponseBody)); !ok {
testkeys.CACertStatus = false
testkeys.APIStatus = "blocked"
errorValue := "invalid_ca"
testkeys.APIFailure = &errorValue
return nil
}
}
inputs := []urlgetter.MultiInput{
inputs = []urlgetter.MultiInput{
// Here we need to provide the method explicitly. See
// https://github.com/ooni/probe-engine/issues/827.
@ -203,30 +230,34 @@ func (m Measurer) Run(ctx context.Context, sess model.ExperimentSession,
FailOnHTTPError: true,
}},
}
multi := urlgetter.Multi{Begin: measurement.MeasurementStartTimeSaved, Getter: m.Getter, Session: sess}
multi = urlgetter.Multi{Begin: measurement.MeasurementStartTimeSaved, Getter: m.Getter, Session: sess}
for entry := range multi.CollectOverall(ctx, inputs, 0, 50, "riseupvpn", callbacks) {
for entry := range multi.CollectOverall(ctx, inputs, 1, 50, "riseupvpn", callbacks) {
testkeys.UpdateProviderAPITestKeys(entry)
}
// test gateways now
testkeys.TransportStatus = map[string]string{}
gateways := parseGateways(testkeys)
openvpnEndpoints := generateMultiInputs(gateways, "openvpn")
obfs4Endpoints := generateMultiInputs(gateways, "obfs4")
overallCount := len(inputs) + len(openvpnEndpoints) + len(obfs4Endpoints)
overallCount := 1 + len(inputs) + len(openvpnEndpoints) + len(obfs4Endpoints)
// measure openvpn in parallel
multi = urlgetter.Multi{Begin: measurement.MeasurementStartTimeSaved, Getter: m.Getter, Session: sess}
for entry := range multi.CollectOverall(ctx, openvpnEndpoints, len(inputs), overallCount, "riseupvpn", callbacks) {
for entry := range multi.CollectOverall(ctx, openvpnEndpoints, 1+len(inputs), overallCount, "riseupvpn", callbacks) {
testkeys.AddGatewayConnectTestKeys(entry, "openvpn")
}
// measure obfs4 in parallel
multi = urlgetter.Multi{Begin: measurement.MeasurementStartTimeSaved, Getter: m.Getter, Session: sess}
for entry := range multi.CollectOverall(ctx, obfs4Endpoints, len(inputs)+len(openvpnEndpoints), overallCount, "riseupvpn", callbacks) {
for entry := range multi.CollectOverall(ctx, obfs4Endpoints, 1+len(inputs)+len(openvpnEndpoints), overallCount, "riseupvpn", callbacks) {
testkeys.AddGatewayConnectTestKeys(entry, "obfs4")
}
// set transport status based on gateway test results
testkeys.updateTransportStatus(len(openvpnEndpoints), len(obfs4Endpoints))
return nil
}
@ -287,10 +318,11 @@ func NewExperimentMeasurer(config Config) model.ExperimentMeasurer {
// Note that this structure is part of the ABI contract with probe-cli
// therefore we should be careful when changing it.
type SummaryKeys struct {
APIBlocked bool `json:"api_blocked"`
ValidCACert bool `json:"valid_ca_cert"`
FailingGateways int `json:"failing_gateways"`
IsAnomaly bool `json:"-"`
APIBlocked bool `json:"api_blocked"`
ValidCACert bool `json:"valid_ca_cert"`
FailingGateways int `json:"failing_gateways"`
TransportStatus map[string]string `json:"transport_status"`
IsAnomaly bool `json:"-"`
}
// GetSummaryKeys implements model.ExperimentMeasurer.GetSummaryKeys.
@ -303,7 +335,8 @@ func (m Measurer) GetSummaryKeys(measurement *model.Measurement) (interface{}, e
sk.APIBlocked = tk.APIStatus != "ok"
sk.ValidCACert = tk.CACertStatus
sk.FailingGateways = len(tk.FailingGateways)
sk.TransportStatus = tk.TransportStatus
sk.IsAnomaly = (sk.APIBlocked == true || tk.CACertStatus == false ||
sk.FailingGateways != 0)
tk.TransportStatus["openvpn"] == "blocked" || tk.TransportStatus["obfs4"] == "blocked")
return sk, nil
}

View File

@ -2,15 +2,14 @@ package riseupvpn_test
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"encoding/json"
"fmt"
"io/ioutil"
"math/rand"
"net/http"
"io"
"strconv"
"strings"
"testing"
"time"
"github.com/ooni/probe-cli/v3/internal/engine/netx/archival"
"github.com/apex/log"
"github.com/google/go-cmp/cmp"
@ -19,36 +18,204 @@ import (
"github.com/ooni/probe-cli/v3/internal/engine/internal/mockable"
"github.com/ooni/probe-cli/v3/internal/engine/model"
"github.com/ooni/probe-cli/v3/internal/engine/netx/errorx"
"github.com/ooni/probe-cli/v3/internal/engine/netx/selfcensor"
)
const (
provider = `{
"api_uri": "https://api.black.riseup.net:443",
"api_version": "3",
"ca_cert_fingerprint": "SHA256: a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494",
"ca_cert_uri": "https://black.riseup.net/ca.crt",
"default_language": "en",
"description": {
"en": "Riseup is a non-profit collective in Seattle that provides online communication tools for people and groups working toward liberatory social change."
},
"domain": "riseup.net",
"enrollment_policy": "closed",
"languages": [
"en"
],
"name": {
"en": "Riseup Networks"
},
"service": {
"allow_anonymous": true,
"allow_free": true,
"allow_limited_bandwidth": false,
"allow_paid": false,
"allow_registration": false,
"allow_unlimited_bandwidth": true,
"bandwidth_limit": 102400,
"default_service_level": 1,
"levels": {
"1": {
"description": "Please donate.",
"name": "free"
}
}
},
"services": [
"openvpn"
]
}`
eipservice = `{
"gateways": [
{
"capabilities": {
"adblock": false,
"filter_dns": false,
"limited": false,
"transport":[
{
"type":"openvpn",
"protocols":[
"tcp"
],
"ports":[
"443"
]
}
],
"user_ips": false
},
"host": "test1.riseup.net",
"ip_address": "123.456.123.456",
"location": "paris"
},
{
"capabilities": {
"adblock": false,
"filter_dns": false,
"limited": false,
"transport":[
{
"type":"obfs4",
"protocols":[
"tcp"
],
"ports":[
"23042"
],
"options": {
"cert": "XXXXXXXXXXXXXXXXXXXXXXXXX",
"iatMode": "0"
}
},
{
"type":"openvpn",
"protocols":[
"tcp"
],
"ports":[
"443"
]
}
],
"user_ips": false
},
"host": "test2.riseup.net",
"ip_address": "234.345.234.345",
"location": "seattle"
}
],
"locations": {
"paris": {
"country_code": "FR",
"hemisphere": "N",
"name": "Paris",
"timezone": "+2"
},
"seattle": {
"country_code": "US",
"hemisphere": "N",
"name": "Seattle",
"timezone": "-7"
}
},
"openvpn_configuration": {
"auth": "SHA1",
"cipher": "AES-128-CBC",
"keepalive": "10 30",
"tls-cipher": "DHE-RSA-AES128-SHA",
"tun-ipv6": true
},
"serial": 3,
"version": 3
}`
geoservice = `{"ip":"51.15.0.88","cc":"NL","city":"Haarlem","lat":52.381,"lon":4.6275,"gateways":["test1.riseup.net","test2.riseup.net"]}`
cacert = `-----BEGIN CERTIFICATE-----
MIIFjTCCA3WgAwIBAgIBATANBgkqhkiG9w0BAQ0FADBZMRgwFgYDVQQKDA9SaXNl
dXAgTmV0d29ya3MxGzAZBgNVBAsMEmh0dHBzOi8vcmlzZXVwLm5ldDEgMB4GA1UE
AwwXUmlzZXVwIE5ldHdvcmtzIFJvb3QgQ0EwHhcNMTQwNDI4MDAwMDAwWhcNMjQw
NDI4MDAwMDAwWjBZMRgwFgYDVQQKDA9SaXNldXAgTmV0d29ya3MxGzAZBgNVBAsM
Emh0dHBzOi8vcmlzZXVwLm5ldDEgMB4GA1UEAwwXUmlzZXVwIE5ldHdvcmtzIFJv
b3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC76J4ciMJ8Sg0m
TP7DF2DT9zNe0Csk4myoMFC57rfJeqsAlJCv1XMzBmXrw8wq/9z7XHv6n/0sWU7a
7cF2hLR33ktjwODlx7vorU39/lXLndo492ZBhXQtG1INMShyv+nlmzO6GT7ESfNE
LliFitEzwIegpMqxCIHXFuobGSCWF4N0qLHkq/SYUMoOJ96O3hmPSl1kFDRMtWXY
iw1SEKjUvpyDJpVs3NGxeLCaA7bAWhDY5s5Yb2fA1o8ICAqhowurowJpW7n5ZuLK
5VNTlNy6nZpkjt1QycYvNycffyPOFm/Q/RKDlvnorJIrihPkyniV3YY5cGgP+Qkx
HUOT0uLA6LHtzfiyaOqkXwc4b0ZcQD5Vbf6Prd20Ppt6ei0zazkUPwxld3hgyw58
m/4UIjG3PInWTNf293GngK2Bnz8Qx9e/6TueMSAn/3JBLem56E0WtmbLVjvko+LF
PM5xA+m0BmuSJtrD1MUCXMhqYTtiOvgLBlUm5zkNxALzG+cXB28k6XikXt6MRG7q
hzIPG38zwkooM55yy5i1YfcIi5NjMH6A+t4IJxxwb67MSb6UFOwg5kFokdONZcwj
shczHdG9gLKSBIvrKa03Nd3W2dF9hMbRu//STcQxOailDBQCnXXfAATj9pYzdY4k
ha8VCAREGAKTDAex9oXf1yRuktES4QIDAQABo2AwXjAdBgNVHQ4EFgQUC4tdmLVu
f9hwfK4AGliaet5KkcgwDgYDVR0PAQH/BAQDAgIEMAwGA1UdEwQFMAMBAf8wHwYD
VR0jBBgwFoAUC4tdmLVuf9hwfK4AGliaet5KkcgwDQYJKoZIhvcNAQENBQADggIB
AGzL+GRnYu99zFoy0bXJKOGCF5XUXP/3gIXPRDqQf5g7Cu/jYMID9dB3No4Zmf7v
qHjiSXiS8jx1j/6/Luk6PpFbT7QYm4QLs1f4BlfZOti2KE8r7KRDPIecUsUXW6P/
3GJAVYH/+7OjA39za9AieM7+H5BELGccGrM5wfl7JeEz8in+V2ZWDzHQO4hMkiTQ
4ZckuaL201F68YpiItBNnJ9N5nHr1MRiGyApHmLXY/wvlrOpclh95qn+lG6/2jk7
3AmihLOKYMlPwPakJg4PYczm3icFLgTpjV5sq2md9bRyAg3oPGfAuWHmKj2Ikqch
Td5CHKGxEEWbGUWEMP0s1A/JHWiCbDigc4Cfxhy56CWG4q0tYtnc2GMw8OAUO6Wf
Xu5pYKNkzKSEtT/MrNJt44tTZWbKV/Pi/N2Fx36my7TgTUj7g3xcE9eF4JV2H/sg
tsK3pwE0FEqGnT4qMFbixQmc8bGyuakr23wjMvfO7eZUxBuWYR2SkcP26sozF9PF
tGhbZHQVGZUTVPyvwahMUEhbPGVerOW0IYpxkm0x/eaWdTc4vPpf/rIlgbAjarnJ
UN9SaWRlWKSdP4haujnzCoJbM7dU9bjvlGZNyXEekgeT0W2qFeGGp+yyUWw8tNsp
0BuC1b7uW/bBn/xKm319wXVDvBgZgcktMolak39V7DVO
-----END CERTIFICATE-----`
eipserviceurl = "https://api.black.riseup.net:443/3/config/eip-service.json"
providerurl = "https://riseup.net/provider.json"
geoserviceurl = "https://api.black.riseup.net:9001/json"
cacerturl = "https://black.riseup.net/ca.crt"
openvpnurl1 = "tcpconnect://234.345.234.345:443"
openvpnurl2 = "tcpconnect://123.456.123.456:443"
obfs4url1 = "tcpconnect://234.345.234.345:23042"
)
var RequestResponse = map[string]string{
eipserviceurl: eipservice,
providerurl: provider,
geoserviceurl: geoservice,
cacerturl: cacert,
openvpnurl1: "",
openvpnurl2: "",
obfs4url1: "",
}
func TestNewExperimentMeasurer(t *testing.T) {
measurer := riseupvpn.NewExperimentMeasurer(riseupvpn.Config{})
if measurer.ExperimentName() != "riseupvpn" {
t.Fatal("unexpected name")
}
if measurer.ExperimentVersion() != "0.1.0" {
if measurer.ExperimentVersion() != "0.2.0" {
t.Fatal("unexpected version")
}
}
func TestGood(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
measurer := riseupvpn.NewExperimentMeasurer(riseupvpn.Config{})
measurement := new(model.Measurement)
err := measurer.Run(
context.Background(),
&mockable.Session{
MockableLogger: log.Log,
},
measurement,
model.NewPrinterCallbacks(log.Log),
)
if err != nil {
t.Fatal(err)
}
measurement := runDefaultMockTest(t, generateDefaultMockGetter(map[string]bool{
cacerturl: true,
eipserviceurl: true,
providerurl: true,
geoserviceurl: true,
openvpnurl1: true,
openvpnurl2: true,
obfs4url1: true,
}))
tk := measurement.TestKeys.(*riseupvpn.TestKeys)
if tk.Agent != "" {
t.Fatal("unexpected Agent: " + tk.Agent)
@ -59,21 +226,6 @@ func TestGood(t *testing.T) {
if tk.Failure != nil {
t.Fatal("unexpected Failure")
}
if len(tk.NetworkEvents) <= 0 {
t.Fatal("no NetworkEvents?!")
}
if len(tk.Queries) <= 0 {
t.Fatal("no Queries?!")
}
if len(tk.Requests) <= 0 {
t.Fatal("no Requests?!")
}
if len(tk.TCPConnect) <= 0 {
t.Fatal("no TCPConnect?!")
}
if len(tk.TLSHandshakes) <= 0 {
t.Fatal("no TLSHandshakes?!")
}
if tk.APIFailure != nil {
t.Fatal("unexpected ApiFailure")
}
@ -86,6 +238,12 @@ func TestGood(t *testing.T) {
if tk.FailingGateways != nil {
t.Fatal("unexpected FailingGateways value")
}
if tk.TransportStatus == nil {
t.Fatal("unexpected nil TransportStatus struct ")
}
if tk.TransportStatus["openvpn"] != "ok" {
t.Fatal("unexpected openvpn transport status")
}
}
// TestUpdateWithMixedResults tests if one operation failed
@ -132,13 +290,39 @@ func TestUpdateWithMixedResults(t *testing.T) {
if *tk.APIFailure != errorx.FailureEOFError {
t.Fatal("invalid ApiFailure")
}
if tk.FailingGateways != nil {
t.Fatal("invalid FailingGateways")
}
if tk.TransportStatus != nil {
t.Fatal("invalid TransportStatus")
}
}
func TestFailureCaCertFetch(t *testing.T) {
measurer := riseupvpn.NewExperimentMeasurer(riseupvpn.Config{})
func TestInvalidCaCert(t *testing.T) {
requestResponseMap := map[string]string{
eipserviceurl: eipservice,
providerurl: provider,
geoserviceurl: geoservice,
cacerturl: "invalid",
openvpnurl1: "",
openvpnurl2: "",
obfs4url1: "",
}
measurer := riseupvpn.Measurer{
Config: riseupvpn.Config{},
Getter: generateMockGetter(requestResponseMap, map[string]bool{
cacerturl: true,
eipserviceurl: true,
providerurl: true,
geoserviceurl: true,
openvpnurl1: false,
openvpnurl2: true,
obfs4url1: true,
}),
}
ctx, cancel := context.WithCancel(context.Background())
// we're cancelling immediately so that the CA Cert fetch fails
cancel()
defer cancel()
sess := &mockable.Session{MockableLogger: log.Log}
measurement := new(model.Measurement)
@ -147,6 +331,26 @@ func TestFailureCaCertFetch(t *testing.T) {
if err != nil {
t.Fatal(err)
}
tk := measurement.TestKeys.(*riseupvpn.TestKeys)
if tk.CACertStatus == true {
t.Fatal("unexpected CaCertStatus")
}
if tk.APIStatus != "blocked" {
t.Fatal("ApiStatus should be blocked")
}
}
func TestFailureCaCertFetch(t *testing.T) {
measurement := runDefaultMockTest(t, generateDefaultMockGetter(map[string]bool{
cacerturl: false,
eipserviceurl: true,
providerurl: true,
geoserviceurl: true,
openvpnurl1: true,
openvpnurl2: true,
obfs4url1: true,
}))
tk := measurement.TestKeys.(*riseupvpn.TestKeys)
if tk.CACertStatus != false {
t.Fatal("invalid CACertStatus ")
@ -164,21 +368,15 @@ func TestFailureCaCertFetch(t *testing.T) {
}
func TestFailureEipServiceBlocked(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
measurer := riseupvpn.NewExperimentMeasurer(riseupvpn.Config{})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
selfcensor.Enable(`{"PoisonSystemDNS":{"api.black.riseup.net":["NXDOMAIN"]}}`)
sess := &mockable.Session{MockableLogger: log.Log}
measurement := new(model.Measurement)
callbacks := model.NewPrinterCallbacks(log.Log)
err := measurer.Run(ctx, sess, measurement, callbacks)
if err != nil {
t.Fatal(err)
}
measurement := runDefaultMockTest(t, generateDefaultMockGetter(map[string]bool{
cacerturl: true,
eipserviceurl: false,
providerurl: true,
geoserviceurl: true,
openvpnurl1: true,
openvpnurl2: true,
obfs4url1: true,
}))
tk := measurement.TestKeys.(*riseupvpn.TestKeys)
if tk.CACertStatus != true {
t.Fatal("invalid CACertStatus ")
@ -202,21 +400,15 @@ func TestFailureEipServiceBlocked(t *testing.T) {
}
func TestFailureProviderUrlBlocked(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
measurer := riseupvpn.NewExperimentMeasurer(riseupvpn.Config{})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
selfcensor.Enable(`{"BlockedEndpoints":{"198.252.153.70:443":"REJECT"}}`)
sess := &mockable.Session{MockableLogger: log.Log}
measurement := new(model.Measurement)
callbacks := model.NewPrinterCallbacks(log.Log)
err := measurer.Run(ctx, sess, measurement, callbacks)
if err != nil {
t.Fatal(err)
}
measurement := runDefaultMockTest(t, generateDefaultMockGetter(map[string]bool{
cacerturl: true,
eipserviceurl: true,
providerurl: false,
geoserviceurl: true,
openvpnurl1: true,
openvpnurl2: true,
obfs4url1: true,
}))
tk := measurement.TestKeys.(*riseupvpn.TestKeys)
for _, entry := range tk.Requests {
@ -240,21 +432,15 @@ func TestFailureProviderUrlBlocked(t *testing.T) {
}
func TestFailureGeoIpServiceBlocked(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
measurer := riseupvpn.NewExperimentMeasurer(riseupvpn.Config{})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
selfcensor.Enable(`{"BlockedEndpoints":{"198.252.153.107:9001":"REJECT"}}`)
sess := &mockable.Session{MockableLogger: log.Log}
measurement := new(model.Measurement)
callbacks := model.NewPrinterCallbacks(log.Log)
err := measurer.Run(ctx, sess, measurement, callbacks)
if err != nil {
t.Fatal(err)
}
measurement := runDefaultMockTest(t, generateDefaultMockGetter(map[string]bool{
cacerturl: true,
eipserviceurl: true,
providerurl: true,
geoserviceurl: false,
openvpnurl1: true,
openvpnurl2: true,
obfs4url1: true,
}))
tk := measurement.TestKeys.(*riseupvpn.TestKeys)
if tk.CACertStatus != true {
t.Fatal("invalid CACertStatus ")
@ -277,138 +463,16 @@ func TestFailureGeoIpServiceBlocked(t *testing.T) {
}
}
func TestFailureGateway(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
var testCases = [...]string{"openvpn", "obfs4"}
eipService, err := fetchEipService()
if err != nil {
t.Log("Preconditions for the test are not met. Skipping due to: " + err.Error())
t.SkipNow()
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("testing censored transport %s", tc), func(t *testing.T) {
censoredGateway, err := selfCensorRandomGateway(eipService, tc)
if err == nil {
censorString := `{"BlockedEndpoints":{"` + censoredGateway.IP + `:` + censoredGateway.Port + `":"REJECT"}}`
selfcensor.Enable(censorString)
} else {
t.Log("Preconditions for the test are not met. Skipping due to: " + err.Error())
t.SkipNow()
}
// - run measurement
runGatewayTest(t, censoredGateway)
})
}
}
type SelfCensoredGateway struct {
IP string
Port string
}
func fetchEipService() (*riseupvpn.EipService, error) {
// - fetch client cert and add to certpool
caFetchClient := &http.Client{
Timeout: time.Second * 30,
}
caCertResponse, err := caFetchClient.Get("https://black.riseup.net/ca.crt")
if err != nil {
return nil, err
}
var bodyString string
if caCertResponse.StatusCode != http.StatusOK {
return nil, errors.New("unexpected HTTP response code")
}
bodyBytes, err := ioutil.ReadAll(caCertResponse.Body)
defer caCertResponse.Body.Close()
if err != nil {
return nil, err
}
bodyString = string(bodyBytes)
certs := x509.NewCertPool()
certs.AppendCertsFromPEM([]byte(bodyString))
// - fetch and parse eip-service.json
client := &http.Client{
Timeout: time.Second * 30,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: certs,
},
},
}
eipResponse, err := client.Get("https://api.black.riseup.net/3/config/eip-service.json")
if err != nil {
return nil, err
}
if eipResponse.StatusCode != http.StatusOK {
return nil, errors.New("Unexpected HTTP response code")
}
bodyBytes, err = ioutil.ReadAll(eipResponse.Body)
defer eipResponse.Body.Close()
if err != nil {
return nil, err
}
bodyString = string(bodyBytes)
eipService, err := riseupvpn.DecodeEIP3(bodyString)
if err != nil {
return nil, err
}
return eipService, nil
}
func selfCensorRandomGateway(eipService *riseupvpn.EipService, transportType string) (*SelfCensoredGateway, error) {
// - self censor random gateway
gateways := eipService.Gateways
if gateways == nil || len(gateways) == 0 {
return nil, errors.New("No gateways found")
}
var selfcensoredGateways []SelfCensoredGateway
for _, gateway := range gateways {
for _, transport := range gateway.Capabilities.Transport {
if transport.Type == transportType {
selfcensoredGateways = append(selfcensoredGateways, SelfCensoredGateway{IP: gateway.IPAddress, Port: transport.Ports[0]})
}
}
}
if len(selfcensoredGateways) == 0 {
return nil, errors.New("transport " + transportType + " doesn't seem to be supported.")
}
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
min := 0
max := len(selfcensoredGateways) - 1
randomIndex := rnd.Intn(max-min+1) + min
return &selfcensoredGateways[randomIndex], nil
}
func runGatewayTest(t *testing.T, censoredGateway *SelfCensoredGateway) {
measurer := riseupvpn.NewExperimentMeasurer(riseupvpn.Config{})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sess := &mockable.Session{MockableLogger: log.Log}
measurement := new(model.Measurement)
callbacks := model.NewPrinterCallbacks(log.Log)
err := measurer.Run(ctx, sess, measurement, callbacks)
if err != nil {
t.Fatal(err)
}
func TestFailureGateway1(t *testing.T) {
measurement := runDefaultMockTest(t, generateDefaultMockGetter(map[string]bool{
cacerturl: true,
eipserviceurl: true,
providerurl: true,
geoserviceurl: true,
openvpnurl1: false,
openvpnurl2: true,
obfs4url1: true,
}))
tk := measurement.TestKeys.(*riseupvpn.TestKeys)
if tk.CACertStatus != true {
t.Fatal("invalid CACertStatus ")
@ -418,18 +482,122 @@ func runGatewayTest(t *testing.T, censoredGateway *SelfCensoredGateway) {
t.Fatal("unexpected amount of failing gateways")
}
entry := tk.FailingGateways[0]
if entry.IP != censoredGateway.IP || fmt.Sprint(entry.Port) != censoredGateway.Port {
t.Fatal("unexpected failed gateway configuration")
gw := tk.FailingGateways[0]
if gw.IP != "234.345.234.345" {
t.Fatal("invalid failed gateway ip: " + fmt.Sprint(gw.IP))
}
if gw.Port != 443 {
t.Fatal("invalid failed gateway port: " + fmt.Sprint(gw.Port))
}
if gw.TransportType != "openvpn" {
t.Fatal("invalid failed transport type: " + fmt.Sprint(gw.TransportType))
}
if tk.APIStatus == "blocked" {
t.Fatal("invalid ApiStatus", tk.APIStatus)
t.Fatal("invalid ApiStatus")
}
if tk.APIFailure != nil {
t.Fatal("ApiFailure should be null")
}
if tk.TransportStatus == nil || tk.TransportStatus["openvpn"] == "blocked" {
t.Fatal("invalid TransportStatus: " + fmt.Sprint(tk.TransportStatus))
}
if tk.TransportStatus == nil || tk.TransportStatus["obfs4"] == "blocked" {
t.Fatal("invalid TransportStatus: " + fmt.Sprint(tk.TransportStatus))
}
}
func TestFailureTransport(t *testing.T) {
measurement := runDefaultMockTest(t, generateDefaultMockGetter(map[string]bool{
cacerturl: true,
eipserviceurl: true,
providerurl: true,
geoserviceurl: true,
openvpnurl1: false,
openvpnurl2: false,
obfs4url1: false,
}))
tk := measurement.TestKeys.(*riseupvpn.TestKeys)
if tk.TransportStatus == nil || tk.TransportStatus["openvpn"] != "blocked" {
t.Fatal("invalid TransportStatus: " + fmt.Sprint(tk.TransportStatus))
}
if tk.TransportStatus == nil || tk.TransportStatus["obfs4"] != "blocked" {
t.Fatal("invalid TransportStatus: " + fmt.Sprint(tk.TransportStatus))
}
}
func TestMissingTransport(t *testing.T) {
eipService, err := riseupvpn.DecodeEIP3(eipservice)
if err != nil {
t.Fatal("Preconditions for the test are not met.")
}
//remove obfs4 capability from 2. gateway so that our
//mock provider supports only openvpn
index := -1
transports := eipService.Gateways[1].Capabilities.Transport
for i, transport := range transports {
if transport.Type == "obfs4" {
index = i
break
}
}
if index == -1 {
t.Fatal("Preconditions for the test are not met. Default eipservice string should contain obfs4 transport.")
}
transports[index] = transports[len(transports)-1]
transports = transports[:len(transports)-1]
eipService.Gateways[1].Capabilities.Transport = transports
eipservicejson, err := json.Marshal(eipservice)
requestResponseMap := map[string]string{
eipserviceurl: string(eipservicejson),
providerurl: provider,
geoserviceurl: geoservice,
cacerturl: cacert,
openvpnurl1: "",
openvpnurl2: "",
obfs4url1: "",
}
measurer := riseupvpn.Measurer{
Config: riseupvpn.Config{},
Getter: generateMockGetter(requestResponseMap, map[string]bool{
cacerturl: true,
eipserviceurl: true,
providerurl: true,
geoserviceurl: true,
openvpnurl1: true,
openvpnurl2: true,
obfs4url1: false,
}),
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sess := &mockable.Session{MockableLogger: log.Log}
measurement := new(model.Measurement)
callbacks := model.NewPrinterCallbacks(log.Log)
err = measurer.Run(ctx, sess, measurement, callbacks)
if err != nil {
t.Fatal(err)
}
tk := measurement.TestKeys.(*riseupvpn.TestKeys)
if tk.TransportStatus == nil || tk.TransportStatus["openvpn"] != "blocked" {
t.Fatal("invalid TransportStatus: " + fmt.Sprint(tk.TransportStatus))
}
if _, found := tk.TransportStatus["obfs"]; found {
t.Fatal("invalid TransportStatus: " + fmt.Sprint(tk.TransportStatus))
}
}
func TestSummaryKeysInvalidType(t *testing.T) {
@ -450,21 +618,27 @@ func TestSummaryKeysWorksAsIntended(t *testing.T) {
APIStatus: "blocked",
CACertStatus: true,
FailingGateways: nil,
TransportStatus: nil,
},
sk: riseupvpn.SummaryKeys{
APIBlocked: true,
ValidCACert: true,
IsAnomaly: true,
APIBlocked: true,
ValidCACert: true,
IsAnomaly: true,
TransportStatus: nil,
FailingGateways: 0,
},
}, {
tk: riseupvpn.TestKeys{
APIStatus: "ok",
CACertStatus: false,
FailingGateways: nil,
TransportStatus: nil,
},
sk: riseupvpn.SummaryKeys{
ValidCACert: false,
IsAnomaly: true,
ValidCACert: false,
IsAnomaly: true,
FailingGateways: 0,
TransportStatus: nil,
},
}, {
tk: riseupvpn.TestKeys{
@ -475,13 +649,39 @@ func TestSummaryKeysWorksAsIntended(t *testing.T) {
Port: 443,
TransportType: "obfs4",
}},
TransportStatus: map[string]string{
"obfs4": "blocked",
"openvpn": "ok",
},
},
sk: riseupvpn.SummaryKeys{
FailingGateways: 1,
IsAnomaly: true,
ValidCACert: true,
TransportStatus: map[string]string{
"obfs4": "blocked",
"openvpn": "ok",
},
},
}}
}, {
tk: riseupvpn.TestKeys{
APIStatus: "ok",
CACertStatus: true,
FailingGateways: nil,
TransportStatus: map[string]string{
"openvpn": "ok",
},
},
sk: riseupvpn.SummaryKeys{
ValidCACert: true,
IsAnomaly: false,
FailingGateways: 0,
TransportStatus: map[string]string{
"openvpn": "ok",
},
},
},
}
for idx, tt := range tests {
t.Run(fmt.Sprintf("%d", idx), func(t *testing.T) {
m := &riseupvpn.Measurer{}
@ -498,3 +698,95 @@ func TestSummaryKeysWorksAsIntended(t *testing.T) {
})
}
}
func generateMockGetter(requestResponse map[string]string, responseStatus map[string]bool) urlgetter.MultiGetter {
return func(ctx context.Context, g urlgetter.Getter) (urlgetter.TestKeys, error) {
url := g.Target
responseBody, foundRequest := requestResponse[url]
isSuccessStatus, foundStatus := responseStatus[url]
if !foundRequest || !foundStatus {
return urlgetter.DefaultMultiGetter(ctx, g)
}
var failure *string
var failedOperation *string
isBlocked := !isSuccessStatus
var responseStatus int64 = 200
if isBlocked {
responseBody = ""
eofError := io.EOF.Error()
failure = &eofError
connectOperation := errorx.ConnectOperation
failedOperation = &connectOperation
responseStatus = 0
}
tcpConnect := archival.TCPConnectEntry{
// use some dummy IP/Port combination for URLs, we don't do DNS resolution
IP: "123.456.234.123",
Port: 443,
Status: archival.TCPConnectStatus{
Success: isSuccessStatus,
Blocked: &isBlocked,
Failure: failure,
},
}
if strings.Contains(url, "tcpconnect://") {
ipPort := strings.Split(strings.Split(url, "//")[1], ":")
port, err := strconv.ParseInt(ipPort[1], 10, 32)
if err == nil {
tcpConnect.IP = ipPort[0]
tcpConnect.Port = int(port)
}
}
tk := urlgetter.TestKeys{
Failure: failure,
FailedOperation: failedOperation,
HTTPResponseStatus: responseStatus,
HTTPResponseBody: responseBody,
Requests: []archival.RequestEntry{archival.RequestEntry{
Failure: failure,
Request: archival.HTTPRequest{
URL: url,
Body: archival.MaybeBinaryValue{},
BodyIsTruncated: false,
},
Response: archival.HTTPResponse{
Body: archival.HTTPBody{
Value: responseBody,
},
BodyIsTruncated: false,
}},
},
TCPConnect: []archival.TCPConnectEntry{tcpConnect},
}
return tk, nil
}
}
func generateDefaultMockGetter(responseStatuses map[string]bool) urlgetter.MultiGetter {
return generateMockGetter(RequestResponse, responseStatuses)
}
func runDefaultMockTest(t *testing.T, multiGetter urlgetter.MultiGetter) *model.Measurement {
measurer := riseupvpn.Measurer{
Config: riseupvpn.Config{},
Getter: multiGetter,
}
measurement := new(model.Measurement)
err := measurer.Run(
context.Background(),
&mockable.Session{
MockableLogger: log.Log,
},
measurement,
model.NewPrinterCallbacks(log.Log),
)
if err != nil {
t.Fatal(err)
}
return measurement
}