feat(torsf): collect tor logs, select rendezvous method, count bytes (#683)
This diff contains significant improvements over the previous implementation of the torsf experiment. We add support for configuring different rendezvous methods after the convo at https://github.com/ooni/probe/issues/2004. In doing that, I've tried to use a terminology that is consistent with the names being actually used by tor developers. In terms of what to do next, this diff basically instruments torsf to always rendezvous using domain fronting. Yet, it's also possible to change the rendezvous method from the command line, when using miniooni, which allows to experiment a bit more. In the same vein, by default we use a persistent tor datadir, but it's also possible to use a temporary datadir using the cmdline. Here's how a generic invocation of `torsf` looks like: ```bash ./miniooni -O DisablePersistentDatadir=true \ -O RendezvousMethod=amp \ -O DisableProgress=true \ torsf ``` (The default is `DisablePersistentDatadir=false` and `RendezvousMethod=domain_fronting`.) With this implementation, we can start measuring whether snowflake and tor together can boostrap, which seems the most important thing to focus on at the beginning. Understanding why the bootstrap most often does not converge with a temporary datadir on Android devices remains instead an open problem for now. (I'll also update the relevant issues or create new issues after commit this.) We also address some methodology improvements that were proposed in https://github.com/ooni/probe/issues/1686. Namely: 1. we record the tor version; 2. we include the bootstrap percentage by reading the logs; 3. we set the anomaly key correctly; 4. we measure the bytes send and received (by `tor` not by `snowflake`, since doing it for snowflake seems more complex at this stage). What remains to be done is the possibility of including Snowflake events into the measurement, which is not possible until the new improvements at common/event in snowflake.git are included into a tagged version of snowflake itself. (I'll make sure to mention this aspect to @cohosh in https://github.com/ooni/probe/issues/2004.)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/torsf
|
||||
@@ -0,0 +1,31 @@
|
||||
Feb 04 15:04:29.000 [notice] Tor 0.4.6.9 opening new log file.
|
||||
Feb 04 15:04:29.360 [notice] We compiled with OpenSSL 101010cf: OpenSSL 1.1.1l FIPS 24 Aug 2021 and we are running with OpenSSL 101010cf: 1.1.1l. These two versions should be binary compatible.
|
||||
Feb 04 15:04:29.363 [notice] Tor 0.4.6.9 running on Linux with Libevent 2.1.12-stable, OpenSSL 1.1.1l, Zlib 1.2.11, Liblzma 5.2.5, Libzstd 1.5.2 and Glibc 2.34 as libc.
|
||||
Feb 04 15:04:29.363 [notice] Tor can't help you if you use it wrong! Learn how to be safe at https://www.torproject.org/download/download#warning
|
||||
Feb 04 15:04:29.363 [warn] Tor was compiled with zstd 1.5.1, but is running with zstd 1.5.2. For safety, we'll avoid using advanced zstd functionality.
|
||||
Feb 04 15:04:29.363 [notice] Read configuration file "/home/sbs/.miniooni/tunnel/torsf/tor/torrc-2981077975".
|
||||
Feb 04 15:04:29.366 [notice] Opening Control listener on 127.0.0.1:0
|
||||
Feb 04 15:04:29.367 [notice] Control listener listening on port 41423.
|
||||
Feb 04 15:04:29.367 [notice] Opened Control listener connection (ready) on 127.0.0.1:41423
|
||||
Feb 04 15:04:29.367 [notice] DisableNetwork is set. Tor will not make or accept non-control network connections. Shutting down all existing connections.
|
||||
Feb 04 15:04:29.000 [notice] Parsing GEOIP IPv4 file /usr/share/tor/geoip.
|
||||
Feb 04 15:04:29.000 [notice] Parsing GEOIP IPv6 file /usr/share/tor/geoip6.
|
||||
Feb 04 15:04:29.000 [notice] Bootstrapped 0% (starting): Starting
|
||||
Feb 04 15:04:29.000 [notice] Starting with guard context "bridges"
|
||||
Feb 04 15:04:29.000 [notice] new bridge descriptor 'flakey4' (cached): $2B280B23E1107BB62ABFC40DDCC8824814F80A72~flakey4 [1zOHpg+FxqQfi/6jDLtCpHHqBTH8gjYmCKXkus1D5Ko] at 192.0.2.3
|
||||
Feb 04 15:04:29.000 [notice] Delaying directory fetches: DisableNetwork is set.
|
||||
Feb 04 15:04:29.000 [notice] New control connection opened from 127.0.0.1.
|
||||
Feb 04 15:04:29.000 [notice] Opening Socks listener on 127.0.0.1:0
|
||||
Feb 04 15:04:29.000 [notice] Socks listener listening on port 42089.
|
||||
Feb 04 15:04:29.000 [notice] Opened Socks listener connection (ready) on 127.0.0.1:42089
|
||||
Feb 04 15:04:29.000 [notice] Tor 0.4.6.9 opening log file.
|
||||
Feb 04 15:04:29.000 [notice] Bootstrapped 1% (conn_pt): Connecting to pluggable transport
|
||||
Feb 04 15:04:30.000 [notice] Bootstrapped 2% (conn_done_pt): Connected to pluggable transport
|
||||
Feb 04 15:04:30.000 [notice] Bootstrapped 10% (conn_done): Connected to a relay
|
||||
Feb 04 15:06:20.000 [notice] Bootstrapped 14% (handshake): Handshaking with a relay
|
||||
Feb 04 15:06:24.000 [notice] Bootstrapped 15% (handshake_done): Handshake with a relay done
|
||||
Feb 04 15:06:24.000 [notice] Bootstrapped 75% (enough_dirinfo): Loaded enough directory info to build circuits
|
||||
Feb 04 15:06:24.000 [notice] Bootstrapped 95% (circuit_create): Establishing a Tor circuit
|
||||
Feb 04 15:06:26.000 [notice] new bridge descriptor 'flakey4' (fresh): $2B280B23E1107BB62ABFC40DDCC8824814F80A72~flakey4 [1zOHpg+FxqQfi/6jDLtCpHHqBTH8gjYmCKXkus1D5Ko] at 192.0.2.3
|
||||
Feb 04 15:06:39.000 [notice] Bootstrapped 100% (done): Done
|
||||
Feb 04 15:06:39.000 [notice] Catching signal TERM, exiting cleanly.
|
||||
@@ -5,11 +5,17 @@
|
||||
package torsf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/ooni/probe-cli/v3/internal/bytecounter"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx/archival"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
"github.com/ooni/probe-cli/v3/internal/ptx"
|
||||
@@ -17,11 +23,18 @@ import (
|
||||
)
|
||||
|
||||
// testVersion is the tor experiment version.
|
||||
const testVersion = "0.1.1"
|
||||
const testVersion = "0.2.0"
|
||||
|
||||
// Config contains the experiment config.
|
||||
type Config struct {
|
||||
// DisablePersistentDatadir disables using a persistent datadir.
|
||||
DisablePersistentDatadir bool `ooni:"Disable using a persistent tor datadir"`
|
||||
|
||||
// DisableProgress disables printing progress messages.
|
||||
DisableProgress bool `ooni:"Disable printing progress messages"`
|
||||
|
||||
// RendezvousMethod allows to choose the method with which to rendezvous.
|
||||
RendezvousMethod string `ooni:"Choose the method with which to rendezvous. Must be one of amp and domain_fronting. Leaving this field empty means we should use the default."`
|
||||
}
|
||||
|
||||
// TestKeys contains the experiment's result.
|
||||
@@ -31,6 +44,18 @@ type TestKeys struct {
|
||||
|
||||
// Failure contains the failure string or nil.
|
||||
Failure *string `json:"failure"`
|
||||
|
||||
// PersistentDatadir indicates whether we're using a persistent tor datadir.
|
||||
PersistentDatadir bool `json:"persistent_datadir"`
|
||||
|
||||
// RendezvousMethod contains the method used to perform the rendezvous.
|
||||
RendezvousMethod string `json:"rendezvous_method"`
|
||||
|
||||
// TorLogs contains the bootstrap logs.
|
||||
TorLogs []string `json:"tor_logs"`
|
||||
|
||||
// TorVersion contains the version of tor (if it's possible to obtain it).
|
||||
TorVersion string `json:"tor_version"`
|
||||
}
|
||||
|
||||
// Measurer performs the measurement.
|
||||
@@ -44,7 +69,8 @@ type Measurer struct {
|
||||
|
||||
// mockStartTunnel is an optional function that allows us to override the
|
||||
// default tunnel.Start function used to start a tunnel.
|
||||
mockStartTunnel func(ctx context.Context, config *tunnel.Config) (tunnel.Tunnel, error)
|
||||
mockStartTunnel func(
|
||||
ctx context.Context, config *tunnel.Config) (tunnel.Tunnel, tunnel.DebugInfo, error)
|
||||
}
|
||||
|
||||
// ExperimentName implements model.ExperimentMeasurer.ExperimentName.
|
||||
@@ -73,22 +99,27 @@ func (m *Measurer) Run(
|
||||
ctx context.Context, sess model.ExperimentSession,
|
||||
measurement *model.Measurement, callbacks model.ExperimentCallbacks,
|
||||
) error {
|
||||
ptl, sfdialer, err := m.setup(ctx, sess.Logger())
|
||||
if err != nil {
|
||||
// we cannot setup the experiment
|
||||
return err
|
||||
}
|
||||
defer ptl.Stop()
|
||||
m.registerExtensions(measurement)
|
||||
testkeys := &TestKeys{}
|
||||
measurement.TestKeys = testkeys
|
||||
start := time.Now()
|
||||
const maxRuntime = 600 * time.Second
|
||||
ctx, cancel := context.WithTimeout(ctx, maxRuntime)
|
||||
defer cancel()
|
||||
errch := make(chan error)
|
||||
tkch := make(chan *TestKeys)
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
defer ticker.Stop()
|
||||
go m.run(ctx, sess, testkeys, errch)
|
||||
go m.bootstrap(ctx, sess, tkch, ptl, sfdialer)
|
||||
for {
|
||||
select {
|
||||
case err := <-errch:
|
||||
case tk := <-tkch:
|
||||
measurement.TestKeys = tk
|
||||
callbacks.OnProgress(1.0, "torsf experiment is finished")
|
||||
return err
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
if !m.config.DisableProgress {
|
||||
elapsedTime := time.Since(start)
|
||||
@@ -101,28 +132,50 @@ func (m *Measurer) Run(
|
||||
}
|
||||
}
|
||||
|
||||
// run runs the bootstrap. This function ONLY returns an error when
|
||||
// there has been a fundamental error starting the test. This behavior
|
||||
// follows the expectations for the ExperimentMeasurer.Run method.
|
||||
func (m *Measurer) run(ctx context.Context,
|
||||
sess model.ExperimentSession, testkeys *TestKeys, errch chan<- error) {
|
||||
sfdialer := &ptx.SnowflakeDialer{}
|
||||
// setup prepares for running the torsf experiment. Returns a valid ptx listener
|
||||
// and snowflake dialer on success. Returns an error on failure. On success,
|
||||
// remember to Stop the ptx listener when you're done.
|
||||
func (m *Measurer) setup(ctx context.Context,
|
||||
logger model.Logger) (*ptx.Listener, *ptx.SnowflakeDialer, error) {
|
||||
rm, err := ptx.NewSnowflakeRendezvousMethod(m.config.RendezvousMethod)
|
||||
if err != nil {
|
||||
// cannot run the experiment with unknown rendezvous method
|
||||
return nil, nil, err
|
||||
}
|
||||
sfdialer := ptx.NewSnowflakeDialerWithRendezvousMethod(rm)
|
||||
ptl := &ptx.Listener{
|
||||
PTDialer: sfdialer,
|
||||
Logger: sess.Logger(),
|
||||
ExperimentByteCounter: bytecounter.ContextExperimentByteCounter(ctx),
|
||||
Logger: logger,
|
||||
PTDialer: sfdialer,
|
||||
SessionByteCounter: bytecounter.ContextSessionByteCounter(ctx),
|
||||
}
|
||||
if err := m.startListener(ptl.Start); err != nil {
|
||||
testkeys.Failure = archival.NewFailure(err)
|
||||
// This error condition mostly means "I could not open a local
|
||||
// listening port", which strikes as fundamental failure.
|
||||
errch <- err
|
||||
return
|
||||
return nil, nil, err
|
||||
}
|
||||
defer ptl.Stop()
|
||||
tun, err := m.startTunnel()(ctx, &tunnel.Config{
|
||||
logger.Infof("torsf: rendezvous method: '%s'", m.config.RendezvousMethod)
|
||||
return ptl, sfdialer, nil
|
||||
}
|
||||
|
||||
// bootstrap runs the bootstrap.
|
||||
func (m *Measurer) bootstrap(ctx context.Context, sess model.ExperimentSession,
|
||||
out chan<- *TestKeys, ptl *ptx.Listener, sfdialer *ptx.SnowflakeDialer) {
|
||||
tk := &TestKeys{
|
||||
BootstrapTime: 0,
|
||||
Failure: nil,
|
||||
PersistentDatadir: !m.config.DisablePersistentDatadir,
|
||||
RendezvousMethod: sfdialer.RendezvousMethod.Name(),
|
||||
}
|
||||
sess.Logger().Infof(
|
||||
"torsf: disable persistent datadir: %+v", m.config.DisablePersistentDatadir)
|
||||
defer func() {
|
||||
out <- tk
|
||||
}()
|
||||
tun, debugInfo, err := m.startTunnel()(ctx, &tunnel.Config{
|
||||
Name: "tor",
|
||||
Session: sess,
|
||||
TunnelDir: path.Join(sess.TempDir(), "torsf"),
|
||||
TunnelDir: path.Join(m.baseTunnelDir(sess), "torsf"),
|
||||
Logger: sess.Logger(),
|
||||
TorArgs: []string{
|
||||
"UseBridges", "1",
|
||||
@@ -130,18 +183,61 @@ func (m *Measurer) run(ctx context.Context,
|
||||
"Bridge", sfdialer.AsBridgeArgument(),
|
||||
},
|
||||
})
|
||||
tk.TorVersion = debugInfo.Version
|
||||
m.readTorLogs(sess.Logger(), tk, debugInfo.LogFilePath)
|
||||
if err != nil {
|
||||
// Note: archival.NewFailure scrubs IP addresses
|
||||
testkeys.Failure = archival.NewFailure(err)
|
||||
// This error condition means we could not bootstrap with snowflake
|
||||
// for $reasons, so the experiment didn't fail, rather it did record
|
||||
// that something prevented snowflake from running.
|
||||
errch <- nil
|
||||
tk.Failure = archival.NewFailure(err)
|
||||
return
|
||||
}
|
||||
defer tun.Stop()
|
||||
testkeys.BootstrapTime = tun.BootstrapTime().Seconds()
|
||||
errch <- nil
|
||||
tk.BootstrapTime = tun.BootstrapTime().Seconds()
|
||||
}
|
||||
|
||||
// torProgressRegexp helps to extract progress info from logs.
|
||||
//
|
||||
// See https://regex101.com/r/3YfIed/1.
|
||||
var torProgressRegexp = regexp.MustCompile(
|
||||
`^[A-Za-z0-9.: ]+ \[notice\] Bootstrapped [0-9]+% \([a-zA-z]+\): [A-Za-z0-9 ]+$`)
|
||||
|
||||
// readTorLogs attempts to read and include the tor logs into
|
||||
// the test keys if this operation is possible.
|
||||
//
|
||||
// This function aims to _only_ include notice information about
|
||||
// bootstrap according to the torProgressRegexp regexp.
|
||||
//
|
||||
// Tor is know to be good software that does not break its output
|
||||
// unnecessarily and that does not include PII into its logs unless
|
||||
// explicitly asked to. This fact gives me confidence that we can
|
||||
// safely include this subset of the logs into the results.
|
||||
//
|
||||
// On this note, I think it's safe to include timestamps from the
|
||||
// logs into the output, since we have a timestamp for the whole
|
||||
// experiment already, so we don't leak much more by also including
|
||||
// the Tor proper timestamps into the results.
|
||||
func (m *Measurer) readTorLogs(logger model.Logger, tk *TestKeys, logFilePath string) {
|
||||
if logFilePath == "" {
|
||||
log.Warn("the tunnel claims there is no log file")
|
||||
return
|
||||
}
|
||||
data, err := os.ReadFile(logFilePath)
|
||||
if err != nil {
|
||||
log.Warnf("could not read tor logs: %s", err.Error())
|
||||
return
|
||||
}
|
||||
for _, bline := range bytes.Split(data, []byte("\n")) {
|
||||
if torProgressRegexp.Match(bline) {
|
||||
tk.TorLogs = append(tk.TorLogs, string(bline))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// baseTunnelDir returns the base directory to use for tunnelling
|
||||
func (m *Measurer) baseTunnelDir(sess model.ExperimentSession) string {
|
||||
if m.config.DisablePersistentDatadir {
|
||||
return sess.TempDir()
|
||||
}
|
||||
return sess.TunnelDir()
|
||||
}
|
||||
|
||||
// startListener either calls f or mockStartListener depending
|
||||
@@ -155,7 +251,7 @@ func (m *Measurer) startListener(f func() error) error {
|
||||
|
||||
// startTunnel returns the proper function to start a tunnel.
|
||||
func (m *Measurer) startTunnel() func(
|
||||
ctx context.Context, config *tunnel.Config) (tunnel.Tunnel, error) {
|
||||
ctx context.Context, config *tunnel.Config) (tunnel.Tunnel, tunnel.DebugInfo, error) {
|
||||
if m.mockStartTunnel != nil {
|
||||
return m.mockStartTunnel
|
||||
}
|
||||
@@ -175,7 +271,22 @@ type SummaryKeys struct {
|
||||
IsAnomaly bool `json:"-"`
|
||||
}
|
||||
|
||||
var (
|
||||
// errInvalidTestKeysType indicates the test keys type is invalid.
|
||||
errInvalidTestKeysType = errors.New("torsf: invalid test keys type")
|
||||
|
||||
//errNilTestKeys indicates that the test keys are nil.
|
||||
errNilTestKeys = errors.New("torsf: nil test keys")
|
||||
)
|
||||
|
||||
// GetSummaryKeys implements model.ExperimentMeasurer.GetSummaryKeys.
|
||||
func (m *Measurer) GetSummaryKeys(measurement *model.Measurement) (interface{}, error) {
|
||||
return SummaryKeys{IsAnomaly: false}, nil
|
||||
testkeys, good := measurement.TestKeys.(*TestKeys)
|
||||
if !good {
|
||||
return nil, errInvalidTestKeysType
|
||||
}
|
||||
if testkeys == nil {
|
||||
return nil, errNilTestKeys
|
||||
}
|
||||
return SummaryKeys{IsAnomaly: testkeys.Failure != nil}, nil
|
||||
}
|
||||
|
||||
@@ -3,14 +3,16 @@ package torsf
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/ooni/probe-cli/v3/internal/atomicx"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
"github.com/ooni/probe-cli/v3/internal/ptx"
|
||||
"github.com/ooni/probe-cli/v3/internal/tunnel"
|
||||
"github.com/ooni/probe-cli/v3/internal/tunnel/mocks"
|
||||
)
|
||||
|
||||
func TestExperimentNameAndVersion(t *testing.T) {
|
||||
@@ -18,85 +20,34 @@ func TestExperimentNameAndVersion(t *testing.T) {
|
||||
if m.ExperimentName() != "torsf" {
|
||||
t.Fatal("invalid experiment name")
|
||||
}
|
||||
if m.ExperimentVersion() != "0.1.1" {
|
||||
if m.ExperimentVersion() != "0.2.0" {
|
||||
t.Fatal("invalid experiment version")
|
||||
}
|
||||
}
|
||||
|
||||
// mockedTunnel is a mocked tunnel.
|
||||
type mockedTunnel struct {
|
||||
bootstrapTime time.Duration
|
||||
proxyURL *url.URL
|
||||
}
|
||||
|
||||
// BootstrapTime implements Tunnel.BootstrapTime.
|
||||
func (mt *mockedTunnel) BootstrapTime() time.Duration {
|
||||
return mt.bootstrapTime
|
||||
}
|
||||
|
||||
// SOCKS5ProxyURL implements Tunnel.SOCKS5ProxyURL.
|
||||
func (mt *mockedTunnel) SOCKS5ProxyURL() *url.URL {
|
||||
return mt.proxyURL
|
||||
}
|
||||
|
||||
// Stop implements Tunnel.Stop.
|
||||
func (mt *mockedTunnel) Stop() {
|
||||
// nothing
|
||||
}
|
||||
|
||||
func TestSuccessWithMockedTunnelStart(t *testing.T) {
|
||||
bootstrapTime := 400 * time.Millisecond
|
||||
func TestFailureWithInvalidRendezvousMethod(t *testing.T) {
|
||||
m := &Measurer{
|
||||
config: Config{},
|
||||
mockStartTunnel: func(ctx context.Context, config *tunnel.Config) (tunnel.Tunnel, error) {
|
||||
// run for some time so we also exercise printing progress.
|
||||
time.Sleep(bootstrapTime)
|
||||
return &mockedTunnel{
|
||||
bootstrapTime: time.Duration(bootstrapTime),
|
||||
}, nil
|
||||
config: Config{
|
||||
DisablePersistentDatadir: false,
|
||||
DisableProgress: false,
|
||||
RendezvousMethod: "antani",
|
||||
},
|
||||
mockStartTunnel: nil,
|
||||
}
|
||||
ctx := context.Background()
|
||||
measurement := &model.Measurement{}
|
||||
sess := &mockable.Session{}
|
||||
sess := &mockable.Session{
|
||||
MockableLogger: model.DiscardLogger,
|
||||
}
|
||||
callbacks := &model.PrinterCallbacks{
|
||||
Logger: log.Log,
|
||||
Logger: model.DiscardLogger,
|
||||
}
|
||||
if err := m.Run(ctx, sess, measurement, callbacks); err != nil {
|
||||
t.Fatal(err)
|
||||
err := m.Run(ctx, sess, measurement, callbacks)
|
||||
if !errors.Is(err, ptx.ErrSnowflakeNoSuchRendezvousMethod) {
|
||||
t.Fatal("unexpected error", err)
|
||||
}
|
||||
tk := measurement.TestKeys.(*TestKeys)
|
||||
if tk.BootstrapTime != bootstrapTime.Seconds() {
|
||||
t.Fatal("unexpected bootstrap time")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFailureToStartTunnel(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
m := &Measurer{
|
||||
config: Config{},
|
||||
mockStartTunnel: func(ctx context.Context, config *tunnel.Config) (tunnel.Tunnel, error) {
|
||||
return nil, expected
|
||||
},
|
||||
}
|
||||
ctx := context.Background()
|
||||
measurement := &model.Measurement{}
|
||||
sess := &mockable.Session{}
|
||||
callbacks := &model.PrinterCallbacks{
|
||||
Logger: log.Log,
|
||||
}
|
||||
if err := m.Run(ctx, sess, measurement, callbacks); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tk := measurement.TestKeys.(*TestKeys)
|
||||
if tk.BootstrapTime != 0 {
|
||||
t.Fatal("unexpected bootstrap time")
|
||||
}
|
||||
if tk.Failure == nil {
|
||||
t.Fatal("unexpectedly nil failure string")
|
||||
}
|
||||
if *tk.Failure != "unknown_failure: mocked error" {
|
||||
t.Fatal("unexpected failure string", *tk.Failure)
|
||||
if measurement.TestKeys != nil {
|
||||
t.Fatal("expected nil test keys")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,11 +63,130 @@ func TestFailureToStartPTXListener(t *testing.T) {
|
||||
measurement := &model.Measurement{}
|
||||
sess := &mockable.Session{}
|
||||
callbacks := &model.PrinterCallbacks{
|
||||
Logger: log.Log,
|
||||
Logger: model.DiscardLogger,
|
||||
}
|
||||
if err := m.Run(ctx, sess, measurement, callbacks); !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected", err)
|
||||
}
|
||||
if tk := measurement.TestKeys; tk != nil {
|
||||
t.Fatal("expected nil bootstrap time here")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSuccessWithMockedTunnelStart(t *testing.T) {
|
||||
bootstrapTime := 3 * time.Second
|
||||
called := &atomicx.Int64{}
|
||||
m := &Measurer{
|
||||
config: Config{},
|
||||
mockStartTunnel: func(
|
||||
ctx context.Context, config *tunnel.Config) (tunnel.Tunnel, tunnel.DebugInfo, error) {
|
||||
// run for some time so we also exercise printing progress.
|
||||
time.Sleep(bootstrapTime)
|
||||
return &mocks.Tunnel{
|
||||
MockBootstrapTime: func() time.Duration {
|
||||
return bootstrapTime
|
||||
},
|
||||
MockStop: func() {
|
||||
called.Add(1)
|
||||
},
|
||||
}, tunnel.DebugInfo{
|
||||
Name: "tor",
|
||||
LogFilePath: filepath.Join("testdata", "tor.log"),
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
ctx := context.Background()
|
||||
measurement := &model.Measurement{}
|
||||
sess := &mockable.Session{
|
||||
MockableLogger: model.DiscardLogger,
|
||||
}
|
||||
callbacks := &model.PrinterCallbacks{
|
||||
Logger: model.DiscardLogger,
|
||||
}
|
||||
if err := m.Run(ctx, sess, measurement, callbacks); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if called.Load() != 1 {
|
||||
t.Fatal("stop was not called")
|
||||
}
|
||||
tk := measurement.TestKeys.(*TestKeys)
|
||||
if tk.BootstrapTime != bootstrapTime.Seconds() {
|
||||
t.Fatal("unexpected bootstrap time")
|
||||
}
|
||||
if tk.Failure != nil {
|
||||
t.Fatal("unexpected failure")
|
||||
}
|
||||
if !tk.PersistentDatadir {
|
||||
t.Fatal("unexpected persistent data dir")
|
||||
}
|
||||
if tk.RendezvousMethod != "domain_fronting" {
|
||||
t.Fatal("unexpected rendezvous method")
|
||||
}
|
||||
if count := len(tk.TorLogs); count != 9 {
|
||||
t.Fatal("unexpected length of tor logs", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithCancelledContext(t *testing.T) {
|
||||
// This test calls the real tunnel.Start function so we cover
|
||||
// it but fails immediately because of the cancelled ctx.
|
||||
m := &Measurer{
|
||||
config: Config{},
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // fail immediately
|
||||
measurement := &model.Measurement{}
|
||||
sess := &mockable.Session{
|
||||
MockableLogger: model.DiscardLogger,
|
||||
}
|
||||
callbacks := &model.PrinterCallbacks{
|
||||
Logger: model.DiscardLogger,
|
||||
}
|
||||
if err := m.Run(ctx, sess, measurement, callbacks); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tk := measurement.TestKeys.(*TestKeys)
|
||||
if tk.BootstrapTime != 0 {
|
||||
t.Fatal("unexpected bootstrap time")
|
||||
}
|
||||
if *tk.Failure != "interrupted" {
|
||||
t.Fatal("unexpected failure")
|
||||
}
|
||||
if !tk.PersistentDatadir {
|
||||
t.Fatal("unexpected persistent data dir")
|
||||
}
|
||||
if tk.RendezvousMethod != "domain_fronting" {
|
||||
t.Fatal("unexpected rendezvous method")
|
||||
}
|
||||
if len(tk.TorLogs) != 0 {
|
||||
t.Fatal("unexpected length of tor logs")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFailureToStartTunnel(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
m := &Measurer{
|
||||
config: Config{},
|
||||
mockStartTunnel: func(
|
||||
ctx context.Context, config *tunnel.Config) (tunnel.Tunnel, tunnel.DebugInfo, error) {
|
||||
return nil,
|
||||
tunnel.DebugInfo{
|
||||
Name: "tor",
|
||||
LogFilePath: filepath.Join("testdata", "tor.log"),
|
||||
}, expected
|
||||
},
|
||||
}
|
||||
ctx := context.Background()
|
||||
measurement := &model.Measurement{}
|
||||
sess := &mockable.Session{
|
||||
MockableLogger: model.DiscardLogger,
|
||||
}
|
||||
callbacks := &model.PrinterCallbacks{
|
||||
Logger: model.DiscardLogger,
|
||||
}
|
||||
if err := m.Run(ctx, sess, measurement, callbacks); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tk := measurement.TestKeys.(*TestKeys)
|
||||
if tk.BootstrapTime != 0 {
|
||||
t.Fatal("unexpected bootstrap time")
|
||||
@@ -127,41 +197,150 @@ func TestFailureToStartPTXListener(t *testing.T) {
|
||||
if *tk.Failure != "unknown_failure: mocked error" {
|
||||
t.Fatal("unexpected failure string", *tk.Failure)
|
||||
}
|
||||
if !tk.PersistentDatadir {
|
||||
t.Fatal("unexpected persistent datadir")
|
||||
}
|
||||
if tk.RendezvousMethod != "domain_fronting" {
|
||||
t.Fatal("unexpected rendezvous method")
|
||||
}
|
||||
if count := len(tk.TorLogs); count != 9 {
|
||||
t.Fatal("unexpected length of tor logs", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartWithCancelledContext(t *testing.T) {
|
||||
m := &Measurer{config: Config{}}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // fail immediately
|
||||
measurement := &model.Measurement{}
|
||||
sess := &mockable.Session{}
|
||||
callbacks := &model.PrinterCallbacks{
|
||||
Logger: log.Log,
|
||||
}
|
||||
if err := m.Run(ctx, sess, measurement, callbacks); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tk := measurement.TestKeys.(*TestKeys)
|
||||
if tk.BootstrapTime != 0 {
|
||||
t.Fatal("unexpected bootstrap time")
|
||||
}
|
||||
if tk.Failure == nil {
|
||||
t.Fatal("unexpected nil failure")
|
||||
}
|
||||
if *tk.Failure != "interrupted" {
|
||||
t.Fatal("unexpected failure string", *tk.Failure)
|
||||
}
|
||||
func TestBaseTunnelDir(t *testing.T) {
|
||||
t.Run("without persistent data dir", func(t *testing.T) {
|
||||
m := &Measurer{
|
||||
config: Config{
|
||||
DisablePersistentDatadir: true,
|
||||
},
|
||||
}
|
||||
sess := &mockable.Session{
|
||||
MockableTunnelDir: "a",
|
||||
MockableTempDir: "b",
|
||||
}
|
||||
dir := m.baseTunnelDir(sess)
|
||||
if dir != "b" {
|
||||
t.Fatal("unexpected base tunnel dir", dir)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with persistent data dir", func(t *testing.T) {
|
||||
m := &Measurer{
|
||||
config: Config{
|
||||
DisablePersistentDatadir: false,
|
||||
},
|
||||
}
|
||||
sess := &mockable.Session{
|
||||
MockableTunnelDir: "a",
|
||||
MockableTempDir: "b",
|
||||
}
|
||||
dir := m.baseTunnelDir(sess)
|
||||
if dir != "a" {
|
||||
t.Fatal("unexpected base tunnel dir", dir)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestReadTorLogs(t *testing.T) {
|
||||
t.Run("with empty file path", func(t *testing.T) {
|
||||
m := &Measurer{}
|
||||
logger := model.DiscardLogger
|
||||
tk := &TestKeys{}
|
||||
m.readTorLogs(logger, tk, "")
|
||||
if len(tk.TorLogs) != 0 {
|
||||
t.Fatal("expected no tor logs")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with nonexistent file path", func(t *testing.T) {
|
||||
m := &Measurer{}
|
||||
logger := model.DiscardLogger
|
||||
tk := &TestKeys{}
|
||||
m.readTorLogs(logger, tk, filepath.Join("testdata", "nonexistent"))
|
||||
if len(tk.TorLogs) != 0 {
|
||||
t.Fatal("expected no tor logs")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with existing file path", func(t *testing.T) {
|
||||
m := &Measurer{}
|
||||
logger := model.DiscardLogger
|
||||
tk := &TestKeys{}
|
||||
m.readTorLogs(logger, tk, filepath.Join("testdata", "tor.log"))
|
||||
if count := len(tk.TorLogs); count != 9 {
|
||||
t.Fatal("unexpected number of tor logs", count)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetSummaryKeys(t *testing.T) {
|
||||
measurement := &model.Measurement{}
|
||||
m := &Measurer{}
|
||||
sk, err := m.GetSummaryKeys(measurement)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rsk := sk.(SummaryKeys)
|
||||
if rsk.IsAnomaly {
|
||||
t.Fatal("expected no anomaly here")
|
||||
}
|
||||
t.Run("in case of untyped nil TestKeys", func(t *testing.T) {
|
||||
measurement := &model.Measurement{
|
||||
TestKeys: nil,
|
||||
}
|
||||
m := &Measurer{}
|
||||
_, err := m.GetSummaryKeys(measurement)
|
||||
if !errors.Is(err, errInvalidTestKeysType) {
|
||||
t.Fatal("unexpected error", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("in case of typed nil TestKeys", func(t *testing.T) {
|
||||
var tk *TestKeys
|
||||
measurement := &model.Measurement{
|
||||
TestKeys: tk,
|
||||
}
|
||||
m := &Measurer{}
|
||||
_, err := m.GetSummaryKeys(measurement)
|
||||
if !errors.Is(err, errNilTestKeys) {
|
||||
t.Fatal("unexpected error", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("in case of invalid TestKeys type", func(t *testing.T) {
|
||||
measurement := &model.Measurement{
|
||||
TestKeys: make(chan int),
|
||||
}
|
||||
m := &Measurer{}
|
||||
_, err := m.GetSummaryKeys(measurement)
|
||||
if !errors.Is(err, errInvalidTestKeysType) {
|
||||
t.Fatal("unexpected error", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("in case of success", func(t *testing.T) {
|
||||
measurement := &model.Measurement{
|
||||
TestKeys: &TestKeys{
|
||||
Failure: nil,
|
||||
},
|
||||
}
|
||||
m := &Measurer{}
|
||||
sk, err := m.GetSummaryKeys(measurement)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rsk := sk.(SummaryKeys)
|
||||
if rsk.IsAnomaly {
|
||||
t.Fatal("expected no anomaly here")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("in case of failure", func(t *testing.T) {
|
||||
failure := "generic_timeout_error"
|
||||
measurement := &model.Measurement{
|
||||
TestKeys: &TestKeys{
|
||||
Failure: &failure,
|
||||
},
|
||||
}
|
||||
m := &Measurer{}
|
||||
sk, err := m.GetSummaryKeys(measurement)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rsk := sk.(SummaryKeys)
|
||||
if !rsk.IsAnomaly {
|
||||
t.Fatal("expected anomaly here")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user