ooni-probe-cli/internal/engine/experiment/riseupvpn/riseupvpn_test.go
Simone Basso 0d4323ae66
Release 3.6.0 (#239)
* chore: update dependencies

* chore: update user agent for measurements

* chore: we're now at v3.6.0

* chore: update assets

* chore: update bundled CA

* fix: address some goreportcard.com warnings

* fix(debian/changelog): zap release that breaks out build scripts

We're forcing the content of changelog with `dch`, so it's fine to
not have any specific new release in there.

* fix: make sure tests are passing locally

Notably, I removed a chunk of code where we were checking for network
activity. Now we don't fetch the databases and it's not important. Before,
it was important because the databases are ~large.

* fix: temporarily comment out riseupvn integration tests

See https://github.com/ooni/probe/issues/1354 for work aimed at
reducing the rate of false positives (thanks @cyBerta!)
2021-03-03 14:42:17 +01:00

499 lines
13 KiB
Go

package riseupvpn_test
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io/ioutil"
"math/rand"
"net/http"
"testing"
"time"
"github.com/apex/log"
"github.com/google/go-cmp/cmp"
"github.com/ooni/probe-cli/v3/internal/engine/experiment/riseupvpn"
"github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter"
"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"
)
func TestNewExperimentMeasurer(t *testing.T) {
measurer := riseupvpn.NewExperimentMeasurer(riseupvpn.Config{})
if measurer.ExperimentName() != "riseupvpn" {
t.Fatal("unexpected name")
}
if measurer.ExperimentVersion() != "0.1.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)
}
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.APIFailure != nil {
t.Fatal("unexpected ApiFailure")
}
if tk.APIStatus != "ok" {
t.Fatal("unexpected ApiStatus")
}
if tk.CACertStatus != true {
t.Fatal("unexpected CaCertStatus")
}
if tk.FailingGateways != nil {
t.Fatal("unexpected FailingGateways value")
}
}
// TestUpdateWithMixedResults tests if one operation failed
// ApiStatus is considered as blocked
func TestUpdateWithMixedResults(t *testing.T) {
tk := riseupvpn.NewTestKeys()
tk.UpdateProviderAPITestKeys(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.UpdateProviderAPITestKeys(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.UpdateProviderAPITestKeys(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.APIStatus != "blocked" {
t.Fatal("ApiStatus should be blocked")
}
if *tk.APIFailure != errorx.FailureEOFError {
t.Fatal("invalid ApiFailure")
}
}
func TestFailureCaCertFetch(t *testing.T) {
measurer := riseupvpn.NewExperimentMeasurer(riseupvpn.Config{})
ctx, cancel := context.WithCancel(context.Background())
// we're cancelling immediately so that the CA Cert fetch fails
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.CACertStatus != false {
t.Fatal("invalid CACertStatus ")
}
if tk.APIStatus != "blocked" {
t.Fatal("invalid ApiStatus")
}
if tk.APIFailure != nil {
t.Fatal("ApiFailure should be null")
}
if len(tk.Requests) > 1 {
t.Fatal("Unexpected requests")
}
}
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)
}
tk := measurement.TestKeys.(*riseupvpn.TestKeys)
if tk.CACertStatus != true {
t.Fatal("invalid CACertStatus ")
}
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.APIStatus != "blocked" {
t.Fatal("invalid ApiStatus")
}
if tk.APIFailure == nil {
t.Fatal("ApiFailure should not be null")
}
}
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)
}
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.CACertStatus != true {
t.Fatal("invalid CACertStatus ")
}
if tk.APIStatus != "blocked" {
t.Fatal("invalid ApiStatus")
}
if tk.APIFailure == nil {
t.Fatal("ApiFailure should not be null")
}
}
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)
}
tk := measurement.TestKeys.(*riseupvpn.TestKeys)
if tk.CACertStatus != true {
t.Fatal("invalid CACertStatus ")
}
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.APIStatus != "blocked" {
t.Fatal("invalid ApiStatus")
}
if tk.APIFailure == nil {
t.Fatal("ApiFailure should not be null")
}
}
func TestFailureGateway(t *testing.T) {
t.Skip("test currently not WAI - will restore after release")
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)
}
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")
}
entry := tk.FailingGateways[0]
if entry.IP != censoredGateway.IP || fmt.Sprint(entry.Port) != censoredGateway.Port {
t.Fatal("unexpected failed gateway configuration")
}
if tk.APIStatus == "blocked" {
t.Fatal("invalid ApiStatus", tk.APIStatus)
}
if tk.APIFailure != nil {
t.Fatal("ApiFailure should be null")
}
}
func TestSummaryKeysInvalidType(t *testing.T) {
measurement := new(model.Measurement)
m := &riseupvpn.Measurer{}
_, err := m.GetSummaryKeys(measurement)
if err.Error() != "invalid test keys type" {
t.Fatal("not the error we expected")
}
}
func TestSummaryKeysWorksAsIntended(t *testing.T) {
tests := []struct {
tk riseupvpn.TestKeys
sk riseupvpn.SummaryKeys
}{{
tk: riseupvpn.TestKeys{
APIStatus: "blocked",
CACertStatus: true,
FailingGateways: nil,
},
sk: riseupvpn.SummaryKeys{
APIBlocked: true,
ValidCACert: true,
IsAnomaly: true,
},
}, {
tk: riseupvpn.TestKeys{
APIStatus: "ok",
CACertStatus: false,
FailingGateways: nil,
},
sk: riseupvpn.SummaryKeys{
ValidCACert: false,
IsAnomaly: true,
},
}, {
tk: riseupvpn.TestKeys{
APIStatus: "ok",
CACertStatus: true,
FailingGateways: []riseupvpn.GatewayConnection{{
IP: "1.1.1.1",
Port: 443,
TransportType: "obfs4",
}},
},
sk: riseupvpn.SummaryKeys{
FailingGateways: 1,
IsAnomaly: true,
ValidCACert: true,
},
}}
for idx, tt := range tests {
t.Run(fmt.Sprintf("%d", idx), func(t *testing.T) {
m := &riseupvpn.Measurer{}
measurement := &model.Measurement{TestKeys: &tt.tk}
got, err := m.GetSummaryKeys(measurement)
if err != nil {
t.Fatal(err)
return
}
sk := got.(riseupvpn.SummaryKeys)
if diff := cmp.Diff(tt.sk, sk); diff != "" {
t.Fatal(diff)
}
})
}
}