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/kingpin v2.2.6+incompatible
|
||||||
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 // indirect
|
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 // indirect
|
||||||
github.com/apex/log v1.9.0
|
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/creack/goselect v0.1.2 // indirect
|
||||||
github.com/cretz/bine v0.1.0
|
github.com/cretz/bine v0.1.0
|
||||||
github.com/dchest/siphash v1.2.2 // indirect
|
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/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-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-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/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-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=
|
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
|
proxyURL := config.ProxyURL
|
||||||
if proxyURL != nil {
|
if proxyURL != nil {
|
||||||
switch proxyURL.Scheme {
|
switch proxyURL.Scheme {
|
||||||
case "psiphon", "tor":
|
case "psiphon", "tor", "fake":
|
||||||
tunnel, err := tunnel.Start(ctx, &tunnel.Config{
|
tunnel, err := tunnel.Start(ctx, &tunnel.Config{
|
||||||
Name: proxyURL.Scheme,
|
Name: proxyURL.Scheme,
|
||||||
Session: &sessionTunnelEarlySession{},
|
Session: &sessionTunnelEarlySession{},
|
||||||
|
|
|
@ -2,8 +2,10 @@ package tunnel
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/armon/go-socks5"
|
||||||
"github.com/cretz/bine/control"
|
"github.com/cretz/bine/control"
|
||||||
"github.com/cretz/bine/tor"
|
"github.com/cretz/bine/tor"
|
||||||
"github.com/ooni/psiphon/oopsi/github.com/Psiphon-Labs/psiphon-tunnel-core/ClientLibrary/clientlib"
|
"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.
|
// structure while in use, because that may lead to data races.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// Name is the mandatory name of the tunnel. We support
|
// 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
|
Name string
|
||||||
|
|
||||||
// Session is the mandatory measurement session, or a suitable
|
// 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 allows us to mock os.MkdirAll in testing code.
|
||||||
testMkdirAll func(path string, perm os.FileMode) error
|
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 allows us to mock psiphon's clientlib.StartTunnel.
|
||||||
testStartPsiphon func(ctx context.Context, config []byte,
|
testStartPsiphon func(ctx context.Context, config []byte,
|
||||||
workdir string) (*clientlib.PsiphonTunnel, error)
|
workdir string) (*clientlib.PsiphonTunnel, error)
|
||||||
|
@ -64,6 +74,22 @@ func (c *Config) mkdirAll(path string, perm os.FileMode) error {
|
||||||
return os.MkdirAll(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.
|
// startPsiphon calls either testStartPsiphon or psiphon's clientlib.StartTunnel.
|
||||||
func (c *Config) startPsiphon(ctx context.Context, config []byte,
|
func (c *Config) startPsiphon(ctx context.Context, config []byte,
|
||||||
workdir string) (*clientlib.PsiphonTunnel, error) {
|
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
|
// Session is a measurement session. We filter for the only
|
||||||
// functionality we're interested to use. That is, fetching the
|
// functionality we're interested to use. That is, fetching the
|
||||||
// psiphon configuration from the OONI backend (if possible).
|
// 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 {
|
type Session interface {
|
||||||
// FetchPsiphonConfig should fetch and return the psiphon config
|
// FetchPsiphonConfig should fetch and return the psiphon config
|
||||||
// as a serialized JSON, or fail with an error.
|
// 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
|
// The "psiphon" tunnel requires a configuration. Some builds of
|
||||||
// ooniprobe embed a configuration into the binary. When this
|
// ooniprobe embed a configuration into the binary. When this
|
||||||
// is the case, the config.Session is a mocked object that just
|
// 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
|
// 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
|
// case, fetching the Psiphon configuration from the backend may
|
||||||
// fail when the backend is not reachable.
|
// 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) {
|
func Start(ctx context.Context, config *Config) (Tunnel, error) {
|
||||||
switch config.Name {
|
switch config.Name {
|
||||||
|
case "fake":
|
||||||
|
return fakeStart(ctx, config)
|
||||||
case "psiphon":
|
case "psiphon":
|
||||||
return psiphonStart(ctx, config)
|
return psiphonStart(ctx, config)
|
||||||
case "tor":
|
case "tor":
|
||||||
|
|
Loading…
Reference in New Issue
Block a user