From 22525001ab61ef1c852625669086df04ae8a6f73 Mon Sep 17 00:00:00 2001 From: ain ghazal Date: Wed, 5 Jun 2024 15:56:05 +0200 Subject: [PATCH 01/14] feat: introduce the wireguard experiment --- go.mod | 5 +- go.sum | 8 + internal/experiment/wireguard/config.go | 94 ++++++ internal/experiment/wireguard/testkeys.go | 27 ++ internal/experiment/wireguard/wireguard.go | 306 ++++++++++++++++++ .../experiment/wireguard/wireguard_test.go | 57 ++++ internal/registry/wireguard.go | 28 ++ 7 files changed, 524 insertions(+), 1 deletion(-) create mode 100644 internal/experiment/wireguard/config.go create mode 100644 internal/experiment/wireguard/testkeys.go create mode 100644 internal/experiment/wireguard/wireguard.go create mode 100644 internal/experiment/wireguard/wireguard_test.go create mode 100644 internal/registry/wireguard.go diff --git a/go.mod b/go.mod index 506266d1a..9d73eac8e 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/AlecAivazis/survey/v2 v2.3.7 github.com/Psiphon-Labs/psiphon-tunnel-core v1.0.11-0.20240424194431-3612a5a6fb4c github.com/alecthomas/kingpin/v2 v2.4.0 + github.com/amnezia-vpn/amneziawg-go v0.2.8 github.com/apex/log v1.9.0 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 github.com/cloudflare/circl v1.3.8 @@ -81,6 +82,7 @@ require ( github.com/segmentio/fasthash v1.0.3 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/testify v1.9.0 // indirect + github.com/tevino/abool/v2 v2.1.0 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect gitlab.com/yawning/edwards25519-extra v0.0.0-20231005122941-2149dcafc266 // indirect go.uber.org/mock v0.4.0 // indirect @@ -88,8 +90,9 @@ require ( golang.org/x/exp/typeparams v0.0.0-20230522175609-2e198f4a06a1 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/time v0.5.0 // indirect + golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - gvisor.dev/gvisor v0.0.0-20230922204349-b3f36d574a7f // indirect + gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 // indirect ) require ( diff --git a/go.sum b/go.sum index c54eab98f..0fcac7a9e 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,8 @@ github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjH github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 h1:ez/4by2iGztzR4L0zgAOR8lTQK9VlyBVVd7G4omaOQs= github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/amnezia-vpn/amneziawg-go v0.2.8 h1:J8PPx+hylx5nNZ5U1+ECFj9noGkcm2ThmSV9rBNDgy8= +github.com/amnezia-vpn/amneziawg-go v0.2.8/go.mod h1:12g0XRbFeGbpXvuCmBOV21YxLWSFnUFJnwgrzyHBUyk= github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/apex/log v1.9.0 h1:FHtw/xuaM8AgmvDDTI9fiwoAL25Sq2cxojnZICUU8l0= @@ -531,6 +533,8 @@ github.com/templexxx/cpu v0.1.0/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6H github.com/templexxx/xorsimd v0.4.1/go.mod h1:W+ffZz8jJMH2SXwuKu9WhygqBMbFnp14G2fqEr8qaNo= github.com/templexxx/xorsimd v0.4.2 h1:ocZZ+Nvu65LGHmCLZ7OoCtg8Fx8jnHKK37SjvngUoVI= github.com/templexxx/xorsimd v0.4.2/go.mod h1:HgwaPoDREdi6OnULpSfxhzaiiSUY4Fi3JPn1wpt28NI= +github.com/tevino/abool/v2 v2.1.0 h1:7w+Vf9f/5gmKT4m4qkayb33/92M+Um45F2BkHOR+L/c= +github.com/tevino/abool/v2 v2.1.0/go.mod h1:+Lmlqk6bHDWHqN1cbxqhwEAwMPXgc8I1SDEamtseuXY= github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk= github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk= @@ -757,6 +761,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= @@ -800,6 +806,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gvisor.dev/gvisor v0.0.0-20230922204349-b3f36d574a7f h1:w4K7S8+VKrhX67mFdUymQUsGVbEElPCN0v7U0DoLpUw= gvisor.dev/gvisor v0.0.0-20230922204349-b3f36d574a7f/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY= +gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ= +gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= diff --git a/internal/experiment/wireguard/config.go b/internal/experiment/wireguard/config.go new file mode 100644 index 000000000..e76afb965 --- /dev/null +++ b/internal/experiment/wireguard/config.go @@ -0,0 +1,94 @@ +package wireguard + +import ( + "encoding/base64" + "encoding/hex" +) + +// Config contains the experiment config. +// +// This contains all the settings that user can set to modify the behaviour +// of this experiment. By tagging these variables with `ooni:"..."`, we allow +// miniooni's -O flag to find them and set them. +type Config struct { + ConfigFile string `ooni:"Configuration file for the WireGuard experiment"` + + // TODO(ainghzal): honor it + PublicTarget bool `ooni:"Treat the target endpoint as public data (if true, it will be included in the report)` + + Verbose bool `ooni:"Use extra-verbose mode in wireguard logs"` + + // Safe_XXX options are not sent to the backend for archival. + SafeRemote string `ooni:"Remote to connect to using WireGuard"` + SafePrivateKey string `ooni:"Private key to connect to remote"` + SafePublicKey string `ooni:"Public key of the remote"` + SafePresharedKey string `ooni:"Pre-shared key for authentication"` + SafeIP string `ooni:"Allocated IP for this peer"` + + // Optional obfuscation parameters for AmneziaWG + Jc string `ooni:"jc"` + Jmin string `ooni:"jmin"` + Jmax string `ooni:"jmax"` + S1 string `ooni:"s1"` + S2 string `ooni:"s2"` + H1 string `ooni:"h1"` + H2 string `ooni:"h2"` + H3 string `ooni:"h3"` + H4 string `ooni:"h4"` +} + +type options struct { + // common wireguard parameters + endpoint string + ip string + pubKey string + privKey string + presharedKey string + ns string + + // parameters from AmneziaWG + // TODO(ainghazal: make these optional) + jc string + jmin string + jmax string + s1 string + s2 string + h1 string + h2 string + h3 string + h4 string +} + +func getOptionsFromConfig(c Config) (options, error) { + o := options{} + + pub, _ := base64.StdEncoding.DecodeString(c.SafePublicKey) + pubHex := hex.EncodeToString(pub) + o.pubKey = pubHex + + priv, _ := base64.StdEncoding.DecodeString(c.SafePrivateKey) + privHex := hex.EncodeToString(priv) + o.privKey = privHex + + psk, _ := base64.StdEncoding.DecodeString(c.SafePresharedKey) + pskHex := hex.EncodeToString(psk) + o.presharedKey = pskHex + + o.ip = c.SafeIP + + // TODO: reconcile this with Input if c.PublicTarget=true + o.endpoint = c.SafeRemote + + o.jc = c.Jc + o.jmin = c.Jmin + o.jmax = c.Jmax + o.s1 = c.S1 + o.s2 = c.S2 + o.h1 = c.H1 + o.h2 = c.H2 + o.h3 = c.H3 + o.h4 = c.H4 + + o.ns = defaultNameserver + return o, nil +} diff --git a/internal/experiment/wireguard/testkeys.go b/internal/experiment/wireguard/testkeys.go new file mode 100644 index 000000000..b84e13a8c --- /dev/null +++ b/internal/experiment/wireguard/testkeys.go @@ -0,0 +1,27 @@ +package wireguard + +// TestKeys contains the experiment's result. +// +// This is what will end up into the Measurement.TestKeys field +// when you run this experiment. +// +// In other words, the variables in this struct will be +// the specific results of this experiment. +type TestKeys struct { + Success bool `json:"success"` + Failure *string `json:"failure"` + NetworkEvents []*Event `json:"network_events"` + URLGet []*URLGetResult `json:"urlget"` +} + +// URLGetResult is the result of fetching a URL via the wireguard tunnel, +// using the standard library. +type URLGetResult struct { + ByteCount int `json:"bytes,omitempty"` + Error string `json:"error,omitempty"` + Failure *string `json:"failure"` + StatusCode int `json:"status_code"` + T0 float64 `json:"t0"` + T float64 `json:"t"` + URL string `json:"url"` +} diff --git a/internal/experiment/wireguard/wireguard.go b/internal/experiment/wireguard/wireguard.go new file mode 100644 index 000000000..2142ab7c9 --- /dev/null +++ b/internal/experiment/wireguard/wireguard.go @@ -0,0 +1,306 @@ +// Package wireguard contains the wireguard experiment. +package wireguard + +import ( + "context" + "fmt" + "io" + "net/http" + "net/netip" + "os" + "strings" + "time" + + "github.com/ooni/probe-cli/v3/internal/measurexlite" + "github.com/ooni/probe-cli/v3/internal/model" + "github.com/ooni/probe-cli/v3/internal/runtimex" + + "github.com/amnezia-vpn/amneziawg-go/conn" + "github.com/amnezia-vpn/amneziawg-go/device" + "github.com/amnezia-vpn/amneziawg-go/tun" + "github.com/amnezia-vpn/amneziawg-go/tun/netstack" +) + +const ( + testName = "wireguard" + testVersion = "0.1.1" + + // defaultNameserver is the dns server using for resolving names inside the wg tunnel. + defaultNameserver = "8.8.8.8" +) + +// Measurer performs the measurement. +type Measurer struct { + config Config + rawconfig []byte + options options + + events *eventLogger + testName string + tnet *netstack.Net +} + +// ExperimentName implements model.ExperimentMeasurer.ExperimentName. +func (m Measurer) ExperimentName() string { + return m.testName +} + +// ExperimentVersion implements model.ExperimentMeasurer.ExperimentVersion. +func (m Measurer) ExperimentVersion() string { + return testVersion +} + +// Run implements model.ExperimentMeasurer.Run. +func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { + measurement := args.Measurement + zeroTime := measurement.MeasurementStartTimeSaved + + sess := args.Session + + var err error + + // 1. setup (parse config file) + err = m.setupConfig() + sess.Logger().Debug(string(m.rawconfig)) + if err != nil { + return err + } + + // 2. create tunnel + err = m.createTunnel(sess, zeroTime) + + testkeys := &TestKeys{ + Success: err == nil, + Failure: measurexlite.NewFailure(err), + URLGet: make([]*URLGetResult, 0), + } + + // 3. use tunnel + if err == nil { + sess.Logger().Info("Using the wireguard tunnel.") + urlgetResult := m.urlget(zeroTime, sess.Logger()) + testkeys.URLGet = append(testkeys.URLGet, urlgetResult) + testkeys.NetworkEvents = m.events.log() + } + + measurement.TestKeys = testkeys + sess.Logger().Infof("%s", "Wireguard experiment done.") + + // NOTE: important to return nil to submit measurement. + return nil +} + +func (m *Measurer) setupConfig() error { + cfg := readConfigFromFile(m.config.ConfigFile) + opts, err := getOptionsFromConfig(m.config) + if err != nil { + return err + } + m.options = opts + if len(cfg) > 0 { + m.rawconfig = cfg + } + return nil +} + +func (m *Measurer) createTunnel(sess model.ExperimentSession, zeroTime time.Time) error { + sess.Logger().Info("wireguard: create tunnel") + sess.Logger().Infof("endpoint: %s", m.options.endpoint) + + _, tnet, err := m.configureWireguardInterface(sess.Logger(), m.events, zeroTime) + if err != nil { + return err + } + m.tnet = tnet + + sess.Logger().Info("wireguard: create tunnel done") + return nil +} + +func newURLResultFromError(url string, zeroTime time.Time, start float64, err error) *URLGetResult { + return &URLGetResult{ + URL: url, + T0: start, + T: time.Since(zeroTime).Seconds(), + Failure: measurexlite.NewFailure(err), + Error: err.Error(), + } +} + +func newURLResultWithStatusCode(url string, zeroTime time.Time, start float64, statusCode int, body []byte) *URLGetResult { + return &URLGetResult{ + ByteCount: len(body), + URL: url, + T0: start, + T: time.Since(zeroTime).Seconds(), + StatusCode: statusCode, + } +} + +func (m *Measurer) urlget(zeroTime time.Time, logger model.Logger) *URLGetResult { + url := "https://info.cern.ch/" + client := http.Client{ + Timeout: 30 * time.Second, + Transport: &http.Transport{ + DialContext: m.tnet.DialContext, + TLSHandshakeTimeout: 30 * time.Second, + }} + + start := time.Since(zeroTime).Seconds() + r, err := client.Get(url) + if err != nil { + logger.Warnf("urlget error: %v", err.Error()) + return newURLResultFromError(url, zeroTime, start, err) + } + body, err := io.ReadAll(r.Body) + if err != nil { + logger.Warnf("urlget error: %v", err.Error()) + return newURLResultFromError(url, zeroTime, start, err) + } + defer r.Body.Close() + + return newURLResultWithStatusCode(url, zeroTime, start, r.StatusCode, body) +} + +// NewExperimentMeasurer creates a new ExperimentMeasurer. +func NewExperimentMeasurer(config Config) model.ExperimentMeasurer { + return Measurer{ + config: config, + events: newEventLogger(), + options: options{}, + testName: testName, + } +} + +func readConfigFromFile(path string) []byte { + cfg, err := os.ReadFile(path) + if err != nil { + return []byte{} + } + runtimex.PanicOnError(err, "cannot open file") + return cfg +} + +func (m *Measurer) configureWireguardInterface( + logger model.Logger, + eventlogger *eventLogger, + zeroTime time.Time) (tun.Device, *netstack.Net, error) { + devTun, tnet, err := netstack.CreateNetTUN( + []netip.Addr{netip.MustParseAddr(m.options.ip)}, + []netip.Addr{netip.MustParseAddr(m.options.ns)}, + 1420) + if err != nil { + return nil, nil, err + } + + dev := device.NewDevice( + devTun, + conn.NewDefaultBind(), + newWireguardLogger(logger, eventlogger, m.config.Verbose, zeroTime), + ) + + var ipcStr string + + // If the rawconfig string has content, it means that we + // did not bother to pass every option separatedly, so we assume + // we got a valid config file. This might be dangerous, so think twice + // about enforcing proper validation of the configuration file. + if len(m.rawconfig) > 0 { + ipcStr = string(m.rawconfig) + } else { + opts := m.options + + ipcStr = `jc=` + opts.jc + ` +jmin=` + opts.jmin + ` +jmax=` + opts.jmax + ` +s1=` + opts.s1 + ` +s2=` + opts.s2 + ` +h1=` + opts.h1 + ` +h2=` + opts.h2 + ` +h3=` + opts.h3 + ` +h4=` + opts.h4 + ` +private_key=` + opts.privKey + ` +public_key=` + opts.pubKey + ` +preshared_key=` + opts.presharedKey + ` +endpoint=` + opts.endpoint + ` +allowed_ip=0.0.0.0/0 +` + } + fmt.Println(ipcStr) + dev.IpcSet(ipcStr) + + err = dev.Up() + if err != nil { + return nil, nil, err + } + return devTun, tnet, nil +} + +// Event is a network event obtained by parsing wireguard logs. +type Event struct { + EventType string `json:"operation"` + T float64 `json:"t"` +} + +func newEvent(etype string) *Event { + return &Event{ + EventType: etype, + } +} + +type eventLogger struct { + events []*Event +} + +func newEventLogger() *eventLogger { + return &eventLogger{events: make([]*Event, 0)} +} + +func (el *eventLogger) append(e *Event) { + el.events = append(el.events, e) +} + +func (el *eventLogger) log() []*Event { + return el.events +} + +func newWireguardLogger( + logger model.Logger, + eventlogger *eventLogger, + verbose bool, + zeroTime time.Time) *device.Logger { + verbosef := func(format string, args ...any) { + msg := fmt.Sprintf(format, args...) + + if verbose { + logger.Debugf(msg) + } + + // TODO(ainghazal): we might be interested in parsing other type of events here. + if strings.Contains(msg, "Receiving keepalive packet") { + evt := newEvent("RECV_KEEPALIVE") + evt.T = time.Since(zeroTime).Seconds() + eventlogger.append(evt) + return + } + if strings.Contains(msg, "Sending handshake initiation") { + evt := newEvent("SEND_HANDSHAKE_INIT") + evt.T = time.Since(zeroTime).Seconds() + eventlogger.append(evt) + return + } + if strings.Contains(msg, "Received handshake response") { + evt := newEvent("RECV_HANDSHAKE_RESP") + evt.T = time.Since(zeroTime).Seconds() + eventlogger.append(evt) + return + } + } + errorf := func(format string, args ...any) { + logger.Warnf(format, args...) + } + return &device.Logger{ + Verbosef: verbosef, + Errorf: errorf, + } +} diff --git a/internal/experiment/wireguard/wireguard_test.go b/internal/experiment/wireguard/wireguard_test.go new file mode 100644 index 000000000..ec322f7d6 --- /dev/null +++ b/internal/experiment/wireguard/wireguard_test.go @@ -0,0 +1,57 @@ +package wireguard_test + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/apex/log" + "github.com/ooni/probe-cli/v3/internal/experiment/example" + "github.com/ooni/probe-cli/v3/internal/legacy/mockable" + "github.com/ooni/probe-cli/v3/internal/model" +) + +func TestSuccess(t *testing.T) { + m := example.NewExperimentMeasurer(example.Config{ + SleepTime: int64(2 * time.Millisecond), + }, "example") + if m.ExperimentName() != "example" { + t.Fatal("invalid ExperimentName") + } + if m.ExperimentVersion() != "0.1.0" { + t.Fatal("invalid ExperimentVersion") + } + ctx := context.Background() + sess := &mockable.Session{MockableLogger: log.Log} + callbacks := model.NewPrinterCallbacks(sess.Logger()) + measurement := new(model.Measurement) + args := &model.ExperimentArgs{ + Callbacks: callbacks, + Measurement: measurement, + Session: sess, + } + err := m.Run(ctx, args) + if err != nil { + t.Fatal(err) + } +} + +func TestFailure(t *testing.T) { + m := example.NewExperimentMeasurer(example.Config{ + SleepTime: int64(2 * time.Millisecond), + ReturnError: true, + }, "example") + ctx := context.Background() + sess := &mockable.Session{MockableLogger: log.Log} + callbacks := model.NewPrinterCallbacks(sess.Logger()) + args := &model.ExperimentArgs{ + Callbacks: callbacks, + Measurement: new(model.Measurement), + Session: sess, + } + err := m.Run(ctx, args) + if !errors.Is(err, example.ErrFailure) { + t.Fatal("expected an error here") + } +} diff --git a/internal/registry/wireguard.go b/internal/registry/wireguard.go new file mode 100644 index 000000000..9ca9a6f33 --- /dev/null +++ b/internal/registry/wireguard.go @@ -0,0 +1,28 @@ +package registry + +// +// Registers the `wireguard' experiment. +// + +import ( + "github.com/ooni/probe-cli/v3/internal/experiment/wireguard" + "github.com/ooni/probe-cli/v3/internal/model" +) + +func init() { + const canonicalName = "wireguard" + AllExperiments["wireguard"] = func() *Factory { + return &Factory{ + build: func(config interface{}) model.ExperimentMeasurer { + return wireguard.NewExperimentMeasurer( + *config.(*wireguard.Config), + ) + }, + canonicalName: canonicalName, + config: &wireguard.Config{}, + enabledByDefault: true, + interruptible: true, + inputPolicy: model.InputNone, + } + } +} From 8f14841db61d0fb8845038642367d71e003c70ee Mon Sep 17 00:00:00 2001 From: ain ghazal Date: Mon, 24 Jun 2024 12:45:56 +0200 Subject: [PATCH 02/14] use richer input targets --- internal/experiment/wireguard/config.go | 2 +- internal/experiment/wireguard/doc.go | 2 + internal/experiment/wireguard/richerinput.go | 86 +++++++++++++++++++ internal/experiment/wireguard/wireguard.go | 46 +++++----- .../experiment/wireguard/wireguard_test.go | 15 ++-- internal/registry/wireguard.go | 9 +- 6 files changed, 119 insertions(+), 41 deletions(-) create mode 100644 internal/experiment/wireguard/doc.go create mode 100644 internal/experiment/wireguard/richerinput.go diff --git a/internal/experiment/wireguard/config.go b/internal/experiment/wireguard/config.go index e76afb965..cb05a10bb 100644 --- a/internal/experiment/wireguard/config.go +++ b/internal/experiment/wireguard/config.go @@ -59,7 +59,7 @@ type options struct { h4 string } -func getOptionsFromConfig(c Config) (options, error) { +func getOptionsFromConfig(c *Config) (options, error) { o := options{} pub, _ := base64.StdEncoding.DecodeString(c.SafePublicKey) diff --git a/internal/experiment/wireguard/doc.go b/internal/experiment/wireguard/doc.go new file mode 100644 index 000000000..e0cc371ce --- /dev/null +++ b/internal/experiment/wireguard/doc.go @@ -0,0 +1,2 @@ +// Package wireguard contains the wireguard experiment. +package wireguard diff --git a/internal/experiment/wireguard/richerinput.go b/internal/experiment/wireguard/richerinput.go new file mode 100644 index 000000000..e4321d6e5 --- /dev/null +++ b/internal/experiment/wireguard/richerinput.go @@ -0,0 +1,86 @@ +package wireguard + +import ( + "context" + + "github.com/ooni/probe-cli/v3/internal/model" + "github.com/ooni/probe-cli/v3/internal/targetloading" +) + +// Target is a richer-input target that this experiment should measure. +type Target struct { + // Options contains the configuration. + Options *Config + + // URL is the input URL. + URL string +} + +var _ model.ExperimentTarget = &Target{} + +// Category implements [model.ExperimentTarget]. +func (t *Target) Category() string { + return model.DefaultCategoryCode +} + +// Country implements [model.ExperimentTarget]. +func (t *Target) Country() string { + return model.DefaultCountryCode +} + +// Input implements [model.ExperimentTarget]. +func (t *Target) Input() string { + return t.URL +} + +// String implements [model.ExperimentTarget]. +func (t *Target) String() string { + return t.URL +} + +// NewLoader constructs a new [model.ExperimentTargerLoader] instance. +// +// This function PANICS if options is not an instance of [*openvpn.Config]. +func NewLoader(loader *targetloading.Loader, gopts any) model.ExperimentTargetLoader { + // Panic if we cannot convert the options to the expected type. + // + // We do not expect a panic here because the type is managed by the registry package. + options := gopts.(*Config) + + // Construct the proper loader instance. + return &targetLoader{ + loader: loader, + options: options, + session: loader.Session, + } +} + +// targetLoader loads targets for this experiment. +type targetLoader struct { + loader *targetloading.Loader + options *Config + session targetloading.Session +} + +// Load implements model.ExperimentTargetLoader. +func (tl *targetLoader) Load(ctx context.Context) ([]model.ExperimentTarget, error) { + // TODO(ainghazal): implement remote loading when backend is ready. + + // Attempt to load the static inputs from CLI and files + inputs, err := targetloading.LoadStatic(tl.loader) + + // Handle the case where we couldn't load from CLI or files + if err != nil { + return nil, err + } + + // Build the list of targets that we should measure. + var targets []model.ExperimentTarget + for _, input := range inputs { + targets = append(targets, &Target{ + Options: tl.options, + URL: input, + }) + } + return targets, nil +} diff --git a/internal/experiment/wireguard/wireguard.go b/internal/experiment/wireguard/wireguard.go index 2142ab7c9..5fc8c60a9 100644 --- a/internal/experiment/wireguard/wireguard.go +++ b/internal/experiment/wireguard/wireguard.go @@ -1,19 +1,17 @@ -// Package wireguard contains the wireguard experiment. package wireguard import ( "context" + "errors" "fmt" "io" "net/http" "net/netip" - "os" "strings" "time" "github.com/ooni/probe-cli/v3/internal/measurexlite" "github.com/ooni/probe-cli/v3/internal/model" - "github.com/ooni/probe-cli/v3/internal/runtimex" "github.com/amnezia-vpn/amneziawg-go/conn" "github.com/amnezia-vpn/amneziawg-go/device" @@ -29,6 +27,10 @@ const ( defaultNameserver = "8.8.8.8" ) +var ( + ErrInputRequired = errors.New("input is required") +) + // Measurer performs the measurement. type Measurer struct { config Config @@ -53,16 +55,23 @@ func (m Measurer) ExperimentVersion() string { // Run implements model.ExperimentMeasurer.Run. func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { measurement := args.Measurement - zeroTime := measurement.MeasurementStartTimeSaved - sess := args.Session + zeroTime := measurement.MeasurementStartTimeSaved var err error + // 0. obtain the richer input target, config, and input or panic + if args.Target == nil { + return ErrInputRequired + } + // 1. setup (parse config file) - err = m.setupConfig() - sess.Logger().Debug(string(m.rawconfig)) - if err != nil { + target := args.Target.(*Target) + + // TODO(ainghazal): process the input when the backend hands us one. + config, _ := target.Options, target.URL + + if err := m.setupConfig(config); err != nil { return err } @@ -90,16 +99,12 @@ func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { return nil } -func (m *Measurer) setupConfig() error { - cfg := readConfigFromFile(m.config.ConfigFile) - opts, err := getOptionsFromConfig(m.config) +func (m *Measurer) setupConfig(config *Config) error { + opts, err := getOptionsFromConfig(config) if err != nil { return err } m.options = opts - if len(cfg) > 0 { - m.rawconfig = cfg - } return nil } @@ -163,24 +168,14 @@ func (m *Measurer) urlget(zeroTime time.Time, logger model.Logger) *URLGetResult } // NewExperimentMeasurer creates a new ExperimentMeasurer. -func NewExperimentMeasurer(config Config) model.ExperimentMeasurer { +func NewExperimentMeasurer() model.ExperimentMeasurer { return Measurer{ - config: config, events: newEventLogger(), options: options{}, testName: testName, } } -func readConfigFromFile(path string) []byte { - cfg, err := os.ReadFile(path) - if err != nil { - return []byte{} - } - runtimex.PanicOnError(err, "cannot open file") - return cfg -} - func (m *Measurer) configureWireguardInterface( logger model.Logger, eventlogger *eventLogger, @@ -226,7 +221,6 @@ endpoint=` + opts.endpoint + ` allowed_ip=0.0.0.0/0 ` } - fmt.Println(ipcStr) dev.IpcSet(ipcStr) err = dev.Up() diff --git a/internal/experiment/wireguard/wireguard_test.go b/internal/experiment/wireguard/wireguard_test.go index ec322f7d6..0cb7975a3 100644 --- a/internal/experiment/wireguard/wireguard_test.go +++ b/internal/experiment/wireguard/wireguard_test.go @@ -4,22 +4,20 @@ import ( "context" "errors" "testing" - "time" "github.com/apex/log" "github.com/ooni/probe-cli/v3/internal/experiment/example" + "github.com/ooni/probe-cli/v3/internal/experiment/wireguard" "github.com/ooni/probe-cli/v3/internal/legacy/mockable" "github.com/ooni/probe-cli/v3/internal/model" ) func TestSuccess(t *testing.T) { - m := example.NewExperimentMeasurer(example.Config{ - SleepTime: int64(2 * time.Millisecond), - }, "example") - if m.ExperimentName() != "example" { + m := wireguard.NewExperimentMeasurer() + if m.ExperimentName() != "wireguard" { t.Fatal("invalid ExperimentName") } - if m.ExperimentVersion() != "0.1.0" { + if m.ExperimentVersion() != "0.1.1" { t.Fatal("invalid ExperimentVersion") } ctx := context.Background() @@ -38,10 +36,7 @@ func TestSuccess(t *testing.T) { } func TestFailure(t *testing.T) { - m := example.NewExperimentMeasurer(example.Config{ - SleepTime: int64(2 * time.Millisecond), - ReturnError: true, - }, "example") + m := wireguard.NewExperimentMeasurer() ctx := context.Background() sess := &mockable.Session{MockableLogger: log.Log} callbacks := model.NewPrinterCallbacks(sess.Logger()) diff --git a/internal/registry/wireguard.go b/internal/registry/wireguard.go index 9ca9a6f33..945737072 100644 --- a/internal/registry/wireguard.go +++ b/internal/registry/wireguard.go @@ -14,15 +14,16 @@ func init() { AllExperiments["wireguard"] = func() *Factory { return &Factory{ build: func(config interface{}) model.ExperimentMeasurer { - return wireguard.NewExperimentMeasurer( - *config.(*wireguard.Config), - ) + return wireguard.NewExperimentMeasurer() }, canonicalName: canonicalName, config: &wireguard.Config{}, enabledByDefault: true, interruptible: true, - inputPolicy: model.InputNone, + // TODO(ainghazal): when the backend is ready to hand us targets, + // we will use InputOrQueryBackend. + inputPolicy: model.InputNone, + newLoader: wireguard.NewLoader, } } } From 6d0f9695e46b918cf60be3c51f14d1d525838b65 Mon Sep 17 00:00:00 2001 From: ain ghazal Date: Mon, 24 Jun 2024 12:58:52 +0200 Subject: [PATCH 03/14] rename functions --- internal/experiment/wireguard/config.go | 6 ++-- internal/experiment/wireguard/wireguard.go | 36 ++++++++-------------- 2 files changed, 16 insertions(+), 26 deletions(-) diff --git a/internal/experiment/wireguard/config.go b/internal/experiment/wireguard/config.go index cb05a10bb..34770aaa0 100644 --- a/internal/experiment/wireguard/config.go +++ b/internal/experiment/wireguard/config.go @@ -37,7 +37,7 @@ type Config struct { H4 string `ooni:"h4"` } -type options struct { +type wireguardOptions struct { // common wireguard parameters endpoint string ip string @@ -59,8 +59,8 @@ type options struct { h4 string } -func getOptionsFromConfig(c *Config) (options, error) { - o := options{} +func getWireguardOptionsFromConfig(c *Config) (wireguardOptions, error) { + o := wireguardOptions{} pub, _ := base64.StdEncoding.DecodeString(c.SafePublicKey) pubHex := hex.EncodeToString(pub) diff --git a/internal/experiment/wireguard/wireguard.go b/internal/experiment/wireguard/wireguard.go index 5fc8c60a9..967551849 100644 --- a/internal/experiment/wireguard/wireguard.go +++ b/internal/experiment/wireguard/wireguard.go @@ -33,10 +33,7 @@ var ( // Measurer performs the measurement. type Measurer struct { - config Config - rawconfig []byte - options options - + options wireguardOptions events *eventLogger testName string tnet *netstack.Net @@ -71,12 +68,12 @@ func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { // TODO(ainghazal): process the input when the backend hands us one. config, _ := target.Options, target.URL - if err := m.setupConfig(config); err != nil { + if err := m.setupWireguardConfig(config); err != nil { return err } // 2. create tunnel - err = m.createTunnel(sess, zeroTime) + err = m.createTunnel(sess, zeroTime, config) testkeys := &TestKeys{ Success: err == nil, @@ -99,8 +96,8 @@ func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { return nil } -func (m *Measurer) setupConfig(config *Config) error { - opts, err := getOptionsFromConfig(config) +func (m *Measurer) setupWireguardConfig(config *Config) error { + opts, err := getWireguardOptionsFromConfig(config) if err != nil { return err } @@ -108,11 +105,11 @@ func (m *Measurer) setupConfig(config *Config) error { return nil } -func (m *Measurer) createTunnel(sess model.ExperimentSession, zeroTime time.Time) error { +func (m *Measurer) createTunnel(sess model.ExperimentSession, zeroTime time.Time, config *Config) error { sess.Logger().Info("wireguard: create tunnel") sess.Logger().Infof("endpoint: %s", m.options.endpoint) - _, tnet, err := m.configureWireguardInterface(sess.Logger(), m.events, zeroTime) + _, tnet, err := m.configureWireguardInterface(sess.Logger(), m.events, zeroTime, config) if err != nil { return err } @@ -171,7 +168,7 @@ func (m *Measurer) urlget(zeroTime time.Time, logger model.Logger) *URLGetResult func NewExperimentMeasurer() model.ExperimentMeasurer { return Measurer{ events: newEventLogger(), - options: options{}, + options: wireguardOptions{}, testName: testName, } } @@ -179,7 +176,8 @@ func NewExperimentMeasurer() model.ExperimentMeasurer { func (m *Measurer) configureWireguardInterface( logger model.Logger, eventlogger *eventLogger, - zeroTime time.Time) (tun.Device, *netstack.Net, error) { + zeroTime time.Time, + config *Config) (tun.Device, *netstack.Net, error) { devTun, tnet, err := netstack.CreateNetTUN( []netip.Addr{netip.MustParseAddr(m.options.ip)}, []netip.Addr{netip.MustParseAddr(m.options.ns)}, @@ -191,21 +189,14 @@ func (m *Measurer) configureWireguardInterface( dev := device.NewDevice( devTun, conn.NewDefaultBind(), - newWireguardLogger(logger, eventlogger, m.config.Verbose, zeroTime), + newWireguardLogger(logger, eventlogger, config.Verbose, zeroTime), ) var ipcStr string - // If the rawconfig string has content, it means that we - // did not bother to pass every option separatedly, so we assume - // we got a valid config file. This might be dangerous, so think twice - // about enforcing proper validation of the configuration file. - if len(m.rawconfig) > 0 { - ipcStr = string(m.rawconfig) - } else { - opts := m.options + opts := m.options - ipcStr = `jc=` + opts.jc + ` + ipcStr = `jc=` + opts.jc + ` jmin=` + opts.jmin + ` jmax=` + opts.jmax + ` s1=` + opts.s1 + ` @@ -220,7 +211,6 @@ preshared_key=` + opts.presharedKey + ` endpoint=` + opts.endpoint + ` allowed_ip=0.0.0.0/0 ` - } dev.IpcSet(ipcStr) err = dev.Up() From 7b94b0c4e1c3cff4c78d2df706590201ce19a65c Mon Sep 17 00:00:00 2001 From: ain ghazal Date: Mon, 24 Jun 2024 13:05:34 +0200 Subject: [PATCH 04/14] test name is module var --- internal/experiment/wireguard/wireguard.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/internal/experiment/wireguard/wireguard.go b/internal/experiment/wireguard/wireguard.go index 967551849..bda1e8202 100644 --- a/internal/experiment/wireguard/wireguard.go +++ b/internal/experiment/wireguard/wireguard.go @@ -33,15 +33,14 @@ var ( // Measurer performs the measurement. type Measurer struct { - options wireguardOptions - events *eventLogger - testName string - tnet *netstack.Net + options wireguardOptions + events *eventLogger + tnet *netstack.Net } // ExperimentName implements model.ExperimentMeasurer.ExperimentName. func (m Measurer) ExperimentName() string { - return m.testName + return testName } // ExperimentVersion implements model.ExperimentMeasurer.ExperimentVersion. From 20f496cf3bad9dbfdccba569c0186fce00683eb3 Mon Sep 17 00:00:00 2001 From: ain ghazal Date: Mon, 24 Jun 2024 17:02:06 +0200 Subject: [PATCH 05/14] add tests for logger --- internal/experiment/wireguard/wireguard.go | 51 +++++--- .../experiment/wireguard/wireguard_test.go | 109 ++++++++++++++++-- 2 files changed, 136 insertions(+), 24 deletions(-) diff --git a/internal/experiment/wireguard/wireguard.go b/internal/experiment/wireguard/wireguard.go index bda1e8202..0e0496cdb 100644 --- a/internal/experiment/wireguard/wireguard.go +++ b/internal/experiment/wireguard/wireguard.go @@ -166,9 +166,8 @@ func (m *Measurer) urlget(zeroTime time.Time, logger model.Logger) *URLGetResult // NewExperimentMeasurer creates a new ExperimentMeasurer. func NewExperimentMeasurer() model.ExperimentMeasurer { return Measurer{ - events: newEventLogger(), - options: wireguardOptions{}, - testName: testName, + events: newEventLogger(), + options: wireguardOptions{}, } } @@ -188,7 +187,7 @@ func (m *Measurer) configureWireguardInterface( dev := device.NewDevice( devTun, conn.NewDefaultBind(), - newWireguardLogger(logger, eventlogger, config.Verbose, zeroTime), + newWireguardLogger(logger, eventlogger, config.Verbose, zeroTime, time.Since), ) var ipcStr string @@ -219,6 +218,10 @@ allowed_ip=0.0.0.0/0 return devTun, tnet, nil } +// +// logging utilities +// + // Event is a network event obtained by parsing wireguard logs. type Event struct { EventType string `json:"operation"` @@ -247,11 +250,29 @@ func (el *eventLogger) log() []*Event { return el.events } +const ( + LOG_KEEPALIVE = "Receiving keepalive packet" + LOG_SEND_HANDSHAKE = "Sending handshake initiation" + LOG_RECV_HANDSHAKE = "Received handshake response" + + EVT_RECV_KEEPALIVE = "RECV_KEEPALIVE" + EVT_SEND_HANDSHAKE_INIT = "SEND_HANDSHAKE_INIT" + EVT_RECV_HANDSHAKE_RESP = "RECV_HANDSHAKE_RESP" +) + +// newWireguardLogger looks at the strings logged by the wireguard +// implementation. It performs simple regex matching and then +// it appends the matchign Event in the passed eventLogger. +// This approach has some potential for brittleness (in the unlikely case +// that upstream wireguard codebase changes the emitted log lines), +// but adding typed log events to the wg codebase might prove to be a +// particularly time-consuming rewrite. func newWireguardLogger( logger model.Logger, eventlogger *eventLogger, verbose bool, - zeroTime time.Time) *device.Logger { + zeroTime time.Time, + sinceFn func(time.Time) time.Duration) *device.Logger { verbosef := func(format string, args ...any) { msg := fmt.Sprintf(format, args...) @@ -259,22 +280,22 @@ func newWireguardLogger( logger.Debugf(msg) } - // TODO(ainghazal): we might be interested in parsing other type of events here. - if strings.Contains(msg, "Receiving keepalive packet") { - evt := newEvent("RECV_KEEPALIVE") - evt.T = time.Since(zeroTime).Seconds() + // TODO(ainghazal): we might be interested in parsing additional events. + if strings.Contains(msg, LOG_KEEPALIVE) { + evt := newEvent(EVT_RECV_KEEPALIVE) + evt.T = sinceFn(zeroTime).Seconds() eventlogger.append(evt) return } - if strings.Contains(msg, "Sending handshake initiation") { - evt := newEvent("SEND_HANDSHAKE_INIT") - evt.T = time.Since(zeroTime).Seconds() + if strings.Contains(msg, LOG_SEND_HANDSHAKE) { + evt := newEvent(EVT_SEND_HANDSHAKE_INIT) + evt.T = sinceFn(zeroTime).Seconds() eventlogger.append(evt) return } - if strings.Contains(msg, "Received handshake response") { - evt := newEvent("RECV_HANDSHAKE_RESP") - evt.T = time.Since(zeroTime).Seconds() + if strings.Contains(msg, LOG_RECV_HANDSHAKE) { + evt := newEvent(EVT_RECV_HANDSHAKE_RESP) + evt.T = sinceFn(zeroTime).Seconds() eventlogger.append(evt) return } diff --git a/internal/experiment/wireguard/wireguard_test.go b/internal/experiment/wireguard/wireguard_test.go index 0cb7975a3..c39c30931 100644 --- a/internal/experiment/wireguard/wireguard_test.go +++ b/internal/experiment/wireguard/wireguard_test.go @@ -1,19 +1,109 @@ -package wireguard_test +package wireguard import ( - "context" - "errors" "testing" + "time" - "github.com/apex/log" - "github.com/ooni/probe-cli/v3/internal/experiment/example" - "github.com/ooni/probe-cli/v3/internal/experiment/wireguard" - "github.com/ooni/probe-cli/v3/internal/legacy/mockable" + "github.com/amnezia-vpn/amneziawg-go/device" + "github.com/google/go-cmp/cmp" "github.com/ooni/probe-cli/v3/internal/model" ) +func TestNewExperimentMeasurer(t *testing.T) { + m := NewExperimentMeasurer() + if m.ExperimentName() != "wireguard" { + t.Fatal("invalid ExperimentName") + } + if m.ExperimentVersion() != "0.1.1" { + t.Fatal("invalid ExperimentVersion") + } +} + +func TestNewEvent(t *testing.T) { + e := newEvent("foo") + if e.EventType != "foo" { + t.Fatal("expected type foo") + } + + e1 := newEvent("bar") + e2 := newEvent("baaz") + + log := newEventLogger() + log.append(e) + log.append(e1) + log.append(e2) + + if diff := cmp.Diff(log.log(), []*Event{e, e1, e2}); diff != "" { + t.Fatal(diff) + } +} + +func TestNewWireguardLogger(t *testing.T) { + wgLogger := func(events *eventLogger, t int) *device.Logger { + wgLogger := newWireguardLogger( + model.DiscardLogger, + events, + false, + time.Now(), + func(time.Time) time.Duration { + return time.Duration(t) * time.Second + }) + return wgLogger + } + + t.Run("keepalive packet", func(t *testing.T) { + eventLogger := newEventLogger() + logger := wgLogger(eventLogger, 2) + logger.Verbosef(LOG_KEEPALIVE) + evts := eventLogger.log() + if len(evts) != 1 { + t.Fatal("expected 1 event") + } + if evts[0].EventType != EVT_RECV_KEEPALIVE { + t.Fatal("expected RECV_KEEPALIVE") + } + if evts[0].T != 2.0 { + t.Fatal("expected T=2") + } + }) + t.Run("handshake send packet", func(t *testing.T) { + eventLogger := newEventLogger() + logger := wgLogger(eventLogger, 3) + logger.Verbosef(LOG_SEND_HANDSHAKE) + evts := eventLogger.log() + if len(evts) != 1 { + t.Fatal("expected 1 event") + } + if evts[0].EventType != EVT_SEND_HANDSHAKE_INIT { + t.Fatal("expected SEND_HANDSHAKE_INIT ") + } + if evts[0].T != 3.0 { + t.Fatal("expected T=3") + } + }) + t.Run("handshake recv packet", func(t *testing.T) { + eventLogger := newEventLogger() + logger := wgLogger(eventLogger, 4) + logger.Verbosef(LOG_RECV_HANDSHAKE) + evts := eventLogger.log() + if len(evts) != 1 { + t.Fatal("expected 1 event") + } + if evts[0].EventType != EVT_RECV_HANDSHAKE_RESP { + t.Fatal("expected RECV_HADNSHAKE_RESP ") + } + if evts[0].T != 4.0 { + t.Fatal("expected T=4") + } + }) + +} + +// TODO(cleanup) ---- +/* + func TestSuccess(t *testing.T) { - m := wireguard.NewExperimentMeasurer() + m := NewExperimentMeasurer() if m.ExperimentName() != "wireguard" { t.Fatal("invalid ExperimentName") } @@ -36,7 +126,7 @@ func TestSuccess(t *testing.T) { } func TestFailure(t *testing.T) { - m := wireguard.NewExperimentMeasurer() + m := NewExperimentMeasurer() ctx := context.Background() sess := &mockable.Session{MockableLogger: log.Log} callbacks := model.NewPrinterCallbacks(sess.Logger()) @@ -50,3 +140,4 @@ func TestFailure(t *testing.T) { t.Fatal("expected an error here") } } +*/ From 7924dd61d4f9f68ff7a5170e7af0297e0991cb3f Mon Sep 17 00:00:00 2001 From: ain ghazal Date: Mon, 24 Jun 2024 17:27:55 +0200 Subject: [PATCH 06/14] refactor --- internal/experiment/wireguard/config.go | 11 +++- internal/experiment/wireguard/testkeys.go | 2 + internal/experiment/wireguard/urlget.go | 58 ++++++++++++++++++ internal/experiment/wireguard/wireguard.go | 71 ++++++---------------- 4 files changed, 86 insertions(+), 56 deletions(-) create mode 100644 internal/experiment/wireguard/urlget.go diff --git a/internal/experiment/wireguard/config.go b/internal/experiment/wireguard/config.go index 34770aaa0..49150dea5 100644 --- a/internal/experiment/wireguard/config.go +++ b/internal/experiment/wireguard/config.go @@ -14,7 +14,8 @@ type Config struct { ConfigFile string `ooni:"Configuration file for the WireGuard experiment"` // TODO(ainghzal): honor it - PublicTarget bool `ooni:"Treat the target endpoint as public data (if true, it will be included in the report)` + PublicTarget bool `ooni:"Treat the target endpoint as public data (if true, it will be included in the report)` + PublicAmneziaParameters bool `ooni:"Obfuscate the Public AmneziaWG advanced security parameters` Verbose bool `ooni:"Use extra-verbose mode in wireguard logs"` @@ -59,7 +60,13 @@ type wireguardOptions struct { h4 string } -func getWireguardOptionsFromConfig(c *Config) (wireguardOptions, error) { +// validate returns true if this looks like a sensible wireguard configuration. +func (wgopt *wireguardOptions) validate() bool { + // TODO(ainghazal): implement + return true +} + +func newWireguardOptionsFromConfig(c *Config) (wireguardOptions, error) { o := wireguardOptions{} pub, _ := base64.StdEncoding.DecodeString(c.SafePublicKey) diff --git a/internal/experiment/wireguard/testkeys.go b/internal/experiment/wireguard/testkeys.go index b84e13a8c..ad8843af2 100644 --- a/internal/experiment/wireguard/testkeys.go +++ b/internal/experiment/wireguard/testkeys.go @@ -9,6 +9,8 @@ package wireguard // the specific results of this experiment. type TestKeys struct { Success bool `json:"success"` + Endpoint string `json:"endpoint"` + EndpointASN string `json:"endpoint_asn,omitempty"` Failure *string `json:"failure"` NetworkEvents []*Event `json:"network_events"` URLGet []*URLGetResult `json:"urlget"` diff --git a/internal/experiment/wireguard/urlget.go b/internal/experiment/wireguard/urlget.go new file mode 100644 index 000000000..18c39aad1 --- /dev/null +++ b/internal/experiment/wireguard/urlget.go @@ -0,0 +1,58 @@ +package wireguard + +import ( + "io" + "net/http" + "time" + + "github.com/ooni/probe-cli/v3/internal/measurexlite" + "github.com/ooni/probe-cli/v3/internal/model" +) + +const ( + defaultURLGetTarget = "https://info.cern.ch/" +) + +func newURLResultFromError(url string, zeroTime time.Time, start float64, err error) *URLGetResult { + return &URLGetResult{ + URL: url, + T0: start, + T: time.Since(zeroTime).Seconds(), + Failure: measurexlite.NewFailure(err), + Error: err.Error(), + } +} + +func newURLResultWithStatusCode(url string, zeroTime time.Time, start float64, statusCode int, body []byte) *URLGetResult { + return &URLGetResult{ + ByteCount: len(body), + URL: url, + T0: start, + T: time.Since(zeroTime).Seconds(), + StatusCode: statusCode, + } +} + +func (m *Measurer) urlget(url string, zeroTime time.Time, logger model.Logger) *URLGetResult { + client := http.Client{ + Timeout: 30 * time.Second, + Transport: &http.Transport{ + DialContext: m.tnet.DialContext, + TLSHandshakeTimeout: 30 * time.Second, + }} + + start := time.Since(zeroTime).Seconds() + r, err := client.Get(url) + if err != nil { + logger.Warnf("urlget error: %v", err.Error()) + return newURLResultFromError(url, zeroTime, start, err) + } + body, err := io.ReadAll(r.Body) + if err != nil { + logger.Warnf("urlget error: %v", err.Error()) + return newURLResultFromError(url, zeroTime, start, err) + } + defer r.Body.Close() + + return newURLResultWithStatusCode(url, zeroTime, start, r.StatusCode, body) +} diff --git a/internal/experiment/wireguard/wireguard.go b/internal/experiment/wireguard/wireguard.go index 0e0496cdb..6fb1c69e0 100644 --- a/internal/experiment/wireguard/wireguard.go +++ b/internal/experiment/wireguard/wireguard.go @@ -4,8 +4,6 @@ import ( "context" "errors" "fmt" - "io" - "net/http" "net/netip" "strings" "time" @@ -29,10 +27,12 @@ const ( var ( ErrInputRequired = errors.New("input is required") + ErrInvalidInput = errors.New("invalid input") ) // Measurer performs the measurement. type Measurer struct { + // TODO(ainghzal): no need to keep track of this options wireguardOptions events *eventLogger tnet *netstack.Net @@ -64,10 +64,9 @@ func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { // 1. setup (parse config file) target := args.Target.(*Target) - // TODO(ainghazal): process the input when the backend hands us one. - config, _ := target.Options, target.URL - - if err := m.setupWireguardConfig(config); err != nil { + // TODO(ainghazal): if the target is not public, substitute it with ASN? + config, input := target.Options, target.URL + if err := m.setupWireguardFromConfig(config); err != nil { return err } @@ -80,10 +79,16 @@ func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { URLGet: make([]*URLGetResult, 0), } + if config.PublicTarget { + testkeys.Endpoint = m.options.endpoint + } else { + testkeys.Endpoint = input + } + // 3. use tunnel if err == nil { sess.Logger().Info("Using the wireguard tunnel.") - urlgetResult := m.urlget(zeroTime, sess.Logger()) + urlgetResult := m.urlget(defaultURLGetTarget, zeroTime, sess.Logger()) testkeys.URLGet = append(testkeys.URLGet, urlgetResult) testkeys.NetworkEvents = m.events.log() } @@ -95,11 +100,14 @@ func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { return nil } -func (m *Measurer) setupWireguardConfig(config *Config) error { - opts, err := getWireguardOptionsFromConfig(config) +func (m *Measurer) setupWireguardFromConfig(config *Config) error { + opts, err := newWireguardOptionsFromConfig(config) if err != nil { return err } + if ok := opts.validate(); !ok { + return fmt.Errorf("%w: %s", ErrInvalidInput, "cannot validate wireguard options") + } m.options = opts return nil } @@ -118,51 +126,6 @@ func (m *Measurer) createTunnel(sess model.ExperimentSession, zeroTime time.Time return nil } -func newURLResultFromError(url string, zeroTime time.Time, start float64, err error) *URLGetResult { - return &URLGetResult{ - URL: url, - T0: start, - T: time.Since(zeroTime).Seconds(), - Failure: measurexlite.NewFailure(err), - Error: err.Error(), - } -} - -func newURLResultWithStatusCode(url string, zeroTime time.Time, start float64, statusCode int, body []byte) *URLGetResult { - return &URLGetResult{ - ByteCount: len(body), - URL: url, - T0: start, - T: time.Since(zeroTime).Seconds(), - StatusCode: statusCode, - } -} - -func (m *Measurer) urlget(zeroTime time.Time, logger model.Logger) *URLGetResult { - url := "https://info.cern.ch/" - client := http.Client{ - Timeout: 30 * time.Second, - Transport: &http.Transport{ - DialContext: m.tnet.DialContext, - TLSHandshakeTimeout: 30 * time.Second, - }} - - start := time.Since(zeroTime).Seconds() - r, err := client.Get(url) - if err != nil { - logger.Warnf("urlget error: %v", err.Error()) - return newURLResultFromError(url, zeroTime, start, err) - } - body, err := io.ReadAll(r.Body) - if err != nil { - logger.Warnf("urlget error: %v", err.Error()) - return newURLResultFromError(url, zeroTime, start, err) - } - defer r.Body.Close() - - return newURLResultWithStatusCode(url, zeroTime, start, r.StatusCode, body) -} - // NewExperimentMeasurer creates a new ExperimentMeasurer. func NewExperimentMeasurer() model.ExperimentMeasurer { return Measurer{ From 57ff92854699a76252e1752f2cb82409dc595291 Mon Sep 17 00:00:00 2001 From: ain ghazal Date: Mon, 24 Jun 2024 20:38:30 +0200 Subject: [PATCH 07/14] x --- internal/experiment/wireguard/wireguard.go | 45 +++++++++++++--------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/internal/experiment/wireguard/wireguard.go b/internal/experiment/wireguard/wireguard.go index 6fb1c69e0..f1c722757 100644 --- a/internal/experiment/wireguard/wireguard.go +++ b/internal/experiment/wireguard/wireguard.go @@ -10,6 +10,7 @@ import ( "github.com/ooni/probe-cli/v3/internal/measurexlite" "github.com/ooni/probe-cli/v3/internal/model" + "github.com/ooni/probe-cli/v3/internal/targetloading" "github.com/amnezia-vpn/amneziawg-go/conn" "github.com/amnezia-vpn/amneziawg-go/device" @@ -26,47 +27,63 @@ const ( ) var ( - ErrInputRequired = errors.New("input is required") - ErrInvalidInput = errors.New("invalid input") + ErrInputRequired = targetloading.ErrInputRequired + ErrInvalidInputType = targetloading.ErrInvalidInputType + + // TODO(ainghazal): fix after adding this error into targetloading + ErrInvalidInput = errors.New("invalid input") ) // Measurer performs the measurement. type Measurer struct { - // TODO(ainghzal): no need to keep track of this - options wireguardOptions events *eventLogger + options wireguardOptions tnet *netstack.Net } +// NewExperimentMeasurer creates a new ExperimentMeasurer. +func NewExperimentMeasurer() model.ExperimentMeasurer { + return &Measurer{ + events: newEventLogger(), + options: wireguardOptions{}, + } +} + // ExperimentName implements model.ExperimentMeasurer.ExperimentName. -func (m Measurer) ExperimentName() string { +func (m *Measurer) ExperimentName() string { return testName } // ExperimentVersion implements model.ExperimentMeasurer.ExperimentVersion. -func (m Measurer) ExperimentVersion() string { +func (m *Measurer) ExperimentVersion() string { return testVersion } // Run implements model.ExperimentMeasurer.Run. -func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { +func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { measurement := args.Measurement sess := args.Session zeroTime := measurement.MeasurementStartTimeSaved var err error - // 0. obtain the richer input target, config, and input or panic + // 0. fail if there is no richer input target. if args.Target == nil { return ErrInputRequired } - // 1. setup (parse config file) - target := args.Target.(*Target) + // 1. setup tunnel after parsing options + target, ok := args.Target.(*Target) + if !ok { + return ErrInvalidInputType + } // TODO(ainghazal): if the target is not public, substitute it with ASN? config, input := target.Options, target.URL if err := m.setupWireguardFromConfig(config); err != nil { + // A failure at this point means that we are not able + // to validate the minimal set of options that we need to probe an endpoint. + // We abort the experiment and submit nothing. return err } @@ -126,14 +143,6 @@ func (m *Measurer) createTunnel(sess model.ExperimentSession, zeroTime time.Time return nil } -// NewExperimentMeasurer creates a new ExperimentMeasurer. -func NewExperimentMeasurer() model.ExperimentMeasurer { - return Measurer{ - events: newEventLogger(), - options: wireguardOptions{}, - } -} - func (m *Measurer) configureWireguardInterface( logger model.Logger, eventlogger *eventLogger, From 671302674296b8a2e2941193ded843d1f1467a44 Mon Sep 17 00:00:00 2001 From: ain ghazal Date: Tue, 25 Jun 2024 01:20:07 +0200 Subject: [PATCH 08/14] add tests for config --- internal/experiment/wireguard/config.go | 158 +++++++++---- internal/experiment/wireguard/config_test.go | 227 +++++++++++++++++++ internal/experiment/wireguard/richerinput.go | 11 +- internal/experiment/wireguard/testkeys.go | 1 + internal/experiment/wireguard/wireguard.go | 9 +- 5 files changed, 356 insertions(+), 50 deletions(-) create mode 100644 internal/experiment/wireguard/config_test.go diff --git a/internal/experiment/wireguard/config.go b/internal/experiment/wireguard/config.go index 49150dea5..9b7b6a38c 100644 --- a/internal/experiment/wireguard/config.go +++ b/internal/experiment/wireguard/config.go @@ -1,8 +1,11 @@ package wireguard import ( + "crypto/sha1" "encoding/base64" "encoding/hex" + "fmt" + "io" ) // Config contains the experiment config. @@ -11,44 +14,45 @@ import ( // of this experiment. By tagging these variables with `ooni:"..."`, we allow // miniooni's -O flag to find them and set them. type Config struct { - ConfigFile string `ooni:"Configuration file for the WireGuard experiment"` + Verbose bool `ooni:"Use extra-verbose mode in wireguard logs"` - // TODO(ainghzal): honor it - PublicTarget bool `ooni:"Treat the target endpoint as public data (if true, it will be included in the report)` - PublicAmneziaParameters bool `ooni:"Obfuscate the Public AmneziaWG advanced security parameters` + // These flags modify what sensitive information is stored in the report and submitted to the backend. + PublicTarget bool `ooni:"Treat the target endpoint as public data (if true, it will be included in the report)"` + PublicAmneziaParameters bool `ooni:"Treat the AmneziaWG advanced security parameters as public data"` - Verbose bool `ooni:"Use extra-verbose mode in wireguard logs"` + // Safe_XXX options are not sent to the backend for archival by default. + SafeRemote string `ooni:"Remote to connect to using WireGuard"` + SafeIP string `ooni:"Allocated IP for this peer"` - // Safe_XXX options are not sent to the backend for archival. - SafeRemote string `ooni:"Remote to connect to using WireGuard"` - SafePrivateKey string `ooni:"Private key to connect to remote"` - SafePublicKey string `ooni:"Public key of the remote"` - SafePresharedKey string `ooni:"Pre-shared key for authentication"` - SafeIP string `ooni:"Allocated IP for this peer"` + // Keys are base-64 encoded + SafePrivateKey string `ooni:"Private key to connect to remote (base64)"` + SafePublicKey string `ooni:"Public key of the remote (base64)"` + SafePresharedKey string `ooni:"Pre-shared key for authentication (base64)"` // Optional obfuscation parameters for AmneziaWG - Jc string `ooni:"jc"` - Jmin string `ooni:"jmin"` - Jmax string `ooni:"jmax"` - S1 string `ooni:"s1"` - S2 string `ooni:"s2"` - H1 string `ooni:"h1"` - H2 string `ooni:"h2"` - H3 string `ooni:"h3"` - H4 string `ooni:"h4"` + SafeJc string `ooni:"jc"` + SafeJmin string `ooni:"jmin"` + SafeJmax string `ooni:"jmax"` + SafeS1 string `ooni:"s1"` + SafeS2 string `ooni:"s2"` + SafeH1 string `ooni:"h1"` + SafeH2 string `ooni:"h2"` + SafeH3 string `ooni:"h3"` + SafeH4 string `ooni:"h4"` } type wireguardOptions struct { // common wireguard parameters - endpoint string - ip string + endpoint string + ip string + ns string + + // keys are hex-encoded pubKey string privKey string presharedKey string - ns string - // parameters from AmneziaWG - // TODO(ainghazal: make these optional) + // optional parameters for AmneziaWG nodes jc string jmin string jmax string @@ -60,42 +64,108 @@ type wireguardOptions struct { h4 string } +// amneziaValues returns an array with all the amnezia-specific configuration +// parameters. +func (wo *wireguardOptions) amneziaValues() []string { + return []string{ + wo.jc, wo.jmin, wo.jmax, + wo.s1, wo.s2, + wo.h1, wo.h2, wo.h3, wo.h4, + } +} + // validate returns true if this looks like a sensible wireguard configuration. -func (wgopt *wireguardOptions) validate() bool { - // TODO(ainghazal): implement +func (wo *wireguardOptions) validate() bool { + if wo.endpoint == "" || wo.ip == "" || wo.pubKey == "" || wo.privKey == "" || wo.presharedKey == "" { + return false + } + if isAnyFilled(wo.amneziaValues()...) { + return !isAnyEmpty(wo.amneziaValues()...) + } return true } -func newWireguardOptionsFromConfig(c *Config) (wireguardOptions, error) { - o := wireguardOptions{} +// isAmneziaFlavored returns true if none of the mandatory amnezia fields are empty. +func (wo *wireguardOptions) isAmneziaFlavored() bool { + return !isAnyEmpty(wo.amneziaValues()...) +} + +// amneziaConfigHash is a hash representation of the custom parameters in this amneziaWG node. +// intended to be used if PublicAmneziaParameters=false, so that we can verify that we're testing +// the same node. +func (wo *wireguardOptions) configurationHash() string { + if !wo.isAmneziaFlavored() { + return "" + } + return sha1Sum(append(wo.amneziaValues(), wo.endpoint)...) +} + +func sha1Sum(strings ...string) string { + hasher := sha1.New() + for _, str := range strings { + io.WriteString(hasher, str) + } + return fmt.Sprintf("%x", hasher.Sum(nil)) +} - pub, _ := base64.StdEncoding.DecodeString(c.SafePublicKey) +func newWireguardOptionsFromConfig(c *Config) (*wireguardOptions, error) { + o := &wireguardOptions{} + + pub, err := base64.StdEncoding.DecodeString(c.SafePublicKey) + if err != nil { + return nil, fmt.Errorf("%w: cannot decode public key", ErrInvalidInput) + } pubHex := hex.EncodeToString(pub) o.pubKey = pubHex - priv, _ := base64.StdEncoding.DecodeString(c.SafePrivateKey) + priv, err := base64.StdEncoding.DecodeString(c.SafePrivateKey) + if err != nil { + return nil, fmt.Errorf("%w: cannot decode private key", ErrInvalidInput) + } privHex := hex.EncodeToString(priv) o.privKey = privHex - psk, _ := base64.StdEncoding.DecodeString(c.SafePresharedKey) + psk, err := base64.StdEncoding.DecodeString(c.SafePresharedKey) + if err != nil { + return nil, fmt.Errorf("%w: cannot decode pre-shared key", ErrInvalidInput) + } pskHex := hex.EncodeToString(psk) o.presharedKey = pskHex - o.ip = c.SafeIP - - // TODO: reconcile this with Input if c.PublicTarget=true + // TODO(ainghazal): reconcile this with Input if c.PublicTarget=true o.endpoint = c.SafeRemote - o.jc = c.Jc - o.jmin = c.Jmin - o.jmax = c.Jmax - o.s1 = c.S1 - o.s2 = c.S2 - o.h1 = c.H1 - o.h2 = c.H2 - o.h3 = c.H3 - o.h4 = c.H4 + o.ip = c.SafeIP + + // amnezia parameters + o.jc = c.SafeJc + o.jmin = c.SafeJmin + o.jmax = c.SafeJmax + o.s1 = c.SafeS1 + o.s2 = c.SafeS2 + o.h1 = c.SafeH1 + o.h2 = c.SafeH2 + o.h3 = c.SafeH3 + o.h4 = c.SafeH4 o.ns = defaultNameserver return o, nil } + +func isAnyFilled(fields ...string) bool { + for _, f := range fields { + if f != "" { + return true + } + } + return false +} + +func isAnyEmpty(fields ...string) bool { + for _, f := range fields { + if f == "" { + return true + } + } + return false +} diff --git a/internal/experiment/wireguard/config_test.go b/internal/experiment/wireguard/config_test.go new file mode 100644 index 000000000..37c9089e5 --- /dev/null +++ b/internal/experiment/wireguard/config_test.go @@ -0,0 +1,227 @@ +package wireguard + +import ( + "errors" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func Test_wireguardOptions(t *testing.T) { + t.Run("amnezia values are the expected set", func(t *testing.T) { + wc := wireguardOptions{ + jc: "1", + jmin: "2", + jmax: "3", + s1: "4", + s2: "5", + h1: "6", + h2: "7", + h3: "8", + h4: "9", + } + expected := []string{"1", "2", "3", "4", "5", "6", "7", "8", "9"} + if diff := cmp.Diff(wc.amneziaValues(), expected); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("validate() is true when the mandatory fields are filled", func(t *testing.T) { + wc := wireguardOptions{ + endpoint: "1.1.1.1:8020", + ip: "10.1.2.8", + pubKey: "foobar", + privKey: "foobar", + presharedKey: "foobar", + } + if wc.validate() != true { + t.Fatal("expected options to be valid") + } + }) + + t.Run("validate() is false when one mandatory field is missing", func(t *testing.T) { + wc := wireguardOptions{ + endpoint: "1.1.1.1:8020", + pubKey: "foobar", + privKey: "foobar", + presharedKey: "foobar", + } + if wc.validate() != false { + t.Fatal("expected options not to be valid") + } + }) + + t.Run("validate() is true when the all amnezia fields are filled", func(t *testing.T) { + wc := wireguardOptions{ + endpoint: "1.1.1.1:8020", + ip: "10.1.2.8", + pubKey: "foobar", + privKey: "foobar", + presharedKey: "foobar", + jc: "1", + jmin: "2", + jmax: "3", + s1: "4", + s2: "5", + h1: "6", + h2: "7", + h3: "8", + h4: "9", + } + if wc.validate() != true { + t.Fatal("expected options to be valid") + } + }) + + t.Run("validate() is false when any of the amnezia fields is missing", func(t *testing.T) { + wc := wireguardOptions{ + endpoint: "1.1.1.1:8020", + ip: "10.1.2.8", + pubKey: "foobar", + privKey: "foobar", + presharedKey: "foobar", + jc: "1", + jmin: "2", + jmax: "3", + s1: "4", + s2: "5", + h1: "6", + h2: "7", + h3: "8", + h4: "", + } + if wc.validate() != false { + t.Fatal("expected options not to be valid") + } + }) + + t.Run("isAmneziaFlavored() is true when none of the amnezia fields is missing", func(t *testing.T) { + wc := wireguardOptions{ + endpoint: "1.1.1.1:8020", + ip: "10.1.2.8", + pubKey: "foobar", + privKey: "foobar", + presharedKey: "foobar", + jc: "1", + jmin: "2", + jmax: "3", + s1: "4", + s2: "5", + h1: "6", + h2: "7", + h3: "8", + h4: "9", + } + if wc.isAmneziaFlavored() != true { + t.Fatal("expected to be amnezia flavored") + } + }) + + t.Run("configurationHash() is empty for non-amnezia values", func(t *testing.T) { + wc := wireguardOptions{ + endpoint: "1.1.1.1:8020", + } + expected := "" + if diff := cmp.Diff(wc.configurationHash(), expected); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("get the expected configurationHash()", func(t *testing.T) { + wc := wireguardOptions{ + endpoint: "1.1.1.1:8020", + jc: "1", + jmin: "2", + jmax: "3", + s1: "4", + s2: "5", + h1: "6", + h2: "7", + h3: "8", + h4: "9", + } + expected := "adb00b0ab179bfbdf9835bc124cbc7ab7e59bd8b" + if diff := cmp.Diff(wc.configurationHash(), expected); diff != "" { + t.Fatal(diff) + } + }) +} + +func Test_newWireguardOptionsFromConfig(t *testing.T) { + t.Run("good config does not fail", func(t *testing.T) { + c := &Config{ + SafePublicKey: "ZGVhZGJlZWY=", + SafePrivateKey: "ZGVhZGJlZWY=", + SafePresharedKey: "ZGVhZGJlZWY=", + SafeRemote: "1.2.3.4:8080", + } + + opts, err := newWireguardOptionsFromConfig(c) + if !errors.Is(err, nil) { + t.Fatal("did not expect error") + } + + hexExpected := "6465616462656566" // deadbeef + + if diff := cmp.Diff(opts.pubKey, hexExpected); diff != "" { + t.Fatal(diff) + } + if diff := cmp.Diff(opts.privKey, hexExpected); diff != "" { + t.Fatal(diff) + } + if diff := cmp.Diff(opts.presharedKey, hexExpected); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("bad pubkey fails", func(t *testing.T) { + c := &Config{ + SafePublicKey: "ZGVhZGJlZWY", + SafePrivateKey: "ZGVhZGJlZWY=", + SafePresharedKey: "ZGVhZGJlZWY=", + SafeRemote: "1.2.3.4:8080", + } + + opts, err := newWireguardOptionsFromConfig(c) + if opts != nil { + t.Fatal("did not expect anything other than nil") + } + if !errors.Is(err, ErrInvalidInput) { + t.Fatal("not the error we expected") + } + }) + + t.Run("bad privkey fails", func(t *testing.T) { + c := &Config{ + SafePublicKey: "ZGVhZGJlZWY=", + SafePrivateKey: "ZGVhZGJlZWY", + SafePresharedKey: "ZGVhZGJlZWY=", + SafeRemote: "1.2.3.4:8080", + } + + opts, err := newWireguardOptionsFromConfig(c) + if opts != nil { + t.Fatal("did not expect anything other than nil") + } + if !errors.Is(err, ErrInvalidInput) { + t.Fatal("not the error we expected") + } + }) + + t.Run("bad preshared key fails", func(t *testing.T) { + c := &Config{ + SafePublicKey: "ZGVhZGJlZWY=", + SafePrivateKey: "ZGVhZGJlZWY=", + SafePresharedKey: "ZGVhZGJlZWY", + SafeRemote: "1.2.3.4:8080", + } + + opts, err := newWireguardOptionsFromConfig(c) + if opts != nil { + t.Fatal("did not expect anything other than nil") + } + if !errors.Is(err, ErrInvalidInput) { + t.Fatal("not the error we expected") + } + }) +} diff --git a/internal/experiment/wireguard/richerinput.go b/internal/experiment/wireguard/richerinput.go index e4321d6e5..05b0d2e80 100644 --- a/internal/experiment/wireguard/richerinput.go +++ b/internal/experiment/wireguard/richerinput.go @@ -76,11 +76,14 @@ func (tl *targetLoader) Load(ctx context.Context) ([]model.ExperimentTarget, err // Build the list of targets that we should measure. var targets []model.ExperimentTarget + for _, input := range inputs { - targets = append(targets, &Target{ - Options: tl.options, - URL: input, - }) + targets = append(targets, + &Target{ + Options: tl.options, + URL: input, + }) + } return targets, nil } diff --git a/internal/experiment/wireguard/testkeys.go b/internal/experiment/wireguard/testkeys.go index ad8843af2..b1b50f0bd 100644 --- a/internal/experiment/wireguard/testkeys.go +++ b/internal/experiment/wireguard/testkeys.go @@ -11,6 +11,7 @@ type TestKeys struct { Success bool `json:"success"` Endpoint string `json:"endpoint"` EndpointASN string `json:"endpoint_asn,omitempty"` + EndpointID string `json:"endpoint_id,omitempty"` Failure *string `json:"failure"` NetworkEvents []*Event `json:"network_events"` URLGet []*URLGetResult `json:"urlget"` diff --git a/internal/experiment/wireguard/wireguard.go b/internal/experiment/wireguard/wireguard.go index f1c722757..5c8d46d5c 100644 --- a/internal/experiment/wireguard/wireguard.go +++ b/internal/experiment/wireguard/wireguard.go @@ -37,7 +37,7 @@ var ( // Measurer performs the measurement. type Measurer struct { events *eventLogger - options wireguardOptions + options *wireguardOptions tnet *netstack.Net } @@ -45,7 +45,7 @@ type Measurer struct { func NewExperimentMeasurer() model.ExperimentMeasurer { return &Measurer{ events: newEventLogger(), - options: wireguardOptions{}, + options: &wireguardOptions{}, } } @@ -102,6 +102,11 @@ func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { testkeys.Endpoint = input } + testkeys.EndpointID = m.options.configurationHash() + if config.PublicAmneziaParameters { + // TODO(ainghazal): copy the parameters as testkeys + } + // 3. use tunnel if err == nil { sess.Logger().Info("Using the wireguard tunnel.") From 7da81e3607854e2c7c8e544e972495229b6e5161 Mon Sep 17 00:00:00 2001 From: ain ghazal Date: Tue, 25 Jun 2024 11:22:07 +0200 Subject: [PATCH 09/14] add tests for urlget --- internal/experiment/wireguard/config.go | 5 + internal/experiment/wireguard/urlget.go | 56 +++++---- internal/experiment/wireguard/urlget_test.go | 117 ++++++++++++++++++ internal/experiment/wireguard/wireguard.go | 19 ++- .../experiment/wireguard/wireguard_test.go | 2 +- 5 files changed, 169 insertions(+), 30 deletions(-) create mode 100644 internal/experiment/wireguard/urlget_test.go diff --git a/internal/experiment/wireguard/config.go b/internal/experiment/wireguard/config.go index 9b7b6a38c..ae129deeb 100644 --- a/internal/experiment/wireguard/config.go +++ b/internal/experiment/wireguard/config.go @@ -8,6 +8,11 @@ import ( "io" ) +var ( + // defaultNameserver is the dns server using for resolving names inside the wg tunnel. + defaultNameserver = "8.8.8.8" +) + // Config contains the experiment config. // // This contains all the settings that user can set to modify the behaviour diff --git a/internal/experiment/wireguard/urlget.go b/internal/experiment/wireguard/urlget.go index 18c39aad1..4f1e3c748 100644 --- a/internal/experiment/wireguard/urlget.go +++ b/internal/experiment/wireguard/urlget.go @@ -10,9 +10,41 @@ import ( ) const ( + // defaultURLGetTarget is the web page that the experiment will fetch by default. defaultURLGetTarget = "https://info.cern.ch/" ) +// urlget implements an straightforward urlget experiment using the standard library. +// By default we pass the wireguard tunnel DialContext to the `http.Transport` on the `http.Client` creation. +func (m *Measurer) urlget(url string, zeroTime time.Time, logger model.Logger) *URLGetResult { + if m.dialContextFn == nil { + m.dialContextFn = m.tnet.DialContext + } + if m.httpClient == nil { + m.httpClient = &http.Client{ + Timeout: 30 * time.Second, + Transport: &http.Transport{ + DialContext: m.dialContextFn, + TLSHandshakeTimeout: 30 * time.Second, + }} + } + + start := time.Since(zeroTime).Seconds() + r, err := m.httpClient.Get(url) + if err != nil { + logger.Warnf("urlget error: %v", err.Error()) + return newURLResultFromError(url, zeroTime, start, err) + } + body, err := io.ReadAll(r.Body) + if err != nil { + logger.Warnf("urlget error: %v", err.Error()) + return newURLResultFromError(url, zeroTime, start, err) + } + defer r.Body.Close() + + return newURLResultWithStatusCode(url, zeroTime, start, r.StatusCode, body) +} + func newURLResultFromError(url string, zeroTime time.Time, start float64, err error) *URLGetResult { return &URLGetResult{ URL: url, @@ -32,27 +64,3 @@ func newURLResultWithStatusCode(url string, zeroTime time.Time, start float64, s StatusCode: statusCode, } } - -func (m *Measurer) urlget(url string, zeroTime time.Time, logger model.Logger) *URLGetResult { - client := http.Client{ - Timeout: 30 * time.Second, - Transport: &http.Transport{ - DialContext: m.tnet.DialContext, - TLSHandshakeTimeout: 30 * time.Second, - }} - - start := time.Since(zeroTime).Seconds() - r, err := client.Get(url) - if err != nil { - logger.Warnf("urlget error: %v", err.Error()) - return newURLResultFromError(url, zeroTime, start, err) - } - body, err := io.ReadAll(r.Body) - if err != nil { - logger.Warnf("urlget error: %v", err.Error()) - return newURLResultFromError(url, zeroTime, start, err) - } - defer r.Body.Close() - - return newURLResultWithStatusCode(url, zeroTime, start, r.StatusCode, body) -} diff --git a/internal/experiment/wireguard/urlget_test.go b/internal/experiment/wireguard/urlget_test.go new file mode 100644 index 000000000..4add4e9f3 --- /dev/null +++ b/internal/experiment/wireguard/urlget_test.go @@ -0,0 +1,117 @@ +package wireguard + +import ( + "context" + "errors" + "math" + "net" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/ooni/probe-cli/v3/internal/model" +) + +type failingHttpClient struct{} + +func (c *failingHttpClient) Get(string) (*http.Response, error) { + return nil, errors.New("some error") +} + +func Test_urlget(t *testing.T) { + t.Run("dummy server gets a URLGetResult, with no error", func(t *testing.T) { + expected := "dummy data" + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.Write([]byte(expected)) + })) + defer svr.Close() + + m := &Measurer{} + m.dialContextFn = func(_ context.Context, network, address string) (net.Conn, error) { + return net.Dial(network, address) + } + r := m.urlget(svr.URL, time.Now(), model.DiscardLogger) + if r.StatusCode != 200 { + t.Fatal("expected statusCode==200") + } + }) + + t.Run("dummy server gets a URLGetResult with 500 status code", func(t *testing.T) { + expected := "dummy data" + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(500) + w.Write([]byte(expected)) + })) + defer svr.Close() + + m := &Measurer{} + m.dialContextFn = func(_ context.Context, network, address string) (net.Conn, error) { + return net.Dial(network, address) + } + r := m.urlget(svr.URL, time.Now(), model.DiscardLogger) + if r.StatusCode != 500 { + t.Fatal("expected statusCode==500") + } + }) + + t.Run("client returns error", func(t *testing.T) { + m := &Measurer{} + m.httpClient = &failingHttpClient{} + + r := m.urlget("http://example.org", time.Now(), model.DiscardLogger) + expectedError := "unknown_failure: some error" + if *r.Failure != expectedError { + t.Fatal("expected error") + } + }) +} + +func Test_newURLResultFromError(t *testing.T) { + url := "https://example.org" + zeroTime := time.Now().Add(-1 * time.Second) + start := 0.1 + err := errors.New("some error") + + r := newURLResultFromError(url, zeroTime, start, err) + if r.URL != url { + t.Fatal("wrong url") + } + if r.T0 != start { + t.Fatal("wrong t0") + } + if math.Abs(r.T-1.0) > 0.01 { + t.Fatal("should be ~now, not", r.T) + } + if r.Error != err.Error() { + t.Fatal("wrong error") + } + expectedFailure := "unknown_failure: " + err.Error() + if *r.Failure != expectedFailure { + t.Fatal(*r.Failure) + } +} + +func Test_newURLResultWithStratusCode(t *testing.T) { + url := "https://example.org" + zeroTime := time.Now().Add(-1 * time.Second) + start := 0.1 + + r := newURLResultWithStatusCode(url, zeroTime, start, 200, []byte("potatoes")) + if r.URL != url { + t.Fatal("wrong url") + } + if r.T0 != start { + t.Fatal("wrong t0") + } + if math.Abs(r.T-1.0) > 0.01 { + t.Fatal("should be ~now, not", r.T) + } + if r.StatusCode != 200 { + t.Fatal("expected statusCode==200") + } + if r.ByteCount != 8 { + t.Fatal("expected byteCount=8") + } +} diff --git a/internal/experiment/wireguard/wireguard.go b/internal/experiment/wireguard/wireguard.go index 5c8d46d5c..acabe54b3 100644 --- a/internal/experiment/wireguard/wireguard.go +++ b/internal/experiment/wireguard/wireguard.go @@ -4,6 +4,8 @@ import ( "context" "errors" "fmt" + "net" + "net/http" "net/netip" "strings" "time" @@ -20,10 +22,7 @@ import ( const ( testName = "wireguard" - testVersion = "0.1.1" - - // defaultNameserver is the dns server using for resolving names inside the wg tunnel. - defaultNameserver = "8.8.8.8" + testVersion = "0.1.2" ) var ( @@ -34,11 +33,19 @@ var ( ErrInvalidInput = errors.New("invalid input") ) +type httpClient interface { + Get(string) (*http.Response, error) +} + // Measurer performs the measurement. type Measurer struct { events *eventLogger options *wireguardOptions tnet *netstack.Net + + // used just for testing + dialContextFn func(context.Context, string, string) (net.Conn, error) + httpClient httpClient } // NewExperimentMeasurer creates a new ExperimentMeasurer. @@ -78,7 +85,6 @@ func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { return ErrInvalidInputType } - // TODO(ainghazal): if the target is not public, substitute it with ASN? config, input := target.Options, target.URL if err := m.setupWireguardFromConfig(config); err != nil { // A failure at this point means that we are not able @@ -99,6 +105,8 @@ func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { if config.PublicTarget { testkeys.Endpoint = m.options.endpoint } else { + // TODO(ainghazal): if the target is not public, + // we might want to substitute it with ASN. testkeys.Endpoint = input } @@ -115,6 +123,7 @@ func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { testkeys.NetworkEvents = m.events.log() } + // 4. assign test keys measurement.TestKeys = testkeys sess.Logger().Infof("%s", "Wireguard experiment done.") diff --git a/internal/experiment/wireguard/wireguard_test.go b/internal/experiment/wireguard/wireguard_test.go index c39c30931..e0ad68752 100644 --- a/internal/experiment/wireguard/wireguard_test.go +++ b/internal/experiment/wireguard/wireguard_test.go @@ -14,7 +14,7 @@ func TestNewExperimentMeasurer(t *testing.T) { if m.ExperimentName() != "wireguard" { t.Fatal("invalid ExperimentName") } - if m.ExperimentVersion() != "0.1.1" { + if m.ExperimentVersion() != "0.1.2" { t.Fatal("invalid ExperimentVersion") } } From a937ccc7a2541bf60d36b8786da605efc2c51cef Mon Sep 17 00:00:00 2001 From: ain ghazal Date: Tue, 25 Jun 2024 11:33:15 +0200 Subject: [PATCH 10/14] add tests for loader --- .../experiment/wireguard/richerinput_test.go | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 internal/experiment/wireguard/richerinput_test.go diff --git a/internal/experiment/wireguard/richerinput_test.go b/internal/experiment/wireguard/richerinput_test.go new file mode 100644 index 000000000..872901351 --- /dev/null +++ b/internal/experiment/wireguard/richerinput_test.go @@ -0,0 +1,141 @@ +package wireguard + +import ( + "context" + "errors" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/ooni/probe-cli/v3/internal/mocks" + "github.com/ooni/probe-cli/v3/internal/model" + "github.com/ooni/probe-cli/v3/internal/targetloading" +) + +func TestTarget(t *testing.T) { + target := &Target{ + URL: "wg://unknown.corp", + } + + t.Run("Category", func(t *testing.T) { + if target.Category() != model.DefaultCategoryCode { + t.Fatal("invalid Category") + } + }) + + t.Run("Country", func(t *testing.T) { + if target.Country() != model.DefaultCountryCode { + t.Fatal("invalid Country") + } + }) + + t.Run("Input", func(t *testing.T) { + if target.Input() != "wg://unknown.corp" { + t.Fatal("invalid Input") + } + }) + + t.Run("String", func(t *testing.T) { + if target.String() != "wg://unknown.corp" { + t.Fatal("invalid String") + } + }) +} + +func TestNewLoader(t *testing.T) { + // create the pointers we expect to see + child := &targetloading.Loader{} + options := &Config{} + + // create the loader and cast it to its private type + loader := NewLoader(child, options).(*targetLoader) + + // make sure the loader is okay + if child != loader.loader { + t.Fatal("invalid loader pointer") + } + + // make sure the options are okay + if options != loader.options { + t.Fatal("invalid options pointer") + } +} + +func TestTargetLoaderLoad(t *testing.T) { + // testcase is a test case implemented by this function + type testcase struct { + // name is the test case name + name string + + // options contains the options to use + options *Config + + // loader is the loader to use + loader *targetloading.Loader + + // expectErr is the error we expect + expectErr error + + // expectResults contains the expected results + expectTargets []model.ExperimentTarget + } + + cases := []testcase{ + + { + name: "with options and inputs", + options: &Config{ + SafeRemote: "1.1.1.1:443", + }, + loader: &targetloading.Loader{ + ExperimentName: "wireguard", + InputPolicy: model.InputNone, + Logger: model.DiscardLogger, + Session: &mocks.Session{}, + StaticInputs: []string{ + "wg://unknown.corp/1.1.1.1", + }, + }, + expectErr: nil, + expectTargets: []model.ExperimentTarget{ + &Target{ + URL: "wg://unknown.corp/1.1.1.1", + Options: &Config{ + SafeRemote: "1.1.1.1:443", + }, + }, + }, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // create a target loader using the given config + tl := &targetLoader{ + loader: tc.loader, + options: tc.options, + } + + // load targets + targets, err := tl.Load(context.Background()) + + // make sure error is consistent + switch { + case err == nil && tc.expectErr == nil: + // fallthrough + + case err != nil && tc.expectErr != nil: + if !errors.Is(err, tc.expectErr) { + t.Fatal("unexpected error", err) + } + // fallthrough + + default: + t.Fatal("expected", tc.expectErr, "got", err) + } + + // make sure the targets are consistent + if diff := cmp.Diff(tc.expectTargets, targets); diff != "" { + t.Fatal(diff) + } + }) + } +} From a9fb7d44f12a36af57a077406e23871da99fb543 Mon Sep 17 00:00:00 2001 From: ain ghazal Date: Tue, 25 Jun 2024 11:39:21 +0200 Subject: [PATCH 11/14] add input as strictly required --- internal/registry/wireguard.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/registry/wireguard.go b/internal/registry/wireguard.go index 945737072..39b2cd4b3 100644 --- a/internal/registry/wireguard.go +++ b/internal/registry/wireguard.go @@ -22,7 +22,7 @@ func init() { interruptible: true, // TODO(ainghazal): when the backend is ready to hand us targets, // we will use InputOrQueryBackend. - inputPolicy: model.InputNone, + inputPolicy: model.InputStrictlyRequired, newLoader: wireguard.NewLoader, } } From 00f9096e5b2d3afc0b9c74bffa163fc874a6ef1e Mon Sep 17 00:00:00 2001 From: ain ghazal Date: Tue, 25 Jun 2024 11:40:32 +0200 Subject: [PATCH 12/14] formatting --- internal/registry/wireguard.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/registry/wireguard.go b/internal/registry/wireguard.go index 39b2cd4b3..480e877d5 100644 --- a/internal/registry/wireguard.go +++ b/internal/registry/wireguard.go @@ -1,7 +1,7 @@ package registry // -// Registers the `wireguard' experiment. +// Registers the `wireguard` experiment. // import ( From 84cb927bb120503e5941d1406f7bdaa7b4d4093f Mon Sep 17 00:00:00 2001 From: ain ghazal Date: Tue, 25 Jun 2024 18:10:43 +0200 Subject: [PATCH 13/14] add expectations for wireguard --- internal/registry/factory_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/registry/factory_test.go b/internal/registry/factory_test.go index c92f03157..33779ea7d 100644 --- a/internal/registry/factory_test.go +++ b/internal/registry/factory_test.go @@ -571,6 +571,11 @@ func TestNewFactory(t *testing.T) { enabledByDefault: true, inputPolicy: model.InputNone, }, + "wireguard": { + enabledByDefault: true, + inputPolicy: model.InputStrictlyRequired, + interruptible: true, + }, } // testCase is a test case checked by this func From 25e56618a96c89adc9543b1640ff4e948d46f135 Mon Sep 17 00:00:00 2001 From: ain ghazal Date: Tue, 25 Jun 2024 18:20:56 +0200 Subject: [PATCH 14/14] use netxlite.ReadAllContext --- internal/experiment/wireguard/urlget.go | 7 ++++--- internal/experiment/wireguard/urlget_test.go | 14 +++++++------- internal/experiment/wireguard/wireguard.go | 2 +- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/internal/experiment/wireguard/urlget.go b/internal/experiment/wireguard/urlget.go index 4f1e3c748..eae2f9a5e 100644 --- a/internal/experiment/wireguard/urlget.go +++ b/internal/experiment/wireguard/urlget.go @@ -1,12 +1,13 @@ package wireguard import ( - "io" + "context" "net/http" "time" "github.com/ooni/probe-cli/v3/internal/measurexlite" "github.com/ooni/probe-cli/v3/internal/model" + "github.com/ooni/probe-cli/v3/internal/netxlite" ) const ( @@ -16,7 +17,7 @@ const ( // urlget implements an straightforward urlget experiment using the standard library. // By default we pass the wireguard tunnel DialContext to the `http.Transport` on the `http.Client` creation. -func (m *Measurer) urlget(url string, zeroTime time.Time, logger model.Logger) *URLGetResult { +func (m *Measurer) urlget(ctx context.Context, url string, zeroTime time.Time, logger model.Logger) *URLGetResult { if m.dialContextFn == nil { m.dialContextFn = m.tnet.DialContext } @@ -35,7 +36,7 @@ func (m *Measurer) urlget(url string, zeroTime time.Time, logger model.Logger) * logger.Warnf("urlget error: %v", err.Error()) return newURLResultFromError(url, zeroTime, start, err) } - body, err := io.ReadAll(r.Body) + body, err := netxlite.ReadAllContext(ctx, r.Body) if err != nil { logger.Warnf("urlget error: %v", err.Error()) return newURLResultFromError(url, zeroTime, start, err) diff --git a/internal/experiment/wireguard/urlget_test.go b/internal/experiment/wireguard/urlget_test.go index 4add4e9f3..f044a6411 100644 --- a/internal/experiment/wireguard/urlget_test.go +++ b/internal/experiment/wireguard/urlget_test.go @@ -22,17 +22,17 @@ func (c *failingHttpClient) Get(string) (*http.Response, error) { func Test_urlget(t *testing.T) { t.Run("dummy server gets a URLGetResult, with no error", func(t *testing.T) { expected := "dummy data" - svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) w.Write([]byte(expected)) })) - defer svr.Close() + defer srv.Close() m := &Measurer{} m.dialContextFn = func(_ context.Context, network, address string) (net.Conn, error) { return net.Dial(network, address) } - r := m.urlget(svr.URL, time.Now(), model.DiscardLogger) + r := m.urlget(context.Background(), srv.URL, time.Now(), model.DiscardLogger) if r.StatusCode != 200 { t.Fatal("expected statusCode==200") } @@ -40,17 +40,17 @@ func Test_urlget(t *testing.T) { t.Run("dummy server gets a URLGetResult with 500 status code", func(t *testing.T) { expected := "dummy data" - svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(500) w.Write([]byte(expected)) })) - defer svr.Close() + defer srv.Close() m := &Measurer{} m.dialContextFn = func(_ context.Context, network, address string) (net.Conn, error) { return net.Dial(network, address) } - r := m.urlget(svr.URL, time.Now(), model.DiscardLogger) + r := m.urlget(context.Background(), srv.URL, time.Now(), model.DiscardLogger) if r.StatusCode != 500 { t.Fatal("expected statusCode==500") } @@ -60,7 +60,7 @@ func Test_urlget(t *testing.T) { m := &Measurer{} m.httpClient = &failingHttpClient{} - r := m.urlget("http://example.org", time.Now(), model.DiscardLogger) + r := m.urlget(context.Background(), "http://example.org", time.Now(), model.DiscardLogger) expectedError := "unknown_failure: some error" if *r.Failure != expectedError { t.Fatal("expected error") diff --git a/internal/experiment/wireguard/wireguard.go b/internal/experiment/wireguard/wireguard.go index acabe54b3..9171ceb14 100644 --- a/internal/experiment/wireguard/wireguard.go +++ b/internal/experiment/wireguard/wireguard.go @@ -118,7 +118,7 @@ func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { // 3. use tunnel if err == nil { sess.Logger().Info("Using the wireguard tunnel.") - urlgetResult := m.urlget(defaultURLGetTarget, zeroTime, sess.Logger()) + urlgetResult := m.urlget(ctx, defaultURLGetTarget, zeroTime, sess.Logger()) testkeys.URLGet = append(testkeys.URLGet, urlgetResult) testkeys.NetworkEvents = m.events.log() }