refactor: move tunnel pkg down one level (#358)
* refactor: move tunnel pkg down one level While there, reduce unnecessary dependency on external packages. * file I forgot to commit
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/armon/go-socks5"
|
||||
"github.com/cretz/bine/control"
|
||||
"github.com/cretz/bine/tor"
|
||||
"github.com/ooni/psiphon/oopsi/github.com/Psiphon-Labs/psiphon-tunnel-core/ClientLibrary/clientlib"
|
||||
"golang.org/x/sys/execabs"
|
||||
)
|
||||
|
||||
// Logger is the logger to use. Its signature is compatibile
|
||||
// with the apex/log logger signature.
|
||||
type Logger interface {
|
||||
// Infof formats and emits an informative message
|
||||
Infof(format string, v ...interface{})
|
||||
}
|
||||
|
||||
// Config contains the configuration for creating a Tunnel instance. You need
|
||||
// to fill all the mandatory fields. You SHOULD NOT modify the content of this
|
||||
// structure while in use, because that may lead to data races.
|
||||
type Config struct {
|
||||
// Name is the MANDATORY name of the tunnel. We support
|
||||
// "tor", "psiphon", and "fake" tunnels. You SHOULD
|
||||
// use "fake" tunnels only for testing: they don't provide
|
||||
// any real tunneling, just a socks5 proxy.
|
||||
Name string
|
||||
|
||||
// Session is the MANDATORY measurement session, or a suitable
|
||||
// mock of the required functionality. That is, the possibility
|
||||
// of obtaining a valid psiphon configuration.
|
||||
Session Session
|
||||
|
||||
// TunnelDir is the MANDATORY directory in which the tunnel SHOULD
|
||||
// store its state, if any. If this field is empty, the
|
||||
// Start function fails with ErrEmptyTunnelDir.
|
||||
TunnelDir string
|
||||
|
||||
// Logger is the optional logger to use. If empty we use a default
|
||||
// implementation that does not emit any output.
|
||||
Logger Logger
|
||||
|
||||
// TorArgs contains the optional arguments that you want us to pass
|
||||
// to the tor binary when invoking it. By default we do not
|
||||
// pass any extra argument. This flag might be useful to
|
||||
// configure pluggable transports.
|
||||
TorArgs []string
|
||||
|
||||
// TorBinary is the optional path of the TorBinary we SHOULD be
|
||||
// executing. When not set, we execute `tor`.
|
||||
TorBinary string
|
||||
|
||||
// testExecabsLookPath allows us to mock exeabs.LookPath
|
||||
testExecabsLookPath func(name string) (string, error)
|
||||
|
||||
// testMkdirAll allows us to mock os.MkdirAll in testing code.
|
||||
testMkdirAll func(path string, perm os.FileMode) error
|
||||
|
||||
// testNetListen allows us to mock net.Listen in testing code.
|
||||
testNetListen func(network string, address string) (net.Listener, error)
|
||||
|
||||
// testSocks5New allows us to mock socks5.New in testing code.
|
||||
testSocks5New func(conf *socks5.Config) (*socks5.Server, error)
|
||||
|
||||
// testStartPsiphon allows us to mock psiphon's clientlib.StartTunnel.
|
||||
testStartPsiphon func(ctx context.Context, config []byte,
|
||||
workdir string) (*clientlib.PsiphonTunnel, error)
|
||||
|
||||
// testTorStart allows us to mock tor.Start.
|
||||
testTorStart func(ctx context.Context, conf *tor.StartConf) (*tor.Tor, error)
|
||||
|
||||
// testTorEnableNetwork allows us to fake a failure when
|
||||
// telling to the tor daemon to enable the network.
|
||||
testTorEnableNetwork func(ctx context.Context, tor *tor.Tor, wait bool) error
|
||||
|
||||
// testTorGetInfo allows us to fake a failure when
|
||||
// getting info from the tor control port.
|
||||
testTorGetInfo func(ctrl *control.Conn, keys ...string) ([]*control.KeyVal, error)
|
||||
}
|
||||
|
||||
// silentLogger is a logger that does not emit output.
|
||||
type silentLogger struct{}
|
||||
|
||||
// Infof implements Logger.Infof.
|
||||
func (sl *silentLogger) Infof(format string, v ...interface{}) {}
|
||||
|
||||
// defaultLogger is the default logger.
|
||||
var defaultLogger = &silentLogger{}
|
||||
|
||||
// logger returns the logger to use.
|
||||
func (c *Config) logger() Logger {
|
||||
if c.Logger != nil {
|
||||
return c.Logger
|
||||
}
|
||||
return defaultLogger
|
||||
}
|
||||
|
||||
// execabsLookPath calls either testExeabsLookPath or execabs.LookPath
|
||||
func (c *Config) execabsLookPath(name string) (string, error) {
|
||||
if c.testExecabsLookPath != nil {
|
||||
return c.testExecabsLookPath(name)
|
||||
}
|
||||
return execabs.LookPath(name)
|
||||
}
|
||||
|
||||
// mkdirAll calls either testMkdirAll or os.MkdirAll.
|
||||
func (c *Config) mkdirAll(path string, perm os.FileMode) error {
|
||||
if c.testMkdirAll != nil {
|
||||
return c.testMkdirAll(path, perm)
|
||||
}
|
||||
return os.MkdirAll(path, perm)
|
||||
}
|
||||
|
||||
// netListen calls either testNetListen or net.Listen.
|
||||
func (c *Config) netListen(network string, address string) (net.Listener, error) {
|
||||
if c.testNetListen != nil {
|
||||
return c.testNetListen(network, address)
|
||||
}
|
||||
return net.Listen(network, address)
|
||||
}
|
||||
|
||||
// socks5New calls either testSocks5New or socks5.New
|
||||
func (c *Config) socks5New(conf *socks5.Config) (*socks5.Server, error) {
|
||||
if c.testSocks5New != nil {
|
||||
return c.testSocks5New(conf)
|
||||
}
|
||||
return socks5.New(conf)
|
||||
}
|
||||
|
||||
// startPsiphon calls either testStartPsiphon or psiphon's clientlib.StartTunnel.
|
||||
func (c *Config) startPsiphon(ctx context.Context, config []byte,
|
||||
workdir string) (*clientlib.PsiphonTunnel, error) {
|
||||
if c.testStartPsiphon != nil {
|
||||
return c.testStartPsiphon(ctx, config, workdir)
|
||||
}
|
||||
return clientlib.StartTunnel(ctx, config, "", clientlib.Parameters{
|
||||
DataRootDirectory: &workdir}, nil, nil)
|
||||
}
|
||||
|
||||
// torBinary returns the tor binary path, if configured, or
|
||||
// the default path, otherwise.
|
||||
func (c *Config) torBinary() string {
|
||||
if c.TorBinary != "" {
|
||||
return c.TorBinary
|
||||
}
|
||||
return "tor"
|
||||
}
|
||||
|
||||
// torStart calls either testTorStart or tor.Start.
|
||||
func (c *Config) torStart(ctx context.Context, conf *tor.StartConf) (*tor.Tor, error) {
|
||||
if c.testTorStart != nil {
|
||||
return c.testTorStart(ctx, conf)
|
||||
}
|
||||
return tor.Start(ctx, conf)
|
||||
}
|
||||
|
||||
// torEnableNetwork calls either testTorEnableNetwork or tor.EnableNetwork.
|
||||
func (c *Config) torEnableNetwork(ctx context.Context, tor *tor.Tor, wait bool) error {
|
||||
if c.testTorEnableNetwork != nil {
|
||||
return c.testTorEnableNetwork(ctx, tor, wait)
|
||||
}
|
||||
return tor.EnableNetwork(ctx, wait)
|
||||
}
|
||||
|
||||
// torGetInfo calls either testTorGetInfo or ctrl.GetInfo.
|
||||
func (c *Config) torGetInfo(ctrl *control.Conn, keys ...string) ([]*control.KeyVal, error) {
|
||||
if c.testTorGetInfo != nil {
|
||||
return c.testTorGetInfo(ctrl, keys...)
|
||||
}
|
||||
return ctrl.GetInfo(keys...)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/apex/log"
|
||||
)
|
||||
|
||||
func TestConfigLoggerDefault(t *testing.T) {
|
||||
config := &Config{}
|
||||
if config.logger() != defaultLogger {
|
||||
t.Fatal("not the logger we expected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigLoggerCustom(t *testing.T) {
|
||||
config := &Config{Logger: log.Log}
|
||||
if config.logger() != log.Log {
|
||||
t.Fatal("not the logger we expected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTorBinaryNotSet(t *testing.T) {
|
||||
config := &Config{}
|
||||
if config.torBinary() != "tor" {
|
||||
t.Fatal("not the result we expected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTorBinarySet(t *testing.T) {
|
||||
path := "/usr/local/bin/tor"
|
||||
config := &Config{TorBinary: path}
|
||||
if config.torBinary() != path {
|
||||
t.Fatal("not the result we expected")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/armon/go-socks5"
|
||||
)
|
||||
|
||||
// fakeTunnel is a fake tunnel.
|
||||
type fakeTunnel struct {
|
||||
addr net.Addr
|
||||
bootstrapTime time.Duration
|
||||
listener net.Listener
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
// BootstrapTime implements Tunnel.BootstrapTime.
|
||||
func (t *fakeTunnel) BootstrapTime() time.Duration {
|
||||
return t.bootstrapTime
|
||||
}
|
||||
|
||||
// Stop implements Tunnel.Stop.
|
||||
func (t *fakeTunnel) Stop() {
|
||||
// Implementation note: closing the listener causes
|
||||
// the socks5 server.Serve to return an error
|
||||
t.once.Do(func() { t.listener.Close() })
|
||||
}
|
||||
|
||||
// SOCKS5ProxyURL returns the SOCKS5 proxy URL.
|
||||
func (t *fakeTunnel) SOCKS5ProxyURL() *url.URL {
|
||||
return &url.URL{
|
||||
Scheme: "socks5",
|
||||
Host: t.addr.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// fakeStart starts the fake tunnel.
|
||||
func fakeStart(ctx context.Context, config *Config) (Tunnel, error) {
|
||||
// do the same things other tunnels do:
|
||||
//
|
||||
// 1. abort if context is cancelled
|
||||
//
|
||||
// 2. check for tunnelDir being not empty
|
||||
//
|
||||
// 3. attempt to create tunnelDir
|
||||
//
|
||||
// after that, it's all fake and we just create a simple
|
||||
// socks5 server that we can use
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err() // simplifies unit testing this code
|
||||
default:
|
||||
}
|
||||
if config.TunnelDir == "" {
|
||||
return nil, ErrEmptyTunnelDir
|
||||
}
|
||||
if err := config.mkdirAll(config.TunnelDir, 0700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
server, err := config.socks5New(&socks5.Config{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
start := time.Now()
|
||||
listener, err := config.netListen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bootstrapTime := time.Since(start)
|
||||
go server.Serve(listener)
|
||||
return &fakeTunnel{
|
||||
addr: listener.Addr(),
|
||||
bootstrapTime: bootstrapTime,
|
||||
listener: listener,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package tunnel_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine"
|
||||
"github.com/ooni/probe-cli/v3/internal/tunnel"
|
||||
)
|
||||
|
||||
func TestFakeStartStop(t *testing.T) {
|
||||
// no need to skip because the bootstrap is obviously fast
|
||||
tunnelDir, err := ioutil.TempDir("testdata", "fake")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
sess, err := engine.NewSession(ctx, engine.SessionConfig{
|
||||
Logger: log.Log,
|
||||
SoftwareName: "miniooni",
|
||||
SoftwareVersion: "0.1.0-dev",
|
||||
TunnelDir: tunnelDir,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tunnel, err := tunnel.Start(context.Background(), &tunnel.Config{
|
||||
Name: "fake",
|
||||
Session: sess,
|
||||
TunnelDir: tunnelDir,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if tunnel.SOCKS5ProxyURL() == nil {
|
||||
t.Fatal("expected non nil URL here")
|
||||
}
|
||||
if tunnel.BootstrapTime() <= 0 {
|
||||
t.Fatal("expected positive bootstrap time here")
|
||||
}
|
||||
tunnel.Stop()
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/armon/go-socks5"
|
||||
)
|
||||
|
||||
func TestFakeWithCancelledContext(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // immediately fail
|
||||
sess := &MockableSession{}
|
||||
tunnel, err := fakeStart(ctx, &Config{
|
||||
Session: sess,
|
||||
TunnelDir: "testdata",
|
||||
})
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
if tunnel != nil {
|
||||
t.Fatal("expected nil tunnel here")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFakeWithEmptyTunnelDir(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
sess := &MockableSession{}
|
||||
tunnel, err := fakeStart(ctx, &Config{
|
||||
Session: sess,
|
||||
TunnelDir: "",
|
||||
})
|
||||
if !errors.Is(err, ErrEmptyTunnelDir) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
if tunnel != nil {
|
||||
t.Fatal("expected nil tunnel here")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFakeWithFailingMkdirAll(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
ctx := context.Background()
|
||||
sess := &MockableSession{}
|
||||
tunnel, err := fakeStart(ctx, &Config{
|
||||
Session: sess,
|
||||
TunnelDir: "testdata",
|
||||
testMkdirAll: func(dir string, mode os.FileMode) error {
|
||||
return expected
|
||||
},
|
||||
})
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
if tunnel != nil {
|
||||
t.Fatal("expected nil tunnel here")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFakeSocks5NewFails(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
ctx := context.Background()
|
||||
sess := &MockableSession{}
|
||||
tunnel, err := fakeStart(ctx, &Config{
|
||||
Session: sess,
|
||||
TunnelDir: "testdata",
|
||||
testSocks5New: func(conf *socks5.Config) (*socks5.Server, error) {
|
||||
return nil, expected
|
||||
},
|
||||
})
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
if tunnel != nil {
|
||||
t.Fatal("expected nil tunnel here")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFakeNetListenFails(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
ctx := context.Background()
|
||||
sess := &MockableSession{}
|
||||
tunnel, err := fakeStart(ctx, &Config{
|
||||
Session: sess,
|
||||
TunnelDir: "testdata",
|
||||
testNetListen: func(network, address string) (net.Listener, error) {
|
||||
return nil, expected
|
||||
},
|
||||
})
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
if tunnel != nil {
|
||||
t.Fatal("expected nil tunnel here")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/ooni/psiphon/oopsi/github.com/Psiphon-Labs/psiphon-tunnel-core/ClientLibrary/clientlib"
|
||||
)
|
||||
|
||||
// psiphonTunnel is a psiphon tunnel
|
||||
type psiphonTunnel struct {
|
||||
// bootstrapTime is the bootstrapTime of the bootstrap
|
||||
bootstrapTime time.Duration
|
||||
|
||||
// tunnel is the underlying psiphon tunnel
|
||||
tunnel *clientlib.PsiphonTunnel
|
||||
}
|
||||
|
||||
// psiphonMakeWorkingDir creates the working directory
|
||||
func psiphonMakeWorkingDir(config *Config) (string, error) {
|
||||
workdir := filepath.Join(config.TunnelDir, config.Name)
|
||||
if err := config.mkdirAll(workdir, 0700); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return workdir, nil
|
||||
}
|
||||
|
||||
// psiphonStart starts the psiphon tunnel.
|
||||
func psiphonStart(ctx context.Context, config *Config) (Tunnel, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err() // simplifies unit testing this code
|
||||
default:
|
||||
}
|
||||
if config.TunnelDir == "" {
|
||||
return nil, ErrEmptyTunnelDir
|
||||
}
|
||||
configJSON, err := config.Session.FetchPsiphonConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
workdir, err := psiphonMakeWorkingDir(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
start := time.Now()
|
||||
tunnel, err := config.startPsiphon(ctx, configJSON, workdir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stop := time.Now()
|
||||
return &psiphonTunnel{tunnel: tunnel, bootstrapTime: stop.Sub(start)}, nil
|
||||
}
|
||||
|
||||
// Stop is an idempotent method that shuts down the tunnel
|
||||
func (t *psiphonTunnel) Stop() {
|
||||
t.tunnel.Stop()
|
||||
}
|
||||
|
||||
// SOCKS5ProxyURL returns the SOCKS5 proxy URL.
|
||||
func (t *psiphonTunnel) SOCKS5ProxyURL() *url.URL {
|
||||
return &url.URL{
|
||||
Scheme: "socks5",
|
||||
Host: net.JoinHostPort(
|
||||
"127.0.0.1", fmt.Sprintf("%d", t.tunnel.SOCKSProxyPort)),
|
||||
}
|
||||
}
|
||||
|
||||
// BootstrapTime returns the bootstrap time
|
||||
func (t *psiphonTunnel) BootstrapTime() time.Duration {
|
||||
return t.bootstrapTime
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package tunnel_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine"
|
||||
"github.com/ooni/probe-cli/v3/internal/tunnel"
|
||||
)
|
||||
|
||||
func TestPsiphonStartStop(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skip test in short mode")
|
||||
}
|
||||
tunnelDir, err := ioutil.TempDir("testdata", "psiphon")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
sess, err := engine.NewSession(ctx, engine.SessionConfig{
|
||||
Logger: log.Log,
|
||||
SoftwareName: "miniooni",
|
||||
SoftwareVersion: "0.1.0-dev",
|
||||
TunnelDir: tunnelDir,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tunnel, err := tunnel.Start(context.Background(), &tunnel.Config{
|
||||
Name: "psiphon",
|
||||
Session: sess,
|
||||
TunnelDir: tunnelDir,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if tunnel.SOCKS5ProxyURL() == nil {
|
||||
t.Fatal("expected non nil URL here")
|
||||
}
|
||||
if tunnel.BootstrapTime() <= 0 {
|
||||
t.Fatal("expected positive bootstrap time here")
|
||||
}
|
||||
tunnel.Stop()
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/ooni/psiphon/oopsi/github.com/Psiphon-Labs/psiphon-tunnel-core/ClientLibrary/clientlib"
|
||||
)
|
||||
|
||||
func TestPsiphonWithCancelledContext(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // immediately fail
|
||||
sess := &MockableSession{}
|
||||
tunnel, err := psiphonStart(ctx, &Config{
|
||||
Session: sess,
|
||||
TunnelDir: "testdata",
|
||||
})
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
if tunnel != nil {
|
||||
t.Fatal("expected nil tunnel here")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPsiphonWithEmptyTunnelDir(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
sess := &MockableSession{}
|
||||
tunnel, err := psiphonStart(ctx, &Config{
|
||||
Session: sess,
|
||||
TunnelDir: "",
|
||||
})
|
||||
if !errors.Is(err, ErrEmptyTunnelDir) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
if tunnel != nil {
|
||||
t.Fatal("expected nil tunnel here")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPsiphonFetchPsiphonConfigFailure(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
sess := &MockableSession{
|
||||
Err: expected,
|
||||
}
|
||||
tunnel, err := psiphonStart(context.Background(), &Config{
|
||||
Session: sess,
|
||||
TunnelDir: "testdata",
|
||||
})
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
if tunnel != nil {
|
||||
t.Fatal("expected nil tunnel here")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPsiphonMkdirAllFailure(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
sess := &MockableSession{
|
||||
Result: []byte(`{}`),
|
||||
}
|
||||
tunnel, err := psiphonStart(context.Background(), &Config{
|
||||
Session: sess,
|
||||
TunnelDir: "testdata",
|
||||
testMkdirAll: func(path string, perm os.FileMode) error {
|
||||
return expected
|
||||
},
|
||||
})
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
if tunnel != nil {
|
||||
t.Fatal("expected nil tunnel here")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPsiphonStartFailure(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
sess := &MockableSession{
|
||||
Result: []byte(`{}`),
|
||||
}
|
||||
tunnel, err := psiphonStart(context.Background(), &Config{
|
||||
Session: sess,
|
||||
TunnelDir: "testdata",
|
||||
testStartPsiphon: func(ctx context.Context, config []byte,
|
||||
workdir string) (*clientlib.PsiphonTunnel, error) {
|
||||
return nil, expected
|
||||
},
|
||||
})
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
if tunnel != nil {
|
||||
t.Fatal("expected nil tunnel here")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package tunnel
|
||||
|
||||
import "context"
|
||||
|
||||
// MockableSession is a mockable session.
|
||||
type MockableSession struct {
|
||||
// Result contains the bytes of the psiphon config.
|
||||
Result []byte
|
||||
|
||||
// Err is the error, if any.
|
||||
Err error
|
||||
}
|
||||
|
||||
// FetchPsiphonConfig implements ExperimentSession.FetchPsiphonConfig
|
||||
func (sess *MockableSession) FetchPsiphonConfig(ctx context.Context) ([]byte, error) {
|
||||
return sess.Result, sess.Err
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
*
|
||||
@@ -0,0 +1,137 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cretz/bine/tor"
|
||||
)
|
||||
|
||||
// torProcess is a running tor process.
|
||||
type torProcess interface {
|
||||
io.Closer
|
||||
}
|
||||
|
||||
// torTunnel is the Tor tunnel
|
||||
type torTunnel struct {
|
||||
// bootstrapTime is the duration of the bootstrap
|
||||
bootstrapTime time.Duration
|
||||
|
||||
// instance is the running tor instance
|
||||
instance torProcess
|
||||
|
||||
// proxy is the SOCKS5 proxy URL
|
||||
proxy *url.URL
|
||||
}
|
||||
|
||||
// BootstrapTime returns the bootstrap time
|
||||
func (tt *torTunnel) BootstrapTime() time.Duration {
|
||||
return tt.bootstrapTime
|
||||
}
|
||||
|
||||
// SOCKS5ProxyURL returns the URL of the SOCKS5 proxy
|
||||
func (tt *torTunnel) SOCKS5ProxyURL() *url.URL {
|
||||
return tt.proxy
|
||||
}
|
||||
|
||||
// Stop stops the Tor tunnel
|
||||
func (tt *torTunnel) Stop() {
|
||||
tt.instance.Close()
|
||||
}
|
||||
|
||||
// ErrTorUnableToGetSOCKSProxyAddress indicates that we could not
|
||||
// get the SOCKS proxy address via the control port.
|
||||
var ErrTorUnableToGetSOCKSProxyAddress = errors.New(
|
||||
"unable to get socks proxy address")
|
||||
|
||||
// ErrTorReturnedUnsupportedProxy indicates that tor returned to
|
||||
// us the address of a proxy that we don't support.
|
||||
var ErrTorReturnedUnsupportedProxy = errors.New(
|
||||
"tor returned unsupported proxy")
|
||||
|
||||
// torStart starts the tor tunnel.
|
||||
func torStart(ctx context.Context, config *Config) (Tunnel, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err() // allows to write unit tests using this code
|
||||
default:
|
||||
}
|
||||
if config.TunnelDir == "" {
|
||||
return nil, ErrEmptyTunnelDir
|
||||
}
|
||||
stateDir := filepath.Join(config.TunnelDir, "tor")
|
||||
logfile := filepath.Join(stateDir, "tor.log")
|
||||
maybeCleanupTunnelDir(stateDir, logfile)
|
||||
extraArgs := append([]string{}, config.TorArgs...)
|
||||
extraArgs = append(extraArgs, "Log")
|
||||
extraArgs = append(extraArgs, "notice stderr")
|
||||
extraArgs = append(extraArgs, "Log")
|
||||
extraArgs = append(extraArgs, fmt.Sprintf(`notice file %s`, logfile))
|
||||
// Implementation note: here we make sure that we're not going to
|
||||
// execute a binary called "tor" in the current directory on Windows
|
||||
// as documented in https://blog.golang.org/path-security.
|
||||
exePath, err := config.execabsLookPath(config.torBinary())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.logger().Infof("tunnel: exec: %s %+v", exePath, extraArgs)
|
||||
instance, err := config.torStart(ctx, &tor.StartConf{
|
||||
DataDir: stateDir,
|
||||
ExtraArgs: extraArgs,
|
||||
ExePath: exePath,
|
||||
NoHush: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
instance.StopProcessOnClose = true
|
||||
start := time.Now()
|
||||
if err := config.torEnableNetwork(ctx, instance, true); err != nil {
|
||||
instance.Close()
|
||||
return nil, err
|
||||
}
|
||||
stop := time.Now()
|
||||
// Adapted from <https://git.io/Jfc7N>
|
||||
info, err := config.torGetInfo(instance.Control, "net/listeners/socks")
|
||||
if err != nil {
|
||||
instance.Close()
|
||||
return nil, err
|
||||
}
|
||||
if len(info) != 1 || info[0].Key != "net/listeners/socks" {
|
||||
instance.Close()
|
||||
return nil, ErrTorUnableToGetSOCKSProxyAddress
|
||||
}
|
||||
proxyAddress := info[0].Val
|
||||
if strings.HasPrefix(proxyAddress, "unix:") {
|
||||
instance.Close()
|
||||
return nil, ErrTorReturnedUnsupportedProxy
|
||||
}
|
||||
return &torTunnel{
|
||||
bootstrapTime: stop.Sub(start),
|
||||
instance: instance,
|
||||
proxy: &url.URL{Scheme: "socks5", Host: proxyAddress},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// maybeCleanupTunnelDir removes stale files inside
|
||||
// of the tunnel directory.
|
||||
func maybeCleanupTunnelDir(dir, logfile string) {
|
||||
os.Remove(logfile)
|
||||
removeWithGlob(filepath.Join(dir, "torrc-*"))
|
||||
removeWithGlob(filepath.Join(dir, "control-port-*"))
|
||||
}
|
||||
|
||||
// removeWithGlob globs and removes files.
|
||||
func removeWithGlob(pattern string) {
|
||||
files, _ := filepath.Glob(pattern)
|
||||
for _, file := range files {
|
||||
os.Remove(file)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package tunnel_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine"
|
||||
"github.com/ooni/probe-cli/v3/internal/tunnel"
|
||||
"golang.org/x/sys/execabs"
|
||||
)
|
||||
|
||||
func TestTorStartStop(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skip test in short mode")
|
||||
}
|
||||
torBinaryPath, err := execabs.LookPath("tor")
|
||||
if err != nil {
|
||||
t.Skip("missing precondition for the test: tor not in PATH")
|
||||
}
|
||||
tunnelDir, err := ioutil.TempDir("testdata", "tor")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
sess, err := engine.NewSession(ctx, engine.SessionConfig{
|
||||
Logger: log.Log,
|
||||
SoftwareName: "miniooni",
|
||||
SoftwareVersion: "0.1.0-dev",
|
||||
TunnelDir: tunnelDir,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tunnel, err := tunnel.Start(context.Background(), &tunnel.Config{
|
||||
Name: "tor",
|
||||
Session: sess,
|
||||
TorBinary: torBinaryPath,
|
||||
TunnelDir: tunnelDir,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if tunnel.SOCKS5ProxyURL() == nil {
|
||||
t.Fatal("expected non nil URL here")
|
||||
}
|
||||
if tunnel.BootstrapTime() <= 0 {
|
||||
t.Fatal("expected positive bootstrap time here")
|
||||
}
|
||||
tunnel.Stop()
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"github.com/cretz/bine/control"
|
||||
"github.com/cretz/bine/tor"
|
||||
)
|
||||
|
||||
// torCloser is used to mock a running tor process, which
|
||||
// we abstract as a io.Closer in tor.go.
|
||||
type torCloser struct {
|
||||
counter int
|
||||
}
|
||||
|
||||
// Close implements io.Closer.Close.
|
||||
func (c *torCloser) Close() error {
|
||||
c.counter++
|
||||
return errors.New("mocked mocked mocked")
|
||||
}
|
||||
|
||||
func TestTorTunnelNonNil(t *testing.T) {
|
||||
closer := new(torCloser)
|
||||
proxy := &url.URL{Scheme: "x", Host: "10.0.0.1:443"}
|
||||
tun := &torTunnel{
|
||||
bootstrapTime: 128,
|
||||
instance: closer,
|
||||
proxy: proxy,
|
||||
}
|
||||
if tun.BootstrapTime() != 128 {
|
||||
t.Fatal("not the bootstrap time we expected")
|
||||
}
|
||||
if tun.SOCKS5ProxyURL() != proxy {
|
||||
t.Fatal("not the url we expected")
|
||||
}
|
||||
tun.Stop()
|
||||
if closer.counter != 1 {
|
||||
t.Fatal("something went wrong while stopping the tunnel")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTorWithCancelledContext(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // fail immediately
|
||||
tun, err := torStart(ctx, &Config{
|
||||
Session: &MockableSession{},
|
||||
TunnelDir: "testdata",
|
||||
})
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
if tun != nil {
|
||||
t.Fatal("expected nil tunnel here")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTorWithEmptyTunnelDir(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
tun, err := torStart(ctx, &Config{
|
||||
Session: &MockableSession{},
|
||||
TunnelDir: "",
|
||||
})
|
||||
if !errors.Is(err, ErrEmptyTunnelDir) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
if tun != nil {
|
||||
t.Fatal("expected nil tunnel here")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTorBinaryNotFoundFailure(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
tun, err := torStart(ctx, &Config{
|
||||
Session: &MockableSession{},
|
||||
TorBinary: "/nonexistent/directory/tor",
|
||||
TunnelDir: "testdata",
|
||||
})
|
||||
if !errors.Is(err, syscall.ENOENT) {
|
||||
t.Fatal("not the error we expected", err)
|
||||
}
|
||||
if tun != nil {
|
||||
t.Fatal("expected nil tunnel here")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTorStartFailure(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
ctx := context.Background()
|
||||
tun, err := torStart(ctx, &Config{
|
||||
Session: &MockableSession{},
|
||||
TunnelDir: "testdata",
|
||||
testExecabsLookPath: func(name string) (string, error) {
|
||||
return "/usr/local/bin/tor", nil
|
||||
},
|
||||
testTorStart: func(ctx context.Context, conf *tor.StartConf) (*tor.Tor, error) {
|
||||
return nil, expected
|
||||
},
|
||||
})
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
if tun != nil {
|
||||
t.Fatal("expected nil tunnel here")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTorEnableNetworkFailure(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
ctx := context.Background()
|
||||
tun, err := torStart(ctx, &Config{
|
||||
Session: &MockableSession{},
|
||||
TunnelDir: "testdata",
|
||||
testExecabsLookPath: func(name string) (string, error) {
|
||||
return "/usr/local/bin/tor", nil
|
||||
},
|
||||
testTorStart: func(ctx context.Context, conf *tor.StartConf) (*tor.Tor, error) {
|
||||
return &tor.Tor{}, nil
|
||||
},
|
||||
testTorEnableNetwork: func(ctx context.Context, tor *tor.Tor, wait bool) error {
|
||||
return expected
|
||||
},
|
||||
})
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
if tun != nil {
|
||||
t.Fatal("expected nil tunnel here")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTorGetInfoFailure(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
ctx := context.Background()
|
||||
tun, err := torStart(ctx, &Config{
|
||||
Session: &MockableSession{},
|
||||
TunnelDir: "testdata",
|
||||
testExecabsLookPath: func(name string) (string, error) {
|
||||
return "/usr/local/bin/tor", nil
|
||||
},
|
||||
testTorStart: func(ctx context.Context, conf *tor.StartConf) (*tor.Tor, error) {
|
||||
return &tor.Tor{}, nil
|
||||
},
|
||||
testTorEnableNetwork: func(ctx context.Context, tor *tor.Tor, wait bool) error {
|
||||
return nil
|
||||
},
|
||||
testTorGetInfo: func(ctrl *control.Conn, keys ...string) ([]*control.KeyVal, error) {
|
||||
return nil, expected
|
||||
},
|
||||
})
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
if tun != nil {
|
||||
t.Fatal("expected nil tunnel here")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTorGetInfoInvalidNumberOfKeys(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
tun, err := torStart(ctx, &Config{
|
||||
Session: &MockableSession{},
|
||||
TunnelDir: "testdata",
|
||||
testExecabsLookPath: func(name string) (string, error) {
|
||||
return "/usr/local/bin/tor", nil
|
||||
},
|
||||
testTorStart: func(ctx context.Context, conf *tor.StartConf) (*tor.Tor, error) {
|
||||
return &tor.Tor{}, nil
|
||||
},
|
||||
testTorEnableNetwork: func(ctx context.Context, tor *tor.Tor, wait bool) error {
|
||||
return nil
|
||||
},
|
||||
testTorGetInfo: func(ctrl *control.Conn, keys ...string) ([]*control.KeyVal, error) {
|
||||
return nil, nil
|
||||
},
|
||||
})
|
||||
if !errors.Is(err, ErrTorUnableToGetSOCKSProxyAddress) {
|
||||
t.Fatal("not the error we expected", err)
|
||||
}
|
||||
if tun != nil {
|
||||
t.Fatal("expected nil tunnel here")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTorGetInfoInvalidKey(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
tun, err := torStart(ctx, &Config{
|
||||
Session: &MockableSession{},
|
||||
TunnelDir: "testdata",
|
||||
testExecabsLookPath: func(name string) (string, error) {
|
||||
return "/usr/local/bin/tor", nil
|
||||
},
|
||||
testTorStart: func(ctx context.Context, conf *tor.StartConf) (*tor.Tor, error) {
|
||||
return &tor.Tor{}, nil
|
||||
},
|
||||
testTorEnableNetwork: func(ctx context.Context, tor *tor.Tor, wait bool) error {
|
||||
return nil
|
||||
},
|
||||
testTorGetInfo: func(ctrl *control.Conn, keys ...string) ([]*control.KeyVal, error) {
|
||||
return []*control.KeyVal{{}}, nil
|
||||
},
|
||||
})
|
||||
if !errors.Is(err, ErrTorUnableToGetSOCKSProxyAddress) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
if tun != nil {
|
||||
t.Fatal("expected nil tunnel here")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTorGetInfoInvalidProxyType(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
tun, err := torStart(ctx, &Config{
|
||||
Session: &MockableSession{},
|
||||
TunnelDir: "testdata",
|
||||
testExecabsLookPath: func(name string) (string, error) {
|
||||
return "/usr/local/bin/tor", nil
|
||||
},
|
||||
testTorStart: func(ctx context.Context, conf *tor.StartConf) (*tor.Tor, error) {
|
||||
return &tor.Tor{}, nil
|
||||
},
|
||||
testTorEnableNetwork: func(ctx context.Context, tor *tor.Tor, wait bool) error {
|
||||
return nil
|
||||
},
|
||||
testTorGetInfo: func(ctrl *control.Conn, keys ...string) ([]*control.KeyVal, error) {
|
||||
return []*control.KeyVal{{Key: "net/listeners/socks", Val: "127.0.0.1:9050"}}, nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if tun == nil {
|
||||
t.Fatal("expected non-nil tunnel here")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTorUnsupportedProxy(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
tun, err := torStart(ctx, &Config{
|
||||
Session: &MockableSession{},
|
||||
TunnelDir: "testdata",
|
||||
testExecabsLookPath: func(name string) (string, error) {
|
||||
return "/usr/local/bin/tor", nil
|
||||
},
|
||||
testTorStart: func(ctx context.Context, conf *tor.StartConf) (*tor.Tor, error) {
|
||||
return &tor.Tor{}, nil
|
||||
},
|
||||
testTorEnableNetwork: func(ctx context.Context, tor *tor.Tor, wait bool) error {
|
||||
return nil
|
||||
},
|
||||
testTorGetInfo: func(ctrl *control.Conn, keys ...string) ([]*control.KeyVal, error) {
|
||||
return []*control.KeyVal{{Key: "net/listeners/socks", Val: "unix:/foo/bar"}}, nil
|
||||
},
|
||||
})
|
||||
if !errors.Is(err, ErrTorReturnedUnsupportedProxy) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
if tun != nil {
|
||||
t.Fatal("expected nil tunnel here")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaybeCleanupTunnelDir(t *testing.T) {
|
||||
fakeTunDir := filepath.Join("testdata", "fake-tun-dir")
|
||||
if err := os.RemoveAll(fakeTunDir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.MkdirAll(fakeTunDir, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fakeData := []byte("deadbeef\n")
|
||||
logfile := filepath.Join(fakeTunDir, "tor.log")
|
||||
if err := ioutil.WriteFile(logfile, fakeData, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for idx := 0; idx < 3; idx++ {
|
||||
filename := filepath.Join(fakeTunDir, fmt.Sprintf("torrc-%d", idx))
|
||||
if err := ioutil.WriteFile(filename, fakeData, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
filename = filepath.Join(fakeTunDir, fmt.Sprintf("control-port-%d", idx))
|
||||
if err := ioutil.WriteFile(filename, fakeData, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
filename = filepath.Join(fakeTunDir, fmt.Sprintf("antani-%d", idx))
|
||||
if err := ioutil.WriteFile(filename, fakeData, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
files, err := filepath.Glob(filepath.Join(fakeTunDir, "*"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(files) != 10 {
|
||||
t.Fatal("unexpected number of files")
|
||||
}
|
||||
maybeCleanupTunnelDir(fakeTunDir, logfile)
|
||||
files, err = filepath.Glob(filepath.Join(fakeTunDir, "*"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(files) != 3 {
|
||||
t.Fatal("unexpected number of files")
|
||||
}
|
||||
expectPrefix := filepath.Join(fakeTunDir, "antani-")
|
||||
for _, file := range files {
|
||||
if !strings.HasPrefix(file, expectPrefix) {
|
||||
t.Fatal("unexpected file name: ", file)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// Package tunnel allows to create tunnels to speak
|
||||
// with OONI backends and other services.
|
||||
//
|
||||
// You need to fill a Config object and call Start to
|
||||
// obtain an instance of Tunnel. The tunnel will expose
|
||||
// a SOCKS5 proxy. You need to configure your HTTP
|
||||
// code to use such a proxy. Remember to call the Stop
|
||||
// method of a tunnel when you are done.
|
||||
//
|
||||
// There are two use cases for this package. The first
|
||||
// use case is to enable urlgetter to perform measurements
|
||||
// over tunnels (mainly psiphon).
|
||||
//
|
||||
// The second use case is to use tunnels to reach to the
|
||||
// OONI backend when it's blocked. For the latter case
|
||||
// we currently mainly use psiphon. In such a case, we'll
|
||||
// use a psiphon configuration embedded into the OONI
|
||||
// binary itself. When you are running a version of OONI
|
||||
// that does not embed such a configuration, it won't
|
||||
// be possible to address this use case.
|
||||
//
|
||||
// See session.go in the engine package for more details
|
||||
// concerning this second use case.
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Session is a measurement session. We filter for the only
|
||||
// functionality we're interested to use. That is, fetching the
|
||||
// psiphon configuration from the OONI backend (if possible).
|
||||
//
|
||||
// Depending on how OONI is compiled, the psiphon configuration
|
||||
// may be embedded into the binary. In such a case, we won't
|
||||
// need to download the configuration from the backend.
|
||||
type Session interface {
|
||||
// FetchPsiphonConfig should fetch and return the psiphon config
|
||||
// as a serialized JSON, or fail with an error.
|
||||
FetchPsiphonConfig(ctx context.Context) ([]byte, error)
|
||||
}
|
||||
|
||||
// Tunnel is a tunnel for communicating with OONI backends
|
||||
// (and other services) to circumvent blocking.
|
||||
type Tunnel interface {
|
||||
// BootstrapTime returns the time it required to
|
||||
// create a new tunnel instance.
|
||||
BootstrapTime() time.Duration
|
||||
|
||||
// SOCKS5ProxyURL returns the SOCSK5 proxy URL.
|
||||
SOCKS5ProxyURL() *url.URL
|
||||
|
||||
// Stop stops the tunnel. You should not attempt to
|
||||
// use any other tunnel method after Stop.
|
||||
Stop()
|
||||
}
|
||||
|
||||
// ErrEmptyTunnelDir indicates that config.TunnelDir is empty.
|
||||
var ErrEmptyTunnelDir = errors.New("TunnelDir is empty")
|
||||
|
||||
// ErrUnsupportedTunnelName indicates that the given tunnel name
|
||||
// is not supported by this package.
|
||||
var ErrUnsupportedTunnelName = errors.New("unsupported tunnel name")
|
||||
|
||||
// Start starts a new tunnel by name or returns an error. We currently
|
||||
// support the following tunnels:
|
||||
//
|
||||
// The "tor" tunnel requires the "tor" binary to be installed on
|
||||
// your system. You can use config.TorArgs and config.TorBinary to
|
||||
// select what binary to execute and with which arguments.
|
||||
//
|
||||
// The "psiphon" tunnel requires a configuration. Some builds of
|
||||
// ooniprobe embed a configuration into the binary. When this
|
||||
// is the case, the config.Session is a mocked object that just
|
||||
// returns such a configuration.
|
||||
//
|
||||
// Otherwise, If there is no embedded psiphon configuration, the
|
||||
// config.Session must be an ordinary engine.Session. In such a
|
||||
// case, fetching the Psiphon configuration from the backend may
|
||||
// fail when the backend is not reachable.
|
||||
//
|
||||
// The "fake" tunnel is a fake tunnel that just exposes a
|
||||
// SOCKS5 proxy and then connects directly to server. We use
|
||||
// this special kind of tunnel to implement tests.
|
||||
func Start(ctx context.Context, config *Config) (Tunnel, error) {
|
||||
switch config.Name {
|
||||
case "fake":
|
||||
return fakeStart(ctx, config)
|
||||
case "psiphon":
|
||||
return psiphonStart(ctx, config)
|
||||
case "tor":
|
||||
return torStart(ctx, config)
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: %s", ErrUnsupportedTunnelName, config.Name)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package tunnel_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/tunnel"
|
||||
)
|
||||
|
||||
func TestStartNoTunnel(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
tun, err := tunnel.Start(ctx, &tunnel.Config{
|
||||
Name: "",
|
||||
Session: &tunnel.MockableSession{},
|
||||
})
|
||||
if !errors.Is(err, tunnel.ErrUnsupportedTunnelName) {
|
||||
t.Fatal("not the error we expected", err)
|
||||
}
|
||||
if tun != nil {
|
||||
t.Fatal("expected nil tunnel here")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartPsiphonWithCancelledContext(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // fail immediately
|
||||
tun, err := tunnel.Start(ctx, &tunnel.Config{
|
||||
Name: "psiphon",
|
||||
Session: &tunnel.MockableSession{},
|
||||
TunnelDir: "testdata",
|
||||
})
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
if tun != nil {
|
||||
t.Fatal("expected nil tunnel here")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartTorWithCancelledContext(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // fail immediately
|
||||
tun, err := tunnel.Start(ctx, &tunnel.Config{
|
||||
Name: "tor",
|
||||
Session: &tunnel.MockableSession{},
|
||||
TunnelDir: "testdata",
|
||||
})
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
if tun != nil {
|
||||
t.Fatal("expected nil tunnel here")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartInvalidTunnel(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
tun, err := tunnel.Start(ctx, &tunnel.Config{
|
||||
Name: "antani",
|
||||
Session: &tunnel.MockableSession{},
|
||||
TunnelDir: "testdata",
|
||||
})
|
||||
if !errors.Is(err, tunnel.ErrUnsupportedTunnelName) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
if tun != nil {
|
||||
t.Fatal("expected nil tunnel here")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user