58adb68b2c
* refactor: move tracex outside of engine/netx Consistently with https://github.com/ooni/probe/issues/2121 and https://github.com/ooni/probe/issues/2115, we can now move tracex outside of engine/netx. The main reason why this makes sense now is that the package is now changed significantly from the one that we imported from ooni/probe-engine. We have improved its implementation, which had not been touched significantly for quite some time, and converted it to unit testing. I will document tomorrow some extra work I'd like to do with this package but likely could not do $soon. * go fmt * regen tutorials
325 lines
11 KiB
Go
325 lines
11 KiB
Go
// Package torsf contains the torsf experiment.
|
|
//
|
|
// See https://github.com/ooni/spec/blob/master/nettests/ts-030-torsf.md
|
|
package torsf
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"path"
|
|
"time"
|
|
|
|
"github.com/ooni/probe-cli/v3/internal/bytecounter"
|
|
"github.com/ooni/probe-cli/v3/internal/model"
|
|
"github.com/ooni/probe-cli/v3/internal/ptx"
|
|
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
|
"github.com/ooni/probe-cli/v3/internal/torlogs"
|
|
"github.com/ooni/probe-cli/v3/internal/tracex"
|
|
"github.com/ooni/probe-cli/v3/internal/tunnel"
|
|
)
|
|
|
|
// Implementation note: this file is written with easy diffing with respect
|
|
// to internal/engine/experiment/vanillator/vanillator.go in mind.
|
|
//
|
|
// We may want to have a single implementation for both nettests in the future.
|
|
|
|
// testVersion is the experiment version.
|
|
const testVersion = "0.3.0"
|
|
|
|
// Config contains the experiment config.
|
|
type Config struct {
|
|
// DisablePersistentDatadir disables using a persistent datadir.
|
|
DisablePersistentDatadir bool `ooni:"Disable using a persistent tor datadir"`
|
|
|
|
// DisableProgress disables printing progress messages.
|
|
DisableProgress bool `ooni:"Disable printing progress messages"`
|
|
|
|
// RendezvousMethod allows to choose the method with which to rendezvous.
|
|
RendezvousMethod string `ooni:"Choose the method with which to rendezvous. Must be one of amp and domain_fronting. Leaving this field empty means we should use the default."`
|
|
}
|
|
|
|
// TestKeys contains the experiment's result.
|
|
type TestKeys struct {
|
|
// BootstrapTime contains the bootstrap time on success.
|
|
BootstrapTime float64 `json:"bootstrap_time"`
|
|
|
|
// Error is one of `null`, `"timeout-reached"`, and `"unknown-error"` (this
|
|
// field exists for backward compatibility with the previous
|
|
// `vanilla_tor` implementation).
|
|
Error *string `json:"error"`
|
|
|
|
// Failure contains the failure string or nil.
|
|
Failure *string `json:"failure"`
|
|
|
|
// Success indicates whether we succeded (this field exists for
|
|
// backward compatibility with the previous `vanilla_tor` implementation).
|
|
Success bool `json:"success"`
|
|
|
|
// PersistentDatadir indicates whether we're using a persistent tor datadir.
|
|
PersistentDatadir bool `json:"persistent_datadir"`
|
|
|
|
// RendezvousMethod contains the method used to perform the rendezvous.
|
|
RendezvousMethod string `json:"rendezvous_method"`
|
|
|
|
// Timeout contains the default timeout for this experiment
|
|
Timeout float64 `json:"timeout"`
|
|
|
|
// TorLogs contains the bootstrap logs.
|
|
TorLogs []string `json:"tor_logs"`
|
|
|
|
// TorProgress contains the percentage of the maximum progress reached.
|
|
TorProgress int64 `json:"tor_progress"`
|
|
|
|
// TorProgressTag contains the tag of the maximum progress reached.
|
|
TorProgressTag string `json:"tor_progress_tag"`
|
|
|
|
// TorProgressSummary contains the summary of the maximum progress reached.
|
|
TorProgressSummary string `json:"tor_progress_summary"`
|
|
|
|
// TorVersion contains the version of tor (if it's possible to obtain it).
|
|
TorVersion string `json:"tor_version"`
|
|
|
|
// TransportName is always set to "snowflake" for this experiment.
|
|
TransportName string `json:"transport_name"`
|
|
}
|
|
|
|
// 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, tunnel.DebugInfo, 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
|
|
}
|
|
|
|
// maxRuntime is the maximum runtime for this experiment
|
|
const maxRuntime = 600 * time.Second
|
|
|
|
// 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 {
|
|
ptl, sfdialer, err := m.setup(ctx, sess.Logger())
|
|
if err != nil {
|
|
// we cannot setup the experiment
|
|
return err
|
|
}
|
|
defer ptl.Stop()
|
|
m.registerExtensions(measurement)
|
|
start := time.Now()
|
|
ctx, cancel := context.WithTimeout(ctx, maxRuntime)
|
|
defer cancel()
|
|
tkch := make(chan *TestKeys)
|
|
ticker := time.NewTicker(2 * time.Second)
|
|
defer ticker.Stop()
|
|
go m.bootstrap(ctx, maxRuntime, sess, tkch, ptl, sfdialer)
|
|
for {
|
|
select {
|
|
case tk := <-tkch:
|
|
measurement.TestKeys = tk
|
|
callbacks.OnProgress(1.0, "torsf experiment is finished")
|
|
return nil
|
|
case <-ticker.C:
|
|
if !m.config.DisableProgress {
|
|
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()))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// setup prepares for running the torsf experiment. Returns a valid ptx listener
|
|
// and snowflake dialer on success. Returns an error on failure. On success,
|
|
// remember to Stop the ptx listener when you're done.
|
|
func (m *Measurer) setup(ctx context.Context,
|
|
logger model.Logger) (*ptx.Listener, *ptx.SnowflakeDialer, error) {
|
|
rm, err := ptx.NewSnowflakeRendezvousMethod(m.config.RendezvousMethod)
|
|
if err != nil {
|
|
// cannot run the experiment with unknown rendezvous method
|
|
return nil, nil, err
|
|
}
|
|
sfdialer := ptx.NewSnowflakeDialerWithRendezvousMethod(rm)
|
|
ptl := &ptx.Listener{
|
|
ExperimentByteCounter: bytecounter.ContextExperimentByteCounter(ctx),
|
|
Logger: logger,
|
|
PTDialer: sfdialer,
|
|
SessionByteCounter: bytecounter.ContextSessionByteCounter(ctx),
|
|
}
|
|
if err := m.startListener(ptl.Start); err != nil {
|
|
// This error condition mostly means "I could not open a local
|
|
// listening port", which strikes as fundamental failure.
|
|
return nil, nil, err
|
|
}
|
|
logger.Infof("torsf: rendezvous method: '%s'", m.config.RendezvousMethod)
|
|
return ptl, sfdialer, nil
|
|
}
|
|
|
|
// values for the backward compatible error field.
|
|
var (
|
|
timeoutReachedError = "timeout-reached"
|
|
unknownError = "unknown-error"
|
|
)
|
|
|
|
// bootstrap runs the bootstrap.
|
|
func (m *Measurer) bootstrap(ctx context.Context, timeout time.Duration, sess model.ExperimentSession,
|
|
out chan<- *TestKeys, ptl *ptx.Listener, sfdialer *ptx.SnowflakeDialer) {
|
|
tk := &TestKeys{
|
|
// initialized later
|
|
BootstrapTime: 0,
|
|
Error: nil,
|
|
Failure: nil,
|
|
Success: false,
|
|
TorLogs: []string{},
|
|
TorProgress: 0,
|
|
TorProgressTag: "",
|
|
TorProgressSummary: "",
|
|
TorVersion: "",
|
|
// initialized now
|
|
PersistentDatadir: !m.config.DisablePersistentDatadir,
|
|
RendezvousMethod: sfdialer.RendezvousMethod.Name(),
|
|
//
|
|
Timeout: timeout.Seconds(),
|
|
TransportName: "snowflake",
|
|
}
|
|
sess.Logger().Infof(
|
|
"torsf: disable persistent datadir: %+v", m.config.DisablePersistentDatadir)
|
|
defer func() {
|
|
out <- tk
|
|
}()
|
|
tun, debugInfo, err := m.startTunnel()(ctx, &tunnel.Config{
|
|
Name: "tor",
|
|
Session: sess,
|
|
TunnelDir: path.Join(m.baseTunnelDir(sess), "torsf"),
|
|
Logger: sess.Logger(),
|
|
TorArgs: []string{
|
|
"UseBridges", "1",
|
|
"ClientTransportPlugin", ptl.AsClientTransportPluginArgument(),
|
|
"Bridge", sfdialer.AsBridgeArgument(),
|
|
},
|
|
})
|
|
tk.TorVersion = debugInfo.Version
|
|
m.readTorLogs(sess.Logger(), tk, debugInfo.LogFilePath)
|
|
if err != nil {
|
|
// Note: tracex.NewFailure scrubs IP addresses
|
|
tk.Failure = tracex.NewFailure(err)
|
|
if errors.Is(err, context.DeadlineExceeded) {
|
|
tk.Error = &timeoutReachedError
|
|
} else {
|
|
tk.Error = &unknownError
|
|
}
|
|
tk.Success = false
|
|
return
|
|
}
|
|
defer tun.Stop()
|
|
tk.BootstrapTime = tun.BootstrapTime().Seconds()
|
|
tk.Success = true
|
|
}
|
|
|
|
// readTorLogs attempts to read and include the tor logs into
|
|
// the test keys if this operation is possible.
|
|
func (m *Measurer) readTorLogs(logger model.Logger, tk *TestKeys, logFilePath string) {
|
|
tk.TorLogs = append(tk.TorLogs, torlogs.ReadBootstrapLogsOrWarn(logger, logFilePath)...)
|
|
if len(tk.TorLogs) <= 0 {
|
|
return
|
|
}
|
|
last := tk.TorLogs[len(tk.TorLogs)-1]
|
|
bi, err := torlogs.ParseBootstrapLogLine(last)
|
|
// Implementation note: parsing cannot fail here because we're using the same code
|
|
// for selecting and for parsing the bootstrap logs, so we panic on error.
|
|
runtimex.PanicOnError(err, fmt.Sprintf("cannot parse bootstrap line: %s", last))
|
|
tk.TorProgress = bi.Progress
|
|
tk.TorProgressTag = bi.Tag
|
|
tk.TorProgressSummary = bi.Summary
|
|
}
|
|
|
|
// baseTunnelDir returns the base directory to use for tunnelling
|
|
func (m *Measurer) baseTunnelDir(sess model.ExperimentSession) string {
|
|
if m.config.DisablePersistentDatadir {
|
|
return sess.TempDir()
|
|
}
|
|
return sess.TunnelDir()
|
|
}
|
|
|
|
// 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, tunnel.DebugInfo, 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 ooniprobe
|
|
// therefore we should be careful when changing it.
|
|
type SummaryKeys struct {
|
|
IsAnomaly bool `json:"-"`
|
|
}
|
|
|
|
var (
|
|
// errInvalidTestKeysType indicates the test keys type is invalid.
|
|
errInvalidTestKeysType = errors.New("torsf: invalid test keys type")
|
|
|
|
//errNilTestKeys indicates that the test keys are nil.
|
|
errNilTestKeys = errors.New("torsf: nil test keys")
|
|
)
|
|
|
|
// GetSummaryKeys implements model.ExperimentMeasurer.GetSummaryKeys.
|
|
func (m *Measurer) GetSummaryKeys(measurement *model.Measurement) (interface{}, error) {
|
|
testkeys, good := measurement.TestKeys.(*TestKeys)
|
|
if !good {
|
|
return nil, errInvalidTestKeysType
|
|
}
|
|
if testkeys == nil {
|
|
return nil, errNilTestKeys
|
|
}
|
|
return SummaryKeys{IsAnomaly: testkeys.Failure != nil}, nil
|
|
}
|