Browse Source

added support for bind-to-device, via a remote service, for VPN routing exclusion (android/linux only); associated refactoring of dialers and conns

Rod Hynes 11 years ago
parent
commit
059485549c

+ 104 - 0
psiphon/LookupIP.go

@@ -0,0 +1,104 @@
+// +build android linux
+
+/*
+ * Copyright (c) 2014, 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 psiphon
+
+import (
+	"errors"
+	dns "github.com/miekg/dns"
+	"net"
+	"os"
+	"syscall"
+	"time"
+)
+
+const DNS_PORT = 53
+
+// LookupIP resolves a hostname. When BindToDevice is not required, it
+// simply uses net.LookuIP.
+// When BindToDevice is required, LookupIP explicitly creates a UDP
+// socket, binds it to the device, and makes an explicit DNS request
+// to the specified DNS resolver.
+func LookupIP(host string, config *DialConfig) (addrs []net.IP, err error) {
+	if config.BindToDeviceServiceAddr != "" {
+		return bindLookupIP(host, config)
+	}
+	return net.LookupIP(host)
+}
+
+// bindLookupIP implements the BindToDevice LookupIP case.
+// To implement socket device binding, the lower-level syscall APIs are used.
+// The sequence of syscalls in this implementation are taken from:
+// https://code.google.com/p/go/issues/detail?id=6966
+func bindLookupIP(host string, config *DialConfig) (addrs []net.IP, err error) {
+	socketFd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, 0)
+	if err != nil {
+		return nil, ContextError(err)
+	}
+	defer syscall.Close(socketFd)
+	err = bindToDevice(socketFd, config)
+	if err != nil {
+		return nil, ContextError(err)
+	}
+	// config.BindToDeviceDnsServer must be an IP address
+	ipAddr := net.ParseIP(config.BindToDeviceDnsServer)
+	if ipAddr == nil {
+		return nil, ContextError(errors.New("invalid IP address"))
+	}
+	// TODO: IPv6 support
+	var ip [4]byte
+	copy(ip[:], ipAddr.To4())
+	sockAddr := syscall.SockaddrInet4{Addr: ip, Port: DNS_PORT}
+	// Note: no timeout or interrupt for this connect, as it's a datagram socket
+	err = syscall.Connect(socketFd, &sockAddr)
+	if err != nil {
+		return nil, ContextError(err)
+	}
+	// Convert the syscall socket to a net.Conn, for use in the dns package
+	file := os.NewFile(uintptr(socketFd), "")
+	defer file.Close()
+	conn, err := net.FileConn(file)
+	if err != nil {
+		return nil, ContextError(err)
+	}
+	// Set DNS query timeouts, using the ConnectTimeout from the overall Dial
+	conn.SetReadDeadline(time.Now().Add(config.ConnectTimeout))
+	conn.SetWriteDeadline(time.Now().Add(config.ConnectTimeout))
+	// Make the DNS query
+	// TODO: make interruptible?
+	dnsConn := &dns.Conn{Conn: conn}
+	defer dnsConn.Close()
+	query := new(dns.Msg)
+	query.SetQuestion(dns.Fqdn(host), dns.TypeA)
+	query.RecursionDesired = true
+	dnsConn.WriteMsg(query)
+	response, err := dnsConn.ReadMsg()
+	if err != nil {
+		return nil, ContextError(err)
+	}
+	addrs = make([]net.IP, 0)
+	for _, answer := range response.Answer {
+		if a, ok := answer.(*dns.A); ok {
+			addrs = append(addrs, a.A)
+		}
+	}
+	return addrs, nil
+}

+ 35 - 0
psiphon/LookupIP_nobind.go

@@ -0,0 +1,35 @@
+// +build !android !linux
+
+/*
+ * Copyright (c) 2014, 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 psiphon
+
+import (
+	"net"
+)
+
+// LookupIP resolves a hostname. When BindToDevice is not required, it
+// simply uses net.LookuIP.
+func LookupIP(host string, config *DialConfig) (addrs []net.IP, err error) {
+	if config.BindToDeviceServiceAddr != "" {
+		Fatal("LookupIP with bind not supported on this platform")
+	}
+	return net.LookupIP(host)
+}

+ 25 - 34
psiphon/directConn.go → psiphon/TCPConn.go

@@ -26,48 +26,36 @@ import (
 	"time"
 )
 
-// DirectConn is a customized network connection that:
+// TCPConn is a customized TCP connection that:
 // - can be interrupted while connecting;
 // - implements idle read/write timeouts;
 // - can be bound to a specific system device (for Android VpnService
 //   routing compatibility, for example).
 // - implements the psiphon.Conn interface
-type DirectConn struct {
+type TCPConn struct {
 	net.Conn
 	mutex         sync.Mutex
 	isClosed      bool
 	closedSignal  chan struct{}
-	interruptible interruptibleConn
+	interruptible interruptibleTCPSocket
 	readTimeout   time.Duration
 	writeTimeout  time.Duration
 }
 
-// NewDirectDialer creates a DirectDialer.
-func NewDirectDialer(
-	connectTimeout, readTimeout, writeTimeout time.Duration,
-	pendingConns *Conns) Dialer {
-
+// NewTCPDialer creates a TCPDialer.
+func NewTCPDialer(config *DialConfig) Dialer {
 	return func(network, addr string) (net.Conn, error) {
 		if network != "tcp" {
-			Fatal("unsupported network type in NewDirectDialer")
+			Fatal("unsupported network type in NewTCPDialer")
 		}
-		return DirectDial(
-			addr,
-			connectTimeout, readTimeout, writeTimeout,
-			pendingConns)
+		return DialTCP(addr, config)
 	}
 }
 
-// DirectDial creates a new, connected DirectConn. The connection may be
-// interrupted using pendingConns.CloseAll(): on platforms that support this,
-// the new DirectConn is added to pendingConns before the socket connect begins
-// and removed from pendingConns once the connect succeeds or fails.
-func DirectDial(
-	addr string,
-	connectTimeout, readTimeout, writeTimeout time.Duration,
-	pendingConns *Conns) (conn *DirectConn, err error) {
+// TCPConn creates a new, connected TCPConn.
+func DialTCP(addr string, config *DialConfig) (conn *TCPConn, err error) {
 
-	conn, err = interruptibleDial(addr, connectTimeout, readTimeout, writeTimeout, pendingConns)
+	conn, err = interruptibleTCPDial(addr, config)
 	if err != nil {
 		return nil, ContextError(err)
 	}
@@ -75,7 +63,7 @@ func DirectDial(
 }
 
 // SetClosedSignal implements psiphon.Conn.SetClosedSignal
-func (conn *DirectConn) SetClosedSignal(closedSignal chan struct{}) (err error) {
+func (conn *TCPConn) SetClosedSignal(closedSignal chan struct{}) (err error) {
 	conn.mutex.Lock()
 	defer conn.mutex.Unlock()
 	if conn.isClosed {
@@ -85,14 +73,14 @@ func (conn *DirectConn) SetClosedSignal(closedSignal chan struct{}) (err error)
 	return nil
 }
 
-// Close terminates a connected (net.Conn) or connecting (socketFd) DirectConn.
+// Close terminates a connected (net.Conn) or connecting (socketFd) TCPConn.
 // A mutex is required to support psiphon.Conn.SetClosedSignal concurrency semantics.
-func (conn *DirectConn) Close() (err error) {
+func (conn *TCPConn) Close() (err error) {
 	conn.mutex.Lock()
 	defer conn.mutex.Unlock()
 	if !conn.isClosed {
 		if conn.Conn == nil {
-			err = interruptibleClose(conn.interruptible)
+			err = interruptibleTCPClose(conn.interruptible)
 		} else {
 			err = conn.Conn.Close()
 		}
@@ -107,7 +95,7 @@ func (conn *DirectConn) Close() (err error) {
 
 // Read wraps standard Read to add an idle timeout. The connection
 // is explicitly closed on timeout.
-func (conn *DirectConn) Read(buffer []byte) (n int, err error) {
+func (conn *TCPConn) Read(buffer []byte) (n int, err error) {
 	// Note: no mutex on the conn.readTimeout access
 	if conn.readTimeout != 0 {
 		err = conn.Conn.SetReadDeadline(time.Now().Add(conn.readTimeout))
@@ -124,7 +112,7 @@ func (conn *DirectConn) Read(buffer []byte) (n int, err error) {
 
 // Write wraps standard Write to add an idle timeout The connection
 // is explicitly closed on timeout.
-func (conn *DirectConn) Write(buffer []byte) (n int, err error) {
+func (conn *TCPConn) Write(buffer []byte) (n int, err error) {
 	// Note: no mutex on the conn.writeTimeout access
 	if conn.writeTimeout != 0 {
 		err = conn.Conn.SetWriteDeadline(time.Now().Add(conn.writeTimeout))
@@ -140,16 +128,19 @@ func (conn *DirectConn) Write(buffer []byte) (n int, err error) {
 }
 
 // Override implementation of net.Conn.SetDeadline
-func (conn *DirectConn) SetDeadline(t time.Time) error {
-	return ContextError(errors.New("not supported"))
+func (conn *TCPConn) SetDeadline(t time.Time) error {
+	Fatal("net.Conn SetDeadline not supported")
+	return nil
 }
 
 // Override implementation of net.Conn.SetReadDeadline
-func (conn *DirectConn) SetReadDeadline(t time.Time) error {
-	return ContextError(errors.New("not supported"))
+func (conn *TCPConn) SetReadDeadline(t time.Time) error {
+	Fatal("net.Conn SetReadDeadline not supported")
+	return nil
 }
 
 // Override implementation of net.Conn.SetWriteDeadline
-func (conn *DirectConn) SetWriteDeadline(t time.Time) error {
-	return ContextError(errors.New("not supported"))
+func (conn *TCPConn) SetWriteDeadline(t time.Time) error {
+	Fatal("net.Conn SetWriteDeadline not supported")
+	return nil
 }

+ 28 - 37
psiphon/directConn_unix.go → psiphon/TCPConn_unix.go

@@ -1,4 +1,4 @@
-// +build darwin dragonfly freebsd linux nacl netbsd openbsd solaris
+// +build android darwin dragonfly freebsd linux nacl netbsd openbsd solaris
 
 /*
  * Copyright (c) 2014, Psiphon Inc.
@@ -30,19 +30,16 @@ import (
 	"time"
 )
 
-type interruptibleConn struct {
+type interruptibleTCPSocket struct {
 	socketFd int
 }
 
-// interruptibleDial creates a socket connection.
-// To implement device binding and interruptible connecting, the lower-level
+// interruptibleTCPDial creates a socket connection.
+// To implement socket device binding and interruptible connecting, the lower-level
 // syscall APIs are used. The sequence of syscalls in this implementation are
 // taken from: https://code.google.com/p/go/issues/detail?id=6966
-func interruptibleDial(
-	addr string,
-	connectTimeout, readTimeout, writeTimeout time.Duration,
-	pendingConns *Conns) (conn *DirectConn, err error) {
-	// Create a socket and then, before connecting, add a DirectConn with
+func interruptibleTCPDial(addr string, config *DialConfig) (conn *TCPConn, err error) {
+	// Create a socket and then, before connecting, add a TCPConn with
 	// the unconnected socket to pendingConns. This allows pendingConns to
 	// abort connections in progress.
 	socketFd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, 0)
@@ -55,27 +52,14 @@ func interruptibleDial(
 			syscall.Close(socketFd)
 		}
 	}()
-	conn = &DirectConn{
-		interruptible: interruptibleConn{socketFd: socketFd},
-		readTimeout:   readTimeout,
-		writeTimeout:  writeTimeout}
-	pendingConns.Add(conn)
-	// Before connecting, ensure the socket doesn't route through a VPN interface
-	// TODO: this method requires root, which we won't have on Android in VpnService mode
-	// an alternative may be to use http://golang.org/pkg/syscall/#UnixRights to send the
-	// fd to the main Android process which receives the fd with
-	// http://developer.android.com/reference/android/net/LocalSocket.html#getAncillaryFileDescriptors%28%29
-	// and then calls
-	// http://developer.android.com/reference/android/net/VpnService.html#protect%28int%29.
-	// See, for example:
-	// https://code.google.com/p/ics-openvpn/source/browse/main/src/main/java/de/blinkt/openvpn/core/OpenVpnManagementThread.java#164
-	/*
-		const SO_BINDTODEVICE = 0x19 // only defined for Linux
-		err = syscall.SetsockoptString(socketFd, syscall.SOL_SOCKET, SO_BINDTODEVICE, deviceName)
-	*/
-	// Resolve domain name
-	// TODO: ensure DNS UDP traffic doesn't route through a VPN interface
-	// ...use https://golang.org/src/pkg/net/dnsclient.go?
+	// Note: this step is not interruptible
+	if config.BindToDeviceServiceAddr != "" {
+		err = bindToDevice(socketFd, config)
+		if err != nil {
+			return nil, ContextError(err)
+		}
+	}
+	// Get the remote IP and port, resolving a domain name if necessary
 	host, strPort, err := net.SplitHostPort(addr)
 	if err != nil {
 		return nil, ContextError(err)
@@ -84,21 +68,28 @@ func interruptibleDial(
 	if err != nil {
 		return nil, ContextError(err)
 	}
-	// TODO: IPv6 support
-	var ip [4]byte
-	ipAddrs, err := net.LookupIP(host)
+	ipAddrs, err := LookupIP(host, config)
 	if err != nil {
 		return nil, ContextError(err)
 	}
 	if len(ipAddrs) < 1 {
 		return nil, ContextError(errors.New("no ip address"))
 	}
+	// TODO: IPv6 support
+	var ip [4]byte
 	copy(ip[:], ipAddrs[0].To4())
+	// Enable interruption
+	conn = &TCPConn{
+		interruptible: interruptibleTCPSocket{socketFd: socketFd},
+		readTimeout:   config.ReadTimeout,
+		writeTimeout:  config.WriteTimeout}
+	config.PendingConns.Add(conn)
 	// Connect the socket
+	// TODO: adjust the timeout to account for time spent resolving hostname
 	sockAddr := syscall.SockaddrInet4{Addr: ip, Port: port}
-	if connectTimeout != 0 {
+	if config.ConnectTimeout != 0 {
 		errChannel := make(chan error, 2)
-		time.AfterFunc(connectTimeout, func() {
+		time.AfterFunc(config.ConnectTimeout, func() {
 			errChannel <- errors.New("connect timeout")
 		})
 		go func() {
@@ -108,7 +99,7 @@ func interruptibleDial(
 	} else {
 		err = syscall.Connect(conn.interruptible.socketFd, &sockAddr)
 	}
-	pendingConns.Remove(conn)
+	config.PendingConns.Remove(conn)
 	if err != nil {
 		return nil, ContextError(err)
 	}
@@ -122,6 +113,6 @@ func interruptibleDial(
 	return conn, nil
 }
 
-func interruptibleClose(interruptible interruptibleConn) error {
+func interruptibleTCPClose(interruptible interruptibleTCPSocket) error {
 	return syscall.Close(interruptible.socketFd)
 }

+ 12 - 12
psiphon/directConn_windows.go → psiphon/TCPConn_windows.go

@@ -26,26 +26,26 @@ import (
 	"time"
 )
 
-type interruptibleConn struct {
+type interruptibleTCPSocket struct {
 }
 
-func interruptibleDial(
-	addr string,
-	connectTimeout, readTimeout, writeTimeout time.Duration,
-	pendingConns *Conns) (conn *DirectConn, err error) {
-	// Note: using net.Dial(); interruptible connections not supported on Windows
-	netConn, err := net.DialTimeout("tcp", addr, connectTimeout)
+func interruptibleTCPDial(addr string, config *TCPConfig) (conn *TCPConn, err error) {
+	if config.BindToDeviceServiceAddr != "" {
+		Fatal("psiphon.interruptibleTCPDial with bind not supported on Windows")
+	}
+	// Note: using standard net.Dial(); interruptible connections not supported on Windows
+	netConn, err := net.DialTimeout("tcp", addr, config.ConnectTimeout)
 	if err != nil {
 		return nil, ContextError(err)
 	}
-	conn = &DirectConn{
+	conn = &TCPConn{
 		Conn:         netConn,
-		readTimeout:  readTimeout,
-		writeTimeout: writeTimeout}
+		readTimeout:  config.ReadTimeout,
+		writeTimeout: config.WriteTimeout}
 	return conn, nil
 }
 
-func interruptibleClose(interruptible interruptibleConn) error {
-	Fatal("interruptibleClose not supported on Windows")
+func interruptibleTCPClose(interruptible interruptibleTCPSocket) error {
+	Fatal("psiphon.interruptibleTCPClose not supported on Windows")
 	return nil
 }

+ 73 - 0
psiphon/bindToDevice.go

@@ -0,0 +1,73 @@
+// +build android linux
+
+/*
+ * Copyright (c) 2014, 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 psiphon
+
+import (
+	"errors"
+	"fmt"
+	"net"
+	"syscall"
+	"time"
+)
+
+// bindToDevice sends a file descriptor a service which will bind the socket to
+// a device so that it doesn't route through a VPN interface. This is used for
+// TCP tunnel connections made while the VPN is active and for UDP DNS requests
+// sent as part of establishing those TCP connections.
+// On Android, where this facility is used, the underlying implementation uses
+// setsockopt(SO_BINDTODEVICE). This socket options requires root, which is
+// why this is delegated to a remote service.
+func bindToDevice(socketFd int, config *DialConfig) error {
+	addr, err := net.ResolveUnixAddr("unix", config.BindToDeviceServiceAddr)
+	if err != nil {
+		return ContextError(err)
+	}
+	conn, err := net.DialUnix("unix", nil, addr)
+	if err != nil {
+		return ContextError(err)
+	}
+	defer conn.Close()
+	// Set request timeouts, using the ConnectTimeout from the overall Dial
+	conn.SetReadDeadline(time.Now().Add(config.ConnectTimeout))
+	conn.SetWriteDeadline(time.Now().Add(config.ConnectTimeout))
+	// The 0 byte payload for the write is a dummy message. The important
+	// payload is the file descriptor.
+	// The response is also a single byte. 0 is success, and any other
+	// byte value is an error code.
+	msg := []byte{byte(0)}
+	rights := syscall.UnixRights(socketFd)
+	bytesWritten, ooBytesWritten, err := conn.WriteMsgUnix(msg, rights, nil)
+	if err != nil {
+		return ContextError(err)
+	}
+	if bytesWritten != len(msg) || ooBytesWritten != len(rights) {
+		return ContextError(errors.New("bindToDevice write request failed"))
+	}
+	bytesRead, err := conn.Read(msg)
+	if err != nil {
+		return ContextError(err)
+	}
+	if bytesRead != len(msg) || msg[0] != 0 {
+		return ContextError(fmt.Errorf("bindToDevice read response failed: %d", int(msg[0])))
+	}
+	return nil
+}

+ 27 - 0
psiphon/bindToDevice_nobind.go

@@ -0,0 +1,27 @@
+// +build !android !linux
+
+/*
+ * Copyright (c) 2014, 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 psiphon
+
+func bindToDevice(socketFd int, config *DialConfig) error {
+	Fatal("bindToDevice not supported on this platform")
+	return nil
+}

+ 2 - 0
psiphon/config.go

@@ -39,6 +39,8 @@ type Config struct {
 	LocalSocksProxyPort                int
 	LocalHttpProxyPort                 int
 	ConnectionWorkerPoolSize           int
+	BindToDeviceServiceAddress         string
+	BindToDeviceDnsServer              string
 }
 
 // LoadConfig reads, and parse, and validates a JSON format Psiphon config

+ 26 - 0
psiphon/conn.go

@@ -22,8 +22,34 @@ package psiphon
 import (
 	"net"
 	"sync"
+	"time"
 )
 
+// DialConfig contains parameters to determine the behavior
+// of a Psiphon dialer (TCPDial, MeekDial, etc.)
+type DialConfig struct {
+	ConnectTimeout time.Duration
+	ReadTimeout    time.Duration
+	WriteTimeout   time.Duration
+
+	// PendingConns is used to interrupt dials in progress.
+	// The dial may be interrupted using PendingConns.CloseAll(): on platforms
+	// that support this, the new conn is added to pendingConns before the network
+	// connect begins and removed from pendingConns once the connect succeeds or fails.
+	PendingConns *Conns
+
+	// BindToDevice parameters are used to exclude connections and
+	// associated DNS requests from VPN routing.
+	// When BindToDeviceServiceAddr is not blank, any underlying socket is
+	// submitted to the device binding service at that address before connecting.
+	// The service should bind the socket to a device so that it doesn't route
+	// through a VPN interface. This service is also used to bind UDP sockets used
+	// for DNS requests, in which case BindToDeviceDnsServer is used as the
+	// DNS server.
+	BindToDeviceServiceAddr string
+	BindToDeviceDnsServer   string
+}
+
 // Dialer is a custom dialer compatible with http.Transport.Dial.
 type Dialer func(string, string) (net.Conn, error)
 

+ 12 - 9
psiphon/meekConn.go

@@ -82,22 +82,25 @@ type MeekConn struct {
 	fullSendBuffer       chan *bytes.Buffer
 }
 
-// NewMeekConn returns an initialized meek connection. A meek connection is
+// DialMeek returns an initialized meek connection. A meek connection is
 // an HTTP session which does not depend on an underlying socket connection (although
 // persistent HTTP connections are used for performance). This function does not
 // wait for the connection to be "established" before returning. A goroutine
 // is spawned which will eventually start HTTP polling.
 // useFronting assumes caller has already checked server entry capabilities.
-func NewMeekConn(
-	serverEntry *ServerEntry, sessionId string, useFronting bool,
-	connectTimeout, readTimeout, writeTimeout time.Duration) (meek *MeekConn, err error) {
+func DialMeek(
+	serverEntry *ServerEntry, sessionId string,
+	useFronting bool, config *DialConfig) (meek *MeekConn, err error) {
 	// Configure transport
 	// Note: MeekConn has its own PendingConns to manage the underlying HTTP transport connections,
 	// which may be interrupted on MeekConn.Close(). This code previously used the establishTunnel
 	// pendingConns here, but that was a lifecycle mismatch: we don't want to abort HTTP transport
 	// connections while MeekConn is still in use
 	pendingConns := new(Conns)
-	directDialer := NewDirectDialer(connectTimeout, readTimeout, writeTimeout, pendingConns)
+	// Use a copy of DialConfig with the meek pendingConns
+	configCopy := new(DialConfig)
+	*configCopy = *config
+	configCopy.PendingConns = pendingConns
 	var host string
 	var dialer Dialer
 	if useFronting {
@@ -108,15 +111,15 @@ func NewMeekConn(
 		//  - disables SNI -- SNI breaks fronting when used with CDNs that support SNI on the server side.
 		dialer = NewCustomTLSDialer(
 			&CustomTLSConfig{
-				Dial:           directDialer,
-				Timeout:        connectTimeout,
+				Dial:           NewTCPDialer(configCopy),
+				Timeout:        configCopy.ConnectTimeout,
 				FrontingAddr:   fmt.Sprintf("%s:%d", serverEntry.MeekFrontingDomain, 443),
 				SendServerName: false,
 			})
 	} else {
 		// In this case, host is both what is dialed and what ends up in the HTTP Host header
 		host = fmt.Sprintf("%s:%d", serverEntry.IpAddress, serverEntry.MeekServerPort)
-		dialer = directDialer
+		dialer = NewTCPDialer(configCopy)
 	}
 	// Scheme is always "http". Otherwise http.Transport will try to do another TLS
 	// handshake inside the explicit TLS session (in fronting mode).
@@ -480,7 +483,7 @@ func makeCookie(serverEntry *ServerEntry, sessionId string) (cookie *http.Cookie
 	copy(encryptedCookie[32:], box)
 	// Obfuscate the encrypted data
 	obfuscator, err := NewObfuscator(
-		&ObfuscatorParams{Keyword: serverEntry.MeekObfuscatedKey, MaxPadding: MEEK_COOKIE_MAX_PADDING})
+		&ObfuscatorConfig{Keyword: serverEntry.MeekObfuscatedKey, MaxPadding: MEEK_COOKIE_MAX_PADDING})
 	if err != nil {
 		return nil, ContextError(err)
 	}

+ 1 - 1
psiphon/obfuscatedSshConn.go

@@ -82,7 +82,7 @@ const (
 // conn must be used for SSH client traffic and must have transferred
 // no traffic.
 func NewObfuscatedSshConn(conn net.Conn, obfuscationKeyword string) (*ObfuscatedSshConn, error) {
-	obfuscator, err := NewObfuscator(&ObfuscatorParams{Keyword: obfuscationKeyword})
+	obfuscator, err := NewObfuscator(&ObfuscatorConfig{Keyword: obfuscationKeyword})
 	if err != nil {
 		return nil, err
 	}

+ 6 - 6
psiphon/obfuscator.go

@@ -46,7 +46,7 @@ type Obfuscator struct {
 	serverToClientCipher *rc4.Cipher
 }
 
-type ObfuscatorParams struct {
+type ObfuscatorConfig struct {
 	Keyword    string
 	MaxPadding int
 }
@@ -54,16 +54,16 @@ type ObfuscatorParams struct {
 // NewObfuscator creates a new Obfuscator, initializes it with
 // a seed message, derives client and server keys, and creates
 // RC4 stream ciphers to obfuscate data.
-func NewObfuscator(params *ObfuscatorParams) (obfuscator *Obfuscator, err error) {
+func NewObfuscator(config *ObfuscatorConfig) (obfuscator *Obfuscator, err error) {
 	seed, err := MakeSecureRandomBytes(OBFUSCATE_SEED_LENGTH)
 	if err != nil {
 		return nil, err
 	}
-	clientToServerKey, err := deriveKey(seed, []byte(params.Keyword), []byte(OBFUSCATE_CLIENT_TO_SERVER_IV))
+	clientToServerKey, err := deriveKey(seed, []byte(config.Keyword), []byte(OBFUSCATE_CLIENT_TO_SERVER_IV))
 	if err != nil {
 		return nil, err
 	}
-	serverToClientKey, err := deriveKey(seed, []byte(params.Keyword), []byte(OBFUSCATE_SERVER_TO_CLIENT_IV))
+	serverToClientKey, err := deriveKey(seed, []byte(config.Keyword), []byte(OBFUSCATE_SERVER_TO_CLIENT_IV))
 	if err != nil {
 		return nil, err
 	}
@@ -76,8 +76,8 @@ func NewObfuscator(params *ObfuscatorParams) (obfuscator *Obfuscator, err error)
 		return nil, err
 	}
 	maxPadding := OBFUSCATE_MAX_PADDING
-	if params.MaxPadding > 0 {
-		maxPadding = params.MaxPadding
+	if config.MaxPadding > 0 {
+		maxPadding = config.MaxPadding
 	}
 	seedMessage, err := makeSeedMessage(maxPadding, seed, clientToServerCipher)
 	if err != nil {

+ 8 - 6
psiphon/serverApi.go

@@ -271,14 +271,16 @@ func makePsiphonHttpsClient(
 	// intended purpose. The readTimeout is to abort NewSession when the Psiphon server responds to
 	// handshake/connected requests but fails to deliver the response body (e.g., ResponseHeaderTimeout
 	// is not sufficient to timeout this case).
-	directDialer := NewDirectDialer(
-		PSIPHON_API_SERVER_TIMEOUT,
-		PSIPHON_API_SERVER_TIMEOUT,
-		PSIPHON_API_SERVER_TIMEOUT,
-		pendingConns)
+	tcpDialer := NewTCPDialer(
+		&DialConfig{
+			ConnectTimeout: PSIPHON_API_SERVER_TIMEOUT,
+			ReadTimeout:    PSIPHON_API_SERVER_TIMEOUT,
+			WriteTimeout:   PSIPHON_API_SERVER_TIMEOUT,
+			PendingConns:   pendingConns,
+		})
 	dialer := NewCustomTLSDialer(
 		&CustomTLSConfig{
-			Dial:                    directDialer,
+			Dial:                    tcpDialer,
 			Timeout:                 PSIPHON_API_SERVER_TIMEOUT,
 			HttpProxyAddress:        localHttpProxyAddress,
 			SendServerName:          false,

+ 8 - 7
psiphon/tunnel.go

@@ -127,11 +127,15 @@ func EstablishTunnel(
 		port = serverEntry.SshPort
 	}
 	// Create the base transport: meek or direct connection
+	dialConfig := &DialConfig{
+		ConnectTimeout: TUNNEL_CONNECT_TIMEOUT,
+		ReadTimeout:    TUNNEL_READ_TIMEOUT,
+		WriteTimeout:   TUNNEL_WRITE_TIMEOUT,
+		PendingConns:   pendingConns,
+	}
 	var conn Conn
 	if useMeek {
-		conn, err = NewMeekConn(
-			serverEntry, sessionId, useFronting,
-			TUNNEL_CONNECT_TIMEOUT, TUNNEL_READ_TIMEOUT, TUNNEL_WRITE_TIMEOUT)
+		conn, err = DialMeek(serverEntry, sessionId, useFronting, dialConfig)
 		if err != nil {
 			return nil, ContextError(err)
 		}
@@ -139,10 +143,7 @@ func EstablishTunnel(
 		// interrupt; underlying HTTP connections may be candidates for interruption, but only
 		// after relay starts polling...
 	} else {
-		conn, err = DirectDial(
-			fmt.Sprintf("%s:%d", serverEntry.IpAddress, port),
-			TUNNEL_CONNECT_TIMEOUT, TUNNEL_READ_TIMEOUT, TUNNEL_WRITE_TIMEOUT,
-			pendingConns)
+		conn, err = DialTCP(fmt.Sprintf("%s:%d", serverEntry.IpAddress, port), dialConfig)
 		if err != nil {
 			return nil, ContextError(err)
 		}