ooni-probe-cli/internal/engine/experiment/torsf/torsf.go
Simone Basso 273b70bacc
refactor: interfaces and data types into the model package (#642)
## Checklist

- [x] I have read the [contribution guidelines](https://github.com/ooni/probe-cli/blob/master/CONTRIBUTING.md)
- [x] reference issue for this pull request: https://github.com/ooni/probe/issues/1885
- [x] related ooni/spec pull request: N/A

Location of the issue tracker: https://github.com/ooni/probe

## Description

This PR contains a set of changes to move important interfaces and data types into the `./internal/model` package.

The criteria for including an interface or data type in here is roughly that the type should be important and used by several packages. We are especially interested to move more interfaces here to increase modularity.

An additional side effect is that, by reading this package, one should be able to understand more quickly how different parts of the codebase interact with each other.

This is what I want to move in `internal/model`:

- [x] most important interfaces from `internal/netxlite`
- [x] everything that was previously part of `internal/engine/model`
- [x] mocks from `internal/netxlite/mocks` should also be moved in here as a subpackage
2022-01-03 13:53:23 +01:00

174 lines
5.5 KiB
Go

// 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/netx/archival"
"github.com/ooni/probe-cli/v3/internal/model"
"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
}