chore: merge probe-engine into probe-cli (#201)
This is how I did it: 1. `git clone https://github.com/ooni/probe-engine internal/engine` 2. ``` (cd internal/engine && git describe --tags) v0.23.0 ``` 3. `nvim go.mod` (merging `go.mod` with `internal/engine/go.mod` 4. `rm -rf internal/.git internal/engine/go.{mod,sum}` 5. `git add internal/engine` 6. `find . -type f -name \*.go -exec sed -i 's@/ooni/probe-engine@/ooni/probe-cli/v3/internal/engine@g' {} \;` 7. `go build ./...` (passes) 8. `go test -race ./...` (temporary failure on RiseupVPN) 9. `go mod tidy` 10. this commit message Once this piece of work is done, we can build a new version of `ooniprobe` that is using `internal/engine` directly. We need to do more work to ensure all the other functionality in `probe-engine` (e.g. making mobile packages) are still WAI. Part of https://github.com/ooni/probe/issues/1335
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
package stunreachability
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
type FakeConn struct {
|
||||
ReadError error
|
||||
ReadData []byte
|
||||
SetDeadlineError error
|
||||
SetReadDeadlineError error
|
||||
SetWriteDeadlineError error
|
||||
WriteError error
|
||||
}
|
||||
|
||||
func (c *FakeConn) Read(b []byte) (int, error) {
|
||||
if len(c.ReadData) > 0 {
|
||||
n := copy(b, c.ReadData)
|
||||
c.ReadData = c.ReadData[n:]
|
||||
return n, nil
|
||||
}
|
||||
if c.ReadError != nil {
|
||||
return 0, c.ReadError
|
||||
}
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
func (c *FakeConn) Write(b []byte) (n int, err error) {
|
||||
if c.WriteError != nil {
|
||||
return 0, c.WriteError
|
||||
}
|
||||
n = len(b)
|
||||
return
|
||||
}
|
||||
|
||||
func (*FakeConn) Close() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (*FakeConn) LocalAddr() net.Addr {
|
||||
return &net.TCPAddr{}
|
||||
}
|
||||
|
||||
func (*FakeConn) RemoteAddr() net.Addr {
|
||||
return &net.TCPAddr{}
|
||||
}
|
||||
|
||||
func (c *FakeConn) SetDeadline(t time.Time) (err error) {
|
||||
return c.SetDeadlineError
|
||||
}
|
||||
|
||||
func (c *FakeConn) SetReadDeadline(t time.Time) (err error) {
|
||||
return c.SetReadDeadlineError
|
||||
}
|
||||
|
||||
func (c *FakeConn) SetWriteDeadline(t time.Time) (err error) {
|
||||
return c.SetWriteDeadlineError
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
// 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"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
"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/errorx"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx/trace"
|
||||
"github.com/pion/stun"
|
||||
)
|
||||
|
||||
const (
|
||||
testName = "stun_reachability"
|
||||
testVersion = "0.1.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 errorx.SafeErrWrapperBuilder{
|
||||
Error: err,
|
||||
Operation: "stun",
|
||||
}.MaybeBuild()
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err := wrap(tk.run(ctx, m.config, sess, measurement, callbacks)); 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,
|
||||
) error {
|
||||
const defaultAddress = "stun.l.google.com:19302"
|
||||
endpoint := string(measurement.Input)
|
||||
if endpoint == "" {
|
||||
endpoint = defaultAddress
|
||||
}
|
||||
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, sess.ASNDatabasePath())...,
|
||||
)
|
||||
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
|
||||
}
|
||||
defer conn.Close()
|
||||
newClient := stun.NewClient
|
||||
if config.newClient != nil {
|
||||
newClient = config.newClient
|
||||
}
|
||||
client, err := newClient(conn, stun.WithNoConnClose)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package stunreachability
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/pion/stun"
|
||||
)
|
||||
|
||||
func (c *Config) SetNewClient(
|
||||
f func(conn stun.Connection, options ...stun.ClientOption) (*stun.Client, error)) {
|
||||
c.newClient = f
|
||||
}
|
||||
|
||||
func (c *Config) SetDialContext(
|
||||
f func(ctx context.Context, network, address string) (net.Conn, error)) {
|
||||
c.dialContext = f
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
package stunreachability_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/stunreachability"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx/errorx"
|
||||
"github.com/pion/stun"
|
||||
)
|
||||
|
||||
func TestMeasurerExperimentNameVersion(t *testing.T) {
|
||||
measurer := stunreachability.NewExperimentMeasurer(stunreachability.Config{})
|
||||
if measurer.ExperimentName() != "stun_reachability" {
|
||||
t.Fatal("unexpected ExperimentName")
|
||||
}
|
||||
if measurer.ExperimentVersion() != "0.1.0" {
|
||||
t.Fatal("unexpected ExperimentVersion")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun(t *testing.T) {
|
||||
if os.Getenv("GITHUB_ACTIONS") == "true" {
|
||||
// See https://github.com/ooni/probe-cli/v3/internal/engine/issues/874#issuecomment-679850652
|
||||
t.Skip("skipping broken test on GitHub Actions")
|
||||
}
|
||||
measurer := stunreachability.NewExperimentMeasurer(stunreachability.Config{})
|
||||
measurement := new(model.Measurement)
|
||||
err := measurer.Run(
|
||||
context.Background(),
|
||||
&mockable.Session{},
|
||||
measurement,
|
||||
model.NewPrinterCallbacks(log.Log),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tk := measurement.TestKeys.(*stunreachability.TestKeys)
|
||||
if tk.Failure != nil {
|
||||
t.Fatal("expected nil failure here")
|
||||
}
|
||||
if tk.Endpoint != "stun.l.google.com:19302" {
|
||||
t.Fatal("unexpected endpoint")
|
||||
}
|
||||
if len(tk.NetworkEvents) <= 0 {
|
||||
t.Fatal("no network events?!")
|
||||
}
|
||||
if len(tk.Queries) <= 0 {
|
||||
t.Fatal("no DNS queries?!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCustomInput(t *testing.T) {
|
||||
input := "stun.ekiga.net:3478"
|
||||
measurer := stunreachability.NewExperimentMeasurer(stunreachability.Config{})
|
||||
measurement := new(model.Measurement)
|
||||
measurement.Input = model.MeasurementTarget(input)
|
||||
err := measurer.Run(
|
||||
context.Background(),
|
||||
&mockable.Session{},
|
||||
measurement,
|
||||
model.NewPrinterCallbacks(log.Log),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tk := measurement.TestKeys.(*stunreachability.TestKeys)
|
||||
if tk.Failure != nil {
|
||||
t.Fatal("expected nil failure here")
|
||||
}
|
||||
if tk.Endpoint != input {
|
||||
t.Fatal("unexpected endpoint")
|
||||
}
|
||||
if len(tk.NetworkEvents) <= 0 {
|
||||
t.Fatal("no network events?!")
|
||||
}
|
||||
if len(tk.Queries) <= 0 {
|
||||
t.Fatal("no DNS queries?!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancelledContext(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // immediately fail everything
|
||||
measurer := stunreachability.NewExperimentMeasurer(stunreachability.Config{})
|
||||
measurement := new(model.Measurement)
|
||||
err := measurer.Run(
|
||||
ctx,
|
||||
&mockable.Session{},
|
||||
measurement,
|
||||
model.NewPrinterCallbacks(log.Log),
|
||||
)
|
||||
if err.Error() != "interrupted" {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
tk := measurement.TestKeys.(*stunreachability.TestKeys)
|
||||
if *tk.Failure != "interrupted" {
|
||||
t.Fatal("expected different failure here")
|
||||
}
|
||||
if tk.Endpoint != "stun.l.google.com:19302" {
|
||||
t.Fatal("unexpected endpoint")
|
||||
}
|
||||
if len(tk.NetworkEvents) <= 0 {
|
||||
t.Fatal("no network events?!")
|
||||
}
|
||||
if len(tk.Queries) <= 0 {
|
||||
t.Fatal("no DNS queries?!")
|
||||
}
|
||||
sk, err := measurer.GetSummaryKeys(measurement)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := sk.(stunreachability.SummaryKeys); !ok {
|
||||
t.Fatal("invalid type for summary keys")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewClientFailure(t *testing.T) {
|
||||
config := &stunreachability.Config{}
|
||||
expected := errors.New("mocked error")
|
||||
config.SetNewClient(
|
||||
func(conn stun.Connection, options ...stun.ClientOption) (*stun.Client, error) {
|
||||
return nil, expected
|
||||
})
|
||||
measurer := stunreachability.NewExperimentMeasurer(*config)
|
||||
measurement := new(model.Measurement)
|
||||
err := measurer.Run(
|
||||
context.Background(),
|
||||
&mockable.Session{},
|
||||
measurement,
|
||||
model.NewPrinterCallbacks(log.Log),
|
||||
)
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
tk := measurement.TestKeys.(*stunreachability.TestKeys)
|
||||
if !strings.HasPrefix(*tk.Failure, "unknown_failure") {
|
||||
t.Fatal("expected different failure here")
|
||||
}
|
||||
if tk.Endpoint != "stun.l.google.com:19302" {
|
||||
t.Fatal("unexpected endpoint")
|
||||
}
|
||||
if len(tk.NetworkEvents) <= 0 {
|
||||
t.Fatal("no network events?!")
|
||||
}
|
||||
if len(tk.Queries) <= 0 {
|
||||
t.Fatal("no DNS queries?!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartFailure(t *testing.T) {
|
||||
config := &stunreachability.Config{}
|
||||
expected := errors.New("mocked error")
|
||||
config.SetDialContext(
|
||||
func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
conn := &stunreachability.FakeConn{WriteError: expected}
|
||||
return conn, nil
|
||||
})
|
||||
measurer := stunreachability.NewExperimentMeasurer(*config)
|
||||
measurement := new(model.Measurement)
|
||||
err := measurer.Run(
|
||||
context.Background(),
|
||||
&mockable.Session{},
|
||||
measurement,
|
||||
model.NewPrinterCallbacks(log.Log),
|
||||
)
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
tk := measurement.TestKeys.(*stunreachability.TestKeys)
|
||||
if !strings.HasPrefix(*tk.Failure, "unknown_failure") {
|
||||
t.Fatal("expected different failure here")
|
||||
}
|
||||
if tk.Endpoint != "stun.l.google.com:19302" {
|
||||
t.Fatal("unexpected endpoint")
|
||||
}
|
||||
// We're bypassing normal network with custom dial function
|
||||
if len(tk.NetworkEvents) > 0 {
|
||||
t.Fatal("network events?!")
|
||||
}
|
||||
if len(tk.Queries) > 0 {
|
||||
t.Fatal("DNS queries?!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFailure(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skip test in short mode")
|
||||
}
|
||||
config := &stunreachability.Config{}
|
||||
expected := errors.New("mocked error")
|
||||
config.SetDialContext(
|
||||
func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
conn := &stunreachability.FakeConn{ReadError: expected}
|
||||
return conn, nil
|
||||
})
|
||||
measurer := stunreachability.NewExperimentMeasurer(*config)
|
||||
measurement := new(model.Measurement)
|
||||
err := measurer.Run(
|
||||
context.Background(),
|
||||
&mockable.Session{},
|
||||
measurement,
|
||||
model.NewPrinterCallbacks(log.Log),
|
||||
)
|
||||
if !errors.Is(err, stun.ErrTransactionTimeOut) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
tk := measurement.TestKeys.(*stunreachability.TestKeys)
|
||||
if *tk.Failure != errorx.FailureGenericTimeoutError {
|
||||
t.Fatal("expected different failure here")
|
||||
}
|
||||
if tk.Endpoint != "stun.l.google.com:19302" {
|
||||
t.Fatal("unexpected endpoint")
|
||||
}
|
||||
// We're bypassing normal network with custom dial function
|
||||
if len(tk.NetworkEvents) > 0 {
|
||||
t.Fatal("network events?!")
|
||||
}
|
||||
if len(tk.Queries) > 0 {
|
||||
t.Fatal("DNS queries?!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSummaryKeysGeneric(t *testing.T) {
|
||||
measurement := &model.Measurement{TestKeys: &stunreachability.TestKeys{}}
|
||||
m := &stunreachability.Measurer{}
|
||||
osk, err := m.GetSummaryKeys(measurement)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sk := osk.(stunreachability.SummaryKeys)
|
||||
if sk.IsAnomaly {
|
||||
t.Fatal("invalid isAnomaly")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user