feat(miniooni): implement torsf tunnel (#921)

This diff adds to miniooni support for using the torsf tunnel. Such a
tunnel consists of a snowflake pluggable transport in front of a custom
instance of tor and requires tor to be installed.

The usage is like:

```
./miniooni --tunnel=torsf [...]
```

The default snowflake rendezvous method is "domain_fronting". You can
select the AMP cache instead using "amp":

```
./miniooni --snowflake-rendezvous=amp --tunnel=torsf [...]
```

Part of https://github.com/ooni/probe/issues/1955
This commit is contained in:
Simone Basso 2022-10-03 16:52:20 +02:00 committed by GitHub
parent 5466f30526
commit 18a9523496
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 438 additions and 57 deletions

View File

@ -25,25 +25,26 @@ import (
// Options contains the options you can set from the CLI. // Options contains the options you can set from the CLI.
type Options struct { type Options struct {
Annotations []string Annotations []string
Emoji bool Emoji bool
ExtraOptions []string ExtraOptions []string
HomeDir string HomeDir string
Inputs []string Inputs []string
InputFilePaths []string InputFilePaths []string
MaxRuntime int64 MaxRuntime int64
NoJSON bool NoJSON bool
NoCollector bool NoCollector bool
ProbeServicesURL string ProbeServicesURL string
Proxy string Proxy string
Random bool Random bool
RepeatEvery int64 RepeatEvery int64
ReportFile string ReportFile string
TorArgs []string SnowflakeRendezvous string
TorBinary string TorArgs []string
Tunnel string TorBinary string
Verbose bool Tunnel string
Yes bool Verbose bool
Yes bool
} }
// main is the main function of miniooni. // main is the main function of miniooni.
@ -125,6 +126,13 @@ func main() {
"set the output report file path (default: \"report.jsonl\")", "set the output report file path (default: \"report.jsonl\")",
) )
flags.StringVar(
&globalOptions.SnowflakeRendezvous,
"snowflake-rendezvous",
"domain_fronting",
"rendezvous method for --tunnel=torsf (one of: \"domain_fronting\" and \"amp\")",
)
flags.StringSliceVar( flags.StringSliceVar(
&globalOptions.TorArgs, &globalOptions.TorArgs,
"tor-args", "tor-args",
@ -143,7 +151,7 @@ func main() {
&globalOptions.Tunnel, &globalOptions.Tunnel,
"tunnel", "tunnel",
"", "",
"tunnel to use to communicate with the OONI backend (one of: tor, psiphon)", "tunnel to use to communicate with the OONI backend (one of: psiphon, tor, torsf)",
) )
flags.BoolVarP( flags.BoolVarP(

View File

@ -36,14 +36,15 @@ func newSessionOrPanic(ctx context.Context, currentOptions *Options,
runtimex.PanicOnError(err, "cannot create tunnelDir") runtimex.PanicOnError(err, "cannot create tunnelDir")
config := engine.SessionConfig{ config := engine.SessionConfig{
KVStore: kvstore, KVStore: kvstore,
Logger: logger, Logger: logger,
ProxyURL: proxyURL, ProxyURL: proxyURL,
SoftwareName: softwareName, SnowflakeRendezvous: currentOptions.SnowflakeRendezvous,
SoftwareVersion: softwareVersion, SoftwareName: softwareName,
TorArgs: currentOptions.TorArgs, SoftwareVersion: softwareVersion,
TorBinary: currentOptions.TorBinary, TorArgs: currentOptions.TorArgs,
TunnelDir: tunnelDir, TorBinary: currentOptions.TorBinary,
TunnelDir: tunnelDir,
} }
if currentOptions.ProbeServicesURL != "" { if currentOptions.ProbeServicesURL != "" {
config.AvailableProbeServices = []model.OOAPIService{{ config.AvailableProbeServices = []model.OOAPIService{{

View File

@ -35,6 +35,10 @@ type SessionConfig struct {
TorArgs []string TorArgs []string
TorBinary string TorBinary string
// SnowflakeRendezvous is the rendezvous method
// to be used by the torsf tunnel
SnowflakeRendezvous string
// TunnelDir is the directory where we should store // TunnelDir is the directory where we should store
// the state of persistent tunnels. This field is // the state of persistent tunnels. This field is
// optional _unless_ you want to use tunnels. In such // optional _unless_ you want to use tunnels. In such
@ -171,16 +175,17 @@ 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", "fake": case "psiphon", "tor", "torsf", "fake":
config.Logger.Infof( config.Logger.Infof(
"starting '%s' tunnel; please be patient...", proxyURL.Scheme) "starting '%s' tunnel; please be patient...", proxyURL.Scheme)
tunnel, _, err := tunnel.Start(ctx, &tunnel.Config{ tunnel, _, err := tunnel.Start(ctx, &tunnel.Config{
Logger: config.Logger, Logger: config.Logger,
Name: proxyURL.Scheme, Name: proxyURL.Scheme,
Session: &sessionTunnelEarlySession{}, SnowflakeRendezvous: config.SnowflakeRendezvous,
TorArgs: config.TorArgs, Session: &sessionTunnelEarlySession{},
TorBinary: config.TorBinary, TorArgs: config.TorArgs,
TunnelDir: config.TunnelDir, TorBinary: config.TorBinary,
TunnelDir: config.TunnelDir,
}) })
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -74,6 +74,10 @@ type Listener struct {
// counts the bytes consumed by the experiment. // counts the bytes consumed by the experiment.
ExperimentByteCounter *bytecounter.Counter ExperimentByteCounter *bytecounter.Counter
// ListenSocks is OPTIONAL and allows you to override the
// function called by default to listen for SOCKS5.
ListenSocks func(network string, laddr string) (SocksListener, error)
// Logger is the OPTIONAL logger. When not set, this library // Logger is the OPTIONAL logger. When not set, this library
// will not emit logs. (But the underlying pluggable transport // will not emit logs. (But the underlying pluggable transport
// may still emit its own log messages.) // may still emit its own log messages.)
@ -98,10 +102,7 @@ type Listener struct {
laddr net.Addr laddr net.Addr
// listener allows us to stop the listener. // listener allows us to stop the listener.
listener ptxSocksListener listener SocksListener
// overrideListenSocks allows us to override pt.ListenSocks.
overrideListenSocks func(network string, laddr string) (ptxSocksListener, error)
} }
// logger returns the Logger, if set, or the defaultLogger. // logger returns the Logger, if set, or the defaultLogger.
@ -148,7 +149,7 @@ func (lst *Listener) forwardWithContext(ctx context.Context, left, right net.Con
// handleSocksConn handles a new SocksConn connection by establishing // handleSocksConn handles a new SocksConn connection by establishing
// the corresponding PT connection and forwarding traffic. This // the corresponding PT connection and forwarding traffic. This
// function TAKES OWNERSHIP of the socksConn argument. // function TAKES OWNERSHIP of the socksConn argument.
func (lst *Listener) handleSocksConn(ctx context.Context, socksConn ptxSocksConn) error { func (lst *Listener) handleSocksConn(ctx context.Context, socksConn SocksConn) error {
err := socksConn.Grant(&net.TCPAddr{IP: net.IPv4zero, Port: 0}) err := socksConn.Grant(&net.TCPAddr{IP: net.IPv4zero, Port: 0})
if err != nil { if err != nil {
lst.logger().Warnf("ptx: socksConn.Grant error: %s", err) lst.logger().Warnf("ptx: socksConn.Grant error: %s", err)
@ -169,10 +170,10 @@ func (lst *Listener) handleSocksConn(ctx context.Context, socksConn ptxSocksConn
return nil // used for testing return nil // used for testing
} }
// ptxSocksListener is a pt.SocksListener-like structure. // SocksListener is the listener for socks connections.
type ptxSocksListener interface { type SocksListener interface {
// AcceptSocks accepts a socks conn // AcceptSocks accepts a socks conn
AcceptSocks() (ptxSocksConn, error) AcceptSocks() (SocksConn, error)
// Addr returns the listening address. // Addr returns the listening address.
Addr() net.Addr Addr() net.Addr
@ -181,8 +182,8 @@ type ptxSocksListener interface {
Close() error Close() error
} }
// ptxSocksConn is a pt.SocksConn-like structure. // SocksConn is a SOCKS connection.
type ptxSocksConn interface { type SocksConn interface {
// net.Conn is the embedded interface. // net.Conn is the embedded interface.
net.Conn net.Conn
@ -192,7 +193,7 @@ type ptxSocksConn interface {
// acceptLoop accepts and handles local socks connection. This function // acceptLoop accepts and handles local socks connection. This function
// DOES NOT take ownership of the socks listener. // DOES NOT take ownership of the socks listener.
func (lst *Listener) acceptLoop(ctx context.Context, ln ptxSocksListener) { func (lst *Listener) acceptLoop(ctx context.Context, ln SocksListener) {
for { for {
conn, err := ln.AcceptSocks() conn, err := ln.AcceptSocks()
if err != nil { if err != nil {
@ -243,15 +244,15 @@ func (lst *Listener) Start() error {
} }
// listenSocks calles either pt.ListenSocks or lst.overrideListenSocks. // listenSocks calles either pt.ListenSocks or lst.overrideListenSocks.
func (lst *Listener) listenSocks(network string, laddr string) (ptxSocksListener, error) { func (lst *Listener) listenSocks(network string, laddr string) (SocksListener, error) {
if lst.overrideListenSocks != nil { if lst.ListenSocks != nil {
return lst.overrideListenSocks(network, laddr) return lst.ListenSocks(network, laddr)
} }
return lst.castListener(pt.ListenSocks(network, laddr)) return lst.castListener(pt.ListenSocks(network, laddr))
} }
// castListener casts a pt.SocksListener to ptxSocksListener. // castListener casts a pt.SocksListener to ptxSocksListener.
func (lst *Listener) castListener(in *pt.SocksListener, err error) (ptxSocksListener, error) { func (lst *Listener) castListener(in *pt.SocksListener, err error) (SocksListener, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -264,7 +265,7 @@ type ptxSocksListenerAdapter struct {
} }
// AcceptSocks adapts pt.SocksListener.AcceptSocks to ptxSockListener.AcceptSocks. // AcceptSocks adapts pt.SocksListener.AcceptSocks to ptxSockListener.AcceptSocks.
func (la *ptxSocksListenerAdapter) AcceptSocks() (ptxSocksConn, error) { func (la *ptxSocksListenerAdapter) AcceptSocks() (SocksConn, error) {
return la.SocksListener.AcceptSocks() return la.SocksListener.AcceptSocks()
} }
@ -307,11 +308,11 @@ func (lst *Listener) Stop() {
// Assuming that we are listening at 127.0.0.1:12345, then this // Assuming that we are listening at 127.0.0.1:12345, then this
// function will return the following string: // function will return the following string:
// //
// obfs4 socks5 127.0.0.1:12345 // obfs4 socks5 127.0.0.1:12345
// //
// The correct configuration line for the `torrc` would be: // The correct configuration line for the `torrc` would be:
// //
// ClientTransportPlugin obfs4 socks5 127.0.0.1:12345 // ClientTransportPlugin obfs4 socks5 127.0.0.1:12345
// //
// Since we pass configuration to tor using the command line, it // Since we pass configuration to tor using the command line, it
// is more convenient to us to avoid including ClientTransportPlugin // is more convenient to us to avoid including ClientTransportPlugin

View File

@ -73,7 +73,7 @@ func TestListenerWorksWithFakeDialer(t *testing.T) {
func TestListenerCannotListen(t *testing.T) { func TestListenerCannotListen(t *testing.T) {
expected := errors.New("mocked error") expected := errors.New("mocked error")
lst := &Listener{ lst := &Listener{
overrideListenSocks: func(network, laddr string) (ptxSocksListener, error) { ListenSocks: func(network, laddr string) (SocksListener, error) {
return nil, expected return nil, expected
}, },
} }
@ -193,7 +193,7 @@ func TestListenerForwardWithNaturalTermination(t *testing.T) {
// mockableSocksListener is a mockable ptxSocksListener. // mockableSocksListener is a mockable ptxSocksListener.
type mockableSocksListener struct { type mockableSocksListener struct {
// MockAcceptSocks allows to mock AcceptSocks. // MockAcceptSocks allows to mock AcceptSocks.
MockAcceptSocks func() (ptxSocksConn, error) MockAcceptSocks func() (SocksConn, error)
// MockAddr allows to mock Addr. // MockAddr allows to mock Addr.
MockAddr func() net.Addr MockAddr func() net.Addr
@ -203,7 +203,7 @@ type mockableSocksListener struct {
} }
// AcceptSocks implemements ptxSocksListener.AcceptSocks. // AcceptSocks implemements ptxSocksListener.AcceptSocks.
func (m *mockableSocksListener) AcceptSocks() (ptxSocksConn, error) { func (m *mockableSocksListener) AcceptSocks() (SocksConn, error) {
return m.MockAcceptSocks() return m.MockAcceptSocks()
} }
@ -220,7 +220,7 @@ func (m *mockableSocksListener) Close() error {
func TestListenerLoopWithTemporaryError(t *testing.T) { func TestListenerLoopWithTemporaryError(t *testing.T) {
isclosed := &atomicx.Int64{} isclosed := &atomicx.Int64{}
sl := &mockableSocksListener{ sl := &mockableSocksListener{
MockAcceptSocks: func() (ptxSocksConn, error) { MockAcceptSocks: func() (SocksConn, error) {
if isclosed.Load() > 0 { if isclosed.Load() > 0 {
return nil, io.EOF return nil, io.EOF
} }

View File

@ -10,6 +10,7 @@ import (
"github.com/cretz/bine/control" "github.com/cretz/bine/control"
"github.com/cretz/bine/tor" "github.com/cretz/bine/tor"
"github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/ptx"
"golang.org/x/sys/execabs" "golang.org/x/sys/execabs"
) )
@ -28,6 +29,10 @@ type Config struct {
// of obtaining a valid psiphon configuration. // of obtaining a valid psiphon configuration.
Session Session Session Session
// SnowflakeRendezvous is the OPTIONAL rendezvous
// method for snowflake
SnowflakeRendezvous string
// TunnelDir is the MANDATORY directory in which the tunnel SHOULD // TunnelDir is the MANDATORY directory in which the tunnel SHOULD
// store its state, if any. If this field is empty, the // store its state, if any. If this field is empty, the
// Start function fails with ErrEmptyTunnelDir. // Start function fails with ErrEmptyTunnelDir.
@ -56,6 +61,18 @@ type Config struct {
// testNetListen allows us to mock net.Listen in testing code. // testNetListen allows us to mock net.Listen in testing code.
testNetListen func(network string, address string) (net.Listener, error) testNetListen func(network string, address string) (net.Listener, error)
// testSfListenSocks is OPTIONAL and allows to override the
// ListenSocks field of a ptx.Listener.
testSfListenSocks func(network string, laddr string) (ptx.SocksListener, error)
// testSfNewPTXListener is OPTIONAL and allows us to wrap the
// constructed ptx.Listener for testing purposes.
testSfWrapPTXListener func(torsfPTXListener) torsfPTXListener
// testSfTorStart is OPTIONAL and allows us to override the
// call to torStart inside the torsf tunnel.
testSfTorStart func(ctx context.Context, config *Config) (Tunnel, DebugInfo, error)
// testSocks5New allows us to mock socks5.New in testing code. // testSocks5New allows us to mock socks5.New in testing code.
testSocks5New func(conf *socks5.Config) (*socks5.Server, error) testSocks5New func(conf *socks5.Config) (*socks5.Server, error)
@ -74,6 +91,14 @@ type Config struct {
testTorGetInfo func(ctrl *control.Conn, keys ...string) ([]*control.KeyVal, error) testTorGetInfo func(ctrl *control.Conn, keys ...string) ([]*control.KeyVal, error)
} }
// snowflakeRendezvousMethod returns the rendezvous method that snowflake should use
func (c *Config) snowflakeRendezvousMethod() string {
if c.SnowflakeRendezvous != "" {
return c.SnowflakeRendezvous
}
return "domain_fronting"
}
// logger returns the logger to use. // logger returns the logger to use.
func (c *Config) logger() model.Logger { func (c *Config) logger() model.Logger {
if c.Logger != nil { if c.Logger != nil {

122
internal/tunnel/torsf.go Normal file
View File

@ -0,0 +1,122 @@
package tunnel
//
// torsf: Tor+snowflake tunnel
//
import (
"context"
"net/url"
"time"
"github.com/ooni/probe-cli/v3/internal/bytecounter"
"github.com/ooni/probe-cli/v3/internal/ptx"
)
// torsfStart starts the torsf (tor+snowflake) tunnel
func torsfStart(ctx context.Context, config *Config) (Tunnel, DebugInfo, error) {
config.logger().Infof("tunnel: starting snowflake with %s rendezvous method", config.snowflakeRendezvousMethod())
if err := ctx.Err(); err != nil {
return nil, DebugInfo{}, err
}
// 1. start a listener using snowflake
sfdialer, err := newSnowflakeDialer(config)
if err != nil {
return nil, DebugInfo{}, err
}
ptl := config.sfNewPTXListener(ctx, sfdialer)
if err := ptl.Start(); err != nil {
return nil, DebugInfo{}, err
}
// 2. append arguments to the configuration
extraArguments := []string{
"UseBridges", "1",
"ClientTransportPlugin", ptl.AsClientTransportPluginArgument(),
"Bridge", sfdialer.AsBridgeArgument(),
}
config.TorArgs = append(config.TorArgs, extraArguments...)
// 3. start tor as we would normally do
torTunnel, debugInfo, err := config.sfTorStart(ctx, config)
debugInfo.Name = "torsf"
if err != nil {
ptl.Stop()
return nil, debugInfo, err
}
// 4. wrap the tunnel and the listener
tsft := &torsfTunnel{
torTunnel: torTunnel,
sfListener: ptl,
}
return tsft, debugInfo, nil
}
func (c *Config) sfNewPTXListener(ctx context.Context, sfdialer *ptx.SnowflakeDialer) (out torsfPTXListener) {
out = &ptx.Listener{
ExperimentByteCounter: nil,
ListenSocks: c.testSfListenSocks,
Logger: c.logger(),
PTDialer: sfdialer,
SessionByteCounter: bytecounter.ContextSessionByteCounter(ctx),
}
if c.testSfWrapPTXListener != nil {
out = c.testSfWrapPTXListener(out)
}
return
}
// torsfPTXListener is an abstract ptx.Listener.
type torsfPTXListener interface {
Start() error
Stop()
AsClientTransportPluginArgument() string
}
func (c *Config) sfTorStart(ctx context.Context, config *Config) (Tunnel, DebugInfo, error) {
if c.testSfTorStart != nil {
return c.testSfTorStart(ctx, config)
}
return torStart(ctx, config)
}
// newSnowflakeDialer returns the correct snowflake dialer.
func newSnowflakeDialer(config *Config) (*ptx.SnowflakeDialer, error) {
rm, err := ptx.NewSnowflakeRendezvousMethod(config.snowflakeRendezvousMethod())
if err != nil {
return nil, err
}
sfDialer := ptx.NewSnowflakeDialerWithRendezvousMethod(rm)
return sfDialer, nil
}
// torsfTunnel implements Tunnel
type torsfTunnel struct {
torTunnel Tunnel
sfListener torsfListener
}
// torsfListener is torsfTunnel's view of a ptx listener for snowflake
type torsfListener interface {
Stop()
}
var _ Tunnel = &torsfTunnel{}
// BootstrapTime implements Tunnel
func (tt *torsfTunnel) BootstrapTime() time.Duration {
return tt.torTunnel.BootstrapTime()
}
// SOCKS5ProxyURL implements Tunnel
func (tt *torsfTunnel) SOCKS5ProxyURL() *url.URL {
return tt.torTunnel.SOCKS5ProxyURL()
}
// Stop implements Tunnel
func (tt *torsfTunnel) Stop() {
tt.torTunnel.Stop()
tt.sfListener.Stop()
}

View File

@ -0,0 +1,201 @@
package tunnel
import (
"context"
"errors"
"os"
"path/filepath"
"sync"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/ooni/probe-cli/v3/internal/atomicx"
"github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/model/mocks"
"github.com/ooni/probe-cli/v3/internal/ptx"
)
type torsfPTXListenerWrapper struct {
torsfPTXListener
counter *atomicx.Int64
}
func (tw *torsfPTXListenerWrapper) Stop() {
tw.counter.Add(1)
tw.torsfPTXListener.Stop()
}
func Test_torsfStart(t *testing.T) {
t.Run("newSnowflakeDialer fails", func(t *testing.T) {
ctx := context.Background()
config := &Config{
Name: "torsf",
Session: &MockableSession{},
SnowflakeRendezvous: "antani", // should cause failure
TunnelDir: filepath.Join(os.TempDir(), "torsf-xx"),
Logger: model.DiscardLogger,
TorArgs: []string{},
TorBinary: "",
testExecabsLookPath: nil,
testMkdirAll: nil,
testNetListen: nil,
testSocks5New: nil,
testTorStart: nil,
testTorProtocolInfo: nil,
testTorEnableNetwork: nil,
testTorGetInfo: nil,
}
expectDebugInfo := DebugInfo{}
tun, debugInfo, err := torsfStart(ctx, config)
if !errors.Is(err, ptx.ErrSnowflakeNoSuchRendezvousMethod) {
t.Fatal("unexpected err", err)
}
if tun != nil {
t.Fatal("expected nil tun")
}
if diff := cmp.Diff(expectDebugInfo, debugInfo); diff != "" {
t.Fatal(diff)
}
})
t.Run("ptl.Start fails", func(t *testing.T) {
ctx := context.Background()
expected := errors.New("mocked error")
config := &Config{
Name: "torsf",
Session: &MockableSession{},
SnowflakeRendezvous: "", // is the default
TunnelDir: filepath.Join(os.TempDir(), "torsf-xx"),
Logger: model.DiscardLogger,
TorArgs: []string{},
TorBinary: "",
testExecabsLookPath: nil,
testMkdirAll: nil,
testNetListen: nil,
testSfListenSocks: func(network, laddr string) (ptx.SocksListener, error) {
return nil, expected
},
testSocks5New: nil,
testTorStart: nil,
testTorProtocolInfo: nil,
testTorEnableNetwork: nil,
testTorGetInfo: nil,
}
expectDebugInfo := DebugInfo{}
tun, debugInfo, err := torsfStart(ctx, config)
if !errors.Is(err, expected) {
t.Fatal("unexpected err", err)
}
if tun != nil {
t.Fatal("expected nil tun")
}
if diff := cmp.Diff(expectDebugInfo, debugInfo); diff != "" {
t.Fatal(diff)
}
})
t.Run("torStart fails", func(t *testing.T) {
ctx := context.Background()
stopCounter := &atomicx.Int64{}
expected := errors.New("expected err")
config := &Config{
Name: "torsf",
Session: &MockableSession{},
SnowflakeRendezvous: "", // is the default
TunnelDir: filepath.Join(os.TempDir(), "torsf-xx"),
Logger: model.DiscardLogger,
TorArgs: []string{},
TorBinary: "",
testExecabsLookPath: nil,
testMkdirAll: nil,
testNetListen: nil,
testSfListenSocks: nil,
testSfWrapPTXListener: func(tp torsfPTXListener) torsfPTXListener {
return &torsfPTXListenerWrapper{
torsfPTXListener: tp,
counter: stopCounter,
}
},
testSfTorStart: func(ctx context.Context, config *Config) (Tunnel, DebugInfo, error) {
return nil, DebugInfo{}, expected
},
testSocks5New: nil,
testTorStart: nil,
testTorProtocolInfo: nil,
testTorEnableNetwork: nil,
testTorGetInfo: nil,
}
expectDebugInfo := DebugInfo{
Name: "torsf",
}
tun, debugInfo, err := torsfStart(ctx, config)
if !errors.Is(err, expected) {
t.Fatal("unexpected err", err)
}
if tun != nil {
t.Fatal("expected nil tun")
}
if diff := cmp.Diff(expectDebugInfo, debugInfo); diff != "" {
t.Fatal(diff)
}
if stopCounter.Load() != 1 {
t.Fatal("did not call stop")
}
})
t.Run("on success", func(t *testing.T) {
ctx := context.Background()
expectDebugInfo := DebugInfo{
Name: "torsf",
}
config := &Config{
Name: "torsf",
Session: &MockableSession{},
SnowflakeRendezvous: "", // is the default
TunnelDir: filepath.Join(os.TempDir(), "torsf-xx"),
Logger: model.DiscardLogger,
TorArgs: []string{},
TorBinary: "",
testExecabsLookPath: nil,
testMkdirAll: nil,
testNetListen: nil,
testSfListenSocks: nil,
testSfTorStart: func(ctx context.Context, config *Config) (Tunnel, DebugInfo, error) {
tun := &fakeTunnel{
addr: &mocks.Addr{
MockString: func() string {
return "127.0.0.1:5555"
},
},
bootstrapTime: 123,
listener: &mocks.Listener{
MockClose: func() error {
return nil
},
},
once: sync.Once{},
}
return tun, expectDebugInfo, nil
},
testSocks5New: nil,
testTorStart: nil,
testTorProtocolInfo: nil,
testTorEnableNetwork: nil,
testTorGetInfo: nil,
}
tun, debugInfo, err := torsfStart(ctx, config)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(expectDebugInfo, debugInfo); diff != "" {
t.Fatal(diff)
}
if tun.BootstrapTime() != 123 {
t.Fatal("invalid bootstrap time")
}
if tun.SOCKS5ProxyURL().String() != "socks5://127.0.0.1:5555" {
t.Fatal("invalid socks5 proxy URL")
}
tun.Stop()
})
}

View File

@ -124,6 +124,8 @@ func Start(ctx context.Context, config *Config) (Tunnel, DebugInfo, error) {
return fakeStart(ctx, config) return fakeStart(ctx, config)
case "psiphon": case "psiphon":
return psiphonStart(ctx, config) return psiphonStart(ctx, config)
case "torsf":
return torsfStart(ctx, config)
case "tor": case "tor":
return torStart(ctx, config) return torStart(ctx, config)
default: default:

View File

@ -59,6 +59,22 @@ func TestStartTorWithCancelledContext(t *testing.T) {
} }
} }
func TestStartTorsfWithCancelledContext(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel() // fail immediately
tun, _, err := tunnel.Start(ctx, &tunnel.Config{
Name: "torsf",
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) { func TestStartInvalidTunnel(t *testing.T) {
ctx := context.Background() ctx := context.Background()
tun, _, err := tunnel.Start(ctx, &tunnel.Config{ tun, _, err := tunnel.Start(ctx, &tunnel.Config{