feat(tunnel): implement the fake tunnel (#298)
This functionality should be helpful to test that the general interface of the tunnel package is okay from the engine package. Part of https://github.com/ooni/probe/issues/985
This commit is contained in:
parent
76a50facc3
commit
973501dd11
1
go.mod
1
go.mod
|
@ -8,6 +8,7 @@ require (
|
|||
github.com/alecthomas/kingpin v2.2.6+incompatible
|
||||
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 // indirect
|
||||
github.com/apex/log v1.9.0
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||
github.com/creack/goselect v0.1.2 // indirect
|
||||
github.com/cretz/bine v0.1.0
|
||||
github.com/dchest/siphash v1.2.2 // indirect
|
||||
|
|
2
go.sum
2
go.sum
|
@ -44,6 +44,8 @@ github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hC
|
|||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
|
||||
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
|
||||
github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
|
|
|
@ -167,7 +167,7 @@ func NewSession(ctx context.Context, config SessionConfig) (*Session, error) {
|
|||
proxyURL := config.ProxyURL
|
||||
if proxyURL != nil {
|
||||
switch proxyURL.Scheme {
|
||||
case "psiphon", "tor":
|
||||
case "psiphon", "tor", "fake":
|
||||
tunnel, err := tunnel.Start(ctx, &tunnel.Config{
|
||||
Name: proxyURL.Scheme,
|
||||
Session: &sessionTunnelEarlySession{},
|
||||
|
|
|
@ -2,8 +2,10 @@ 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"
|
||||
|
@ -14,7 +16,9 @@ import (
|
|||
// 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" and "psiphon" tunnels.
|
||||
// "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
|
||||
|
@ -40,6 +44,12 @@ type Config struct {
|
|||
// 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)
|
||||
|
@ -64,6 +74,22 @@ func (c *Config) mkdirAll(path string, perm os.FileMode) error {
|
|||
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) {
|
||||
|
|
67
internal/engine/tunnel/fake.go
Normal file
67
internal/engine/tunnel/fake.go
Normal file
|
@ -0,0 +1,67 @@
|
|||
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) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err() // simplifies unit testing this code
|
||||
default:
|
||||
}
|
||||
if config.TunnelDir == "" {
|
||||
return nil, ErrEmptyTunnelDir
|
||||
}
|
||||
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
|
||||
}
|
44
internal/engine/tunnel/fake_integration_test.go
Normal file
44
internal/engine/tunnel/fake_integration_test.go
Normal file
|
@ -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/engine/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()
|
||||
}
|
80
internal/engine/tunnel/fake_test.go
Normal file
80
internal/engine/tunnel/fake_test.go
Normal file
|
@ -0,0 +1,80 @@
|
|||
package tunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/armon/go-socks5"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/mockable"
|
||||
)
|
||||
|
||||
func TestFakeWithCancelledContext(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // immediately fail
|
||||
sess := &mockable.Session{}
|
||||
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 := &mockable.Session{}
|
||||
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 TestFakeSocks5NewFails(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
ctx := context.Background()
|
||||
sess := &mockable.Session{}
|
||||
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 := &mockable.Session{}
|
||||
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")
|
||||
}
|
||||
}
|
|
@ -34,6 +34,10 @@ import (
|
|||
// 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.
|
||||
|
@ -72,14 +76,20 @@ var ErrUnsupportedTunnelName = errors.New("unsupported tunnel name")
|
|||
// 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
|
||||
// retuns such configuration.
|
||||
// returns such a configuration.
|
||||
//
|
||||
// Otherwise, If there is no embedded psiphon configuration, the
|
||||
// config.Session will must be an ordinary session. In such a
|
||||
// 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":
|
||||
|
|
Loading…
Reference in New Issue
Block a user