2021-06-18 13:51:18 +02:00
|
|
|
// 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"
|
2022-01-24 12:39:27 +01:00
|
|
|
"fmt"
|
2021-06-18 13:51:18 +02:00
|
|
|
"path"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/ooni/probe-cli/v3/internal/engine/netx/archival"
|
2022-01-03 13:53:23 +01:00
|
|
|
"github.com/ooni/probe-cli/v3/internal/model"
|
2021-06-18 13:51:18 +02:00
|
|
|
"github.com/ooni/probe-cli/v3/internal/ptx"
|
|
|
|
"github.com/ooni/probe-cli/v3/internal/tunnel"
|
|
|
|
)
|
|
|
|
|
|
|
|
// testVersion is the tor experiment version.
|
2022-01-19 17:23:27 +01:00
|
|
|
const testVersion = "0.1.1"
|
2021-06-18 13:51:18 +02:00
|
|
|
|
|
|
|
// Config contains the experiment config.
|
2022-01-19 17:23:27 +01:00
|
|
|
type Config struct {
|
|
|
|
DisableProgress bool `ooni:"Disable printing progress messages"`
|
|
|
|
}
|
2021-06-18 13:51:18 +02:00
|
|
|
|
|
|
|
// 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()
|
2022-01-19 17:23:27 +01:00
|
|
|
const maxRuntime = 600 * time.Second
|
2021-06-18 13:51:18 +02:00
|
|
|
ctx, cancel := context.WithTimeout(ctx, maxRuntime)
|
|
|
|
defer cancel()
|
|
|
|
errch := make(chan error)
|
2022-01-24 12:39:27 +01:00
|
|
|
ticker := time.NewTicker(2 * time.Second)
|
2021-06-18 13:51:18 +02:00
|
|
|
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:
|
2022-01-19 17:23:27 +01:00
|
|
|
if !m.config.DisableProgress {
|
2022-01-24 12:39:27 +01:00
|
|
|
elapsedTime := time.Since(start)
|
|
|
|
progress := elapsedTime.Seconds() / maxRuntime.Seconds()
|
|
|
|
callbacks.OnProgress(progress, fmt.Sprintf(
|
|
|
|
"torsf: elapsedTime: %.0f s; maxRuntime: %.0f s",
|
|
|
|
elapsedTime.Seconds(), maxRuntime.Seconds()))
|
2022-01-19 17:23:27 +01:00
|
|
|
}
|
2021-06-18 13:51:18 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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) {
|
2022-01-21 12:32:08 +01:00
|
|
|
return SummaryKeys{IsAnomaly: false}, nil
|
2021-06-18 13:51:18 +02:00
|
|
|
}
|