Эх сурвалжийг харах

Integrate Conjure protocol

Rod Hynes 5 жил өмнө
parent
commit
f6efbd8027

+ 1 - 0
README.md

@@ -198,6 +198,7 @@ Psiphon Tunnel Core uses:
 * [wader/filtertransport](https://github.com/wader/filtertransport)
 * [Yawning/chacha20](https://github.com/Yawning/chacha20)
 * [Yawning/goptlib](https://github.com/Yawning/goptlib)
+* [yawning/obfs4](https://gitlab.com/yawning/obfs4)
 * [zach-klippenstein/goregen](https://github.com/zach-klippenstein/goregen)
 * [zap](https://go.uber.org/zap)
 

+ 1 - 1
psiphon/TCPConn.go

@@ -46,7 +46,7 @@ type TCPConn struct {
 // Note: do not set an UpstreamProxyURL in the config when using NewTCPDialer
 // as a custom dialer for NewProxyAuthTransport (or http.Transport with a
 // ProxyUrl), as that would result in double proxy chaining.
-func NewTCPDialer(config *DialConfig) Dialer {
+func NewTCPDialer(config *DialConfig) common.Dialer {
 	return func(ctx context.Context, network, addr string) (net.Conn, error) {
 		if network != "tcp" {
 			return nil, errors.Tracef("%s unsupported", network)

+ 3 - 0
psiphon/common/net.go

@@ -33,6 +33,9 @@ import (
 	"github.com/wader/filtertransport"
 )
 
+// Dialer is a custom network dialer.
+type Dialer func(context.Context, string, string) (net.Conn, error)
+
 // NetDialer mimicks the net.Dialer interface.
 type NetDialer interface {
 	Dial(network, address string) (net.Conn, error)

+ 9 - 0
psiphon/common/parameters/parameters.go

@@ -222,6 +222,8 @@ const (
 	ReplayHostname                                   = "ReplayHostname"
 	ReplayQUICVersion                                = "ReplayQUICVersion"
 	ReplayObfuscatedQUIC                             = "ReplayObfuscatedQUIC"
+	ReplayConjureRegistration                        = "ReplayConjureRegistration"
+	ReplayConjureTransport                           = "ReplayConjureTransport"
 	ReplayLivenessTest                               = "ReplayLivenessTest"
 	ReplayUserAgent                                  = "ReplayUserAgent"
 	ReplayAPIRequestPadding                          = "ReplayAPIRequestPadding"
@@ -269,6 +271,8 @@ const (
 	ClientBurstUpstreamTargetBytes                   = "ClientBurstUpstreamTargetBytes"
 	ClientBurstDownstreamDeadline                    = "ClientBurstDownstreamDeadline"
 	ClientBurstDownstreamTargetBytes                 = "ClientBurstDownstreamTargetBytes"
+	ConjureDecoyRegistrarWidth                       = "ConjureDecoyRegistrarWidth"
+	ConjureTransportObfs4Probability                 = "ConjureTransportObfs4Probability"
 )
 
 const (
@@ -502,6 +506,8 @@ var defaultParameters = map[string]struct {
 	ReplayHostname:                         {value: true},
 	ReplayQUICVersion:                      {value: true},
 	ReplayObfuscatedQUIC:                   {value: true},
+	ReplayConjureRegistration:              {value: true},
+	ReplayConjureTransport:                 {value: true},
 	ReplayLivenessTest:                     {value: true},
 	ReplayUserAgent:                        {value: true},
 	ReplayAPIRequestPadding:                {value: true},
@@ -558,6 +564,9 @@ var defaultParameters = map[string]struct {
 	ClientBurstUpstreamDeadline:      {value: time.Duration(0), minimum: time.Duration(0)},
 	ClientBurstDownstreamTargetBytes: {value: 0, minimum: 0},
 	ClientBurstDownstreamDeadline:    {value: time.Duration(0), minimum: time.Duration(0)},
+
+	ConjureDecoyRegistrarWidth:       {value: 5, minimum: 1},
+	ConjureTransportObfs4Probability: {value: 0.0, minimum: 0.0},
 }
 
 // IsServerSideOnly indicates if the parameter specified by name is used

+ 12 - 4
psiphon/common/protocol/protocol.go

@@ -78,6 +78,9 @@ const (
 	RANDOM_STREAM_CHANNEL_TYPE = "random@psiphon.ca"
 
 	PSIPHON_API_HANDSHAKE_AUTHORIZATIONS = "authorizations"
+
+	CONJURE_TRANSPORT_MIN_OSSH   = "Min-OSSH"
+	CONJURE_TRANSPORT_OBFS4_OSSH = "Obfs4-OSSH"
 )
 
 type TunnelProtocols []string
@@ -187,12 +190,16 @@ func TunnelProtocolUsesMarionette(protocol string) bool {
 	return protocol == TUNNEL_PROTOCOL_MARIONETTE_OBFUSCATED_SSH
 }
 
-func TunnelProtocolUsesTapdance(protocol string) bool {
+func TunnelProtocolUsesRefractionNetworking(protocol string) bool {
 	return protocol == TUNNEL_PROTOCOL_TAPDANCE_OBFUSCATED_SSH ||
 		protocol == TUNNEL_PROTOCOL_CONJURE_OBFUSCATED_SSH
 }
 
-func TunnelProtocolUsesDarkDecoys(protocol string) bool {
+func TunnelProtocolUsesTapDance(protocol string) bool {
+	return protocol == TUNNEL_PROTOCOL_TAPDANCE_OBFUSCATED_SSH
+}
+
+func TunnelProtocolUsesConjure(protocol string) bool {
 	return protocol == TUNNEL_PROTOCOL_CONJURE_OBFUSCATED_SSH
 }
 
@@ -200,7 +207,7 @@ func TunnelProtocolIsResourceIntensive(protocol string) bool {
 	return TunnelProtocolUsesMeek(protocol) ||
 		TunnelProtocolUsesQUIC(protocol) ||
 		TunnelProtocolUsesMarionette(protocol) ||
-		TunnelProtocolUsesTapdance(protocol)
+		TunnelProtocolUsesRefractionNetworking(protocol)
 }
 
 func TunnelProtocolIsCompatibleWithFragmentor(protocol string) bool {
@@ -210,7 +217,8 @@ func TunnelProtocolIsCompatibleWithFragmentor(protocol string) bool {
 		protocol == TUNNEL_PROTOCOL_UNFRONTED_MEEK_HTTPS ||
 		protocol == TUNNEL_PROTOCOL_UNFRONTED_MEEK_SESSION_TICKET ||
 		protocol == TUNNEL_PROTOCOL_FRONTED_MEEK ||
-		protocol == TUNNEL_PROTOCOL_FRONTED_MEEK_HTTP
+		protocol == TUNNEL_PROTOCOL_FRONTED_MEEK_HTTP ||
+		protocol == TUNNEL_PROTOCOL_CONJURE_OBFUSCATED_SSH
 }
 
 func TunnelProtocolRequiresTLS12SessionTickets(protocol string) bool {

+ 3 - 2
psiphon/common/protocol/serverEntry.go

@@ -468,7 +468,7 @@ func (serverEntry *ServerEntry) SupportsProtocol(protocol string) bool {
 type ConditionallyEnabledComponents interface {
 	QUICEnabled() bool
 	MarionetteEnabled() bool
-	TapdanceEnabled() bool
+	RefractionNetworkingEnabled() bool
 }
 
 // GetSupportedProtocols returns a list of tunnel protocols supported
@@ -503,7 +503,8 @@ func (serverEntry *ServerEntry) GetSupportedProtocols(
 
 		if (TunnelProtocolUsesQUIC(protocol) && !conditionallyEnabled.QUICEnabled()) ||
 			(TunnelProtocolUsesMarionette(protocol) && !conditionallyEnabled.MarionetteEnabled()) ||
-			(TunnelProtocolUsesTapdance(protocol) && !conditionallyEnabled.TapdanceEnabled()) {
+			(TunnelProtocolUsesRefractionNetworking(protocol) &&
+				!conditionallyEnabled.RefractionNetworkingEnabled()) {
 			continue
 		}
 

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 58 - 0
psiphon/common/refraction/embedded_config.go


+ 270 - 112
psiphon/common/tapdance/tapdance.go → psiphon/common/refraction/refraction.go

@@ -1,4 +1,4 @@
-// +build PSIPHON_ENABLE_TAPDANCE
+// +build PSIPHON_ENABLE_REFRACTION_NETWORKING
 
 /*
  * Copyright (c) 2018, Psiphon Inc.
@@ -21,14 +21,15 @@
 
 /*
 
-Package tapdance wraps github.com/refraction-networking/gotapdance with net.Listener
-and net.Conn types that provide drop-in integration with Psiphon.
+Package refraction wraps github.com/refraction-networking/gotapdance with
+net.Listener and net.Conn types that provide drop-in integration with Psiphon.
 
 */
-package tapdance
+package refraction
 
 import (
 	"context"
+	"crypto/sha256"
 	"io/ioutil"
 	"net"
 	"os"
@@ -39,15 +40,17 @@ import (
 
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
 	"github.com/armon/go-proxyproto"
-	refraction_networking_tapdance "github.com/refraction-networking/gotapdance/tapdance"
+	refraction_networking_proto "github.com/refraction-networking/gotapdance/protobuf"
+	refraction_networking_client "github.com/refraction-networking/gotapdance/tapdance"
 )
 
 const (
 	READ_PROXY_PROTOCOL_HEADER_TIMEOUT = 5 * time.Second
 )
 
-// Enabled indicates if Tapdance functionality is enabled.
+// Enabled indicates if Refraction Networking functionality is enabled.
 func Enabled() bool {
 	return true
 }
@@ -57,13 +60,15 @@ type Listener struct {
 	net.Listener
 }
 
-// Listen creates a new Tapdance listener on top of an existing TCP listener.
+// Listen creates a new Refraction Networking listener on top of an existing
+// TCP listener.
 //
-// The Tapdance station will send the original client address via the HAProxy
-// proxy protocol v1, https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt.
-// The original client address is read and returned by accepted conns'
-// RemoteAddr. RemoteAddr _must_ be called non-concurrently before calling Read
-// on accepted conns as the HAProxy proxy protocol header reading logic sets
+// The Refraction Networking station (TapDance or Conjure) will send the
+// original client address via the HAProxy proxy protocol v1,
+// https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt. The original
+// client address is read and returned by accepted conns' RemoteAddr.
+// RemoteAddr _must_ be called non-concurrently before calling Read on
+// accepted conns as the HAProxy proxy protocol header reading logic sets
 // SetReadDeadline and performs a Read.
 func Listen(tcpListener net.Listener) (net.Listener, error) {
 
@@ -83,15 +88,14 @@ func Listen(tcpListener net.Listener) (net.Listener, error) {
 }
 
 // stationListener uses the proxyproto.Listener SourceCheck callback to
-// capture and record the direct remote address, the Tapdance station address,
-// and wraps accepted conns to provide station address metrics via GetMetrics.
+// capture and record the direct remote address, the station address, and
+// wraps accepted conns to provide station address metrics via GetMetrics.
 // These metrics enable identifying which station fronted a connection, which
 // is useful for network operations and troubleshooting.
 //
 // go-proxyproto.Conn.RemoteAddr reports the originating client IP address,
 // which is geolocated and recorded for metrics. The underlying conn's remote
-// address, the Tapdance station address, is not accessible via the
-// go-proxyproto API.
+// address, the station address, is not accessible via the go-proxyproto API.
 //
 // stationListener is not safe for concurrent access.
 type stationListener struct {
@@ -162,16 +166,231 @@ func (c *stationConn) GetMetrics() common.LogFields {
 	return logFields
 }
 
+// DialTapDance establishes a new TapDance connection to a TapDance station
+// specified in the config assets and forwarding through to the Psiphon server
+// specified by address.
+//
+// The TapDance station config assets (which are also the Conjure station
+// assets) are read from dataDirectory/"refraction-networking". When no config
+// is found, default assets are paved.
+//
+// The input ctx is expected to have a timeout for the dial.
+//
+// Limitation: the parameters emitLogs and dataDirectory are used for one-time
+// initialization and are ignored after the first DialTapDance/Conjure call.
+func DialTapDance(
+	ctx context.Context,
+	emitLogs bool,
+	dataDirectory string,
+	dialer common.NetDialer,
+	address string) (net.Conn, error) {
+
+	return dial(
+		ctx,
+		emitLogs,
+		dataDirectory,
+		dialer,
+		false,
+		nil,
+		0,
+		"",
+		address)
+}
+
+// DialConjure establishes a new Conjure connection to a Conjure station.
+//
+// See DialTapdance comment.
+func DialConjure(
+	ctx context.Context,
+	emitLogs bool,
+	dataDirectory string,
+	dialer common.NetDialer,
+	conjureDecoyRegistrarDialer common.NetDialer,
+	conjureDecoyRegistrarWidth int,
+	conjureTransport string,
+	address string) (net.Conn, error) {
+
+	return dial(
+		ctx,
+		emitLogs,
+		dataDirectory,
+		dialer,
+		true,
+		conjureDecoyRegistrarDialer,
+		conjureDecoyRegistrarWidth,
+		conjureTransport,
+		address)
+}
+
+func dial(
+	ctx context.Context,
+	emitLogs bool,
+	dataDirectory string,
+	dialer common.NetDialer,
+	useConjure bool,
+	conjureDecoyRegistrarDialer common.NetDialer,
+	conjureDecoyRegistrarWidth int,
+	conjureTransport string,
+	address string) (net.Conn, error) {
+
+	err := initRefractionNetworking(emitLogs, dataDirectory)
+	if err != nil {
+		return nil, errors.Trace(err)
+	}
+
+	if _, ok := ctx.Deadline(); !ok {
+		return nil, errors.TraceNew("dial context has no timeout")
+	}
+
+	manager := newDialManager()
+
+	refractionDialer := &refraction_networking_client.Dialer{
+		TcpDialer:      manager.makeManagedDialer(dialer.DialContext),
+		UseProxyHeader: true,
+	}
+
+	if useConjure {
+
+		refractionDialer.DarkDecoy = true
+
+		refractionDialer.DarkDecoyRegistrar = refraction_networking_client.DecoyRegistrar{
+			TcpDialer: manager.makeManagedDialer(conjureDecoyRegistrarDialer.DialContext),
+		}
+		refractionDialer.Width = conjureDecoyRegistrarWidth
+
+		switch conjureTransport {
+		case protocol.CONJURE_TRANSPORT_MIN_OSSH:
+			refractionDialer.Transport = refraction_networking_proto.TransportType_Min
+			refractionDialer.TcpDialer = newMinTransportDialer(refractionDialer.TcpDialer)
+		case protocol.CONJURE_TRANSPORT_OBFS4_OSSH:
+			refractionDialer.Transport = refraction_networking_proto.TransportType_Obfs4
+		default:
+			return nil, errors.Tracef("invalid Conjure transport: %s", conjureTransport)
+		}
+	}
+
+	// If the dial context is cancelled, use dialManager to interrupt
+	// refractionDialer.DialContext. See dialManager comment explaining why
+	// refractionDialer.DialContext may block even when the input context is
+	// cancelled.
+	dialComplete := make(chan struct{})
+	go func() {
+		select {
+		case <-ctx.Done():
+		case <-dialComplete:
+		}
+		select {
+		// Prioritize the dialComplete case.
+		case <-dialComplete:
+			return
+		default:
+		}
+		manager.close()
+	}()
+
+	conn, err := refractionDialer.DialContext(ctx, "tcp", address)
+	close(dialComplete)
+	if err != nil {
+		manager.close()
+		return nil, errors.Trace(err)
+	}
+
+	manager.startUsingRunCtx()
+
+	return &refractionConn{
+		Conn:    conn,
+		manager: manager,
+	}, nil
+}
+
+// minTransportConn buffers the first 32-byte random HMAC write performed by
+// Conjure TransportType_Min, and prepends it to the subsequent first write
+// made by OSSH. The purpose is to avoid a distinct fingerprint consisting of
+// the initial TCP data packet always containing exactly 32 bytes of payload.
+// The first write by OSSH will be a variable length multi-packet-sized
+// sequence of random bytes.
+type minTransportConn struct {
+	net.Conn
+
+	mutex  sync.Mutex
+	state  int
+	buffer []byte
+	err    error
+}
+
+const (
+	stateMinTransportInit = iota
+	stateMinTransportBufferedHMAC
+	stateMinTransportWroteHMAC
+	stateMinTransportFailed
+)
+
+func newMinTransportConn(conn net.Conn) *minTransportConn {
+	return &minTransportConn{
+		Conn:  conn,
+		state: stateMinTransportInit,
+	}
+}
+
+func (conn *minTransportConn) Write(p []byte) (int, error) {
+	conn.mutex.Lock()
+	defer conn.mutex.Unlock()
+
+	switch conn.state {
+	case stateMinTransportInit:
+		if len(p) != sha256.Size {
+			conn.state = stateMinTransportFailed
+			conn.err = errors.TraceNew("unexpected HMAC write size")
+			return 0, conn.err
+		}
+		conn.buffer = make([]byte, sha256.Size)
+		copy(conn.buffer, p)
+		conn.state = stateMinTransportBufferedHMAC
+		return sha256.Size, nil
+	case stateMinTransportBufferedHMAC:
+		conn.buffer = append(conn.buffer, p...)
+		n, err := conn.Conn.Write(conn.buffer)
+		if n < sha256.Size {
+			conn.state = stateMinTransportFailed
+			conn.err = errors.TraceNew("failed to write HMAC")
+			if err == nil {
+				// As Write must return an error when failing to write the entire buffer,
+				// we don't expect to hit this case.
+				err = conn.err
+			}
+		} else {
+			conn.state = stateMinTransportWroteHMAC
+		}
+		n -= sha256.Size
+		// Do not wrap Conn.Write errors, and do not return conn.err here.
+		return n, err
+	case stateMinTransportWroteHMAC:
+		return conn.Conn.Write(p)
+	case stateMinTransportFailed:
+		return 0, conn.err
+	default:
+		return 0, errors.TraceNew("unexpected state")
+	}
+}
+
+func newMinTransportDialer(dialer common.Dialer) common.Dialer {
+	return func(ctx context.Context, network, address string) (net.Conn, error) {
+		conn, err := dialer(ctx, network, address)
+		if err != nil {
+			return nil, errors.Trace(err)
+		}
+		return newMinTransportConn(conn), nil
+	}
+}
+
 // dialManager tracks all dials performed by and dialed conns used by a
-// refraction_networking_tapdance client. dialManager.close interrupts/closes
+// refraction_networking_client conn. dialManager.close interrupts/closes
 // all pending dials and established conns immediately. This ensures that
-// blocking calls within refraction_networking_tapdance, such as tls.Handhake,
+// blocking calls within refraction_networking_client, such as tls.Handhake,
 // are interrupted:
 // E.g., https://github.com/refraction-networking/gotapdance/blob/4d84655dad2e242b0af0459c31f687b12085dcca/tapdance/conn_raw.go#L307
 // (...preceeding SetDeadline is insufficient for immediate cancellation.)
 type dialManager struct {
-	tcpDialer func(ctx context.Context, network, address string) (net.Conn, error)
-
 	ctxMutex       sync.Mutex
 	useRunCtx      bool
 	initialDialCtx context.Context
@@ -181,36 +400,43 @@ type dialManager struct {
 	conns *common.Conns
 }
 
-func newDialManager(
-	tcpDialer func(ctx context.Context, network, address string) (net.Conn, error)) *dialManager {
-
+func newDialManager() *dialManager {
 	runCtx, stopRunning := context.WithCancel(context.Background())
-
 	return &dialManager{
-		tcpDialer:   tcpDialer,
 		runCtx:      runCtx,
 		stopRunning: stopRunning,
 		conns:       common.NewConns(),
 	}
 }
 
-func (manager *dialManager) dial(ctx context.Context, network, address string) (net.Conn, error) {
+func (manager *dialManager) makeManagedDialer(dialer common.Dialer) common.Dialer {
+
+	return func(ctx context.Context, network, address string) (net.Conn, error) {
+		return manager.dialWithDialer(dialer, ctx, network, address)
+	}
+}
+
+func (manager *dialManager) dialWithDialer(
+	dialer common.Dialer,
+	ctx context.Context,
+	network string,
+	address string) (net.Conn, error) {
 
 	if network != "tcp" {
 		return nil, errors.Tracef("unsupported network: %s", network)
 	}
 
 	// The context for this dial is either:
-	// - ctx, during the initial tapdance.DialContext, when this is Psiphon tunnel
-	//   establishment.
-	// - manager.runCtx after the initial tapdance.Dial completes, in which case
-	//   this is a Tapdance protocol reconnection that occurs periodically for
-	//   already established tunnels.
+	// - ctx, during the initial refraction_networking_client.DialContext, when
+	//   this is Psiphon tunnel establishment.
+	// - manager.runCtx after the initial refraction_networking_client.Dial
+	//   completes, in which case this is a TapDance protocol reconnection that
+	//   occurs periodically for already established tunnels.
 
 	manager.ctxMutex.Lock()
 	if manager.useRunCtx {
 
-		// Preserve the random timeout configured by the tapdance client:
+		// Preserve the random timeout configured by the TapDance client:
 		// https://github.com/refraction-networking/gotapdance/blob/4d84655dad2e242b0af0459c31f687b12085dcca/tapdance/conn_raw.go#L263
 		deadline, ok := ctx.Deadline()
 		if !ok {
@@ -222,14 +448,14 @@ func (manager *dialManager) dial(ctx context.Context, network, address string) (
 	}
 	manager.ctxMutex.Unlock()
 
-	conn, err := manager.tcpDialer(ctx, network, address)
+	conn, err := dialer(ctx, network, address)
 	if err != nil {
 		return nil, errors.Trace(err)
 	}
 
 	// Fail immediately if CloseWrite isn't available in the underlying dialed
 	// conn. The equivalent check in managedConn.CloseWrite isn't fatal and
-	// tapdance will run in a degraded state.
+	// TapDance will run in a degraded state.
 	// Limitation: if the underlying conn _also_ passes through CloseWrite, this
 	// check may be insufficient.
 	if _, ok := conn.(common.CloseWriter); !ok {
@@ -267,7 +493,7 @@ type managedConn struct {
 }
 
 // CloseWrite exposes the net.TCPConn.CloseWrite() functionality
-// required by tapdance.
+// required by TapDance.
 func (conn *managedConn) CloseWrite() error {
 	if closeWriter, ok := conn.Conn.(common.CloseWriter); ok {
 		return closeWriter.CloseWrite()
@@ -282,103 +508,35 @@ func (conn *managedConn) Close() error {
 	return conn.Conn.Close()
 }
 
-type tapdanceConn struct {
+type refractionConn struct {
 	net.Conn
 	manager  *dialManager
 	isClosed int32
 }
 
-func (conn *tapdanceConn) Close() error {
+func (conn *refractionConn) Close() error {
 	conn.manager.close()
 	err := conn.Conn.Close()
 	atomic.StoreInt32(&conn.isClosed, 1)
 	return err
 }
 
-func (conn *tapdanceConn) IsClosed() bool {
+func (conn *refractionConn) IsClosed() bool {
 	return atomic.LoadInt32(&conn.isClosed) == 1
 }
 
-// Dial establishes a new Tapdance session to a Tapdance station specified in
-// the config assets and forwarding through to the Psiphon server specified by
-// address.
-//
-// The Tapdance station config assets are read from dataDirectory/"tapdance".
-// When no config is found, default assets are paved. ctx is expected to have
-// a timeout for the dial.
-//
-// Limitation: the parameters emitLogs and dataDirectory are used for one-time
-// initialization and are ignored after the first Dial call.
-func Dial(
-	ctx context.Context,
-	emitLogs bool,
-	dataDirectory string,
-	netDialer common.NetDialer,
-	address string) (net.Conn, error) {
-
-	err := initTapdance(emitLogs, dataDirectory)
-	if err != nil {
-		return nil, errors.Trace(err)
-	}
-
-	if _, ok := ctx.Deadline(); !ok {
-		return nil, errors.TraceNew("dial context has no timeout")
-	}
-
-	manager := newDialManager(netDialer.DialContext)
-
-	tapdanceDialer := &refraction_networking_tapdance.Dialer{
-		TcpDialer: manager.dial,
-	}
-
-	// If the dial context is cancelled, use dialManager to interrupt
-	// tapdanceDialer.DialContext. See dialManager comment explaining why
-	// tapdanceDialer.DialContext may block even when the input context is
-	// cancelled.
-	dialComplete := make(chan struct{})
-	go func() {
-		select {
-		case <-ctx.Done():
-		case <-dialComplete:
-		}
-		select {
-		// Prioritize the dialComplete case.
-		case <-dialComplete:
-			return
-		default:
-		}
-		manager.close()
-	}()
-
-	conn, err := tapdanceDialer.DialContext(ctx, "tcp", address)
-	close(dialComplete)
-	if err != nil {
-		manager.close()
-		return nil, errors.Trace(err)
-	}
-
-	manager.startUsingRunCtx()
-
-	return &tapdanceConn{
-		Conn:    conn,
-		manager: manager,
-	}, nil
-}
-
-var initTapdanceOnce sync.Once
+var initRefractionNetworkingOnce sync.Once
 
-func initTapdance(emitLogs bool, dataDirectory string) error {
+func initRefractionNetworking(emitLogs bool, dataDirectory string) error {
 
 	var initErr error
-	initTapdanceOnce.Do(func() {
+	initRefractionNetworkingOnce.Do(func() {
 
 		if !emitLogs {
-			refraction_networking_tapdance.Logger().Out = ioutil.Discard
+			refraction_networking_client.Logger().Out = ioutil.Discard
 		}
 
-		refraction_networking_tapdance.EnableProxyProtocol()
-
-		assetsDir := filepath.Join(dataDirectory, "tapdance")
+		assetsDir := filepath.Join(dataDirectory, "refraction-networking")
 
 		err := os.MkdirAll(assetsDir, 0700)
 		if err != nil {
@@ -396,7 +554,7 @@ func initTapdance(emitLogs bool, dataDirectory string) error {
 			return
 		}
 
-		refraction_networking_tapdance.AssetsSetDir(assetsDir)
+		refraction_networking_client.AssetsSetDir(assetsDir)
 	})
 
 	return initErr

+ 11 - 6
psiphon/common/tapdance/tapdance_disabled.go → psiphon/common/refraction/refraction_disabled.go

@@ -1,4 +1,4 @@
-// +build !PSIPHON_ENABLE_TAPDANCE
+// +build !PSIPHON_ENABLE_REFRACTION_NETWORKING
 
 /*
  * Copyright (c) 2018, Psiphon Inc.
@@ -19,7 +19,7 @@
  *
  */
 
-package tapdance
+package refraction
 
 import (
 	"context"
@@ -29,7 +29,7 @@ import (
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
 )
 
-// Enabled indicates if Tapdance functionality is enabled.
+// Enabled indicates if Refraction Networking functionality is enabled.
 func Enabled() bool {
 	return false
 }
@@ -39,12 +39,17 @@ type Listener struct {
 	net.Listener
 }
 
-// Listen creates a new Tapdance listener.
+// Listen creates a new Refraction Networking listener.
 func Listen(_ net.Listener) (net.Listener, error) {
 	return nil, errors.TraceNew("operation is not enabled")
 }
 
-// Dial establishes a new Tapdance session to a Tapdance station.
-func Dial(_ context.Context, _ bool, _ string, _ common.NetDialer, _ string) (net.Conn, error) {
+// DialTapDance establishes a new Tapdance connection to a Tapdance station.
+func DialTapDance(_ context.Context, _ bool, _ string, _ common.NetDialer, _ string) (net.Conn, error) {
+	return nil, errors.TraceNew("operation is not enabled")
+}
+
+// DialConjure establishes a new Conjure connection to a Conjure station.
+func DialConjure(_ context.Context, _ bool, _ string, _, _ common.NetDialer, _ int, _, _ string) (net.Conn, error) {
 	return nil, errors.TraceNew("operation is not enabled")
 }

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 58
psiphon/common/tapdance/embedded_config.go


+ 10 - 29
psiphon/config.go

@@ -158,11 +158,12 @@ type Config struct {
 	NetworkLatencyMultiplier float64
 
 	// LimitTunnelProtocols indicates which protocols to use. Valid values
-	// include:
-	// "SSH", "OSSH", "UNFRONTED-MEEK-OSSH", "UNFRONTED-MEEK-HTTPS-OSSH",
-	// "UNFRONTED-MEEK-SESSION-TICKET-OSSH", "FRONTED-MEEK-OSSH",
-	// "FRONTED-MEEK-HTTP-OSSH", "QUIC-OSSH", "MARIONETTE-OSSH", and
-	// "TAPDANCE-OSSH".
+	// include: "SSH", "OSSH", "UNFRONTED-MEEK-OSSH",
+	// "UNFRONTED-MEEK-HTTPS-OSSH", "UNFRONTED-MEEK-SESSION-TICKET-OSSH",
+	// "FRONTED-MEEK-OSSH", "FRONTED-MEEK-HTTP-OSSH", "QUIC-OSSH",
+	// "FRONTED-MEEK-QUIC-OSSH", "MARIONETTE-OSSH", "TAPDANCE-OSSH", and
+	// "CONJURE-OSSH".
+
 	// For the default, an empty list, all protocols are used.
 	LimitTunnelProtocols []string
 
@@ -451,10 +452,10 @@ type Config struct {
 	// and testing only.
 	EmitSLOKs bool
 
-	// EmitTapdanceLogs indicates whether to emit gotapdance log messages
-	// to stdout. Note that gotapdance log messages do not conform to the
-	// Notice format standard. Default is off.
-	EmitTapdanceLogs bool
+	// EmitRefractionNetworkingLogs indicates whether to emit gotapdance log
+	// messages to stdout. Note that gotapdance log messages do not conform to
+	// the Notice format standard. Default is off.
+	EmitRefractionNetworkingLogs bool
 
 	// EmitServerAlerts indicates whether to emit notices for server alerts.
 	EmitServerAlerts bool
@@ -978,15 +979,6 @@ func (config *Config) Commit(migrateFromLegacyFields bool) error {
 		}
 	}
 
-	// Create tapdance directory
-	tapdanceDirectoryPath := config.GetTapdanceDirectory()
-	if !common.FileExists(tapdanceDirectoryPath) {
-		err := os.Mkdir(tapdanceDirectoryPath, os.ModePerm)
-		if err != nil {
-			return errors.Tracef("failed to create tapdance directory %s with error: %s", tapdanceDirectoryPath, err.Error())
-		}
-	}
-
 	if config.ClientVersion == "" {
 		config.ClientVersion = "0"
 	}
@@ -1331,12 +1323,6 @@ func (config *Config) GetUpgradeDownloadFilename() string {
 	return filepath.Join(config.GetPsiphonDataDirectory(), UpgradeDownloadFilename)
 }
 
-// GetTapdanceDirectory returns the directory under which tapdance will create
-// and manage files.
-func (config *Config) GetTapdanceDirectory() string {
-	return filepath.Join(config.GetPsiphonDataDirectory(), "tapdance")
-}
-
 // UseUpstreamProxy indicates if an upstream proxy has been
 // configured.
 func (config *Config) UseUpstreamProxy() bool {
@@ -1902,11 +1888,6 @@ func migrationsFromLegacyFilePaths(config *Config) ([]common.FileMigration, erro
 			OldPath: filepath.Join(config.MigrateDataStoreDirectory, "psiphon.boltdb.lock"),
 			NewPath: filepath.Join(config.GetDataStoreDirectory(), "psiphon.boltdb.lock"),
 		},
-		{
-			OldPath: filepath.Join(config.MigrateDataStoreDirectory, "tapdance"),
-			NewPath: filepath.Join(config.GetTapdanceDirectory(), "tapdance"),
-			IsDir:   true,
-		},
 	}
 
 	if config.MigrateRemoteServerListDownloadFilename != "" {

+ 0 - 27
psiphon/config_test.go

@@ -299,17 +299,6 @@ func LoadConfigMigrateTest(oslDirChildrenPreMigration []FileTree, oslDirChildren
 					{
 						Name: "psiphon.boltdb.lock",
 					},
-					{
-						Name: "tapdance",
-						Children: []FileTree{
-							{
-								Name: "file1",
-							},
-							{
-								Name: "file2",
-							},
-						},
-					},
 					{
 						Name: "non_tunnel_core_file_should_not_be_migrated",
 					},
@@ -454,22 +443,6 @@ func LoadConfigMigrateTest(oslDirChildrenPreMigration []FileTree, oslDirChildren
 									},
 								},
 							},
-							{
-								Name: "tapdance",
-								Children: []FileTree{
-									{
-										Name: "tapdance",
-										Children: []FileTree{
-											{
-												Name: "file1",
-											},
-											{
-												Name: "file2",
-											},
-										},
-									},
-								},
-							},
 							{
 								Name: "upgrade",
 							},

+ 22 - 1
psiphon/dialParameters.go

@@ -113,6 +113,9 @@ type DialParameters struct {
 	QUICDialSNIAddress        string
 	ObfuscatedQUICPaddingSeed *prng.Seed
 
+	ConjureDecoyRegistrarWidth int
+	ConjureTransport           string
+
 	LivenessTestSeed *prng.Seed
 
 	APIRequestPaddingSeed *prng.Seed
@@ -168,6 +171,8 @@ func MakeDialParameters(
 	replayHostname := p.Bool(parameters.ReplayHostname)
 	replayQUICVersion := p.Bool(parameters.ReplayQUICVersion)
 	replayObfuscatedQUIC := p.Bool(parameters.ReplayObfuscatedQUIC)
+	replayConjureRegistration := p.Bool(parameters.ReplayConjureRegistration)
+	replayConjureTransport := p.Bool(parameters.ReplayConjureTransport)
 	replayLivenessTest := p.Bool(parameters.ReplayLivenessTest)
 	replayUserAgent := p.Bool(parameters.ReplayUserAgent)
 	replayAPIRequestPadding := p.Bool(parameters.ReplayAPIRequestPadding)
@@ -498,6 +503,22 @@ func MakeDialParameters(
 		}
 	}
 
+	if (!isReplay || !replayConjureRegistration) &&
+		protocol.TunnelProtocolUsesConjure(dialParams.TunnelProtocol) {
+
+		dialParams.ConjureDecoyRegistrarWidth = p.Int(parameters.ConjureDecoyRegistrarWidth)
+	}
+
+	if (!isReplay || !replayConjureTransport) &&
+		protocol.TunnelProtocolUsesConjure(dialParams.TunnelProtocol) {
+
+		dialParams.ConjureTransport = protocol.CONJURE_TRANSPORT_MIN_OSSH
+		if p.WeightedCoinFlip(
+			parameters.ConjureTransportObfs4Probability) {
+			dialParams.ConjureTransport = protocol.CONJURE_TRANSPORT_OBFS4_OSSH
+		}
+	}
+
 	if !isReplay || !replayLivenessTest {
 
 		// TODO: initialize only when LivenessTestMaxUp/DownstreamBytes > 0?
@@ -525,7 +546,7 @@ func MakeDialParameters(
 	case protocol.TUNNEL_PROTOCOL_OBFUSCATED_SSH:
 		dialParams.DirectDialAddress = fmt.Sprintf("%s:%d", serverEntry.IpAddress, serverEntry.SshObfuscatedPort)
 
-	case protocol.TUNNEL_PROTOCOL_TAPDANCE_OBFUSCATED_SSH:
+	case protocol.TUNNEL_PROTOCOL_TAPDANCE_OBFUSCATED_SSH, protocol.TUNNEL_PROTOCOL_CONJURE_OBFUSCATED_SSH:
 		dialParams.DirectDialAddress = fmt.Sprintf("%s:%d", serverEntry.IpAddress, serverEntry.SshObfuscatedTapdancePort)
 
 	case protocol.TUNNEL_PROTOCOL_QUIC_OBFUSCATED_SSH:

+ 8 - 7
psiphon/interrupt_dials_test.go

@@ -29,33 +29,34 @@ import (
 	"testing"
 	"time"
 
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
 )
 
 func TestInterruptDials(t *testing.T) {
 
-	makeDialers := make(map[string]func(string) Dialer)
+	makeDialers := make(map[string]func(string) common.Dialer)
 
-	makeDialers["TCP"] = func(string) Dialer {
+	makeDialers["TCP"] = func(string) common.Dialer {
 		return NewTCPDialer(&DialConfig{})
 	}
 
-	makeDialers["SOCKS4-Proxied"] = func(mockServerAddr string) Dialer {
+	makeDialers["SOCKS4-Proxied"] = func(mockServerAddr string) common.Dialer {
 		return NewTCPDialer(
 			&DialConfig{
 				UpstreamProxyURL: "socks4a://" + mockServerAddr,
 			})
 	}
 
-	makeDialers["SOCKS5-Proxied"] = func(mockServerAddr string) Dialer {
+	makeDialers["SOCKS5-Proxied"] = func(mockServerAddr string) common.Dialer {
 		return NewTCPDialer(
 			&DialConfig{
 				UpstreamProxyURL: "socks5://" + mockServerAddr,
 			})
 	}
 
-	makeDialers["HTTP-CONNECT-Proxied"] = func(mockServerAddr string) Dialer {
+	makeDialers["HTTP-CONNECT-Proxied"] = func(mockServerAddr string) common.Dialer {
 		return NewTCPDialer(
 			&DialConfig{
 				UpstreamProxyURL: "http://" + mockServerAddr,
@@ -74,7 +75,7 @@ func TestInterruptDials(t *testing.T) {
 		t.Fatalf("NewSeed failed: %s", err)
 	}
 
-	makeDialers["TLS"] = func(string) Dialer {
+	makeDialers["TLS"] = func(string) common.Dialer {
 		return NewCustomTLSDialer(
 			&CustomTLSConfig{
 				Parameters:               params,
@@ -104,7 +105,7 @@ func TestInterruptDials(t *testing.T) {
 func runInterruptDials(
 	t *testing.T,
 	doTimeout bool,
-	makeDialer func(string) Dialer,
+	makeDialer func(string) common.Dialer,
 	dialGoroutineFunctionNames []string) {
 
 	t.Logf("Test timeout: %+v", doTimeout)

+ 3 - 3
psiphon/meekConn.go

@@ -434,7 +434,7 @@ func DialMeek(
 
 		scheme = "http"
 
-		var dialer Dialer
+		var dialer common.Dialer
 
 		// For HTTP, and when the meekConfig.DialAddress matches the
 		// meekConfig.HostHeader, we let http.Transport handle proxying.
@@ -573,13 +573,13 @@ func DialMeek(
 type cachedTLSDialer struct {
 	usedCachedConn int32
 	cachedConn     net.Conn
-	dialer         Dialer
+	dialer         common.Dialer
 
 	mutex      sync.Mutex
 	requestCtx context.Context
 }
 
-func newCachedTLSDialer(cachedConn net.Conn, dialer Dialer) *cachedTLSDialer {
+func newCachedTLSDialer(cachedConn net.Conn, dialer common.Dialer) *cachedTLSDialer {
 	return &cachedTLSDialer{
 		cachedConn: cachedConn,
 		dialer:     dialer,

+ 25 - 6
psiphon/net.go

@@ -35,6 +35,7 @@ import (
 	"sync/atomic"
 	"time"
 
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/fragmentor"
 	"github.com/miekg/dns"
@@ -105,6 +106,19 @@ type DialConfig struct {
 	UpstreamProxyErrorCallback func(error)
 }
 
+// WithoutFragmentor returns a copy of the DialConfig with any fragmentor
+// configuration disabled. The return value is not a deep copy and may be the
+// input DialConfig; it should not be modified.
+func (config *DialConfig) WithoutFragmentor() *DialConfig {
+	if config.FragmentorConfig == nil {
+		return config
+	}
+	newConfig := new(DialConfig)
+	*newConfig = *config
+	newConfig.FragmentorConfig = nil
+	return newConfig
+}
+
 // NetworkConnectivityChecker defines the interface to the external
 // HasNetworkConnectivity provider, which call into the host application to
 // check for network connectivity.
@@ -159,13 +173,10 @@ type NetworkIDGetter interface {
 	GetNetworkID() string
 }
 
-// Dialer is a custom network dialer.
-type Dialer func(context.Context, string, string) (net.Conn, error)
-
 // NetDialer implements an interface that matches net.Dialer.
 // Limitation: only "tcp" Dials are supported.
 type NetDialer struct {
-	dialTCP Dialer
+	dialTCP common.Dialer
 }
 
 // NewNetDialer creates a new NetDialer.
@@ -176,13 +187,21 @@ func NewNetDialer(config *DialConfig) *NetDialer {
 }
 
 func (d *NetDialer) Dial(network, address string) (net.Conn, error) {
-	return d.DialContext(context.Background(), network, address)
+	conn, err := d.DialContext(context.Background(), network, address)
+	if err != nil {
+		return nil, errors.Trace(err)
+	}
+	return conn, nil
 }
 
 func (d *NetDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
 	switch network {
 	case "tcp":
-		return d.dialTCP(ctx, "tcp", address)
+		conn, err := d.dialTCP(ctx, "tcp", address)
+		if err != nil {
+			return nil, errors.Trace(err)
+		}
+		return conn, nil
 	default:
 		return nil, errors.Tracef("unsupported network: %s", network)
 	}

+ 5 - 5
psiphon/server/tunnelServer.go

@@ -48,8 +48,8 @@ import (
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/quic"
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/refraction"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/tactics"
-	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/tapdance"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/tun"
 	"github.com/marusama/semaphore"
 	cache "github.com/patrickmn/go-cache"
@@ -173,13 +173,13 @@ func (server *TunnelServer) Run() error {
 				support.Config.ServerIPAddress,
 				support.Config.MarionetteFormat)
 
+		} else if protocol.TunnelProtocolUsesRefractionNetworking(tunnelProtocol) {
+
+			listener, err = refraction.Listen(listener)
+
 		} else {
 
 			listener, BPFProgramName, err = newTCPListenerWithBPF(support, localAddress)
-
-			if protocol.TunnelProtocolUsesTapdance(tunnelProtocol) {
-				listener, err = tapdance.Listen(listener)
-			}
 		}
 
 		if err != nil {

+ 2 - 2
psiphon/tlsDialer.go

@@ -81,7 +81,7 @@ type CustomTLSConfig struct {
 
 	// Dial is the network connection dialer. TLS is layered on
 	// top of a new network connection created with dialer.
-	Dial Dialer
+	Dial common.Dialer
 
 	// DialAddr overrides the "addr" input to Dial when specified
 	DialAddr string
@@ -363,7 +363,7 @@ func IsTLSConnUsingHTTP2(conn net.Conn) bool {
 }
 
 // NewCustomTLSDialer creates a new dialer based on CustomTLSDial.
-func NewCustomTLSDialer(config *CustomTLSConfig) Dialer {
+func NewCustomTLSDialer(config *CustomTLSConfig) common.Dialer {
 	return func(ctx context.Context, network, addr string) (net.Conn, error) {
 		return CustomTLSDial(ctx, network, addr, config)
 	}

+ 27 - 5
psiphon/tunnel.go

@@ -42,8 +42,8 @@ import (
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/quic"
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/refraction"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/tactics"
-	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/tapdance"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/transferstats"
 )
 
@@ -742,18 +742,40 @@ func dialTunnel(
 			return nil, errors.Trace(err)
 		}
 
-	} else if protocol.TunnelProtocolUsesTapdance(dialParams.TunnelProtocol) {
+	} else if protocol.TunnelProtocolUsesTapDance(dialParams.TunnelProtocol) {
 
-		dialConn, err = tapdance.Dial(
+		dialConn, err = refraction.DialTapDance(
 			ctx,
-			config.EmitTapdanceLogs,
-			config.GetTapdanceDirectory(),
+			config.EmitRefractionNetworkingLogs,
+			config.GetPsiphonDataDirectory(),
 			NewNetDialer(dialParams.GetDialConfig()),
 			dialParams.DirectDialAddress)
 		if err != nil {
 			return nil, errors.Trace(err)
 		}
 
+	} else if protocol.TunnelProtocolUsesConjure(dialParams.TunnelProtocol) {
+
+		// The Conjure "phantom" connection is compatible with fragmentation, but
+		// the decoy registrar connection, like Tapdance, is not, so force it off.
+		// Any tunnel fragmentation metrics will refer to the "phantom" connection
+		// only.
+		decoyRegistrarDialer := NewNetDialer(
+			dialParams.GetDialConfig().WithoutFragmentor())
+
+		dialConn, err = refraction.DialConjure(
+			ctx,
+			config.EmitRefractionNetworkingLogs,
+			config.GetPsiphonDataDirectory(),
+			NewNetDialer(dialParams.GetDialConfig()),
+			decoyRegistrarDialer,
+			dialParams.ConjureDecoyRegistrarWidth,
+			dialParams.ConjureTransport,
+			dialParams.DirectDialAddress)
+		if err != nil {
+			return nil, errors.Trace(err)
+		}
+
 	} else {
 
 		dialConn, err = DialTCP(

+ 3 - 3
psiphon/utils.go

@@ -39,8 +39,8 @@ import (
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/marionette"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/quic"
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/refraction"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/stacktrace"
-	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/tapdance"
 )
 
 // MakePsiphonUserAgent constructs a User-Agent value to use for web service
@@ -277,6 +277,6 @@ func (c conditionallyEnabledComponents) MarionetteEnabled() bool {
 	return marionette.Enabled()
 }
 
-func (c conditionallyEnabledComponents) TapdanceEnabled() bool {
-	return tapdance.Enabled()
+func (c conditionallyEnabledComponents) RefractionNetworkingEnabled() bool {
+	return refraction.Enabled()
 }

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно