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:
Simone Basso
2021-02-02 12:05:47 +01:00
committed by GitHub
parent b1ce300c8d
commit d57c78bc71
535 changed files with 66182 additions and 23 deletions
@@ -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")
}
}