Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(torsf): experiment that bootstraps tor using snowflake #387

Merged
merged 4 commits into from
Jun 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
/ooniprobe_checksums.txt.asc
/ooniprobe.exe
/probe-cli.cov
/ptxclient
/ptxclient.exe
/*.tar.gz
/testdata/gotmp
/*.zip
13 changes: 13 additions & 0 deletions internal/engine/allexperiments.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/ooni/probe-cli/v3/internal/engine/experiment/telegram"
"github.com/ooni/probe-cli/v3/internal/engine/experiment/tlstool"
"github.com/ooni/probe-cli/v3/internal/engine/experiment/tor"
"github.com/ooni/probe-cli/v3/internal/engine/experiment/torsf"
"github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter"
"github.com/ooni/probe-cli/v3/internal/engine/experiment/webconnectivity"
"github.com/ooni/probe-cli/v3/internal/engine/experiment/whatsapp"
Expand Down Expand Up @@ -289,6 +290,18 @@ var experimentsByName = map[string]func(*Session) *ExperimentBuilder{
}
},

"torsf": func(session *Session) *ExperimentBuilder {
return &ExperimentBuilder{
build: func(config interface{}) *Experiment {
return NewExperiment(session, torsf.NewExperimentMeasurer(
*config.(*torsf.Config),
))
},
config: &torsf.Config{},
inputPolicy: InputNone,
}
},

"urlgetter": func(session *Session) *ExperimentBuilder {
return &ExperimentBuilder{
build: func(config interface{}) *Experiment {
Expand Down
40 changes: 40 additions & 0 deletions internal/engine/experiment/torsf/integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package torsf_test

import (
"context"
"io/ioutil"
"testing"

"github.com/apex/log"
"github.com/ooni/probe-cli/v3/internal/engine/experiment/torsf"
"github.com/ooni/probe-cli/v3/internal/engine/internal/mockable"
"github.com/ooni/probe-cli/v3/internal/engine/model"
"golang.org/x/sys/execabs"
)

func TestRunWithExistingTor(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
path, err := execabs.LookPath("tor")
if err != nil {
t.Skip("there is no tor executable installed")
}
t.Log("found tor in path:", path)
tempdir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatal(err)
}
t.Log("using this tempdir", tempdir)
m := torsf.NewExperimentMeasurer(torsf.Config{})
ctx := context.Background()
measurement := &model.Measurement{}
callbacks := model.NewPrinterCallbacks(log.Log)
sess := &mockable.Session{
MockableLogger: log.Log,
MockableTempDir: tempdir,
}
if err = m.Run(ctx, sess, measurement, callbacks); err != nil {
t.Fatal(err)
}
}
173 changes: 173 additions & 0 deletions internal/engine/experiment/torsf/torsf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// Package torsf contains the torsf experiment. This experiment
// measures the bootstrapping of tor using snowflake.
//
// See https://github.com/ooni/spec/blob/master/nettests/ts-030-torsf.md
package torsf

import (
"context"
"path"
"time"

"github.com/ooni/probe-cli/v3/internal/engine/model"
"github.com/ooni/probe-cli/v3/internal/engine/netx/archival"
"github.com/ooni/probe-cli/v3/internal/ptx"
"github.com/ooni/probe-cli/v3/internal/tunnel"
)

// testVersion is the tor experiment version.
const testVersion = "0.1.0"

// Config contains the experiment config.
type Config struct{}

// TestKeys contains the experiment's result.
type TestKeys struct {
// BootstrapTime contains the bootstrap time on success.
BootstrapTime float64 `json:"bootstrap_time"`

// Failure contains the failure string or nil.
Failure *string `json:"failure"`
}

// Measurer performs the measurement.
type Measurer struct {
// config contains the experiment settings.
config Config

// mockStartListener is an optional function that allows us to override
// the function we actually use to start the ptx listener.
mockStartListener func() error

// mockStartTunnel is an optional function that allows us to override the
// default tunnel.Start function used to start a tunnel.
mockStartTunnel func(ctx context.Context, config *tunnel.Config) (tunnel.Tunnel, error)
}

// ExperimentName implements model.ExperimentMeasurer.ExperimentName.
func (m *Measurer) ExperimentName() string {
return "torsf"
}

// ExperimentVersion implements model.ExperimentMeasurer.ExperimentVersion.
func (m *Measurer) ExperimentVersion() string {
return testVersion
}

// registerExtensions registers the extensions used by this experiment.
func (m *Measurer) registerExtensions(measurement *model.Measurement) {
// currently none
}

// Run runs the experiment with the specified context, session,
// measurement, and experiment calbacks. This method should only
// return an error in case the experiment could not run (e.g.,
// a required input is missing). Otherwise, the code should just
// set the relevant OONI error inside of the measurement and
// return nil. This is important because the caller may not submit
// the measurement if this method returns an error.
func (m *Measurer) Run(
ctx context.Context, sess model.ExperimentSession,
measurement *model.Measurement, callbacks model.ExperimentCallbacks,
) error {
m.registerExtensions(measurement)
testkeys := &TestKeys{}
measurement.TestKeys = testkeys
start := time.Now()
const maxRuntime = 300 * time.Second
ctx, cancel := context.WithTimeout(ctx, maxRuntime)
defer cancel()
errch := make(chan error)
ticker := time.NewTicker(250 * time.Millisecond)
defer ticker.Stop()
go m.run(ctx, sess, testkeys, errch)
for {
select {
case err := <-errch:
callbacks.OnProgress(1.0, "torsf experiment is finished")
return err
case <-ticker.C:
progress := time.Since(start).Seconds() / maxRuntime.Seconds()
callbacks.OnProgress(progress, "torsf experiment is running")
}
}
}

// run runs the bootstrap. This function ONLY returns an error when
// there has been a fundamental error starting the test. This behavior
// follows the expectations for the ExperimentMeasurer.Run method.
func (m *Measurer) run(ctx context.Context,
sess model.ExperimentSession, testkeys *TestKeys, errch chan<- error) {
sfdialer := &ptx.SnowflakeDialer{}
ptl := &ptx.Listener{
PTDialer: sfdialer,
Logger: sess.Logger(),
}
if err := m.startListener(ptl.Start); err != nil {
testkeys.Failure = archival.NewFailure(err)
// This error condition mostly means "I could not open a local
// listening port", which strikes as fundamental failure.
errch <- err
return
}
defer ptl.Stop()
tun, err := m.startTunnel()(ctx, &tunnel.Config{
Name: "tor",
Session: sess,
TunnelDir: path.Join(sess.TempDir(), "torsf"),
Logger: sess.Logger(),
TorArgs: []string{
"UseBridges", "1",
"ClientTransportPlugin", ptl.AsClientTransportPluginArgument(),
"Bridge", sfdialer.AsBridgeArgument(),
},
})
if err != nil {
// Note: archival.NewFailure scrubs IP addresses
testkeys.Failure = archival.NewFailure(err)
// This error condition means we could not bootstrap with snowflake
// for $reasons, so the experiment didn't fail, rather it did record
// that something prevented snowflake from running.
errch <- nil
return
}
defer tun.Stop()
testkeys.BootstrapTime = tun.BootstrapTime().Seconds()
errch <- nil
}

// startListener either calls f or mockStartListener depending
// on whether mockStartListener is nil or not.
func (m *Measurer) startListener(f func() error) error {
if m.mockStartListener != nil {
return m.mockStartListener()
}
return f()
}

// startTunnel returns the proper function to start a tunnel.
func (m *Measurer) startTunnel() func(
ctx context.Context, config *tunnel.Config) (tunnel.Tunnel, error) {
if m.mockStartTunnel != nil {
return m.mockStartTunnel
}
return tunnel.Start
}

// NewExperimentMeasurer creates a new ExperimentMeasurer.
func NewExperimentMeasurer(config Config) model.ExperimentMeasurer {
return &Measurer{config: config}
}

// SummaryKeys contains summary keys for this experiment.
//
// Note that this structure is part of the ABI contract with probe-cli
// therefore we should be careful when changing it.
type SummaryKeys struct {
IsAnomaly bool `json:"-"`
}

// GetSummaryKeys implements model.ExperimentMeasurer.GetSummaryKeys.
func (m *Measurer) GetSummaryKeys(measurement *model.Measurement) (interface{}, error) {
return &SummaryKeys{IsAnomaly: false}, nil
}
Loading