ooni-probe-cli/internal/model/experiment.go

306 lines
10 KiB
Go
Raw Permalink Normal View History

package model
//
// Definition of experiment and types used by the
// implementation of all experiments.
//
import (
"context"
)
// ExperimentSession is the experiment's view of a session.
type ExperimentSession interface {
// GetTestHelpersByName returns a list of test helpers with the given name.
GetTestHelpersByName(name string) ([]OOAPIService, bool)
// DefaultHTTPClient returns the default HTTPClient used by the session.
DefaultHTTPClient() HTTPClient
// FetchPsiphonConfig returns psiphon's config as a serialized JSON or an error.
FetchPsiphonConfig(ctx context.Context) ([]byte, error)
// FetchTorTargets returns the targets for the Tor experiment or an error.
FetchTorTargets(ctx context.Context, cc string) (map[string]OOAPITorTarget, error)
// Logger returns the logger used by the session.
Logger() Logger
// ProbeCC returns the country code.
ProbeCC() string
// ResolverIP returns the resolver's IP.
ResolverIP() string
// TempDir returns the session's temporary directory.
TempDir() string
// TorArgs returns the arguments we should pass to tor when executing it.
TorArgs() []string
// TorBinary returns the path of the tor binary.
TorBinary() string
// TunnelDir is the directory where to store tunnel information.
feat(torsf): collect tor logs, select rendezvous method, count bytes (#683) This diff contains significant improvements over the previous implementation of the torsf experiment. We add support for configuring different rendezvous methods after the convo at https://github.com/ooni/probe/issues/2004. In doing that, I've tried to use a terminology that is consistent with the names being actually used by tor developers. In terms of what to do next, this diff basically instruments torsf to always rendezvous using domain fronting. Yet, it's also possible to change the rendezvous method from the command line, when using miniooni, which allows to experiment a bit more. In the same vein, by default we use a persistent tor datadir, but it's also possible to use a temporary datadir using the cmdline. Here's how a generic invocation of `torsf` looks like: ```bash ./miniooni -O DisablePersistentDatadir=true \ -O RendezvousMethod=amp \ -O DisableProgress=true \ torsf ``` (The default is `DisablePersistentDatadir=false` and `RendezvousMethod=domain_fronting`.) With this implementation, we can start measuring whether snowflake and tor together can boostrap, which seems the most important thing to focus on at the beginning. Understanding why the bootstrap most often does not converge with a temporary datadir on Android devices remains instead an open problem for now. (I'll also update the relevant issues or create new issues after commit this.) We also address some methodology improvements that were proposed in https://github.com/ooni/probe/issues/1686. Namely: 1. we record the tor version; 2. we include the bootstrap percentage by reading the logs; 3. we set the anomaly key correctly; 4. we measure the bytes send and received (by `tor` not by `snowflake`, since doing it for snowflake seems more complex at this stage). What remains to be done is the possibility of including Snowflake events into the measurement, which is not possible until the new improvements at common/event in snowflake.git are included into a tagged version of snowflake itself. (I'll make sure to mention this aspect to @cohosh in https://github.com/ooni/probe/issues/2004.)
2022-02-07 17:05:36 +01:00
TunnelDir() string
// UserAgent returns the user agent we should be using when we're fine
// with identifying ourselves as ooniprobe.
UserAgent() string
}
// ExperimentAsyncTestKeys is the type of test keys returned by an experiment
// when running in async fashion rather than in sync fashion.
type ExperimentAsyncTestKeys struct {
// Extensions contains the extensions used by this experiment.
Extensions map[string]int64
// Input is the input this measurement refers to.
Input MeasurementTarget
// MeasurementRuntime is the total measurement runtime.
MeasurementRuntime float64
// TestHelpers contains the test helpers used in the experiment
TestHelpers map[string]interface{}
// TestKeys contains the actual test keys.
TestKeys interface{}
}
// ExperimentMeasurerAsync is a measurer that can run in async fashion.
//
// Currently this functionality is optional, but we will likely
// migrate all experiments to use this functionality in 2022.
type ExperimentMeasurerAsync interface {
// RunAsync runs the experiment in async fashion.
//
// Arguments:
//
// - ctx is the context for deadline/timeout/cancellation
//
// - sess is the measurement session
//
// - input is the input URL to measure
//
// - callbacks contains the experiment callbacks
//
// Returns either a channel where TestKeys are posted or an error.
//
// An error indicates that specific preconditions for running the experiment
// are not met (e.g., the input URL is invalid).
//
// On success, the experiment will post on the channel each new
// measurement until it is done and closes the channel.
RunAsync(ctx context.Context, sess ExperimentSession, input string,
callbacks ExperimentCallbacks) (<-chan *ExperimentAsyncTestKeys, error)
}
// ExperimentCallbacks contains experiment event-handling callbacks
type ExperimentCallbacks interface {
// OnProgress provides information about an experiment progress.
OnProgress(percentage float64, message string)
}
// PrinterCallbacks is the default event handler
type PrinterCallbacks struct {
Logger
}
// NewPrinterCallbacks returns a new default callback handler
func NewPrinterCallbacks(logger Logger) PrinterCallbacks {
return PrinterCallbacks{Logger: logger}
}
// OnProgress provides information about an experiment progress.
func (d PrinterCallbacks) OnProgress(percentage float64, message string) {
d.Logger.Infof("[%5.1f%%] %s", percentage*100, message)
}
// ExperimentMeasurer is the interface that allows to run a
// measurement for a specific experiment.
type ExperimentMeasurer interface {
// ExperimentName returns the experiment name.
ExperimentName() string
// ExperimentVersion returns the experiment version.
ExperimentVersion() string
// 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 WILL NOT submit
// the measurement if this method returns an error.
Run(
ctx context.Context, sess ExperimentSession,
measurement *Measurement, callbacks ExperimentCallbacks,
) error
// GetSummaryKeys returns summary keys expected by ooni/probe-cli.
GetSummaryKeys(*Measurement) (interface{}, error)
}
// Experiment is an experiment instance.
type Experiment interface {
// KibiBytesReceived accounts for the KibiBytes received by the experiment.
KibiBytesReceived() float64
// KibiBytesSent is like KibiBytesReceived but for the bytes sent.
KibiBytesSent() float64
// Name returns the experiment name.
Name() string
// GetSummaryKeys returns a data structure containing a
// summary of the test keys for ooniprobe.
GetSummaryKeys(m *Measurement) (any, error)
// ReportID returns the open report's ID, if we have opened a report
// successfully before, or an empty string, otherwise.
//
// Deprecated: new code should use a Submitter.
ReportID() string
// MeasureAsync runs an async measurement. This operation could post
// one or more measurements onto the returned channel. We'll close the
// channel when we've emitted all the measurements.
//
// Arguments:
//
// - ctx is the context for deadline/cancellation/timeout;
//
// - input is the input (typically a URL but it could also be
// just an endpoint or an empty string for input-less experiments
// such as, e.g., ndt7 and dash).
//
// Return value:
//
// - on success, channel where to post measurements (the channel
// will be closed when done) and nil error;
//
// - on failure, nil channel and non-nil error.
MeasureAsync(ctx context.Context, input string) (<-chan *Measurement, error)
// MeasureWithContext performs a synchronous measurement.
//
// Return value: strictly either a non-nil measurement and
// a nil error or a nil measurement and a non-nil error.
//
// CAVEAT: while this API is perfectly fine for experiments that
// return a single measurement, it will only return the first measurement
// when used with an asynchronous experiment.
MeasureWithContext(ctx context.Context, input string) (measurement *Measurement, err error)
// SaveMeasurement saves a measurement on the specified file path.
//
// Deprecated: new code should use a Saver.
SaveMeasurement(measurement *Measurement, filePath string) error
// SubmitAndUpdateMeasurementContext submits a measurement and updates the
// fields whose value has changed as part of the submission.
//
// Deprecated: new code should use a Submitter.
SubmitAndUpdateMeasurementContext(
ctx context.Context, measurement *Measurement) error
// OpenReportContext will open a report using the given context
// to possibly limit the lifetime of this operation.
//
// Deprecated: new code should use a Submitter.
OpenReportContext(ctx context.Context) error
}
// InputPolicy describes the experiment policy with respect to input. That is
// whether it requires input, optionally accepts input, does not want input.
type InputPolicy string
const (
// InputOrQueryBackend indicates that the experiment requires
// external input to run and that this kind of input is URLs
// from the citizenlab/test-lists repository. If this input
// not provided to the experiment, then the code that runs the
// experiment is supposed to fetch from URLs from OONI's backends.
InputOrQueryBackend = InputPolicy("or_query_backend")
// InputStrictlyRequired indicates that the experiment
// requires input and we currently don't have an API for
// fetching such input. Therefore, either the user specifies
// input or the experiment will fail for the lack of input.
InputStrictlyRequired = InputPolicy("strictly_required")
// InputOptional indicates that the experiment handles input,
// if any; otherwise it fetchs input/uses a default.
InputOptional = InputPolicy("optional")
// InputNone indicates that the experiment does not want any
// input and ignores the input if provided with it.
InputNone = InputPolicy("none")
// We gather input from StaticInput and SourceFiles. If there is
// input, we return it. Otherwise, we return an internal static
// list of inputs to be used with this experiment.
InputOrStaticDefault = InputPolicy("or_static_default")
)
// ExperimentBuilder builds an experiment.
type ExperimentBuilder interface {
// Interruptible tells you whether this is an interruptible experiment. This kind
// of experiments (e.g. ndt7) may be interrupted mid way.
Interruptible() bool
// InputPolicy returns the experiment input policy.
InputPolicy() InputPolicy
// Options returns information about the experiment's options.
Options() (map[string]ExperimentOptionInfo, error)
// SetOptionAny sets an option whose value is an any value. We will use reasonable
// heuristics to convert the any value to the proper type of the field whose name is
// contained by the key variable. If we cannot convert the provided any value to
// the proper type, then this function returns an error.
SetOptionAny(key string, value any) error
// SetOptionsAny sets options from a map[string]any. See the documentation of
// the SetOptionAny method for more information.
SetOptionsAny(options map[string]any) error
// SetCallbacks sets the experiment's interactive callbacks.
SetCallbacks(callbacks ExperimentCallbacks)
// NewExperiment creates the experiment instance.
NewExperiment() Experiment
}
// ExperimentOptionInfo contains info about an experiment option.
type ExperimentOptionInfo struct {
// Doc contains the documentation.
Doc string
// Type contains the type.
Type string
}
// ExperimentInputLoader loads inputs from local or remote sources.
type ExperimentInputLoader interface {
Load(ctx context.Context) ([]OOAPIURLInfo, error)
}
// Submitter submits a measurement to the OONI collector.
type Submitter interface {
// Submit submits the measurement and updates its
// report ID field in case of success.
Submit(ctx context.Context, m *Measurement) error
}
// Saver saves a measurement on some persistent storage.
type Saver interface {
SaveMeasurement(m *Measurement) error
}
// ExperimentInputProcessor processes inputs for an experiment.
type ExperimentInputProcessor interface {
Run(ctx context.Context) error
}