ooni-probe-cli/internal/engine/experiment/stunreachability/stunreachability.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

191 lines
5.3 KiB
Go

// Package stunreachability contains the STUN reachability experiment.
//
// See https://github.com/ooni/spec/blob/master/nettests/ts-025-stun-reachability.md.
package stunreachability
import (
"context"
"errors"
"fmt"
"net"
"net/url"
"time"
"github.com/ooni/probe-cli/v3/internal/engine/legacy/errorsx"
"github.com/ooni/probe-cli/v3/internal/engine/netx"
"github.com/ooni/probe-cli/v3/internal/engine/netx/archival"
"github.com/ooni/probe-cli/v3/internal/engine/netx/dialer"
"github.com/ooni/probe-cli/v3/internal/engine/netx/trace"
"github.com/ooni/probe-cli/v3/internal/model"
"github.com/pion/stun"
)
const (
testName = "stunreachability"
testVersion = "0.3.0"
)
// Config contains the experiment config.
type Config struct {
dialContext func(ctx context.Context, network, address string) (net.Conn, error)
newClient func(conn stun.Connection, options ...stun.ClientOption) (*stun.Client, error)
}
// TestKeys contains the experiment's result.
type TestKeys struct {
Endpoint string `json:"endpoint"`
Failure *string `json:"failure"`
NetworkEvents []archival.NetworkEvent `json:"network_events"`
Queries []archival.DNSQueryEntry `json:"queries"`
}
func registerExtensions(m *model.Measurement) {
archival.ExtDNS.AddTo(m)
archival.ExtNetevents.AddTo(m)
}
// Measurer performs the measurement.
type Measurer struct {
config Config
}
// ExperimentName implements ExperimentMeasurer.ExperiExperimentName.
func (m *Measurer) ExperimentName() string {
return testName
}
// ExperimentVersion implements ExperimentMeasurer.ExperimentVersion.
func (m *Measurer) ExperimentVersion() string {
return testVersion
}
func wrap(err error) error {
return errorsx.SafeErrWrapperBuilder{
Error: err,
Operation: "stun",
}.MaybeBuild()
}
// errStunMissingInput means that the user did not provide any input
var errStunMissingInput = errors.New("stun: missing input")
// errStunMissingPortInURL means the URL is missing the port
var errStunMissingPortInURL = errors.New("stun: missing port in URL")
// errUnsupportedURLScheme means we don't support the URL scheme
var errUnsupportedURLScheme = errors.New("stun: unsupported URL scheme")
// Run implements ExperimentMeasurer.Run.
func (m *Measurer) Run(
ctx context.Context, sess model.ExperimentSession,
measurement *model.Measurement, callbacks model.ExperimentCallbacks,
) error {
tk := new(TestKeys)
measurement.TestKeys = tk
registerExtensions(measurement)
input := string(measurement.Input)
if input == "" {
return errStunMissingInput
}
URL, err := url.Parse(input)
if err != nil {
return err
}
if URL.Port() == "" {
return errStunMissingPortInURL
}
if URL.Scheme != "stun" {
return errUnsupportedURLScheme
}
if err := wrap(tk.run(ctx, m.config, sess, measurement, callbacks, URL.Host)); err != nil {
s := err.Error()
tk.Failure = &s
return err
}
return nil
}
func (tk *TestKeys) run(
ctx context.Context, config Config, sess model.ExperimentSession,
measurement *model.Measurement, callbacks model.ExperimentCallbacks,
endpoint string,
) error {
callbacks.OnProgress(0, fmt.Sprintf("stunreachability: measuring: %s...", endpoint))
defer callbacks.OnProgress(
1, fmt.Sprintf("stunreachability: measuring: %s... done", endpoint))
tk.Endpoint = endpoint
saver := new(trace.Saver)
begin := time.Now()
err := tk.do(ctx, config, netx.NewDialer(netx.Config{
ContextByteCounting: true,
DialSaver: saver,
Logger: sess.Logger(),
ReadWriteSaver: saver,
ResolveSaver: saver,
}), endpoint)
events := saver.Read()
tk.NetworkEvents = append(
tk.NetworkEvents, archival.NewNetworkEventsList(begin, events)...,
)
tk.Queries = append(
tk.Queries, archival.NewDNSQueriesList(begin, events)...,
)
return err
}
func (tk *TestKeys) do(
ctx context.Context, config Config, dialer dialer.Dialer, endpoint string) error {
dialContext := dialer.DialContext
if config.dialContext != nil {
dialContext = config.dialContext
}
conn, err := dialContext(ctx, "udp", endpoint)
if err != nil {
return err
}
newClient := stun.NewClient
if config.newClient != nil {
newClient = config.newClient
}
client, err := newClient(conn)
if err != nil {
return err
}
defer client.Close()
message := stun.MustBuild(stun.TransactionID, stun.BindingRequest)
ch := make(chan error)
err = client.Start(message, func(ev stun.Event) {
// As mentioned below this code will run after Start has returned.
if ev.Error != nil {
ch <- ev.Error
return
}
var xorAddr stun.XORMappedAddress
ch <- xorAddr.GetFrom(ev.Message)
})
// Implementation note: if we successfully started, then the callback
// will be called when we receive a response or fail.
if err != nil {
return err
}
return <-ch
}
// 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
}