Просмотр исходного кода

Preliminary Tapdance and Marionette integration

Rod Hynes 7 лет назад
Родитель
Сommit
ae6d0f8f37

+ 16 - 11
Server/main.go

@@ -33,6 +33,7 @@ import (
 
 	"github.com/Psiphon-Inc/rotate-safe-writer"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/server"
 	"github.com/mitchellh/panicwrap"
 )
@@ -145,24 +146,27 @@ func main() {
 		}
 
 		tunnelProtocolPorts := make(map[string]int)
+		marionetteFormat := ""
+
 		for _, protocolPort := range generateProtocolPorts {
 			parts := strings.Split(protocolPort, ":")
 			if len(parts) == 2 {
-				port, err := strconv.Atoi(parts[1])
-				if err != nil {
-					fmt.Printf("generate failed: %s\n", err)
-					os.Exit(1)
+				if protocol.TunnelProtocolUsesMarionette(parts[0]) {
+					tunnelProtocolPorts[parts[0]] = 0
+					marionetteFormat = parts[1]
+				} else {
+					port, err := strconv.Atoi(parts[1])
+					if err != nil {
+						fmt.Printf("generate failed: %s\n", err)
+						os.Exit(1)
+					}
+					tunnelProtocolPorts[parts[0]] = port
 				}
-				tunnelProtocolPorts[parts[0]] = port
 			}
 		}
 
-		configJSON,
-			trafficRulesConfigJSON,
-			OSLConfigJSON,
-			tacticsConfigJSON,
-			encodedServerEntry,
-			err :=
+		configJSON, trafficRulesConfigJSON, OSLConfigJSON,
+			tacticsConfigJSON, encodedServerEntry, err :=
 			server.GenerateConfig(
 				&server.GenerateConfigParams{
 					LogFilename:                generateLogFilename,
@@ -170,6 +174,7 @@ func main() {
 					EnableSSHAPIRequests:       true,
 					WebServerPort:              generateWebServerPort,
 					TunnelProtocolPorts:        tunnelProtocolPorts,
+					MarionetteFormat:           marionetteFormat,
 					TrafficRulesConfigFilename: generateTrafficRulesConfigFilename,
 					OSLConfigFilename:          generateOSLConfigFilename,
 					TacticsConfigFilename:      generateTacticsConfigFilename,

+ 2 - 2
psiphon/TCPConn.go

@@ -40,7 +40,7 @@ type TCPConn struct {
 	isClosed int32
 }
 
-// NewTCPDialer creates a TCPDialer.
+// NewTCPDialer creates a TCP Dialer.
 //
 // Note: do not set an UpstreamProxyURL in the config when using NewTCPDialer
 // as a custom dialer for NewProxyAuthTransport (or http.Transport with a
@@ -86,7 +86,7 @@ func DialTCP(
 func proxiedTcpDial(
 	ctx context.Context, addr string, config *DialConfig) (net.Conn, error) {
 
-	var interruptConns common.Conns
+	interruptConns := common.NewConns()
 
 	// Note: using interruptConns to interrupt a proxy dial assumes
 	// that the underlying proxy code will immediately exit with an

+ 153 - 0
psiphon/common/marionette/marionette.go

@@ -0,0 +1,153 @@
+// +build MARIONETTE
+
+/*
+ * Copyright (c) 2018, Psiphon Inc.
+ * All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+/*
+
+Package marionette wraps github.com/redjack/marionette with net.Listener and
+net.Conn types that provide a drop-in replacement for net.TCPConn.
+
+Each marionette session has exactly one stream, which is the equivilent of a TCP
+stream.
+
+*/
+package marionette
+
+import (
+	"context"
+	"net"
+
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
+	redjack_marionette "github.com/redjack/marionette"
+	"github.com/redjack/marionette/mar"
+	_ "github.com/redjack/marionette/plugins"
+	"go.uber.org/zap"
+)
+
+func init() {
+	// Override the Logger initialized by redjack_marionette.init()
+	redjack_marionette.Logger = zap.NewNop()
+}
+
+// Enabled indicates if Marionette functionality is enabled.
+func Enabled() bool {
+	return true
+}
+
+// Listener is a net.Listener.
+type Listener struct {
+	net.Listener
+}
+
+// Listen creates a new Marionette Listener. The address input should not
+// include a port number as the port is defined in the Marionette format.
+func Listen(address, format string) (*Listener, error) {
+
+	data, err := mar.ReadFormat(format)
+	if err != nil {
+		return nil, common.ContextError(err)
+	}
+
+	doc, err := mar.Parse(redjack_marionette.PartyServer, data)
+	if err != nil {
+		return nil, common.ContextError(err)
+	}
+
+	listener, err := redjack_marionette.Listen(doc, address)
+	if err != nil {
+		return nil, common.ContextError(err)
+	}
+
+	return &Listener{Listener: listener}, nil
+}
+
+// Dial establishes a new Marionette session and stream to the server
+// specified by address. The address input should not include a port number as
+// that's defined in the Marionette format.
+func Dial(
+	ctx context.Context,
+	netDialer common.NetDialer,
+	format string,
+	address string) (net.Conn, error) {
+
+	data, err := mar.ReadFormat(format)
+	if err != nil {
+		return nil, common.ContextError(err)
+	}
+
+	doc, err := mar.Parse(redjack_marionette.PartyClient, data)
+	if err != nil {
+		return nil, common.ContextError(err)
+	}
+
+	streamSet := redjack_marionette.NewStreamSet()
+
+	dialer := redjack_marionette.NewDialer(doc, address, streamSet)
+
+	dialer.Dialer = netDialer
+
+	err = dialer.Open()
+	if err != nil {
+		streamSet.Close()
+		return nil, common.ContextError(err)
+	}
+
+	// dialer.Dial does not block on network I/O
+	conn, err := dialer.Dial()
+	if err != nil {
+		streamSet.Close()
+		dialer.Close()
+		return nil, common.ContextError(err)
+	}
+
+	return &Conn{
+		Conn:      conn,
+		streamSet: streamSet,
+		dialer:    dialer,
+	}, nil
+}
+
+// Conn is a net.Conn and psiphon/common.Closer.
+type Conn struct {
+	net.Conn
+
+	streamSet *redjack_marionette.StreamSet
+	dialer    *redjack_marionette.Dialer
+}
+
+func (conn *Conn) Close() error {
+	if conn.IsClosed() {
+		return nil
+	}
+	retErr := conn.Conn.Close()
+	err := conn.streamSet.Close()
+	if retErr == nil && err != nil {
+		retErr = err
+	}
+	err = conn.dialer.Close()
+	if retErr == nil && err != nil {
+		retErr = err
+	}
+	return retErr
+}
+
+func (conn *Conn) IsClosed() bool {
+	return conn.dialer.Closed()
+}

+ 55 - 0
psiphon/common/marionette/marionette_disabled.go

@@ -0,0 +1,55 @@
+// +build !MARIONETTE
+
+/*
+ * Copyright (c) 2018, Psiphon Inc.
+ * All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package marionette
+
+import (
+	"context"
+	"errors"
+	"net"
+
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
+)
+
+var disabledError = errors.New("operation is not enabled")
+
+// Enabled indicates if Marionette functionality is enabled.
+func Enabled() bool {
+	return false
+}
+
+// Listener is a net.Listener.
+type Listener struct {
+	net.Listener
+}
+
+// Listen creates a new Marionette Listener. The address input should not
+// include a port number as that's defined in the Marionette format.
+func Listen(_, _ string) (*Listener, error) {
+	return nil, common.ContextError(disabledError)
+}
+
+// Dial establishes a new Marionette session and stream to the server
+// specified by address. The address input should not include a port number as
+// that's defined in the Marionette format.
+func Dial(_ context.Context, _ common.NetDialer, _ string, _ string) (net.Conn, error) {
+	return nil, common.ContextError(disabledError)
+}

+ 187 - 0
psiphon/common/marionette/marionette_test.go

@@ -0,0 +1,187 @@
+/*
+ * Copyright (c) 2018, Psiphon Inc.
+ * All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package marionette
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"net"
+	"net/http"
+	_ "net/http/pprof"
+	"sync/atomic"
+	"testing"
+	"time"
+
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
+	"golang.org/x/sync/errgroup"
+)
+
+func TestMarionette(t *testing.T) {
+
+	go func() {
+		fmt.Println(http.ListenAndServe("localhost:6060", nil))
+	}()
+
+	// Create a number of concurrent Marionette clients, each of which sends
+	// data to the server. The server echoes back the data.
+
+	clients := 5
+	bytesToSend := 1 << 15
+
+	serverReceivedBytes := int64(0)
+	clientReceivedBytes := int64(0)
+
+	serverAddress := "127.0.0.1"
+	format := "http_simple_nonblocking"
+
+	listener, err := Listen(serverAddress, format)
+	if err != nil {
+		t.Fatalf("Listen failed: %s", err)
+	}
+
+	testGroup, testCtx := errgroup.WithContext(context.Background())
+
+	testGroup.Go(func() error {
+
+		var serverGroup errgroup.Group
+
+		for i := 0; i < clients; i++ {
+
+			conn, err := listener.Accept()
+			if err != nil {
+				return common.ContextError(err)
+			}
+
+			serverGroup.Go(func() error {
+				defer func() {
+					fmt.Printf("Start server conn.Close\n")
+					start := time.Now()
+					conn.Close()
+					fmt.Printf("Done server conn.Close: %s\n", time.Now().Sub(start))
+				}()
+				bytesFromClient := 0
+				b := make([]byte, 1024)
+				for bytesFromClient < bytesToSend {
+					n, err := conn.Read(b)
+					bytesFromClient += n
+					atomic.AddInt64(&serverReceivedBytes, int64(n))
+					if err != nil {
+						fmt.Printf("Server read error: %s\n", err)
+						return common.ContextError(err)
+					}
+					_, err = conn.Write(b[:n])
+					if err != nil {
+						fmt.Printf("Server write error: %s\n", err)
+						return common.ContextError(err)
+					}
+				}
+				return nil
+			})
+		}
+
+		err := serverGroup.Wait()
+		if err != nil {
+			return common.ContextError(err)
+		}
+
+		return nil
+	})
+
+	for i := 0; i < clients; i++ {
+
+		testGroup.Go(func() error {
+
+			ctx, cancelFunc := context.WithTimeout(
+				context.Background(), 1*time.Second)
+			defer cancelFunc()
+
+			conn, err := Dial(ctx, &net.Dialer{}, format, serverAddress)
+			if err != nil {
+				return common.ContextError(err)
+			}
+
+			var clientGroup errgroup.Group
+
+			clientGroup.Go(func() error {
+				defer func() {
+					fmt.Printf("Start client conn.Close\n")
+					start := time.Now()
+					conn.Close()
+					fmt.Printf("Done client conn.Close: %s\n", time.Now().Sub(start))
+				}()
+				b := make([]byte, 1024)
+				bytesRead := 0
+				for bytesRead < bytesToSend {
+					n, err := conn.Read(b)
+					bytesRead += n
+					atomic.AddInt64(&clientReceivedBytes, int64(n))
+					if err == io.EOF {
+						break
+					} else if err != nil {
+						fmt.Printf("Client read error: %s\n", err)
+						return common.ContextError(err)
+					}
+				}
+				return nil
+			})
+
+			clientGroup.Go(func() error {
+				b := make([]byte, bytesToSend)
+				_, err := conn.Write(b)
+				if err != nil {
+					fmt.Printf("Client write error: %s\n", err)
+					return common.ContextError(err)
+				}
+				return nil
+			})
+
+			return clientGroup.Wait()
+		})
+
+	}
+
+	go func() {
+		testGroup.Wait()
+	}()
+
+	<-testCtx.Done()
+
+	fmt.Printf("Start listener.Close\n")
+	start := time.Now()
+	listener.Close()
+	fmt.Printf("Done listener.Close: %s\n", time.Now().Sub(start))
+
+	err = testGroup.Wait()
+	if err != nil {
+		t.Errorf("goroutine failed: %s", err)
+	}
+
+	bytes := atomic.LoadInt64(&serverReceivedBytes)
+	expectedBytes := int64(clients * bytesToSend)
+	if bytes != expectedBytes {
+		t.Errorf("unexpected serverReceivedBytes: %d vs. %d", bytes, expectedBytes)
+	}
+
+	bytes = atomic.LoadInt64(&clientReceivedBytes)
+	if bytes != expectedBytes {
+		t.Errorf("unexpected clientReceivedBytes: %d vs. %d", bytes, expectedBytes)
+	}
+}

+ 50 - 38
psiphon/common/net.go

@@ -21,6 +21,7 @@ package common
 
 import (
 	"container/list"
+	"context"
 	"net"
 	"net/http"
 	"sync"
@@ -30,6 +31,50 @@ import (
 	"github.com/Psiphon-Labs/goarista/monotime"
 )
 
+// NetDialer mimicks the net.Dialer interface.
+type NetDialer interface {
+	Dial(network, address string) (net.Conn, error)
+	DialContext(ctx context.Context, network, address string) (net.Conn, error)
+}
+
+// Closer defines the interface to a type, typically
+// a net.Conn, that can be closed.
+type Closer interface {
+	IsClosed() bool
+}
+
+// TerminateHTTPConnection sends a 404 response to a client and also closes
+// the persistent connection.
+func TerminateHTTPConnection(
+	responseWriter http.ResponseWriter, request *http.Request) {
+
+	http.NotFound(responseWriter, request)
+
+	hijack, ok := responseWriter.(http.Hijacker)
+	if !ok {
+		return
+	}
+	conn, buffer, err := hijack.Hijack()
+	if err != nil {
+		return
+	}
+	buffer.Flush()
+	conn.Close()
+}
+
+// IPAddressFromAddr is a helper which extracts an IP address
+// from a net.Addr or returns "" if there is no IP address.
+func IPAddressFromAddr(addr net.Addr) string {
+	ipAddress := ""
+	if addr != nil {
+		host, _, err := net.SplitHostPort(addr.String())
+		if err == nil {
+			ipAddress = host
+		}
+	}
+	return ipAddress
+}
+
 // Conns is a synchronized list of Conns that is used to coordinate
 // interrupting a set of goroutines establishing connections, or
 // close a set of open connections, etc.
@@ -41,6 +86,11 @@ type Conns struct {
 	conns    map[net.Conn]bool
 }
 
+// NewConns initializes a new Conns.
+func NewConns() *Conns {
+	return &Conns{}
+}
+
 func (conns *Conns) Reset() {
 	conns.mutex.Lock()
 	defer conns.mutex.Unlock()
@@ -77,38 +127,6 @@ func (conns *Conns) CloseAll() {
 	conns.conns = make(map[net.Conn]bool)
 }
 
-// TerminateHTTPConnection sends a 404 response to a client and also closes
-// the persistent connection.
-func TerminateHTTPConnection(
-	responseWriter http.ResponseWriter, request *http.Request) {
-
-	http.NotFound(responseWriter, request)
-
-	hijack, ok := responseWriter.(http.Hijacker)
-	if !ok {
-		return
-	}
-	conn, buffer, err := hijack.Hijack()
-	if err != nil {
-		return
-	}
-	buffer.Flush()
-	conn.Close()
-}
-
-// IPAddressFromAddr is a helper which extracts an IP address
-// from a net.Addr or returns "" if there is no IP address.
-func IPAddressFromAddr(addr net.Addr) string {
-	ipAddress := ""
-	if addr != nil {
-		host, _, err := net.SplitHostPort(addr.String())
-		if err == nil {
-			ipAddress = host
-		}
-	}
-	return ipAddress
-}
-
 // LRUConns is a concurrency-safe list of net.Conns ordered
 // by recent activity. Its purpose is to facilitate closing
 // the oldest connection in a set of connections.
@@ -194,12 +212,6 @@ func (entry *LRUConnsEntry) Touch() {
 	entry.lruConns.list.MoveToFront(entry.element)
 }
 
-// Closer defines the interface to a type, typically
-// a net.Conn, that can be closed.
-type Closer interface {
-	IsClosed() bool
-}
-
 // ActivityMonitoredConn wraps a net.Conn, adding logic to deal with
 // events triggered by I/O activity.
 //

+ 18 - 1
psiphon/common/protocol/protocol.go

@@ -36,6 +36,8 @@ const (
 	TUNNEL_PROTOCOL_FRONTED_MEEK                  = "FRONTED-MEEK-OSSH"
 	TUNNEL_PROTOCOL_FRONTED_MEEK_HTTP             = "FRONTED-MEEK-HTTP-OSSH"
 	TUNNEL_PROTOCOL_QUIC_OBFUSCATED_SSH           = "QUIC-OSSH"
+	TUNNEL_PROTOCOL_MARIONETTE_OBFUSCATED_SSH     = "MARIONETTE-OSSH"
+	TUNNEL_PROTOCOL_TAPDANCE_OBFUSCATED_SSH       = "TAPDANCE-OSSH"
 
 	SERVER_ENTRY_SOURCE_EMBEDDED   = "EMBEDDED"
 	SERVER_ENTRY_SOURCE_REMOTE     = "REMOTE"
@@ -96,10 +98,14 @@ var SupportedTunnelProtocols = TunnelProtocols{
 	TUNNEL_PROTOCOL_FRONTED_MEEK,
 	TUNNEL_PROTOCOL_FRONTED_MEEK_HTTP,
 	TUNNEL_PROTOCOL_QUIC_OBFUSCATED_SSH,
+	TUNNEL_PROTOCOL_MARIONETTE_OBFUSCATED_SSH,
+	TUNNEL_PROTOCOL_TAPDANCE_OBFUSCATED_SSH,
 }
 
 var DefaultDisabledTunnelProtocols = TunnelProtocols{
 	TUNNEL_PROTOCOL_QUIC_OBFUSCATED_SSH,
+	TUNNEL_PROTOCOL_MARIONETTE_OBFUSCATED_SSH,
+	TUNNEL_PROTOCOL_TAPDANCE_OBFUSCATED_SSH,
 }
 
 var SupportedServerEntrySources = TunnelProtocols{
@@ -142,8 +148,19 @@ func TunnelProtocolUsesQUIC(protocol string) bool {
 	return protocol == TUNNEL_PROTOCOL_QUIC_OBFUSCATED_SSH
 }
 
+func TunnelProtocolUsesMarionette(protocol string) bool {
+	return protocol == TUNNEL_PROTOCOL_MARIONETTE_OBFUSCATED_SSH
+}
+
+func TunnelProtocolUsesTapdance(protocol string) bool {
+	return protocol == TUNNEL_PROTOCOL_TAPDANCE_OBFUSCATED_SSH
+}
+
 func TunnelProtocolIsResourceIntensive(protocol string) bool {
-	return TunnelProtocolUsesMeek(protocol) || TunnelProtocolUsesQUIC(protocol)
+	return TunnelProtocolUsesMeek(protocol) ||
+		TunnelProtocolUsesQUIC(protocol) ||
+		TunnelProtocolUsesMarionette(protocol) ||
+		TunnelProtocolUsesTapdance(protocol)
 }
 
 func UseClientTunnelProtocol(

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

@@ -62,6 +62,7 @@ type ServerEntry struct {
 	MeekFrontingDisableSNI        bool     `json:"meekFrontingDisableSNI"`
 	TacticsRequestPublicKey       string   `json:"tacticsRequestPublicKey"`
 	TacticsRequestObfuscatedKey   string   `json:"tacticsRequestObfuscatedKey"`
+	MarionetteFormat              string   `json:"marionetteFormat"`
 	ConfigurationVersion          int      `json:"configurationVersion"`
 
 	// These local fields are not expected to be present in downloaded server
@@ -145,6 +146,8 @@ func (serverEntry *ServerEntry) GetSupportedProtocols(
 
 	for _, protocol := range SupportedTunnelProtocols {
 
+		// TODO: Marionette UDP formats are incompatible with
+		// useUpstreamProxy, but not currently supported
 		if useUpstreamProxy && TunnelProtocolUsesQUIC(protocol) {
 			continue
 		}

+ 1 - 1
psiphon/common/quic/quic_test.go

@@ -41,7 +41,7 @@ func TestQUIC(t *testing.T) {
 
 	listener, err := Listen("127.0.0.1:0")
 	if err != nil {
-		t.Errorf("Listen failed: %s", err)
+		t.Fatalf("Listen failed: %s", err)
 	}
 
 	serverAddress := listener.Addr().String()

+ 314 - 0
psiphon/common/tapdance/tapdance.go

@@ -0,0 +1,314 @@
+// +build TAPDANCE
+
+/*
+ * Copyright (c) 2018, Psiphon Inc.
+ * All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+/*
+
+Package tapdance wraps github.com/sergeyfrolov/gotapdance with net.Listener
+and net.Conn types that provide drop-in integration with Psiphon.
+
+*/
+package tapdance
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"net"
+	"os"
+	"path/filepath"
+	"sync"
+	"sync/atomic"
+	"time"
+
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
+	"github.com/armon/go-proxyproto"
+	refraction_networking_tapdance "github.com/sergeyfrolov/gotapdance/tapdance"
+)
+
+const (
+	READ_PROXY_PROTOCOL_HEADER_TIMEOUT = 5 * time.Second
+	REDIAL_TCP_TIMEOUT_MIN             = 10 * time.Second
+	REDIAL_TCP_TIMEOUT_MAX             = 15 * time.Second
+)
+
+func init() {
+	refraction_networking_tapdance.Logger().Out = ioutil.Discard
+	refraction_networking_tapdance.EnableProxyProtocol()
+}
+
+// Enabled indicates if Tapdance functionality is enabled.
+func Enabled() bool {
+	return true
+}
+
+// Listener is a net.Listener.
+type Listener struct {
+	net.Listener
+}
+
+// Listen creates a new Tapdance 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
+// SetReadDeadline and performs a Read.
+func Listen(address string) (*Listener, error) {
+
+	listener, err := net.Listen("tcp", address)
+	if err != nil {
+		return nil, common.ContextError(err)
+	}
+
+	// Setting a timeout ensures that reading the proxy protocol
+	// header completes or times out and RemoteAddr will not block. See:
+	// https://godoc.org/github.com/armon/go-proxyproto#Conn.RemoteAddr
+
+	listener = &proxyproto.Listener{
+		Listener:           listener,
+		ProxyHeaderTimeout: READ_PROXY_PROTOCOL_HEADER_TIMEOUT}
+
+	return &Listener{Listener: listener}, nil
+}
+
+// dialManager tracks all dials performed by and dialed conns used by a
+// refraction_networking_tapdance client. dialManager.close interrupts/closes
+// all pending dials and established conns immediately. This ensures that
+// blocking calls within refraction_networking_tapdance, such as tls.Handhake,
+// are interrupted:
+// E.g., https://github.com/sergeyfrolov/gotapdance/blob/4581c3f01ac46b90ed4b58cce9c0438f732bf915/tapdance/conn_raw.go#L274
+type dialManager struct {
+	tcpDialer func(ctx context.Context, network, address string) (net.Conn, error)
+
+	ctxMutex       sync.Mutex
+	useRunCtx      bool
+	initialDialCtx context.Context
+	runCtx         context.Context
+	stopRunning    context.CancelFunc
+
+	conns *common.Conns
+}
+
+func newDialManager(
+	tcpDialer func(ctx context.Context, network, address string) (net.Conn, error),
+	initialDialCtx context.Context) *dialManager {
+
+	runCtx, stopRunning := context.WithCancel(context.Background())
+
+	return &dialManager{
+		tcpDialer:      tcpDialer,
+		initialDialCtx: initialDialCtx,
+		runCtx:         runCtx,
+		stopRunning:    stopRunning,
+		conns:          common.NewConns(),
+	}
+}
+
+func (manager *dialManager) dial(network, address string) (net.Conn, error) {
+
+	if network != "tcp" {
+		return nil, common.ContextError(fmt.Errorf("unsupported network: %s", network))
+	}
+
+	// The context for this dial is either:
+	// - manager.initialDialCtx during the initial tapdance.Dial, in which case
+	//   this is Psiphon tunnel establishment, which has an externally specified
+	//   timeout.
+	// - manager.runCtx after the initial tapdance.Dial completes, in which case
+	//   this is a Tapdance protocol reconnection that occurs periodically for
+	//   already established tunnels; this uses an internal timeout.
+
+	manager.ctxMutex.Lock()
+	var ctx context.Context
+	var cancelFunc context.CancelFunc
+	if manager.useRunCtx {
+		// Random timeout replicates tapdance client behavior with stock dialer:
+		// https://github.com/sergeyfrolov/gotapdance/blob/4581c3f01ac46b90ed4b58cce9c0438f732bf915/tapdance/conn_raw.go#L246
+		timeout, err := common.MakeSecureRandomPeriod(REDIAL_TCP_TIMEOUT_MIN, REDIAL_TCP_TIMEOUT_MAX)
+		if err != nil {
+			manager.ctxMutex.Unlock()
+			return nil, common.ContextError(err)
+		}
+		ctx, cancelFunc = context.WithTimeout(manager.runCtx, timeout)
+	} else {
+		ctx = manager.initialDialCtx
+	}
+	manager.ctxMutex.Unlock()
+
+	conn, err := manager.tcpDialer(ctx, network, address)
+	if err != nil {
+		return nil, common.ContextError(err)
+	}
+
+	if cancelFunc != nil {
+		cancelFunc()
+	}
+
+	conn = &managedConn{
+		Conn:    conn,
+		manager: manager,
+	}
+
+	if !manager.conns.Add(conn) {
+		conn.Close()
+		return nil, common.ContextError(errors.New("already closed"))
+	}
+
+	return conn, nil
+}
+
+func (manager *dialManager) startUsingRunCtx() {
+	manager.ctxMutex.Lock()
+	manager.initialDialCtx = nil
+	manager.useRunCtx = true
+	manager.ctxMutex.Unlock()
+}
+
+func (manager *dialManager) close() {
+	manager.conns.CloseAll()
+	manager.stopRunning()
+}
+
+type managedConn struct {
+	net.Conn
+	manager *dialManager
+}
+
+type closeWriter interface {
+	CloseWrite() error
+}
+
+// CloseWrite exposes the net.TCPConn.CloseWrite() functionality
+// required by tapdance.
+func (conn *managedConn) CloseWrite() error {
+	if closeWriter, ok := conn.Conn.(closeWriter); ok {
+		return closeWriter.CloseWrite()
+	}
+	return common.ContextError(errors.New("dialedConn is not a closeWriter"))
+}
+
+func (conn *managedConn) Close() error {
+	// Remove must be invoked asynchronously, as this Close may be called by
+	// conns.CloseAll, leading to a reentrant lock situation.
+	go conn.manager.conns.Remove(conn)
+	return conn.Conn.Close()
+}
+
+type tapdanceConn struct {
+	net.Conn
+	manager  *dialManager
+	isClosed int32
+}
+
+func (conn *tapdanceConn) Close() error {
+	conn.manager.close()
+	err := conn.Conn.Close()
+	atomic.StoreInt32(&conn.isClosed, 1)
+	return err
+}
+
+func (conn *tapdanceConn) 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.
+func Dial(
+	ctx context.Context,
+	dataDirectory string,
+	netDialer common.NetDialer,
+	address string) (net.Conn, error) {
+
+	err := initAssets(dataDirectory)
+	if err != nil {
+		return nil, common.ContextError(err)
+	}
+
+	if _, ok := ctx.Deadline(); !ok {
+		return nil, common.ContextError(errors.New("dial context has no timeout"))
+	}
+
+	manager := newDialManager(netDialer.DialContext, ctx)
+
+	tapdanceDialer := &refraction_networking_tapdance.Dialer{
+		TcpDialer: manager.dial,
+	}
+
+	conn, err := tapdanceDialer.Dial("tcp", address)
+	if err != nil {
+		manager.close()
+		return nil, common.ContextError(err)
+	}
+
+	manager.startUsingRunCtx()
+
+	return &tapdanceConn{
+		Conn:    conn,
+		manager: manager,
+	}, nil
+}
+
+var setAssetsOnce sync.Once
+
+func initAssets(dataDirectory string) error {
+
+	var initErr error
+	setAssetsOnce.Do(func() {
+
+		assetsDir := filepath.Join(dataDirectory, "tapdance")
+
+		err := os.MkdirAll(assetsDir, 0600)
+		if err != nil {
+			initErr = common.ContextError(err)
+			return
+		}
+
+		clientConfFileName := filepath.Join(assetsDir, "ClientConf")
+		if _, err = os.Stat(clientConfFileName); os.IsNotExist(err) {
+
+			// Default ClientConf from:
+			// https://github.com/sergeyfrolov/gotapdance/blob/089794326cf0b8a5d0e1f3cbb703ff3ee289f0ed/assets/ClientConf
+			clientConf := []byte{
+				10, 33, 10, 31, 10, 24, 116, 97, 112, 100, 97, 110, 99, 101, 49, 46,
+				102, 114, 101, 101, 97, 101, 115, 107, 101, 121, 46, 120, 121, 122,
+				21, 104, 190, 122, 192, 16, 148, 145, 6, 26, 36, 10, 32, 81, 88, 104,
+				190, 127, 69, 171, 111, 49, 10, 254, 212, 178, 41, 183, 164, 121, 252,
+				159, 222, 85, 61, 234, 76, 205, 179, 105, 171, 24, 153, 231, 12, 16, 90}
+
+			err = ioutil.WriteFile(clientConfFileName, clientConf, 0644)
+			if err != nil {
+				initErr = common.ContextError(err)
+				return
+			}
+		}
+
+		refraction_networking_tapdance.AssetsSetDir(assetsDir)
+	})
+
+	return initErr
+}

+ 52 - 0
psiphon/common/tapdance/tapdance_disabled.go

@@ -0,0 +1,52 @@
+// +build !TAPDANCE
+
+/*
+ * Copyright (c) 2018, Psiphon Inc.
+ * All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package tapdance
+
+import (
+	"context"
+	"errors"
+	"net"
+
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
+)
+
+var disabledError = errors.New("operation is not enabled")
+
+// Enabled indicates if Tapdance functionality is enabled.
+func Enabled() bool {
+	return false
+}
+
+// Listener is a net.Listener.
+type Listener struct {
+	net.Listener
+}
+
+// Listen creates a new Tapdance listener.
+func Listen(_ string) (*Listener, error) {
+	return nil, common.ContextError(disabledError)
+}
+
+// Dial establishes a new Tapdance session to a Tapdance station.
+func Dial(_ context.Context, _ string, _ common.NetDialer, _ string) (net.Conn, error) {
+	return nil, common.ContextError(disabledError)
+}

+ 2 - 2
psiphon/common/tun/tun_test.go

@@ -349,7 +349,7 @@ func startTestServer(
 		metricsUpdater: metricsUpdater,
 		tunServer:      tunServer,
 		unixListener:   unixListener,
-		clientConns:    new(common.Conns),
+		clientConns:    common.NewConns(),
 		workers:        new(sync.WaitGroup),
 	}
 
@@ -549,7 +549,7 @@ func startTestTCPServer(useIPv6 bool) (*testTCPServer, error) {
 	server := &testTCPServer{
 		listenerIPAddress: hostIPaddress,
 		tcpListener:       tcpListener,
-		clientConns:       new(common.Conns),
+		clientConns:       common.NewConns(),
 		workers:           new(sync.WaitGroup),
 	}
 

+ 5 - 4
psiphon/config.go

@@ -132,10 +132,11 @@ type Config struct {
 	TunnelProtocol string
 
 	// 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".
-	//
+	// 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".
 	// For the default, an empty list, all protocols are used.
 	LimitTunnelProtocols []string
 

+ 1 - 1
psiphon/httpProxy.go

@@ -172,7 +172,7 @@ func NewHttpProxy(
 		urlProxyDirectRelay:    urlProxyDirectRelay,
 		urlProxyDirectClient:   urlProxyDirectClient,
 		responseHeaderTimeout:  responseHeaderTimeout,
-		openConns:              new(common.Conns),
+		openConns:              common.NewConns(),
 		stopListeningBroadcast: make(chan struct{}),
 		listenIP:               proxyIP,
 		listenPort:             proxyPort,

+ 26 - 0
psiphon/net.go

@@ -143,6 +143,32 @@ type NetworkIDGetter interface {
 // 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
+}
+
+// NewNetDialer creates a new NetDialer.
+func NewNetDialer(config *DialConfig) *NetDialer {
+	return &NetDialer{
+		dialTCP: NewTCPDialer(config),
+	}
+}
+
+func (d *NetDialer) Dial(network, address string) (net.Conn, error) {
+	return d.DialContext(context.Background(), network, address)
+}
+
+func (d *NetDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
+	switch network {
+	case "tcp":
+		return d.dialTCP(context.Background(), "tcp", address)
+	default:
+		return nil, common.ContextError(fmt.Errorf("unsupported network: %s", network))
+	}
+}
+
 // LocalProxyRelay sends to remoteConn bytes received from localConn,
 // and sends to localConn bytes received from remoteConn.
 func LocalProxyRelay(proxyType string, localConn, remoteConn net.Conn) {

+ 24 - 4
psiphon/server/config.go

@@ -127,9 +127,14 @@ type Config struct {
 
 	// TunnelProtocolPorts specifies which tunnel protocols to run
 	// and which ports to listen on for each protocol. Valid tunnel
-	// protocols include: "SSH", "OSSH", "UNFRONTED-MEEK-OSSH",
-	// "UNFRONTED-MEEK-HTTPS-OSSH", "UNFRONTED-MEEK-SESSION-TICKET-OSSH",
-	// "FRONTED-MEEK-OSSH", "FRONTED-MEEK-HTTP-OSSH".
+	// protocols 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".
+	//
+	// In the case of "MARIONETTE-OSSH" the port value is ignored and must be
+	// set to 0. The port value specified in the Marionette format is used.
 	TunnelProtocolPorts map[string]int
 
 	// SSHPrivateKey is the SSH host key. The same key is used for
@@ -305,6 +310,11 @@ type Config struct {
 	// TacticsConfigFilename is the path of a file containing a JSON-encoded
 	// tactics server configuration.
 	TacticsConfigFilename string
+
+	// MarionetteFormat specifies a Marionette format to use with the
+	// MARIONETTE-OSSH tunnel protocol. The format specifies the network
+	// protocol port to listen on.
+	MarionetteFormat string
 }
 
 // RunWebServer indicates whether to run a web server component.
@@ -360,7 +370,7 @@ func LoadConfig(configJSON []byte) (*Config, error) {
 		}
 	}
 
-	for tunnelProtocol := range config.TunnelProtocolPorts {
+	for tunnelProtocol, port := range config.TunnelProtocolPorts {
 		if !common.Contains(protocol.SupportedTunnelProtocols, tunnelProtocol) {
 			return nil, fmt.Errorf("Unsupported tunnel protocol: %s", tunnelProtocol)
 		}
@@ -388,6 +398,13 @@ func LoadConfig(configJSON []byte) (*Config, error) {
 					tunnelProtocol)
 			}
 		}
+		if protocol.TunnelProtocolUsesMarionette(tunnelProtocol) {
+			if port != 0 {
+				return nil, fmt.Errorf(
+					"Tunnel protocol %s port is specified in format, not TunnelProtocolPorts",
+					tunnelProtocol)
+			}
+		}
 	}
 
 	if config.UDPInterceptUdpgwServerAddress != "" {
@@ -439,6 +456,7 @@ type GenerateConfigParams struct {
 	WebServerPort               int
 	EnableSSHAPIRequests        bool
 	TunnelProtocolPorts         map[string]int
+	MarionetteFormat            string
 	TrafficRulesConfigFilename  string
 	OSLConfigFilename           string
 	TacticsConfigFilename       string
@@ -630,6 +648,7 @@ func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, []byte, []byt
 		TrafficRulesFilename:           params.TrafficRulesConfigFilename,
 		OSLConfigFilename:              params.OSLConfigFilename,
 		TacticsConfigFilename:          params.TacticsConfigFilename,
+		MarionetteFormat:               params.MarionetteFormat,
 	}
 
 	encodedConfig, err := json.MarshalIndent(config, "\n", "    ")
@@ -784,6 +803,7 @@ func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, []byte, []byt
 		MeekFrontingDisableSNI:        false,
 		TacticsRequestPublicKey:       tacticsRequestPublicKey,
 		TacticsRequestObfuscatedKey:   tacticsRequestObfuscatedKey,
+		MarionetteFormat:              params.MarionetteFormat,
 		ConfigurationVersion:          1,
 	}
 

+ 1 - 1
psiphon/server/meek.go

@@ -136,7 +136,7 @@ func NewMeekServer(
 		support:           support,
 		listener:          listener,
 		clientHandler:     clientHandler,
-		openConns:         new(common.Conns),
+		openConns:         common.NewConns(),
 		stopBroadcast:     stopBroadcast,
 		sessions:          make(map[string]*meekSession),
 		checksumTable:     checksumTable,

+ 27 - 2
psiphon/server/server_test.go

@@ -40,6 +40,7 @@ import (
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/accesscontrol"
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/marionette"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/tactics"
@@ -67,7 +68,7 @@ func TestMain(m *testing.M) {
 		}
 	}
 	if err != nil {
-		fmt.Printf("error getting server IP address: %s", err)
+		fmt.Printf("error getting server IP address: %s\n", err)
 		os.Exit(1)
 	}
 
@@ -208,6 +209,24 @@ func TestQUICOSSH(t *testing.T) {
 		})
 }
 
+func TestMarionetteOSSH(t *testing.T) {
+	if !marionette.Enabled() {
+		t.Skip("Marionette is not enabled")
+	}
+	runServer(t,
+		&runServerConfig{
+			tunnelProtocol:       "MARIONETTE-OSSH",
+			enableSSHAPIRequests: true,
+			doHotReload:          false,
+			doDefaultSponsorID:   false,
+			denyTrafficRules:     false,
+			requireAuthorization: true,
+			omitAuthorization:    false,
+			doTunneledWebRequest: true,
+			doTunneledNTPRequest: true,
+		})
+}
+
 func TestWebTransportAPIRequests(t *testing.T) {
 	runServer(t,
 		&runServerConfig{
@@ -398,7 +417,8 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 	// create a server
 
 	psiphonServerIPAddress := serverIPAddress
-	if protocol.TunnelProtocolUsesQUIC(runConfig.tunnelProtocol) {
+	if protocol.TunnelProtocolUsesQUIC(runConfig.tunnelProtocol) ||
+		protocol.TunnelProtocolUsesMarionette(runConfig.tunnelProtocol) {
 		// Workaround for macOS firewall.
 		psiphonServerIPAddress = "127.0.0.1"
 	}
@@ -410,6 +430,11 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 		TunnelProtocolPorts:  map[string]int{runConfig.tunnelProtocol: 4000},
 	}
 
+	if protocol.TunnelProtocolUsesMarionette(runConfig.tunnelProtocol) {
+		generateConfigParams.TunnelProtocolPorts[runConfig.tunnelProtocol] = 0
+		generateConfigParams.MarionetteFormat = "http_simple_nonblocking"
+	}
+
 	if doTactics {
 		generateConfigParams.TacticsRequestPublicKey = tacticsRequestPublicKey
 		generateConfigParams.TacticsRequestObfuscatedKey = tacticsRequestObfuscatedKey

+ 18 - 1
psiphon/server/tunnelServer.go

@@ -38,11 +38,13 @@ import (
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/accesscontrol"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/crypto/ssh"
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/marionette"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/obfuscator"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/osl"
 	"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/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"
@@ -139,10 +141,23 @@ func (server *TunnelServer) Run() error {
 
 		var listener net.Listener
 		var err error
+
 		if protocol.TunnelProtocolUsesQUIC(tunnelProtocol) {
+
 			listener, err = quic.Listen(localAddress)
 
+		} else if protocol.TunnelProtocolUsesMarionette(tunnelProtocol) {
+
+			listener, err = marionette.Listen(
+				support.Config.ServerIPAddress,
+				support.Config.MarionetteFormat)
+
+		} else if protocol.TunnelProtocolUsesTapdance(tunnelProtocol) {
+
+			listener, err = tapdance.Listen(localAddress)
+
 		} else {
+
 			listener, err = net.Listen("tcp", localAddress)
 		}
 
@@ -803,6 +818,9 @@ func (sshServer *sshServer) stopClients() {
 
 func (sshServer *sshServer) handleClient(tunnelProtocol string, clientConn net.Conn) {
 
+	// Calling clientConn.RemoteAddr at this point, before any Read calls,
+	// satisfies the constraint documented in tapdance.Listen.
+
 	geoIPData := sshServer.support.GeoIPService.Lookup(
 		common.IPAddressFromAddr(clientConn.RemoteAddr()))
 
@@ -1289,7 +1307,6 @@ func (sshClient *sshClient) authLogCallback(conn ssh.ConnMetadata, method string
 // the connection has terminated but sshClient.run() may still be
 // running and in the process of exiting.
 func (sshClient *sshClient) stop() {
-
 	sshClient.sshConn.Close()
 	sshClient.sshConn.Wait()
 }

+ 1 - 1
psiphon/socksProxy.go

@@ -63,7 +63,7 @@ func NewSocksProxy(
 		tunneler:               tunneler,
 		listener:               listener,
 		serveWaitGroup:         new(sync.WaitGroup),
-		openConns:              new(common.Conns),
+		openConns:              common.NewConns(),
 		stopListeningBroadcast: make(chan struct{}),
 	}
 	proxy.serveWaitGroup.Add(1)

+ 33 - 3
psiphon/tunnel.go

@@ -36,11 +36,13 @@ import (
 	"github.com/Psiphon-Labs/goarista/monotime"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/crypto/ssh"
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/marionette"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/obfuscator"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
 	"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/tactics"
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/tapdance"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/transferstats"
 	regen "github.com/zach-klippenstein/goregen"
 )
@@ -160,7 +162,8 @@ func ConnectTunnel(
 	adjustedEstablishStartTime monotime.Time) (*Tunnel, error) {
 
 	if !serverEntry.SupportsProtocol(selectedProtocol) {
-		return nil, common.ContextError(fmt.Errorf("server does not support selected protocol"))
+		return nil, common.ContextError(
+			fmt.Errorf("server does not support tunnel protocol: %s", selectedProtocol))
 	}
 
 	// Build transport layers and establish SSH connection. Note that
@@ -648,7 +651,8 @@ func initMeekConfig(
 		}
 
 	default:
-		return nil, common.ContextError(errors.New("unexpected selectedProtocol"))
+		return nil, common.ContextError(
+			fmt.Errorf("unknown tunnel protocol: %s", selectedProtocol))
 	}
 
 	if config.clientParameters.Get().Bool(parameters.MeekDialDomainsOnly) {
@@ -829,7 +833,7 @@ func dialSsh(
 	var err error
 
 	switch selectedProtocol {
-	case protocol.TUNNEL_PROTOCOL_OBFUSCATED_SSH:
+	case protocol.TUNNEL_PROTOCOL_OBFUSCATED_SSH, protocol.TUNNEL_PROTOCOL_TAPDANCE_OBFUSCATED_SSH:
 		useObfuscatedSsh = true
 		directDialAddress = fmt.Sprintf("%s:%d", serverEntry.IpAddress, serverEntry.SshObfuscatedPort)
 
@@ -838,6 +842,10 @@ func dialSsh(
 		directDialAddress = fmt.Sprintf("%s:%d", serverEntry.IpAddress, serverEntry.SshObfuscatedQUICPort)
 		quicDialSNIAddress = fmt.Sprintf("%s:%d", common.GenerateHostName(), serverEntry.SshObfuscatedQUICPort)
 
+	case protocol.TUNNEL_PROTOCOL_MARIONETTE_OBFUSCATED_SSH:
+		useObfuscatedSsh = true
+		directDialAddress = serverEntry.IpAddress
+
 	case protocol.TUNNEL_PROTOCOL_SSH:
 		selectedSSHClientVersion = true
 		SSHClientVersion = pickSSHClientVersion()
@@ -901,6 +909,28 @@ func dialSsh(
 			return nil, common.ContextError(err)
 		}
 
+	} else if protocol.TunnelProtocolUsesMarionette(selectedProtocol) {
+
+		dialConn, err = marionette.Dial(
+			ctx,
+			NewNetDialer(dialConfig),
+			serverEntry.MarionetteFormat,
+			directDialAddress)
+		if err != nil {
+			return nil, common.ContextError(err)
+		}
+
+	} else if protocol.TunnelProtocolUsesTapdance(selectedProtocol) {
+
+		dialConn, err = tapdance.Dial(
+			ctx,
+			config.DataStoreDirectory,
+			NewNetDialer(dialConfig),
+			directDialAddress)
+		if err != nil {
+			return nil, common.ContextError(err)
+		}
+
 	} else {
 
 		dialConn, err = DialTCPFragmentor(