Explorar o código

Merge pull request #179 from rod-hynes/master

integrated meek; general enhancements/fixes from live server testing
Rod Hynes %!s(int64=9) %!d(string=hai) anos
pai
achega
5c3e3e43e5

+ 1 - 1
.travis.yml

@@ -8,7 +8,7 @@ addons:
 install:
 install:
 - go get -t -d -v ./... && go build -v ./...
 - go get -t -d -v ./... && go build -v ./...
 script:
 script:
-- go test -v ./...
+- sudo -E go test -v ./...
 - cd psiphon
 - cd psiphon
 - go test -v -covermode=count -coverprofile=coverage.out
 - go test -v -covermode=count -coverprofile=coverage.out
 - $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $COVERALLS_TOKEN
 - $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $COVERALLS_TOKEN

+ 13 - 28
Server/main.go

@@ -26,6 +26,7 @@ import (
 	"os"
 	"os"
 	"strings"
 	"strings"
 
 
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/server"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/server"
 )
 )
 
 
@@ -33,7 +34,6 @@ func main() {
 
 
 	var generateServerIPaddress, generateServerNetworkInterface string
 	var generateServerIPaddress, generateServerNetworkInterface string
 	var generateConfigFilename, generateServerEntryFilename string
 	var generateConfigFilename, generateServerEntryFilename string
-	var generateWebServerPort, generateSSHServerPort, generateObfuscatedSSHServerPort int
 	var runConfigFilenames stringListFlag
 	var runConfigFilenames stringListFlag
 
 
 	flag.StringVar(
 	flag.StringVar(
@@ -52,7 +52,7 @@ func main() {
 		&generateServerNetworkInterface,
 		&generateServerNetworkInterface,
 		"interface",
 		"interface",
 		"",
 		"",
-		"generate server entry with this `network-interface`")
+		"generate with server IP address from this `network-interface`")
 
 
 	flag.StringVar(
 	flag.StringVar(
 		&generateServerIPaddress,
 		&generateServerIPaddress,
@@ -60,24 +60,6 @@ func main() {
 		server.DEFAULT_SERVER_IP_ADDRESS,
 		server.DEFAULT_SERVER_IP_ADDRESS,
 		"generate with this server `IP address`")
 		"generate with this server `IP address`")
 
 
-	flag.IntVar(
-		&generateWebServerPort,
-		"webport",
-		server.DEFAULT_WEB_SERVER_PORT,
-		"generate with this web server `port`; 0 for no web server")
-
-	flag.IntVar(
-		&generateSSHServerPort,
-		"sshport",
-		server.DEFAULT_SSH_SERVER_PORT,
-		"generate with this SSH server `port`; 0 for no SSH server")
-
-	flag.IntVar(
-		&generateObfuscatedSSHServerPort,
-		"osshport",
-		server.DEFAULT_OBFUSCATED_SSH_SERVER_PORT,
-		"generate with this Obfuscated SSH server `port`; 0 for no Obfuscated SSH server")
-
 	flag.Var(
 	flag.Var(
 		&runConfigFilenames,
 		&runConfigFilenames,
 		"config",
 		"config",
@@ -101,19 +83,22 @@ func main() {
 		os.Exit(1)
 		os.Exit(1)
 	} else if args[0] == "generate" {
 	} else if args[0] == "generate" {
 
 
-		configFileContents, serverEntryFileContents, err := server.GenerateConfig(
-			&server.GenerateConfigParams{
-				ServerIPAddress:         generateServerIPaddress,
-				ServerNetworkInterface:  generateServerNetworkInterface,
-				WebServerPort:           generateWebServerPort,
-				SSHServerPort:           generateSSHServerPort,
-				ObfuscatedSSHServerPort: generateObfuscatedSSHServerPort,
-			})
+		serverIPaddress := generateServerIPaddress
+
+		if generateServerNetworkInterface != "" {
+			var err error
+			serverIPaddress, err = psiphon.GetInterfaceIPAddress(generateServerNetworkInterface)
+			fmt.Errorf("generate failed: %s", err)
+			os.Exit(1)
+		}
 
 
+		configFileContents, serverEntryFileContents, err :=
+			server.GenerateConfig(serverIPaddress)
 		if err != nil {
 		if err != nil {
 			fmt.Errorf("generate failed: %s", err)
 			fmt.Errorf("generate failed: %s", err)
 			os.Exit(1)
 			os.Exit(1)
 		}
 		}
+
 		err = ioutil.WriteFile(generateConfigFilename, configFileContents, 0600)
 		err = ioutil.WriteFile(generateConfigFilename, configFileContents, 0600)
 		if err != nil {
 		if err != nil {
 			fmt.Errorf("error writing configuration file: %s", err)
 			fmt.Errorf("error writing configuration file: %s", err)

+ 2 - 2
psiphon/meekConn.go

@@ -264,7 +264,7 @@ func DialMeek(
 		}
 		}
 	}
 	}
 
 
-	cookie, err := makeCookie(meekConfig)
+	cookie, err := makeMeekCookie(meekConfig)
 	if err != nil {
 	if err != nil {
 		return nil, ContextError(err)
 		return nil, ContextError(err)
 	}
 	}
@@ -642,7 +642,7 @@ type meekCookieData struct {
 // all consequent HTTP requests
 // all consequent HTTP requests
 // In unfronted meek mode, the cookie is visible over the adversary network, so the
 // In unfronted meek mode, the cookie is visible over the adversary network, so the
 // cookie is encrypted and obfuscated.
 // cookie is encrypted and obfuscated.
-func makeCookie(meekConfig *MeekConfig) (cookie *http.Cookie, err error) {
+func makeMeekCookie(meekConfig *MeekConfig) (cookie *http.Cookie, err error) {
 
 
 	// Make the JSON data
 	// Make the JSON data
 	serverAddress := meekConfig.PsiphonServerAddress
 	serverAddress := meekConfig.PsiphonServerAddress

+ 244 - 30
psiphon/net.go

@@ -17,9 +17,41 @@
  *
  *
  */
  */
 
 
+// for HTTPSServer.ServeTLS:
+/*
+Copyright (c) 2012 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+   * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+   * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+   * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
 package psiphon
 package psiphon
 
 
 import (
 import (
+	"container/list"
 	"crypto/tls"
 	"crypto/tls"
 	"crypto/x509"
 	"crypto/x509"
 	"errors"
 	"errors"
@@ -32,6 +64,7 @@ import (
 	"os"
 	"os"
 	"reflect"
 	"reflect"
 	"sync"
 	"sync"
+	"sync/atomic"
 	"time"
 	"time"
 
 
 	"github.com/Psiphon-Inc/dns"
 	"github.com/Psiphon-Inc/dns"
@@ -189,6 +222,92 @@ func (conns *Conns) CloseAll() {
 	conns.conns = make(map[net.Conn]bool)
 	conns.conns = make(map[net.Conn]bool)
 }
 }
 
 
+// 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.
+//
+// New connections added are referenced by a LRUConnsEntry,
+// which is used to Touch() active connections, which
+// promotes them to the front of the order and to Remove()
+// connections that are no longer LRU candidates.
+//
+// CloseOldest() will remove the oldest connection from the
+// list and call net.Conn.Close() on the connection.
+//
+// After an entry has been removed, LRUConnsEntry Touch()
+// and Remove() will have no effect.
+type LRUConns struct {
+	mutex sync.Mutex
+	list  *list.List
+}
+
+// NewLRUConns initializes a new LRUConns.
+func NewLRUConns() *LRUConns {
+	return &LRUConns{list: list.New()}
+}
+
+// Add inserts a net.Conn as the freshest connection
+// in a LRUConns and returns an LRUConnsEntry to be
+// used to freshen the connection or remove the connection
+// from the LRU list.
+func (conns *LRUConns) Add(conn net.Conn) *LRUConnsEntry {
+	conns.mutex.Lock()
+	defer conns.mutex.Unlock()
+	return &LRUConnsEntry{
+		lruConns: conns,
+		element:  conns.list.PushFront(conn),
+	}
+}
+
+// CloseOldest closes the oldest connection in a
+// LRUConns. It calls net.Conn.Close() on the
+// connection.
+func (conns *LRUConns) CloseOldest() {
+	conns.mutex.Lock()
+	oldest := conns.list.Back()
+	conn, ok := oldest.Value.(net.Conn)
+	if oldest != nil {
+		conns.list.Remove(oldest)
+	}
+	// Release mutex before closing conn
+	conns.mutex.Unlock()
+	if ok {
+		conn.Close()
+	}
+}
+
+// LRUConnsEntry is an entry in a LRUConns list.
+type LRUConnsEntry struct {
+	lruConns *LRUConns
+	element  *list.Element
+}
+
+// Remove deletes the connection referenced by the
+// LRUConnsEntry from the associated LRUConns.
+// Has no effect if the entry was not initialized
+// or previously removed.
+func (entry *LRUConnsEntry) Remove() {
+	if entry.lruConns == nil || entry.element == nil {
+		return
+	}
+	entry.lruConns.mutex.Lock()
+	defer entry.lruConns.mutex.Unlock()
+	entry.lruConns.list.Remove(entry.element)
+}
+
+// Touch promotes the connection referenced by the
+// LRUConnsEntry to the front of the associated LRUConns.
+// Has no effect if the entry was not initialized
+// or previously removed.
+func (entry *LRUConnsEntry) Touch() {
+	if entry.lruConns == nil || entry.element == nil {
+		return
+	}
+	entry.lruConns.mutex.Lock()
+	defer entry.lruConns.mutex.Unlock()
+	entry.lruConns.list.MoveToFront(entry.element)
+}
+
 // LocalProxyRelay sends to remoteConn bytes received from localConn,
 // LocalProxyRelay sends to remoteConn bytes received from localConn,
 // and sends to localConn bytes received from remoteConn.
 // and sends to localConn bytes received from remoteConn.
 func LocalProxyRelay(proxyType string, localConn, remoteConn net.Conn) {
 func LocalProxyRelay(proxyType string, localConn, remoteConn net.Conn) {
@@ -582,56 +701,125 @@ func IPAddressFromAddr(addr net.Addr) string {
 	return ipAddress
 	return ipAddress
 }
 }
 
 
-// IdleTimeoutConn wraps a net.Conn and sets an initial ReadDeadline. The
-// deadline is extended whenever data is received from the connection.
-// Optionally, IdleTimeoutConn will also extend the deadline when data is
-// written to the connection.
-type IdleTimeoutConn struct {
+// HTTPSServer is a wrapper around http.Server which adds the
+// ServeTLS function.
+type HTTPSServer struct {
+	http.Server
+}
+
+// ServeTLS is a offers the equivalent interface as http.Serve.
+// The http package has both ListenAndServe and ListenAndServeTLS higher-
+// level interfaces, but only Serve (not TLS) offers a lower-level interface that
+// allows the caller to keep a refererence to the Listener, allowing for external
+// shutdown. ListenAndServeTLS also requires the TLS cert and key to be in files
+// and we avoid that here.
+// tcpKeepAliveListener is used in http.ListenAndServeTLS but not exported,
+// so we use a copy from https://golang.org/src/net/http/server.go.
+func (server *HTTPSServer) ServeTLS(listener net.Listener) error {
+	tlsListener := tls.NewListener(tcpKeepAliveListener{listener.(*net.TCPListener)}, server.TLSConfig)
+	return server.Serve(tlsListener)
+}
+
+type tcpKeepAliveListener struct {
+	*net.TCPListener
+}
+
+func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
+	tc, err := ln.AcceptTCP()
+	if err != nil {
+		return
+	}
+	tc.SetKeepAlive(true)
+	tc.SetKeepAlivePeriod(3 * time.Minute)
+	return tc, nil
+}
+
+// ActivityMonitoredConn wraps a net.Conn, adding logic to deal with
+// events triggered by I/O activity.
+//
+// When an inactivity timeout is specified, the net.Conn Read() will
+// timeout after the specified period of read inactivity. Optionally,
+// ActivityMonitoredConn will also consider the connection active when
+// data is written to it.
+//
+// When a LRUConnsEntry is specified, then the LRU entry is promoted on
+// either a successful read or write.
+//
+type ActivityMonitoredConn struct {
 	net.Conn
 	net.Conn
-	deadline     time.Duration
-	resetOnWrite bool
+	inactivityTimeout time.Duration
+	activeOnWrite     bool
+	lruEntry          *LRUConnsEntry
 }
 }
 
 
-func NewIdleTimeoutConn(
-	conn net.Conn, deadline time.Duration, resetOnWrite bool) *IdleTimeoutConn {
+func NewActivityMonitoredConn(
+	conn net.Conn,
+	inactivityTimeout time.Duration,
+	activeOnWrite bool,
+	lruEntry *LRUConnsEntry) *ActivityMonitoredConn {
 
 
-	conn.SetReadDeadline(time.Now().Add(deadline))
-	return &IdleTimeoutConn{
-		Conn:         conn,
-		deadline:     deadline,
-		resetOnWrite: resetOnWrite,
+	if inactivityTimeout > 0 {
+		conn.SetReadDeadline(time.Now().Add(inactivityTimeout))
+	}
+	return &ActivityMonitoredConn{
+		Conn:              conn,
+		inactivityTimeout: inactivityTimeout,
+		activeOnWrite:     activeOnWrite,
+		lruEntry:          lruEntry,
 	}
 	}
 }
 }
 
 
-func (conn *IdleTimeoutConn) Read(buffer []byte) (int, error) {
+func (conn *ActivityMonitoredConn) Read(buffer []byte) (int, error) {
 	n, err := conn.Conn.Read(buffer)
 	n, err := conn.Conn.Read(buffer)
 	if err == nil {
 	if err == nil {
-		conn.Conn.SetReadDeadline(time.Now().Add(conn.deadline))
+		if conn.inactivityTimeout > 0 {
+			conn.Conn.SetReadDeadline(time.Now().Add(conn.inactivityTimeout))
+		}
+		if conn.lruEntry != nil {
+			conn.lruEntry.Touch()
+		}
 	}
 	}
 	return n, err
 	return n, err
 }
 }
 
 
-func (conn *IdleTimeoutConn) Write(buffer []byte) (int, error) {
+func (conn *ActivityMonitoredConn) Write(buffer []byte) (int, error) {
 	n, err := conn.Conn.Write(buffer)
 	n, err := conn.Conn.Write(buffer)
-	if err == nil && conn.resetOnWrite {
-		conn.Conn.SetReadDeadline(time.Now().Add(conn.deadline))
+	if err == nil {
+		if conn.inactivityTimeout > 0 && conn.activeOnWrite {
+			conn.Conn.SetReadDeadline(time.Now().Add(conn.inactivityTimeout))
+		}
+		if conn.lruEntry != nil {
+			conn.lruEntry.Touch()
+		}
 	}
 	}
 	return n, err
 	return n, err
 }
 }
 
 
 // ThrottledConn wraps a net.Conn with read and write rate limiters.
 // ThrottledConn wraps a net.Conn with read and write rate limiters.
-// Rates are specified as bytes per second. The underlying rate limiter
-// uses the token bucket algorithm to calculate delay times for read
-// and write operations. Specify limit values of 0 set no limit.
+// Rates are specified as bytes per second. Optional unlimited byte
+// counts allow for a number of bytes to read or write before
+// applying rate limiting. Specify limit values of 0 to set no rate
+// limit (unlimited counts are ignored in this case).
+// The underlying rate limiter uses the token bucket algorithm to
+// calculate delay times for read and write operations.
 type ThrottledConn struct {
 type ThrottledConn struct {
 	net.Conn
 	net.Conn
-	reader io.Reader
-	writer io.Writer
+	unlimitedReadBytes  int64
+	limitingReads       int32
+	limitedReader       io.Reader
+	unlimitedWriteBytes int64
+	limitingWrites      int32
+	limitedWriter       io.Writer
 }
 }
 
 
+// NewThrottledConn initializes a new ThrottledConn.
 func NewThrottledConn(
 func NewThrottledConn(
 	conn net.Conn,
 	conn net.Conn,
-	limitReadBytesPerSecond, limitWriteBytesPerSecond int64) *ThrottledConn {
+	unlimitedReadBytes, limitReadBytesPerSecond,
+	unlimitedWriteBytes, limitWriteBytesPerSecond int64) *ThrottledConn {
+
+	// When no limit is specified, the rate limited reader/writer
+	// is simply the base reader/writer.
 
 
 	var reader io.Reader
 	var reader io.Reader
 	if limitReadBytesPerSecond == 0 {
 	if limitReadBytesPerSecond == 0 {
@@ -641,6 +829,7 @@ func NewThrottledConn(
 			ratelimit.NewBucketWithRate(
 			ratelimit.NewBucketWithRate(
 				float64(limitReadBytesPerSecond), limitReadBytesPerSecond))
 				float64(limitReadBytesPerSecond), limitReadBytesPerSecond))
 	}
 	}
+
 	var writer io.Writer
 	var writer io.Writer
 	if limitWriteBytesPerSecond == 0 {
 	if limitWriteBytesPerSecond == 0 {
 		writer = conn
 		writer = conn
@@ -649,17 +838,42 @@ func NewThrottledConn(
 			ratelimit.NewBucketWithRate(
 			ratelimit.NewBucketWithRate(
 				float64(limitWriteBytesPerSecond), limitWriteBytesPerSecond))
 				float64(limitWriteBytesPerSecond), limitWriteBytesPerSecond))
 	}
 	}
+
 	return &ThrottledConn{
 	return &ThrottledConn{
-		Conn:   conn,
-		reader: reader,
-		writer: writer,
+		Conn:                conn,
+		unlimitedReadBytes:  unlimitedReadBytes,
+		limitingReads:       0,
+		limitedReader:       reader,
+		unlimitedWriteBytes: unlimitedWriteBytes,
+		limitingWrites:      0,
+		limitedWriter:       writer,
 	}
 	}
 }
 }
 
 
 func (conn *ThrottledConn) Read(buffer []byte) (int, error) {
 func (conn *ThrottledConn) Read(buffer []byte) (int, error) {
-	return conn.reader.Read(buffer)
+
+	// Use the base reader until the unlimited count is exhausted.
+	if atomic.LoadInt32(&conn.limitingReads) == 0 {
+		if atomic.AddInt64(&conn.unlimitedReadBytes, -int64(len(buffer))) <= 0 {
+			atomic.StoreInt32(&conn.limitingReads, 1)
+		} else {
+			return conn.Read(buffer)
+		}
+	}
+
+	return conn.limitedReader.Read(buffer)
 }
 }
 
 
 func (conn *ThrottledConn) Write(buffer []byte) (int, error) {
 func (conn *ThrottledConn) Write(buffer []byte) (int, error) {
-	return conn.writer.Write(buffer)
+
+	// Use the base writer until the unlimited count is exhausted.
+	if atomic.LoadInt32(&conn.limitingWrites) == 0 {
+		if atomic.AddInt64(&conn.unlimitedWriteBytes, -int64(len(buffer))) <= 0 {
+			atomic.StoreInt32(&conn.limitingWrites, 1)
+		} else {
+			return conn.Write(buffer)
+		}
+	}
+
+	return conn.limitedWriter.Write(buffer)
 }
 }

+ 7 - 8
psiphon/obfuscator.go

@@ -26,7 +26,6 @@ import (
 	"encoding/binary"
 	"encoding/binary"
 	"errors"
 	"errors"
 	"io"
 	"io"
-	"net"
 )
 )
 
 
 const (
 const (
@@ -86,12 +85,12 @@ func NewClientObfuscator(
 }
 }
 
 
 // NewServerObfuscator creates a new Obfuscator, reading a seed message directly
 // NewServerObfuscator creates a new Obfuscator, reading a seed message directly
-// from the clientConn and initializing stream ciphers to obfuscate data.
+// from the clientReader and initializing stream ciphers to obfuscate data.
 func NewServerObfuscator(
 func NewServerObfuscator(
-	clientConn net.Conn, config *ObfuscatorConfig) (obfuscator *Obfuscator, err error) {
+	clientReader io.Reader, config *ObfuscatorConfig) (obfuscator *Obfuscator, err error) {
 
 
 	clientToServerCipher, serverToClientCipher, err := readSeedMessage(
 	clientToServerCipher, serverToClientCipher, err := readSeedMessage(
-		clientConn, config)
+		clientReader, config)
 	if err != nil {
 	if err != nil {
 		return nil, ContextError(err)
 		return nil, ContextError(err)
 	}
 	}
@@ -195,10 +194,10 @@ func makeSeedMessage(maxPadding int, seed []byte, clientToServerCipher *rc4.Ciph
 }
 }
 
 
 func readSeedMessage(
 func readSeedMessage(
-	clientConn net.Conn, config *ObfuscatorConfig) (*rc4.Cipher, *rc4.Cipher, error) {
+	clientReader io.Reader, config *ObfuscatorConfig) (*rc4.Cipher, *rc4.Cipher, error) {
 
 
 	seed := make([]byte, OBFUSCATE_SEED_LENGTH)
 	seed := make([]byte, OBFUSCATE_SEED_LENGTH)
-	_, err := io.ReadFull(clientConn, seed)
+	_, err := io.ReadFull(clientReader, seed)
 	if err != nil {
 	if err != nil {
 		return nil, nil, ContextError(err)
 		return nil, nil, ContextError(err)
 	}
 	}
@@ -209,7 +208,7 @@ func readSeedMessage(
 	}
 	}
 
 
 	fixedLengthFields := make([]byte, 8) // 4 bytes each for magic value and padding length
 	fixedLengthFields := make([]byte, 8) // 4 bytes each for magic value and padding length
-	_, err = io.ReadFull(clientConn, fixedLengthFields)
+	_, err = io.ReadFull(clientReader, fixedLengthFields)
 	if err != nil {
 	if err != nil {
 		return nil, nil, ContextError(err)
 		return nil, nil, ContextError(err)
 	}
 	}
@@ -237,7 +236,7 @@ func readSeedMessage(
 	}
 	}
 
 
 	padding := make([]byte, paddingLength)
 	padding := make([]byte, paddingLength)
-	_, err = io.ReadFull(clientConn, padding)
+	_, err = io.ReadFull(clientReader, padding)
 	if err != nil {
 	if err != nil {
 		return nil, nil, ContextError(err)
 		return nil, nil, ContextError(err)
 	}
 	}

+ 287 - 254
psiphon/server/config.go

@@ -28,41 +28,35 @@ import (
 	"encoding/pem"
 	"encoding/pem"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
-	"math/big"
 	"net"
 	"net"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
 	"time"
 	"time"
 
 
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon"
+	"golang.org/x/crypto/nacl/box"
 	"golang.org/x/crypto/ssh"
 	"golang.org/x/crypto/ssh"
 )
 )
 
 
 const (
 const (
-	SERVER_CONFIG_FILENAME                 = "psiphon-server.config"
-	SERVER_ENTRY_FILENAME                  = "serverEntry.dat"
-	DEFAULT_LOG_LEVEL                      = "info"
-	DEFAULT_SYSLOG_TAG                     = "psiphon-server"
-	DEFAULT_GEO_IP_DATABASE_FILENAME       = "GeoLite2-City.mmdb"
-	DEFAULT_SERVER_IP_ADDRESS              = "127.0.0.1"
-	WEB_SERVER_SECRET_BYTE_LENGTH          = 32
-	WEB_SERVER_CERTIFICATE_RSA_KEY_BITS    = 2048
-	WEB_SERVER_CERTIFICATE_VALIDITY_PERIOD = 10 * 365 * 24 * time.Hour // approx. 10 years
-	DEFAULT_WEB_SERVER_PORT                = 8000
-	WEB_SERVER_READ_TIMEOUT                = 10 * time.Second
-	WEB_SERVER_WRITE_TIMEOUT               = 10 * time.Second
-	SSH_USERNAME_SUFFIX_BYTE_LENGTH        = 8
-	SSH_PASSWORD_BYTE_LENGTH               = 32
-	SSH_RSA_HOST_KEY_BITS                  = 2048
-	DEFAULT_SSH_SERVER_PORT                = 2222
-	SSH_HANDSHAKE_TIMEOUT                  = 30 * time.Second
-	SSH_CONNECTION_READ_DEADLINE           = 5 * time.Minute
-	SSH_TCP_PORT_FORWARD_DIAL_TIMEOUT      = 30 * time.Second
-	SSH_OBFUSCATED_KEY_BYTE_LENGTH         = 32
-	DEFAULT_OBFUSCATED_SSH_SERVER_PORT     = 3333
-	REDIS_POOL_MAX_IDLE                    = 50
-	REDIS_POOL_MAX_ACTIVE                  = 1000
-	REDIS_POOL_IDLE_TIMEOUT                = 5 * time.Minute
+	SERVER_CONFIG_FILENAME                = "psiphon-server.config"
+	SERVER_ENTRY_FILENAME                 = "serverEntry.dat"
+	DEFAULT_SERVER_IP_ADDRESS             = "127.0.0.1"
+	WEB_SERVER_SECRET_BYTE_LENGTH         = 32
+	DISCOVERY_VALUE_KEY_BYTE_LENGTH       = 32
+	WEB_SERVER_READ_TIMEOUT               = 10 * time.Second
+	WEB_SERVER_WRITE_TIMEOUT              = 10 * time.Second
+	SSH_USERNAME_SUFFIX_BYTE_LENGTH       = 8
+	SSH_PASSWORD_BYTE_LENGTH              = 32
+	SSH_RSA_HOST_KEY_BITS                 = 2048
+	SSH_HANDSHAKE_TIMEOUT                 = 30 * time.Second
+	SSH_CONNECTION_READ_DEADLINE          = 5 * time.Minute
+	SSH_TCP_PORT_FORWARD_DIAL_TIMEOUT     = 30 * time.Second
+	SSH_TCP_PORT_FORWARD_COPY_BUFFER_SIZE = 8192
+	SSH_OBFUSCATED_KEY_BYTE_LENGTH        = 32
+	REDIS_POOL_MAX_IDLE                   = 50
+	REDIS_POOL_MAX_ACTIVE                 = 1000
+	REDIS_POOL_IDLE_TIMEOUT               = 5 * time.Minute
 )
 )
 
 
 // TODO: break config into sections (sub-structs)
 // TODO: break config into sections (sub-structs)
@@ -107,6 +101,10 @@ type Config struct {
 	// performed.
 	// performed.
 	GeoIPDatabaseFilename string
 	GeoIPDatabaseFilename string
 
 
+	// RedisServerAddress is the TCP address of a redis server. When
+	// set, redis is used to store per-session GeoIP information.
+	RedisServerAddress string
+
 	// ServerIPAddress is the public IP address of the server.
 	// ServerIPAddress is the public IP address of the server.
 	ServerIPAddress string
 	ServerIPAddress string
 
 
@@ -126,40 +124,82 @@ type Config struct {
 	// authenticate itself to clients.
 	// authenticate itself to clients.
 	WebServerPrivateKey string
 	WebServerPrivateKey string
 
 
-	// SSHServerPort is the listening port of the SSH server.
-	// When <= 0, no SSH server component is run.
-	SSHServerPort int
+	// 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", "FRONTED-MEEK-OSSH",
+	// "FRONTED-MEEK-HTTP-OSSH".
+	TunnelProtocolPorts map[string]int
 
 
 	// SSHPrivateKey is the SSH host key. The same key is used for
 	// SSHPrivateKey is the SSH host key. The same key is used for
-	// both the SSH and Obfuscated SSH servers.
+	// all protocols, run by this server instance, which use SSH.
 	SSHPrivateKey string
 	SSHPrivateKey string
 
 
 	// SSHServerVersion is the server version presented in the
 	// SSHServerVersion is the server version presented in the
-	// identification string. The same value is used for both SSH
-	// and Obfuscated SSH servers.
+	// identification string. The same value is used for all
+	// protocols, run by this server instance, which use SSH.
 	SSHServerVersion string
 	SSHServerVersion string
 
 
 	// SSHUserName is the SSH user name to be presented by the
 	// SSHUserName is the SSH user name to be presented by the
-	// the tunnel-core client. The same value is used for both SSH
-	// and Obfuscated SSH servers.
+	// the tunnel-core client. The same value is used for all
+	// protocols, run by this server instance, which use SSH.
 	SSHUserName string
 	SSHUserName string
 
 
 	// SSHPassword is the SSH password to be presented by the
 	// SSHPassword is the SSH password to be presented by the
-	// the tunnel-core client. The same value is used for both SSH
-	// and Obfuscated SSH servers.
+	// the tunnel-core client. The same value is used for all
+	// protocols, run by this server instance, which use SSH.
 	SSHPassword string
 	SSHPassword string
 
 
-	// ObfuscatedSSHServerPort is the listening port of the Obfuscated SSH server.
-	// When <= 0, no Obfuscated SSH server component is run.
-	ObfuscatedSSHServerPort int
-
 	// ObfuscatedSSHKey is the secret key for use in the Obfuscated
 	// ObfuscatedSSHKey is the secret key for use in the Obfuscated
-	// SSH protocol.
+	// SSH protocol. The same secret key is used for all protocols,
+	// run by this server instance, which use Obfuscated SSH.
 	ObfuscatedSSHKey string
 	ObfuscatedSSHKey string
 
 
-	// RedisServerAddress is the TCP address of a redis server. When
-	// set, redis is used to store per-session GeoIP information.
-	RedisServerAddress string
+	// MeekCookieEncryptionPrivateKey is the NaCl private key used
+	// to decrypt meek cookie payload sent from clients. The same
+	// key is used for all meek protocols run by this server instance.
+	MeekCookieEncryptionPrivateKey string
+
+	// MeekObfuscatedKey is the secret key used for obfuscating
+	// meek cookies sent from clients. The same key is used for all
+	// meek protocols run by this server instance.
+	MeekObfuscatedKey string
+
+	// MeekCertificateCommonName is the value used for the hostname
+	// in the self-signed certificate generated and used for meek
+	// HTTPS modes. The same value is used for all HTTPS meek
+	// protocols.
+	MeekCertificateCommonName string
+
+	// MeekProhibitedHeaders is a list of HTTP headers to check for
+	// in client requests. If one of these headers is found, the
+	// request fails. This is used to defend against abuse.
+	MeekProhibitedHeaders []string
+
+	// MeekProxyForwardedForHeaders is a list of HTTP headers which
+	// may be added by downstream HTTP proxies or CDNs in front
+	// of clients. These headers supply the original client IP
+	// address, which is geolocated for stats purposes. Headers
+	// include, for example, X-Forwarded-For. The header's value
+	// is assumed to be a comma delimted list of IP addresses where
+	// the client IP is the first IP address in the list. Meek protocols
+	// look for these headers and use the client IP address from
+	// the header if any one is present and the value is a valid
+	// IP address; otherwise the direct connection remote address is
+	// used as the client IP.
+	MeekProxyForwardedForHeaders []string
+
+	// UDPInterceptUdpgwServerAddress specifies the network address of
+	// a udpgw server which clients may be port forwarding to. When
+	// specified, these TCP port forwards are intercepted and handled
+	// directly by this server, which parses the SSH channel using the
+	// udpgw protocol.
+	UDPInterceptUdpgwServerAddress string
+
+	// DNSServerAddress specifies the network address of a DNS server
+	// to which DNS UDP packets will be forwarded to. When set, any
+	// tunneled DNS UDP packets will be re-routed to this destination.
+	UDPForwardDNSServerAddress string
 
 
 	// DefaultTrafficRules specifies the traffic rules to be used when
 	// DefaultTrafficRules specifies the traffic rules to be used when
 	// no regional-specific rules are set.
 	// no regional-specific rules are set.
@@ -171,45 +211,60 @@ type Config struct {
 	// is one or more space delimited ISO 3166-1 alpha-2 country codes.
 	// is one or more space delimited ISO 3166-1 alpha-2 country codes.
 	RegionalTrafficRules map[string]TrafficRules
 	RegionalTrafficRules map[string]TrafficRules
 
 
-	// DNSServerAddress specifies the network address of a DNS server
-	// to which DNS UDP packets will be forwarded to. When set, any
-	// tunneled DNS UDP packets will be re-routed to this destination.
-	DNSServerAddress string
-
-	// UdpgwServerAddress specifies the network address of a udpgw
-	// server which clients may be port forwarding to. When specified,
-	// these TCP port forwards are intercepted and handled directly
-	// by this server, which parses the SSH channel using the udpgw
-	// protocol.
-	UdpgwServerAddress string
-
 	// LoadMonitorPeriodSeconds indicates how frequently to log server
 	// LoadMonitorPeriodSeconds indicates how frequently to log server
 	// load information (number of connected clients per tunnel protocol,
 	// load information (number of connected clients per tunnel protocol,
-	// number of running goroutines, amount of memory allocated).
+	// number of running goroutines, amount of memory allocated, etc.)
 	// The default, 0, disables load logging.
 	// The default, 0, disables load logging.
 	LoadMonitorPeriodSeconds int
 	LoadMonitorPeriodSeconds int
 }
 }
 
 
-// TrafficRules specify the limits placed on client traffic.
-type TrafficRules struct {
+// RateLimits specify the rate limits for tunneled data transfer
+// between an individual client and the server.
+type RateLimits struct {
 
 
-	// LimitDownstreamBytesPerSecond specifies a rate limit for
-	// downstream data transfer between a single client and the
-	// server.
-	// The default, 0, is no rate limit.
-	LimitDownstreamBytesPerSecond int
+	// DownstreamUnlimitedBytes specifies the number of downstream
+	// bytes to transfer, approximately, before starting rate
+	// limiting.
+	DownstreamUnlimitedBytes int64
 
 
-	// LimitUpstreamBytesPerSecond specifies a rate limit for
-	// upstream data transfer between a single client and the
-	// server.
-	// The default, 0, is no rate limit.
-	LimitUpstreamBytesPerSecond int
+	// DownstreamBytesPerSecond specifies a rate limit for downstream
+	// data transfer. The default, 0, is no limit.
+	DownstreamBytesPerSecond int
 
 
-	// IdlePortForwardTimeoutMilliseconds is the timeout period
+	// UpstreamUnlimitedBytes specifies the number of upstream
+	// bytes to transfer, approximately, before starting rate
+	// limiting.
+	UpstreamUnlimitedBytes int64
+
+	// UpstreamBytesPerSecond specifies a rate limit for upstream
+	// data transfer. The default, 0, is no limit.
+	UpstreamBytesPerSecond int
+}
+
+// TrafficRules specify the limits placed on client traffic.
+type TrafficRules struct {
+	// DefaultRateLimitsare the rate limits to be applied when
+	// no protocol-specific rates are set.
+	DefaultRateLimits RateLimits
+
+	// ProtocolRateLimits specifies the rate limits for particular
+	// tunnel protocols. The key for each rate limit entry is one
+	// or more space delimited Psiphon tunnel protocol names. Valid
+	// tunnel protocols includes the same list as for
+	// TunnelProtocolPorts.
+	ProtocolRateLimits map[string]RateLimits
+
+	// IdleTCPPortForwardTimeoutMilliseconds is the timeout period
+	// after which idle (no bytes flowing in either direction)
+	// client TCP port forwards are preemptively closed.
+	// The default, 0, is no idle timeout.
+	IdleTCPPortForwardTimeoutMilliseconds int
+
+	// IdleUDPPortForwardTimeoutMilliseconds is the timeout period
 	// after which idle (no bytes flowing in either direction)
 	// after which idle (no bytes flowing in either direction)
-	// SSH client port forwards are preemptively closed.
+	// client UDP port forwards are preemptively closed.
 	// The default, 0, is no idle timeout.
 	// The default, 0, is no idle timeout.
-	IdlePortForwardTimeoutMilliseconds int
+	IdleUDPPortForwardTimeoutMilliseconds int
 
 
 	// MaxTCPPortForwardCount is the maximum number of TCP port
 	// MaxTCPPortForwardCount is the maximum number of TCP port
 	// forwards each client may have open concurrently.
 	// forwards each client may have open concurrently.
@@ -247,16 +302,6 @@ func (config *Config) RunWebServer() bool {
 	return config.WebServerPort > 0
 	return config.WebServerPort > 0
 }
 }
 
 
-// RunSSHServer indicates whether to run an SSH server component.
-func (config *Config) RunSSHServer() bool {
-	return config.SSHServerPort > 0
-}
-
-// RunObfuscatedSSHServer indicates whether to run an Obfuscated SSH server component.
-func (config *Config) RunObfuscatedSSHServer() bool {
-	return config.ObfuscatedSSHServerPort > 0
-}
-
 // RunLoadMonitor indicates whether to monitor and log server load.
 // RunLoadMonitor indicates whether to monitor and log server load.
 func (config *Config) RunLoadMonitor() bool {
 func (config *Config) RunLoadMonitor() bool {
 	return config.LoadMonitorPeriodSeconds > 0
 	return config.LoadMonitorPeriodSeconds > 0
@@ -275,12 +320,12 @@ func (config *Config) UseFail2Ban() bool {
 }
 }
 
 
 // GetTrafficRules looks up the traffic rules for the specified country. If there
 // GetTrafficRules looks up the traffic rules for the specified country. If there
-// are no RegionalTrafficRules for the country, DefaultTrafficRules are returned.
-func (config *Config) GetTrafficRules(targetCountryCode string) TrafficRules {
+// are no RegionalTrafficRules for the country, DefaultTrafficRules are used.
+func (config *Config) GetTrafficRules(clientCountryCode string) TrafficRules {
 	// TODO: faster lookup?
 	// TODO: faster lookup?
 	for countryCodes, trafficRules := range config.RegionalTrafficRules {
 	for countryCodes, trafficRules := range config.RegionalTrafficRules {
 		for _, countryCode := range strings.Split(countryCodes, " ") {
 		for _, countryCode := range strings.Split(countryCodes, " ") {
-			if countryCode == targetCountryCode {
+			if countryCode == clientCountryCode {
 				return trafficRules
 				return trafficRules
 			}
 			}
 		}
 		}
@@ -288,6 +333,20 @@ func (config *Config) GetTrafficRules(targetCountryCode string) TrafficRules {
 	return config.DefaultTrafficRules
 	return config.DefaultTrafficRules
 }
 }
 
 
+// GetRateLimits looks up the rate limits for the specified tunnel protocol.
+// If there are no ProtocolRateLimits for the protocol, DefaultRateLimits are used.
+func (rules *TrafficRules) GetRateLimits(clientTunnelProtocol string) RateLimits {
+	// TODO: faster lookup?
+	for tunnelProtocols, rateLimits := range rules.ProtocolRateLimits {
+		for _, tunnelProtocol := range strings.Split(tunnelProtocols, " ") {
+			if tunnelProtocol == clientTunnelProtocol {
+				return rateLimits
+			}
+		}
+	}
+	return rules.DefaultRateLimits
+}
+
 // LoadConfig loads and validates a JSON encoded server config. If more than one
 // LoadConfig loads and validates a JSON encoded server config. If more than one
 // JSON config is specified, then all are loaded and values are merged together,
 // JSON config is specified, then all are loaded and values are merged together,
 // in order. Multiple configs allows for use cases like storing static, server-specific
 // in order. Multiple configs allows for use cases like storing static, server-specific
@@ -320,18 +379,38 @@ func LoadConfig(configJSONs [][]byte) (*Config, error) {
 			"Web server requires WebServerSecret, WebServerCertificate, WebServerPrivateKey")
 			"Web server requires WebServerSecret, WebServerCertificate, WebServerPrivateKey")
 	}
 	}
 
 
-	if config.SSHServerPort > 0 && (config.SSHPrivateKey == "" || config.SSHServerVersion == "" ||
-		config.SSHUserName == "" || config.SSHPassword == "") {
-
-		return nil, errors.New(
-			"SSH server requires SSHPrivateKey, SSHServerVersion, SSHUserName, SSHPassword")
-	}
-
-	if config.ObfuscatedSSHServerPort > 0 && (config.SSHPrivateKey == "" || config.SSHServerVersion == "" ||
-		config.SSHUserName == "" || config.SSHPassword == "" || config.ObfuscatedSSHKey == "") {
-
-		return nil, errors.New(
-			"Obfuscated SSH server requires SSHPrivateKey, SSHServerVersion, SSHUserName, SSHPassword, ObfuscatedSSHKey")
+	for tunnelProtocol, _ := range config.TunnelProtocolPorts {
+		if psiphon.TunnelProtocolUsesSSH(tunnelProtocol) ||
+			psiphon.TunnelProtocolUsesObfuscatedSSH(tunnelProtocol) {
+			if config.SSHPrivateKey == "" || config.SSHServerVersion == "" ||
+				config.SSHUserName == "" || config.SSHPassword == "" {
+				return nil, fmt.Errorf(
+					"Tunnel protocol %s requires SSHPrivateKey, SSHServerVersion, SSHUserName, SSHPassword",
+					tunnelProtocol)
+			}
+		}
+		if psiphon.TunnelProtocolUsesObfuscatedSSH(tunnelProtocol) {
+			if config.ObfuscatedSSHKey == "" {
+				return nil, fmt.Errorf(
+					"Tunnel protocol %s requires ObfuscatedSSHKey",
+					tunnelProtocol)
+			}
+		}
+		if psiphon.TunnelProtocolUsesMeekHTTP(tunnelProtocol) ||
+			psiphon.TunnelProtocolUsesMeekHTTPS(tunnelProtocol) {
+			if config.MeekCookieEncryptionPrivateKey == "" || config.MeekObfuscatedKey == "" {
+				return nil, fmt.Errorf(
+					"Tunnel protocol %s requires MeekCookieEncryptionPrivateKey, MeekObfuscatedKey",
+					tunnelProtocol)
+			}
+		}
+		if psiphon.TunnelProtocolUsesMeekHTTPS(tunnelProtocol) {
+			if config.MeekCertificateCommonName == "" {
+				return nil, fmt.Errorf(
+					"Tunnel protocol %s requires MeekCertificateCommonName",
+					tunnelProtocol)
+			}
+		}
 	}
 	}
 
 
 	validateNetworkAddress := func(address string) error {
 	validateNetworkAddress := func(address string) error {
@@ -345,91 +424,53 @@ func LoadConfig(configJSONs [][]byte) (*Config, error) {
 		return err
 		return err
 	}
 	}
 
 
-	if config.DNSServerAddress != "" {
-		if err := validateNetworkAddress(config.DNSServerAddress); err != nil {
-			return nil, fmt.Errorf("DNSServerAddress is invalid: %s", err)
+	if config.UDPForwardDNSServerAddress != "" {
+		if err := validateNetworkAddress(config.UDPForwardDNSServerAddress); err != nil {
+			return nil, fmt.Errorf("UDPForwardDNSServerAddress is invalid: %s", err)
 		}
 		}
 	}
 	}
 
 
-	if config.UdpgwServerAddress != "" {
-		if err := validateNetworkAddress(config.UdpgwServerAddress); err != nil {
-			return nil, fmt.Errorf("UdpgwServerAddress is invalid: %s", err)
+	if config.UDPInterceptUdpgwServerAddress != "" {
+		if err := validateNetworkAddress(config.UDPInterceptUdpgwServerAddress); err != nil {
+			return nil, fmt.Errorf("UDPInterceptUdpgwServerAddress is invalid: %s", err)
 		}
 		}
 	}
 	}
 
 
 	return &config, nil
 	return &config, nil
 }
 }
 
 
-// GenerateConfigParams specifies customizations to be applied to
-// a generated server config.
-type GenerateConfigParams struct {
-
-	// ServerIPAddress is the public IP address of the server.
-	ServerIPAddress string
-
-	// ServerNetworkInterface specifies a network interface to
-	// use to determine the ServerIPAddress automatically. When
-	// set, ServerIPAddress is ignored.
-	ServerNetworkInterface string
-
-	// WebServerPort is the listening port of the web server.
-	// When <= 0, no web server component is run.
-	WebServerPort int
-
-	// SSHServerPort is the listening port of the SSH server.
-	// When <= 0, no SSH server component is run.
-	SSHServerPort int
-
-	// ObfuscatedSSHServerPort is the listening port of the Obfuscated SSH server.
-	// When <= 0, no Obfuscated SSH server component is run.
-	ObfuscatedSSHServerPort int
-}
-
-// GenerateConfig create a new Psiphon server config. It returns a JSON
+// GenerateConfig creates a new Psiphon server config. It returns a JSON
 // encoded config and a client-compatible "server entry" for the server. It
 // encoded config and a client-compatible "server entry" for the server. It
 // generates all necessary secrets and key material, which are emitted in
 // generates all necessary secrets and key material, which are emitted in
 // the config file and server entry as necessary.
 // the config file and server entry as necessary.
-func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, error) {
-
-	serverIPaddress := params.ServerIPAddress
-	if serverIPaddress == "" {
-		serverIPaddress = DEFAULT_SERVER_IP_ADDRESS
-	}
-
-	if params.ServerNetworkInterface != "" {
-		var err error
-		serverIPaddress, err = psiphon.GetInterfaceIPAddress(params.ServerNetworkInterface)
-		if err != nil {
-			return nil, nil, psiphon.ContextError(err)
-		}
-	}
+// GenerateConfig creates a maximal config with many tunnel protocols enabled.
+// It uses sample values for many fields. The intention is for a generated
+// config to be used for testing or as a template for production setup, not
+// to generate production-ready configurations.
+func GenerateConfig(serverIPaddress string) ([]byte, []byte, error) {
 
 
 	// Web server config
 	// Web server config
 
 
-	webServerPort := params.WebServerPort
-	if webServerPort == 0 {
-		webServerPort = DEFAULT_WEB_SERVER_PORT
+	webServerPort := 8088
+
+	webServerSecret, err := psiphon.MakeRandomStringHex(WEB_SERVER_SECRET_BYTE_LENGTH)
+	if err != nil {
+		return nil, nil, psiphon.ContextError(err)
 	}
 	}
 
 
-	webServerSecret, err := psiphon.MakeRandomString(WEB_SERVER_SECRET_BYTE_LENGTH)
+	webServerCertificate, webServerPrivateKey, err := GenerateWebServerCertificate("")
 	if err != nil {
 	if err != nil {
 		return nil, nil, psiphon.ContextError(err)
 		return nil, nil, psiphon.ContextError(err)
 	}
 	}
 
 
-	webServerCertificate, webServerPrivateKey, err := generateWebServerCertificate()
+	discoveryValueHMACKey, err := psiphon.MakeRandomStringBase64(DISCOVERY_VALUE_KEY_BYTE_LENGTH)
 	if err != nil {
 	if err != nil {
 		return nil, nil, psiphon.ContextError(err)
 		return nil, nil, psiphon.ContextError(err)
 	}
 	}
 
 
 	// SSH config
 	// SSH config
 
 
-	sshServerPort := params.SSHServerPort
-	if sshServerPort == 0 {
-		sshServerPort = DEFAULT_SSH_SERVER_PORT
-	}
-
 	// TODO: use other key types: anti-fingerprint by varying params
 	// TODO: use other key types: anti-fingerprint by varying params
-
 	rsaKey, err := rsa.GenerateKey(rand.Reader, SSH_RSA_HOST_KEY_BITS)
 	rsaKey, err := rsa.GenerateKey(rand.Reader, SSH_RSA_HOST_KEY_BITS)
 	if err != nil {
 	if err != nil {
 		return nil, nil, psiphon.ContextError(err)
 		return nil, nil, psiphon.ContextError(err)
@@ -449,14 +490,14 @@ func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, error) {
 
 
 	sshPublicKey := signer.PublicKey()
 	sshPublicKey := signer.PublicKey()
 
 
-	sshUserNameSuffix, err := psiphon.MakeRandomString(SSH_USERNAME_SUFFIX_BYTE_LENGTH)
+	sshUserNameSuffix, err := psiphon.MakeRandomStringHex(SSH_USERNAME_SUFFIX_BYTE_LENGTH)
 	if err != nil {
 	if err != nil {
 		return nil, nil, psiphon.ContextError(err)
 		return nil, nil, psiphon.ContextError(err)
 	}
 	}
 
 
 	sshUserName := "psiphon_" + sshUserNameSuffix
 	sshUserName := "psiphon_" + sshUserNameSuffix
 
 
-	sshPassword, err := psiphon.MakeRandomString(SSH_PASSWORD_BYTE_LENGTH)
+	sshPassword, err := psiphon.MakeRandomStringHex(SSH_PASSWORD_BYTE_LENGTH)
 	if err != nil {
 	if err != nil {
 		return nil, nil, psiphon.ContextError(err)
 		return nil, nil, psiphon.ContextError(err)
 	}
 	}
@@ -466,38 +507,82 @@ func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, error) {
 
 
 	// Obfuscated SSH config
 	// Obfuscated SSH config
 
 
-	obfuscatedSSHServerPort := params.ObfuscatedSSHServerPort
-	if obfuscatedSSHServerPort == 0 {
-		obfuscatedSSHServerPort = DEFAULT_OBFUSCATED_SSH_SERVER_PORT
+	obfuscatedSSHKey, err := psiphon.MakeRandomStringHex(SSH_OBFUSCATED_KEY_BYTE_LENGTH)
+	if err != nil {
+		return nil, nil, psiphon.ContextError(err)
+	}
+
+	// Meek config
+
+	meekCookieEncryptionPublicKey, meekCookieEncryptionPrivateKey, err :=
+		box.GenerateKey(rand.Reader)
+	if err != nil {
+		return nil, nil, psiphon.ContextError(err)
 	}
 	}
 
 
-	obfuscatedSSHKey, err := psiphon.MakeRandomString(SSH_OBFUSCATED_KEY_BYTE_LENGTH)
+	meekObfuscatedKey, err := psiphon.MakeRandomStringHex(SSH_OBFUSCATED_KEY_BYTE_LENGTH)
 	if err != nil {
 	if err != nil {
 		return nil, nil, psiphon.ContextError(err)
 		return nil, nil, psiphon.ContextError(err)
 	}
 	}
 
 
 	// Assemble config and server entry
 	// Assemble config and server entry
 
 
+	// Note: this config is intended for either testing or as an illustrative
+	// example or template and is not intended for production deployment.
+
+	sshPort := 22
+	obfuscatedSSHPort := 53
+	meekPort := 8188
+
 	config := &Config{
 	config := &Config{
-		LogLevel:                DEFAULT_LOG_LEVEL,
-		SyslogFacility:          "",
-		SyslogTag:               DEFAULT_SYSLOG_TAG,
-		Fail2BanFormat:          "",
-		DiscoveryValueHMACKey:   "",
-		GeoIPDatabaseFilename:   DEFAULT_GEO_IP_DATABASE_FILENAME,
-		ServerIPAddress:         serverIPaddress,
-		WebServerPort:           webServerPort,
-		WebServerSecret:         webServerSecret,
-		WebServerCertificate:    webServerCertificate,
-		WebServerPrivateKey:     webServerPrivateKey,
-		SSHPrivateKey:           string(sshPrivateKey),
-		SSHServerVersion:        sshServerVersion,
-		SSHUserName:             sshUserName,
-		SSHPassword:             sshPassword,
-		SSHServerPort:           sshServerPort,
-		ObfuscatedSSHKey:        obfuscatedSSHKey,
-		ObfuscatedSSHServerPort: obfuscatedSSHServerPort,
-		RedisServerAddress:      "",
+		LogLevel:              "info",
+		SyslogFacility:        "user",
+		SyslogTag:             "psiphon-server",
+		Fail2BanFormat:        "Authentication failure for psiphon-client from %s",
+		GeoIPDatabaseFilename: "",
+		ServerIPAddress:       serverIPaddress,
+		DiscoveryValueHMACKey: discoveryValueHMACKey,
+		WebServerPort:         webServerPort,
+		WebServerSecret:       webServerSecret,
+		WebServerCertificate:  webServerCertificate,
+		WebServerPrivateKey:   webServerPrivateKey,
+		SSHPrivateKey:         string(sshPrivateKey),
+		SSHServerVersion:      sshServerVersion,
+		SSHUserName:           sshUserName,
+		SSHPassword:           sshPassword,
+		ObfuscatedSSHKey:      obfuscatedSSHKey,
+		TunnelProtocolPorts: map[string]int{
+			"SSH":                    sshPort,
+			"OSSH":                   obfuscatedSSHPort,
+			"FRONTED-MEEK-OSSH":      443,
+			"UNFRONTED-MEEK-OSSH":    meekPort,
+			"FRONTED-MEEK-HTTP-OSSH": 80,
+		},
+		RedisServerAddress:             "",
+		UDPForwardDNSServerAddress:     "8.8.8.8:53",
+		UDPInterceptUdpgwServerAddress: "127.0.0.1:7300",
+		MeekCookieEncryptionPrivateKey: base64.StdEncoding.EncodeToString(meekCookieEncryptionPrivateKey[:]),
+		MeekObfuscatedKey:              meekObfuscatedKey,
+		MeekCertificateCommonName:      "www.example.org",
+		MeekProhibitedHeaders:          nil,
+		MeekProxyForwardedForHeaders:   []string{"X-Forwarded-For"},
+		DefaultTrafficRules: TrafficRules{
+			DefaultRateLimits: RateLimits{
+				DownstreamUnlimitedBytes: 0,
+				DownstreamBytesPerSecond: 0,
+				UpstreamUnlimitedBytes:   0,
+				UpstreamBytesPerSecond:   0,
+			},
+			IdleTCPPortForwardTimeoutMilliseconds: 30000,
+			IdleUDPPortForwardTimeoutMilliseconds: 30000,
+			MaxTCPPortForwardCount:                1024,
+			MaxUDPPortForwardCount:                32,
+			AllowTCPPorts:                         nil,
+			AllowUDPPorts:                         nil,
+			DenyTCPPorts:                          nil,
+			DenyUDPPorts:                          nil,
+		},
+		LoadMonitorPeriodSeconds: 300,
 	}
 	}
 
 
 	encodedConfig, err := json.MarshalIndent(config, "\n", "    ")
 	encodedConfig, err := json.MarshalIndent(config, "\n", "    ")
@@ -512,21 +597,33 @@ func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, error) {
 	capabilities := []string{
 	capabilities := []string{
 		psiphon.GetCapability(psiphon.TUNNEL_PROTOCOL_SSH),
 		psiphon.GetCapability(psiphon.TUNNEL_PROTOCOL_SSH),
 		psiphon.GetCapability(psiphon.TUNNEL_PROTOCOL_OBFUSCATED_SSH),
 		psiphon.GetCapability(psiphon.TUNNEL_PROTOCOL_OBFUSCATED_SSH),
+		psiphon.GetCapability(psiphon.TUNNEL_PROTOCOL_FRONTED_MEEK),
+		psiphon.GetCapability(psiphon.TUNNEL_PROTOCOL_UNFRONTED_MEEK),
 	}
 	}
 
 
+	// Note: fronting params are a stub; this server entry will exercise
+	// client and server fronting code paths, but not actually traverse
+	// a fronting hop.
+
 	serverEntry := &psiphon.ServerEntry{
 	serverEntry := &psiphon.ServerEntry{
-		IpAddress:            serverIPaddress,
-		WebServerPort:        fmt.Sprintf("%d", webServerPort),
-		WebServerSecret:      webServerSecret,
-		WebServerCertificate: strippedWebServerCertificate,
-		SshPort:              sshServerPort,
-		SshUsername:          sshUserName,
-		SshPassword:          sshPassword,
-		SshHostKey:           base64.RawStdEncoding.EncodeToString(sshPublicKey.Marshal()),
-		SshObfuscatedPort:    obfuscatedSSHServerPort,
-		SshObfuscatedKey:     obfuscatedSSHKey,
-		Capabilities:         capabilities,
-		Region:               "US",
+		IpAddress:                     serverIPaddress,
+		WebServerPort:                 fmt.Sprintf("%d", webServerPort),
+		WebServerSecret:               webServerSecret,
+		WebServerCertificate:          strippedWebServerCertificate,
+		SshPort:                       sshPort,
+		SshUsername:                   sshUserName,
+		SshPassword:                   sshPassword,
+		SshHostKey:                    base64.RawStdEncoding.EncodeToString(sshPublicKey.Marshal()),
+		SshObfuscatedPort:             obfuscatedSSHPort,
+		SshObfuscatedKey:              obfuscatedSSHKey,
+		Capabilities:                  capabilities,
+		Region:                        "US",
+		MeekServerPort:                meekPort,
+		MeekCookieEncryptionPublicKey: base64.StdEncoding.EncodeToString(meekCookieEncryptionPublicKey[:]),
+		MeekObfuscatedKey:             meekObfuscatedKey,
+		MeekFrontingHosts:             []string{serverIPaddress},
+		MeekFrontingAddresses:         []string{serverIPaddress},
+		MeekFrontingDisableSNI:        false,
 	}
 	}
 
 
 	encodedServerEntry, err := psiphon.EncodeServerEntry(serverEntry)
 	encodedServerEntry, err := psiphon.EncodeServerEntry(serverEntry)
@@ -536,67 +633,3 @@ func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, error) {
 
 
 	return encodedConfig, []byte(encodedServerEntry), nil
 	return encodedConfig, []byte(encodedServerEntry), nil
 }
 }
-
-func generateWebServerCertificate() (string, string, error) {
-
-	// Based on https://golang.org/src/crypto/tls/generate_cert.go
-
-	// TODO: use other key types: anti-fingerprint by varying params
-
-	rsaKey, err := rsa.GenerateKey(rand.Reader, WEB_SERVER_CERTIFICATE_RSA_KEY_BITS)
-	if err != nil {
-		return "", "", psiphon.ContextError(err)
-	}
-
-	notBefore := time.Now()
-	notAfter := notBefore.Add(WEB_SERVER_CERTIFICATE_VALIDITY_PERIOD)
-
-	// TODO: psi_ops_install sets serial number to 0?
-	// TODO: psi_ops_install sets RSA exponent to 3, digest type to 'sha1', and version to 2?
-
-	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
-	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
-	if err != nil {
-		return "", "", psiphon.ContextError(err)
-	}
-
-	template := x509.Certificate{
-
-		// TODO: psi_ops_install leaves subject blank?
-		/*
-			Subject: pkix.Name{
-				Organization: []string{""},
-			},
-			IPAddresses: ...
-		*/
-
-		SerialNumber:          serialNumber,
-		NotBefore:             notBefore,
-		NotAfter:              notAfter,
-		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
-		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
-		BasicConstraintsValid: true,
-		IsCA: true,
-	}
-
-	derCert, err := x509.CreateCertificate(rand.Reader, &template, &template, rsaKey.Public(), rsaKey)
-	if err != nil {
-		return "", "", psiphon.ContextError(err)
-	}
-
-	webServerCertificate := pem.EncodeToMemory(
-		&pem.Block{
-			Type:  "CERTIFICATE",
-			Bytes: derCert,
-		},
-	)
-
-	webServerPrivateKey := pem.EncodeToMemory(
-		&pem.Block{
-			Type:  "RSA PRIVATE KEY",
-			Bytes: x509.MarshalPKCS1PrivateKey(rsaKey),
-		},
-	)
-
-	return string(webServerCertificate), string(webServerPrivateKey), nil
-}

+ 783 - 0
psiphon/server/meek.go

@@ -0,0 +1,783 @@
+/*
+ * Copyright (c) 2016, 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 server
+
+import (
+	"bytes"
+	"crypto/tls"
+	"encoding/base64"
+	"encoding/json"
+	"errors"
+	"io"
+	"net"
+	"net/http"
+	"strings"
+	"sync"
+	"sync/atomic"
+	"time"
+
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon"
+	"golang.org/x/crypto/nacl/box"
+)
+
+// MeekServer is based on meek-server.go from Tor and Psiphon:
+//
+// https://gitweb.torproject.org/pluggable-transports/meek.git/blob/HEAD:/meek-client/meek-client.go
+// CC0 1.0 Universal
+//
+// https://bitbucket.org/psiphon/psiphon-circumvention-system/src/default/go/meek-client/meek-client.go
+
+// Protocol version 1 clients can handle arbitrary length response bodies. Older clients
+// report no version number and expect at most 64K response bodies.
+const MEEK_PROTOCOL_VERSION_1 = 1
+
+// Protocol version 2 clients initiate a session by sending a encrypted and obfuscated meek
+// cookie with their initial HTTP request. Connection information is contained within the
+// encrypted cookie payload. The server inspects the cookie and establishes a new session and
+// returns a new random session ID back to client via Set-Cookie header. The client uses this
+// session ID on all subsequent requests for the remainder of the session.
+const MEEK_PROTOCOL_VERSION_2 = 2
+
+// TODO: protocol version 3, to support rapid shutdown of meek connections. Currently, there's no
+// signal from the clients that the payload within meek is EOF and that a given request is the
+// last request for a session; instead, session expiry is always what closes a session.
+
+const MEEK_MAX_PAYLOAD_LENGTH = 0x10000
+const MEEK_TURN_AROUND_TIMEOUT = 20 * time.Millisecond
+const MEEK_EXTENDED_TURN_AROUND_TIMEOUT = 100 * time.Millisecond
+const MEEK_MAX_SESSION_STALENESS = 45 * time.Second
+const MEEK_HTTP_CLIENT_READ_TIMEOUT = 45 * time.Second
+const MEEK_HTTP_CLIENT_WRITE_TIMEOUT = 10 * time.Second
+const MEEK_MIN_SESSION_ID_LENGTH = 8
+const MEEK_MAX_SESSION_ID_LENGTH = 20
+
+// MeekServer implements the meek protocol, which tunnels TCP traffic (in the case of Psiphon,
+// Obfusated SSH traffic) over HTTP. Meek may be fronted (through a CDN) or direct and may be
+// HTTP or HTTPS.
+//
+// Upstream traffic arrives in HTTP request bodies and downstream traffic is sent in response
+// bodies. The sequence of traffic for a given flow is associated using a session ID that's
+// set as a HTTP cookie for the client to submit with each request.
+//
+// MeekServer hooks into TunnelServer via the net.Conn interface by transforming the
+// HTTP payload traffic for a given session into net.Conn conforming Read()s and Write()s via
+// the meekConn struct.
+type MeekServer struct {
+	config        *Config
+	listener      net.Listener
+	tlsConfig     *tls.Config
+	clientHandler func(clientConn net.Conn)
+	openConns     *psiphon.Conns
+	stopBroadcast <-chan struct{}
+	sessionsLock  sync.RWMutex
+	sessions      map[string]*meekSession
+}
+
+// NewMeekServer initializes a new meek server.
+func NewMeekServer(
+	config *Config,
+	listener net.Listener,
+	useTLS bool,
+	clientHandler func(clientConn net.Conn),
+	stopBroadcast <-chan struct{}) (*MeekServer, error) {
+
+	meekServer := &MeekServer{
+		config:        config,
+		listener:      listener,
+		clientHandler: clientHandler,
+		openConns:     new(psiphon.Conns),
+		stopBroadcast: stopBroadcast,
+		sessions:      make(map[string]*meekSession),
+	}
+
+	if useTLS {
+		tlsConfig, err := makeMeekTLSConfig(config)
+		if err != nil {
+			return nil, psiphon.ContextError(err)
+		}
+		meekServer.tlsConfig = tlsConfig
+	}
+
+	return meekServer, nil
+}
+
+// Run runs the meek server; this function blocks while serving HTTP or
+// HTTPS connections on the specified listener. This function also runs
+// a goroutine which cleans up expired meek client sessions.
+//
+// To stop the meek server, both Close() the listener and set the stopBroadcast
+// signal specified in NewMeekServer.
+func (server *MeekServer) Run() error {
+	defer server.listener.Close()
+	defer server.openConns.CloseAll()
+
+	// Expire sessions
+
+	reaperWaitGroup := new(sync.WaitGroup)
+	reaperWaitGroup.Add(1)
+	go func() {
+		defer reaperWaitGroup.Done()
+		ticker := time.NewTicker(MEEK_MAX_SESSION_STALENESS / 2)
+		defer ticker.Stop()
+		for {
+			select {
+			case <-ticker.C:
+				server.closeExpireSessions()
+			case <-server.stopBroadcast:
+				return
+			}
+		}
+	}()
+
+	// Serve HTTP or HTTPS
+
+	httpServer := &http.Server{
+		ReadTimeout:  MEEK_HTTP_CLIENT_READ_TIMEOUT,
+		WriteTimeout: MEEK_HTTP_CLIENT_WRITE_TIMEOUT,
+		Handler:      server,
+		ConnState:    server.httpConnStateCallback,
+
+		// Disable auto HTTP/2 (https://golang.org/doc/go1.6)
+		TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
+	}
+
+	// Note: Serve() will be interrupted by listener.Close() call
+	var err error
+	if server.tlsConfig != nil {
+		httpServer.TLSConfig = server.tlsConfig
+		httpsServer := psiphon.HTTPSServer{Server: *httpServer}
+		err = httpsServer.ServeTLS(server.listener)
+	} else {
+		err = httpServer.Serve(server.listener)
+	}
+
+	// Can't check for the exact error that Close() will cause in Accept(),
+	// (see: https://code.google.com/p/go/issues/detail?id=4373). So using an
+	// explicit stop signal to stop gracefully.
+	select {
+	case <-server.stopBroadcast:
+		err = nil
+	default:
+	}
+
+	reaperWaitGroup.Wait()
+
+	return err
+}
+
+// ServeHTTP handles meek client HTTP requests, where the request body
+// contains upstream traffic and the response will contain downstream
+// traffic.
+func (server *MeekServer) ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) {
+
+	// Note: no longer requiring that the request method is POST
+
+	// Check for the expected meek/session ID cookie.
+	// Also check for prohibited HTTP headers.
+
+	var meekCookie *http.Cookie
+	for _, c := range request.Cookies() {
+		meekCookie = c
+		break
+	}
+	if meekCookie == nil || len(meekCookie.Value) == 0 {
+		log.WithContext().Warning("missing meek cookie")
+		server.terminateConnection(responseWriter, request)
+		return
+	}
+
+	if len(server.config.MeekProhibitedHeaders) > 0 {
+		for _, header := range server.config.MeekProhibitedHeaders {
+			value := request.Header.Get(header)
+			if header != "" {
+				log.WithContextFields(LogFields{
+					"header": header,
+					"value":  value,
+				}).Warning("prohibited meek header")
+				server.terminateConnection(responseWriter, request)
+				return
+			}
+		}
+	}
+
+	// Lookup or create a new session for given meek cookie/session ID.
+
+	sessionID, session, err := server.getSession(request, meekCookie)
+	if err != nil {
+		log.WithContextFields(LogFields{"error": err}).Warning("session lookup failed")
+		server.terminateConnection(responseWriter, request)
+		return
+	}
+
+	// PumpReads causes a TunnelServer/SSH goroutine blocking on a Read to
+	// read the request body as upstream traffic.
+	// TODO: run PumpReads and PumpWrites concurrently?
+
+	err = session.clientConn.PumpReads(request.Body)
+	if err != nil {
+		if err != io.EOF {
+			log.WithContextFields(LogFields{"error": err}).Warning("pump reads failed")
+		}
+		server.terminateConnection(responseWriter, request)
+		server.closeSession(sessionID)
+		return
+	}
+
+	// Set cookie before writing the response.
+
+	if session.meekProtocolVersion >= MEEK_PROTOCOL_VERSION_2 && session.sessionIDSent == false {
+		// Replace the meek cookie with the session ID.
+		// SetCookie for the the session ID cookie is only set once, to reduce overhead. This
+		// session ID value replaces the original meek cookie value.
+		http.SetCookie(responseWriter, &http.Cookie{Name: meekCookie.Name, Value: sessionID})
+		session.sessionIDSent = true
+	}
+
+	// PumpWrites causes a TunnelServer/SSH goroutine blocking on a Write to
+	// write its downstream traffic through to the response body.
+
+	err = session.clientConn.PumpWrites(responseWriter)
+	if err != nil {
+		if err != io.EOF {
+			log.WithContextFields(LogFields{"error": err}).Warning("pump writes failed")
+		}
+		server.terminateConnection(responseWriter, request)
+		server.closeSession(sessionID)
+		return
+	}
+}
+
+// getSession returns the meek client session corresponding the
+// meek cookie/session ID. If no session is found, the cookie is
+// treated as a meek cookie for a new session and its payload is
+// extracted and used to establish a new session.
+func (server *MeekServer) getSession(
+	request *http.Request, meekCookie *http.Cookie) (string, *meekSession, error) {
+
+	// Check for an existing session
+
+	server.sessionsLock.RLock()
+	existingSessionID := meekCookie.Value
+	session, ok := server.sessions[existingSessionID]
+	server.sessionsLock.RUnlock()
+	if ok {
+		session.touch()
+		return existingSessionID, session, nil
+	}
+
+	// TODO: can multiple http client connections using same session cookie
+	// cause race conditions on session struct?
+
+	// The session is new (or expired). Treat the cookie value as a new meek
+	// cookie, extract the payload, and create a new session.
+
+	payloadJSON, err := getMeekCookiePayload(server.config, meekCookie.Value)
+	if err != nil {
+		return "", nil, psiphon.ContextError(err)
+	}
+
+	// Note: this meek server ignores all but Version MeekProtocolVersion;
+	// the other values are legacy or currently unused.
+	var clientSessionData struct {
+		MeekProtocolVersion    int    `json:"v"`
+		PsiphonClientSessionId string `json:"s"`
+		PsiphonServerAddress   string `json:"p"`
+	}
+
+	err = json.Unmarshal(payloadJSON, &clientSessionData)
+	if err != nil {
+		return "", nil, psiphon.ContextError(err)
+	}
+
+	// Determine the client remote address, which is used for geolocation
+	// and stats. When an intermediate proxy of CDN is in use, we may be
+	// able to determine the original client address by inspecting HTTP
+	// headers such as X-Forwarded-For.
+
+	clientIP := strings.Split(request.RemoteAddr, ":")[0]
+
+	if len(server.config.MeekProxyForwardedForHeaders) > 0 {
+		for _, header := range server.config.MeekProxyForwardedForHeaders {
+			value := request.Header.Get(header)
+			if len(value) > 0 {
+				// Some headers, such as X-Forwarded-For, are a comma-separated
+				// list of IPs (each proxy in a chain). The first IP should be
+				// the client IP.
+				proxyClientIP := strings.Split(header, ",")[0]
+				if net.ParseIP(clientIP) != nil {
+					clientIP = proxyClientIP
+					break
+				}
+			}
+		}
+	}
+
+	// Create a new meek conn that will relay the payload
+	// between meek request/responses and the tunnel server client
+	// handler. The client IP is also used to initialize the
+	// meek conn with a useful value to return when the tunnel
+	// server calls conn.RemoteAddr() to get the client's IP address.
+
+	// Assumes clientIP is a value IP address; the port value is a stub
+	// and is expected to be ignored.
+	clientConn := newMeekConn(
+		&net.TCPAddr{
+			IP:   net.ParseIP(clientIP),
+			Port: 0,
+		},
+		clientSessionData.MeekProtocolVersion)
+
+	session = &meekSession{
+		clientConn:          clientConn,
+		meekProtocolVersion: clientSessionData.MeekProtocolVersion,
+		sessionIDSent:       false,
+	}
+	session.touch()
+
+	// Note: MEEK_PROTOCOL_VERSION_1 doesn't support changing the
+	// meek cookie to a session ID; v1 clients always send the
+	// original meek cookie value with each request. The issue with
+	// v1 is that clients which wake after a device sleep will attempt
+	// to resume a meek session and the server can't differentiate
+	// between resuming a session and creating a new session. This
+	// causes the v1 client connection to hang/timeout.
+	sessionID := meekCookie.Value
+	if clientSessionData.MeekProtocolVersion >= MEEK_PROTOCOL_VERSION_2 {
+		sessionID, err = makeMeekSessionID()
+		if err != nil {
+			return "", nil, psiphon.ContextError(err)
+		}
+	}
+
+	server.sessionsLock.Lock()
+	server.sessions[sessionID] = session
+	server.sessionsLock.Unlock()
+
+	// Note: from the tunnel server's perspective, this client connection
+	// will close when closeSessionHelper calls Close() on the meekConn.
+	server.clientHandler(session.clientConn)
+
+	return sessionID, session, nil
+}
+
+func (server *MeekServer) closeSessionHelper(
+	sessionID string, session *meekSession) {
+
+	// TODO: close the persistent HTTP client connection, if one exists
+	session.clientConn.Close()
+	// Note: assumes caller holds lock on sessionsLock
+	delete(server.sessions, sessionID)
+}
+
+func (server *MeekServer) closeSession(sessionID string) {
+	server.sessionsLock.Lock()
+	session, ok := server.sessions[sessionID]
+	if ok {
+		server.closeSessionHelper(sessionID, session)
+	}
+	server.sessionsLock.Unlock()
+}
+
+func (server *MeekServer) closeExpireSessions() {
+	server.sessionsLock.Lock()
+	for sessionID, session := range server.sessions {
+		if session.expired() {
+			server.closeSessionHelper(sessionID, session)
+		}
+	}
+	server.sessionsLock.Unlock()
+}
+
+// httpConnStateCallback tracks open persistent HTTP/HTTPS connections to the
+// meek server.
+func (server *MeekServer) httpConnStateCallback(conn net.Conn, connState http.ConnState) {
+	switch connState {
+	case http.StateNew:
+		server.openConns.Add(conn)
+	case http.StateHijacked, http.StateClosed:
+		server.openConns.Remove(conn)
+	}
+}
+
+// terminateConnection sends a 404 response to a client and also closes
+// a persisitent connection.
+func (server *MeekServer) terminateConnection(
+	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()
+}
+
+type meekSession struct {
+	clientConn          *meekConn
+	meekProtocolVersion int
+	sessionIDSent       bool
+	lastActivity        int64
+}
+
+func (session *meekSession) touch() {
+	atomic.StoreInt64(&session.lastActivity, time.Now().UnixNano())
+}
+
+func (session *meekSession) expired() bool {
+	lastActivity := atomic.LoadInt64(&session.lastActivity)
+	return time.Since(time.Unix(0, lastActivity)) > MEEK_MAX_SESSION_STALENESS
+}
+
+// makeMeekTLSConfig creates a TLS config for a meek HTTPS listener.
+// Currently, this config is optimized for fronted meek where the nature
+// of the connection is non-circumvention; it's optimized for performance
+// assuming the peer is an uncensored CDN.
+func makeMeekTLSConfig(config *Config) (*tls.Config, error) {
+
+	certificate, privateKey, err := GenerateWebServerCertificate(
+		config.MeekCertificateCommonName)
+	if err != nil {
+		return nil, psiphon.ContextError(err)
+	}
+
+	tlsCertificate, err := tls.X509KeyPair(
+		[]byte(certificate), []byte(privateKey))
+	if err != nil {
+		return nil, psiphon.ContextError(err)
+	}
+
+	return &tls.Config{
+		Certificates: []tls.Certificate{tlsCertificate},
+		NextProtos:   []string{"http/1.1"},
+		MinVersion:   tls.VersionTLS10,
+
+		// This is a reordering of the supported CipherSuites in golang 1.6. Non-ephemeral key
+		// CipherSuites greatly reduce server load, and we try to select these since the meek
+		// protocol is providing obfuscation, not privacy/integrity (this is provided by the
+		// tunneled SSH), so we don't benefit from the perfect forward secrecy property provided
+		// by ephemeral key CipherSuites.
+		// https://github.com/golang/go/blob/1cb3044c9fcd88e1557eca1bf35845a4108bc1db/src/crypto/tls/cipher_suites.go#L75
+		CipherSuites: []uint16{
+			tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
+			tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
+			tls.TLS_RSA_WITH_RC4_128_SHA,
+			tls.TLS_RSA_WITH_AES_128_CBC_SHA,
+			tls.TLS_RSA_WITH_AES_256_CBC_SHA,
+			tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
+			tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
+			tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
+			tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
+			tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
+			tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA,
+			tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,
+			tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
+			tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
+			tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
+			tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
+			tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
+		},
+		PreferServerCipherSuites: true,
+	}, nil
+}
+
+// getMeekCookiePayload extracts the payload from a meek cookie. The cookie
+// paylod is base64 encoded, obfuscated, and NaCl encrypted.
+func getMeekCookiePayload(config *Config, cookieValue string) ([]byte, error) {
+	decodedValue, err := base64.StdEncoding.DecodeString(cookieValue)
+	if err != nil {
+		return nil, psiphon.ContextError(err)
+	}
+
+	// The data consists of an obfuscated seed message prepended
+	// to the obfuscated, encrypted payload. The server obfuscator
+	// will read the seed message, leaving the remaining encrypted
+	// data in the reader.
+
+	reader := bytes.NewReader(decodedValue[:])
+
+	obfuscator, err := psiphon.NewServerObfuscator(
+		reader,
+		&psiphon.ObfuscatorConfig{Keyword: config.MeekObfuscatedKey})
+	if err != nil {
+		return nil, psiphon.ContextError(err)
+	}
+
+	offset, err := reader.Seek(0, 1)
+	if err != nil {
+		return nil, psiphon.ContextError(err)
+	}
+	encryptedPayload := decodedValue[offset:]
+
+	obfuscator.ObfuscateClientToServer(encryptedPayload)
+
+	var nonce [24]byte
+	var privateKey, ephemeralPublicKey [32]byte
+
+	decodedPrivateKey, err := base64.StdEncoding.DecodeString(config.MeekCookieEncryptionPrivateKey)
+	if err != nil {
+		return nil, psiphon.ContextError(err)
+	}
+	copy(privateKey[:], decodedPrivateKey)
+
+	if len(encryptedPayload) < 32 {
+		return nil, psiphon.ContextError(errors.New("unexpected encrypted payload size"))
+	}
+	copy(ephemeralPublicKey[0:32], encryptedPayload[0:32])
+
+	payload, ok := box.Open(nil, encryptedPayload[32:], &nonce, &ephemeralPublicKey, &privateKey)
+	if !ok {
+		return nil, psiphon.ContextError(errors.New("open box failed"))
+	}
+
+	return payload, nil
+}
+
+// makeMeekSessionID creates a new session ID. The variable size is intended to
+// frustrate traffic analysis of both plaintext and TLS meek traffic.
+func makeMeekSessionID() (string, error) {
+	size := MEEK_MIN_SESSION_ID_LENGTH
+	n, err := psiphon.MakeSecureRandomInt(MEEK_MAX_SESSION_ID_LENGTH - MEEK_MIN_SESSION_ID_LENGTH)
+	if err != nil {
+		return "", psiphon.ContextError(err)
+	}
+	size += n
+	sessionID, err := psiphon.MakeRandomStringBase64(size)
+	if err != nil {
+		return "", psiphon.ContextError(err)
+	}
+	return sessionID, nil
+}
+
+// meekConn implements the net.Conn interface and is to be used as a client
+// connection by the tunnel server (being passed to sshServer.handleClient).
+// meekConn doesn't perform any real I/O, but instead shuttles io.Readers and
+// io.Writers between goroutines blocking on Read()s and Write()s.
+type meekConn struct {
+	remoteAddr      net.Addr
+	protocolVersion int
+	closeBroadcast  chan struct{}
+	closed          int32
+	readLock        sync.Mutex
+	readyReader     chan io.Reader
+	readResult      chan error
+	writeLock       sync.Mutex
+	nextWriteBuffer chan []byte
+	writeResult     chan error
+}
+
+func newMeekConn(remoteAddr net.Addr, protocolVersion int) *meekConn {
+	return &meekConn{
+		remoteAddr:      remoteAddr,
+		protocolVersion: protocolVersion,
+		closeBroadcast:  make(chan struct{}),
+		closed:          0,
+		readyReader:     make(chan io.Reader, 1),
+		readResult:      make(chan error, 1),
+		nextWriteBuffer: make(chan []byte, 1),
+		writeResult:     make(chan error, 1),
+	}
+}
+
+// PumpReads causes goroutines blocking on meekConn.Read() to read
+// from the specified reader. This function blocks until the reader
+// is fully consumed or the meekConn is closed.
+// Note: channel scheme assumes only one concurrent call to PumpReads
+func (conn *meekConn) PumpReads(reader io.Reader) error {
+
+	// Assumes that readyReader won't block.
+	conn.readyReader <- reader
+
+	// Receiving readResult means Read(s) have consumed the
+	// reader sent to readyReader. readyReader is now empty and
+	// no reference is kept to the reader.
+	select {
+	case err := <-conn.readResult:
+		return err
+	case <-conn.closeBroadcast:
+		return io.EOF
+	}
+}
+
+// Read reads from the meekConn into buffer. Read blocks until
+// some data is read or the meekConn closes. Under the hood, it
+// waits for PumpReads to submit a reader to read from.
+// Note: lock is to conform with net.Conn concurrency semantics
+func (conn *meekConn) Read(buffer []byte) (int, error) {
+	conn.readLock.Lock()
+	defer conn.readLock.Unlock()
+
+	var reader io.Reader
+	select {
+	case reader = <-conn.readyReader:
+	case <-conn.closeBroadcast:
+		return 0, io.EOF
+	}
+
+	n, err := reader.Read(buffer)
+
+	if err != nil {
+		if err == io.EOF {
+			err = nil
+		}
+		// Assumes readerResult won't block.
+		conn.readResult <- err
+	} else {
+		// There may be more data in the reader, but the caller's
+		// buffer is full, so put the reader back into the ready
+		// channel. PumpReads remains blocked waiting for another
+		// Read call.
+		// Note that the reader could be at EOF, while another call is
+		// required to get that result (https://golang.org/pkg/io/#Reader).
+		conn.readyReader <- reader
+	}
+
+	return n, err
+}
+
+// PumpReads causes goroutines blocking on meekConn.Write() to write
+// to the specified writer. This function blocks until the meek response
+// body limits (size for protocol v1, turn around time for protocol v2+)
+// are met, or the meekConn is closed.
+// Note: channel scheme assumes only one concurrent call to PumpWrites
+func (conn *meekConn) PumpWrites(writer io.Writer) error {
+
+	startTime := time.Now()
+	timeout := time.NewTimer(MEEK_TURN_AROUND_TIMEOUT)
+	defer timeout.Stop()
+
+	for {
+		select {
+		case buffer := <-conn.nextWriteBuffer:
+			_, err := writer.Write(buffer)
+
+			// Assumes that writeResult won't block.
+			// Note: always send the err to writeResult,
+			// as the Write() caller is blocking on this.
+			conn.writeResult <- err
+
+			if err != nil {
+				return err
+			}
+
+			if conn.protocolVersion < MEEK_PROTOCOL_VERSION_2 {
+				// Protocol v1 clients expect at most
+				// MEEK_MAX_PAYLOAD_LENGTH response bodies
+				return nil
+			}
+			totalElapsedTime := time.Now().Sub(startTime) / time.Millisecond
+			if totalElapsedTime >= MEEK_EXTENDED_TURN_AROUND_TIMEOUT {
+				return nil
+			}
+			timeout.Reset(MEEK_TURN_AROUND_TIMEOUT)
+		case <-timeout.C:
+			return nil
+		case <-conn.closeBroadcast:
+			return io.EOF
+		}
+	}
+}
+
+// Write writes the buffer to the meekConn. It blocks until the
+// entire buffer is written to or the meekConn closes. Under the
+// hood, it waits for sufficient PumpWrites calls to consume the
+// write buffer.
+// Note: lock is to conform with net.Conn concurrency semantics
+func (conn *meekConn) Write(buffer []byte) (int, error) {
+	conn.writeLock.Lock()
+	defer conn.writeLock.Unlock()
+
+	// TODO: may be more efficient to send whole buffer
+	// and have PumpWrites stash partial buffer when can't
+	// send it all.
+
+	n := 0
+	for n < len(buffer) {
+		end := n + MEEK_MAX_PAYLOAD_LENGTH
+		if end > len(buffer) {
+			end = len(buffer)
+		}
+
+		// Only write MEEK_MAX_PAYLOAD_LENGTH at a time,
+		// to ensure compatibility with v1 protocol.
+		chunk := buffer[n:end]
+
+		select {
+		case conn.nextWriteBuffer <- chunk:
+		case <-conn.closeBroadcast:
+			return n, io.EOF
+		}
+
+		// Wait for the buffer to be processed.
+		select {
+		case err := <-conn.writeResult:
+			if err != nil {
+				return n, err
+			}
+		case <-conn.closeBroadcast:
+			return n, io.EOF
+		}
+		n += len(chunk)
+	}
+	return n, nil
+}
+
+// Close closes the meekConn. This will interrupt any blocked
+// Read, Write, PumpReads, and PumpWrites.
+func (conn *meekConn) Close() error {
+	if atomic.CompareAndSwapInt32(&conn.closed, 0, 1) {
+		close(conn.closeBroadcast)
+	}
+	return nil
+}
+
+// Stub implementation of net.Conn.LocalAddr
+func (conn *meekConn) LocalAddr() net.Addr {
+	return nil
+}
+
+// RemoteAddr returns the remoteAddr specified in newMeekConn. This
+// acts as a proxy for the actual remote address, which is either a
+// direct HTTP/HTTPS connection remote address, or in the case of
+// downstream proxy of CDN fronts, some other value determined via
+// HTTP headers.
+func (conn *meekConn) RemoteAddr() net.Addr {
+	return conn.remoteAddr
+}
+
+// Stub implementation of net.Conn.SetDeadline
+func (conn *meekConn) SetDeadline(t time.Time) error {
+	return psiphon.ContextError(errors.New("not supported"))
+}
+
+// Stub implementation of net.Conn.SetReadDeadline
+func (conn *meekConn) SetReadDeadline(t time.Time) error {
+	return psiphon.ContextError(errors.New("not supported"))
+}
+
+// Stub implementation of net.Conn.SetWriteDeadline
+func (conn *meekConn) SetWriteDeadline(t time.Time) error {
+	return psiphon.ContextError(errors.New("not supported"))
+}

+ 29 - 5
psiphon/server/server_test.go

@@ -41,12 +41,32 @@ func TestMain(m *testing.M) {
 	os.Exit(m.Run())
 	os.Exit(m.Run())
 }
 }
 
 
-func TestServer(t *testing.T) {
+func TestSSH(t *testing.T) {
+	runServer(t, "SSH")
+}
+
+func TestOSSH(t *testing.T) {
+	runServer(t, "OSSH")
+}
+
+func TestFrontedMeek(t *testing.T) {
+	runServer(t, "FRONTED-MEEK-OSSH")
+}
+
+func TestUnfrontedMeek(t *testing.T) {
+	runServer(t, "UNFRONTED-MEEK-OSSH")
+}
+
+func TestFrontedMeekHTTP(t *testing.T) {
+	runServer(t, "FRONTED-MEEK-HTTP-OSSH")
+}
+
+func runServer(t *testing.T, tunnelProtocol string) {
 
 
 	// create a server
 	// create a server
 
 
 	serverConfigFileContents, serverEntryFileContents, err := GenerateConfig(
 	serverConfigFileContents, serverEntryFileContents, err := GenerateConfig(
-		&GenerateConfigParams{})
+		"127.0.0.1")
 	if err != nil {
 	if err != nil {
 		t.Fatalf("error generating server config: %s", err)
 		t.Fatalf("error generating server config: %s", err)
 	}
 	}
@@ -113,7 +133,7 @@ func TestServer(t *testing.T) {
 	clientConfig.DisableRemoteServerListFetcher = true
 	clientConfig.DisableRemoteServerListFetcher = true
 	clientConfig.EstablishTunnelPausePeriodSeconds = &establishTunnelPausePeriodSeconds
 	clientConfig.EstablishTunnelPausePeriodSeconds = &establishTunnelPausePeriodSeconds
 	clientConfig.TargetServerEntry = string(serverEntryFileContents)
 	clientConfig.TargetServerEntry = string(serverEntryFileContents)
-	clientConfig.TunnelProtocol = "OSSH"
+	clientConfig.TunnelProtocol = tunnelProtocol
 	clientConfig.LocalHttpProxyPort = localHTTPProxyPort
 	clientConfig.LocalHttpProxyPort = localHTTPProxyPort
 
 
 	err = psiphon.InitDataStore(clientConfig)
 	err = psiphon.InitDataStore(clientConfig)
@@ -150,10 +170,14 @@ func TestServer(t *testing.T) {
 			}
 			}
 		}))
 		}))
 
 
+	controllerShutdownBroadcast := make(chan struct{})
+	controllerWaitGroup := new(sync.WaitGroup)
+	controllerWaitGroup.Add(1)
 	go func() {
 	go func() {
-		shutdownBroadcast := make(chan struct{})
-		controller.Run(shutdownBroadcast)
+		defer controllerWaitGroup.Done()
+		controller.Run(controllerShutdownBroadcast)
 	}()
 	}()
+	defer close(controllerShutdownBroadcast)
 
 
 	// Test: tunnels must be established within 30 seconds
 	// Test: tunnels must be established within 30 seconds
 
 

+ 64 - 9
psiphon/server/services.go

@@ -26,7 +26,9 @@ package server
 import (
 import (
 	"os"
 	"os"
 	"os/signal"
 	"os/signal"
+	"runtime"
 	"sync"
 	"sync"
+	"time"
 
 
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon"
 )
 )
@@ -67,23 +69,28 @@ func RunServices(encodedConfigs [][]byte) error {
 	shutdownBroadcast := make(chan struct{})
 	shutdownBroadcast := make(chan struct{})
 	errors := make(chan error)
 	errors := make(chan error)
 
 
-	if config.RunWebServer() {
+	tunnelServer, err := NewTunnelServer(config, shutdownBroadcast)
+	if err != nil {
+		log.WithContextFields(LogFields{"error": err}).Error("init tunnel server failed")
+		return psiphon.ContextError(err)
+	}
+
+	if config.RunLoadMonitor() {
 		waitGroup.Add(1)
 		waitGroup.Add(1)
 		go func() {
 		go func() {
-			defer waitGroup.Done()
-			err := RunWebServer(config, shutdownBroadcast)
-			select {
-			case errors <- err:
-			default:
-			}
+			waitGroup.Done()
+			runLoadMonitor(
+				tunnelServer,
+				time.Duration(config.LoadMonitorPeriodSeconds)*time.Second,
+				shutdownBroadcast)
 		}()
 		}()
 	}
 	}
 
 
-	if config.RunSSHServer() || config.RunObfuscatedSSHServer() {
+	if config.RunWebServer() {
 		waitGroup.Add(1)
 		waitGroup.Add(1)
 		go func() {
 		go func() {
 			defer waitGroup.Done()
 			defer waitGroup.Done()
-			err := RunSSHServer(config, shutdownBroadcast)
+			err := RunWebServer(config, shutdownBroadcast)
 			select {
 			select {
 			case errors <- err:
 			case errors <- err:
 			default:
 			default:
@@ -91,6 +98,18 @@ func RunServices(encodedConfigs [][]byte) error {
 		}()
 		}()
 	}
 	}
 
 
+	// The tunnel server is always run; it launches multiple
+	// listeners, depending on which tunnel protocols are enabled.
+	waitGroup.Add(1)
+	go func() {
+		defer waitGroup.Done()
+		err := tunnelServer.Run()
+		select {
+		case errors <- err:
+		default:
+		}
+	}()
+
 	// An OS signal triggers an orderly shutdown
 	// An OS signal triggers an orderly shutdown
 	systemStopSignal := make(chan os.Signal, 1)
 	systemStopSignal := make(chan os.Signal, 1)
 	signal.Notify(systemStopSignal, os.Interrupt, os.Kill)
 	signal.Notify(systemStopSignal, os.Interrupt, os.Kill)
@@ -109,3 +128,39 @@ func RunServices(encodedConfigs [][]byte) error {
 
 
 	return err
 	return err
 }
 }
+
+// runLoadMonitor periodically logs golang runtime and tunnel server stats
+func runLoadMonitor(
+	server *TunnelServer,
+	loadMonitorPeriod time.Duration,
+	shutdownBroadcast <-chan struct{}) {
+
+	ticker := time.NewTicker(loadMonitorPeriod)
+	defer ticker.Stop()
+	for {
+		select {
+		case <-shutdownBroadcast:
+			return
+		case <-ticker.C:
+
+			// golang runtime stats
+			var memStats runtime.MemStats
+			runtime.ReadMemStats(&memStats)
+			fields := LogFields{
+				"NumGoroutine":        runtime.NumGoroutine(),
+				"MemStats.Alloc":      memStats.Alloc,
+				"MemStats.TotalAlloc": memStats.TotalAlloc,
+				"MemStats.Sys":        memStats.Sys,
+			}
+
+			// tunnel server stats
+			for tunnelProtocol, stats := range server.GetLoadStats() {
+				for stat, value := range stats {
+					fields[tunnelProtocol+"."+stat] = value
+				}
+			}
+
+			log.WithContextFields(fields).Info("load")
+		}
+	}
+}

+ 389 - 265
psiphon/server/sshService.go → psiphon/server/tunnelServer.go

@@ -26,7 +26,6 @@ import (
 	"fmt"
 	"fmt"
 	"io"
 	"io"
 	"net"
 	"net"
-	"runtime"
 	"sync"
 	"sync"
 	"sync/atomic"
 	"sync/atomic"
 	"time"
 	"time"
@@ -35,46 +34,66 @@ import (
 	"golang.org/x/crypto/ssh"
 	"golang.org/x/crypto/ssh"
 )
 )
 
 
-// RunSSHServer runs an SSH server, the core tunneling component of the Psiphon
-// server. The SSH server runs a selection of listeners that handle connections
-// using various, optional obfuscation protocols layered on top of SSH.
-// (Currently, just Obfuscated SSH).
-//
-// RunSSHServer listens on the designated port(s) and spawns new goroutines to handle
-// each client connection. It halts when shutdownBroadcast is signaled. A list of active
-// clients is maintained, and when halting all clients are first shutdown.
-//
-// Each client goroutine handles its own obfuscation (optional), SSH handshake, SSH
-// authentication, and then looping on client new channel requests. At this time, only
-// "direct-tcpip" channels, dynamic port fowards, are expected and supported.
-//
-// A new goroutine is spawned to handle each port forward for each client. Each port
-// forward tracks its bytes transferred. Overall per-client stats for connection duration,
-// GeoIP, number of port forwards, and bytes transferred are tracked and logged when the
-// client shuts down.
-func RunSSHServer(
-	config *Config, shutdownBroadcast <-chan struct{}) error {
+// TunnelServer is the main server that accepts Psiphon client
+// connections, via various obfuscation protocols, and provides
+// port forwarding (TCP and UDP) services to the Psiphon client.
+// At its core, TunnelServer is an SSH server. SSH is the base
+// protocol that provides port forward multiplexing, and transport
+// security. Layered on top of SSH, optionally, is Obfuscated SSH
+// and meek protocols, which provide further circumvention
+// capabilities.
+type TunnelServer struct {
+	config            *Config
+	runWaitGroup      *sync.WaitGroup
+	listenerError     chan error
+	shutdownBroadcast <-chan struct{}
+	sshServer         *sshServer
+}
 
 
-	privateKey, err := ssh.ParseRawPrivateKey([]byte(config.SSHPrivateKey))
-	if err != nil {
-		return psiphon.ContextError(err)
-	}
+// NewTunnelServer initializes a new tunnel server.
+func NewTunnelServer(
+	config *Config, shutdownBroadcast <-chan struct{}) (*TunnelServer, error) {
 
 
-	// TODO: use cert (ssh.NewCertSigner) for anti-fingerprint?
-	signer, err := ssh.NewSignerFromKey(privateKey)
+	sshServer, err := newSSHServer(config, shutdownBroadcast)
 	if err != nil {
 	if err != nil {
-		return psiphon.ContextError(err)
+		return nil, psiphon.ContextError(err)
 	}
 	}
 
 
-	sshServer := &sshServer{
+	return &TunnelServer{
 		config:            config,
 		config:            config,
 		runWaitGroup:      new(sync.WaitGroup),
 		runWaitGroup:      new(sync.WaitGroup),
 		listenerError:     make(chan error),
 		listenerError:     make(chan error),
 		shutdownBroadcast: shutdownBroadcast,
 		shutdownBroadcast: shutdownBroadcast,
-		sshHostKey:        signer,
-		nextClientID:      1,
-		clients:           make(map[sshClientID]*sshClient),
-	}
+		sshServer:         sshServer,
+	}, nil
+}
+
+// GetLoadStats returns load stats for the tunnel server. The stats are
+// broken down by protocol ("SSH", "OSSH", etc.) and type. Types of stats
+// include current connected client count, total number of current port
+// forwards.
+func (server *TunnelServer) GetLoadStats() map[string]map[string]int64 {
+	return server.sshServer.getLoadStats()
+}
+
+// Run runs the tunnel server; this function blocks while running a selection of
+// listeners that handle connection using various obfuscation protocols.
+//
+// Run listens on each designated tunnel port and spawns new goroutines to handle
+// each client connection. It halts when shutdownBroadcast is signaled. A list of active
+// clients is maintained, and when halting all clients are cleanly shutdown.
+//
+// Each client goroutine handles its own obfuscation (optional), SSH handshake, SSH
+// authentication, and then looping on client new channel requests. "direct-tcpip"
+// channels, dynamic port fowards, are supported. When the UDPInterceptUdpgwServerAddress
+// config parameter is configured, UDP port forwards over a TCP stream, following
+// the udpgw protocol, are handled.
+//
+// A new goroutine is spawned to handle each port forward for each client. Each port
+// forward tracks its bytes transferred. Overall per-client stats for connection duration,
+// GeoIP, number of port forwards, and bytes transferred are tracked and logged when the
+// client shuts down.
+func (server *TunnelServer) Run() error {
 
 
 	type sshListener struct {
 	type sshListener struct {
 		net.Listener
 		net.Listener
@@ -82,78 +101,75 @@ func RunSSHServer(
 		tunnelProtocol string
 		tunnelProtocol string
 	}
 	}
 
 
+	// First bind all listeners; once all are successful,
+	// start accepting connections on each.
+
 	var listeners []*sshListener
 	var listeners []*sshListener
 
 
-	if config.RunSSHServer() {
-		listeners = append(listeners, &sshListener{
-			localAddress: fmt.Sprintf(
-				"%s:%d", config.ServerIPAddress, config.SSHServerPort),
-			tunnelProtocol: psiphon.TUNNEL_PROTOCOL_SSH,
-		})
-	}
+	for tunnelProtocol, listenPort := range server.config.TunnelProtocolPorts {
 
 
-	if config.RunObfuscatedSSHServer() {
-		listeners = append(listeners, &sshListener{
-			localAddress: fmt.Sprintf(
-				"%s:%d", config.ServerIPAddress, config.ObfuscatedSSHServerPort),
-			tunnelProtocol: psiphon.TUNNEL_PROTOCOL_OBFUSCATED_SSH,
-		})
-	}
-
-	// TODO: add additional protocol listeners here (e.g, meek)
+		localAddress := fmt.Sprintf(
+			"%s:%d", server.config.ServerIPAddress, listenPort)
 
 
-	for i, listener := range listeners {
-		var err error
-		listener.Listener, err = net.Listen("tcp", listener.localAddress)
+		listener, err := net.Listen("tcp", localAddress)
 		if err != nil {
 		if err != nil {
-			for j := 0; j < i; j++ {
-				listener.Listener.Close()
+			for _, existingListener := range listeners {
+				existingListener.Listener.Close()
 			}
 			}
 			return psiphon.ContextError(err)
 			return psiphon.ContextError(err)
 		}
 		}
+
 		log.WithContextFields(
 		log.WithContextFields(
 			LogFields{
 			LogFields{
-				"localAddress":   listener.localAddress,
-				"tunnelProtocol": listener.tunnelProtocol,
+				"localAddress":   localAddress,
+				"tunnelProtocol": tunnelProtocol,
 			}).Info("listening")
 			}).Info("listening")
+
+		listeners = append(
+			listeners,
+			&sshListener{
+				Listener:       listener,
+				localAddress:   localAddress,
+				tunnelProtocol: tunnelProtocol,
+			})
 	}
 	}
 
 
 	for _, listener := range listeners {
 	for _, listener := range listeners {
-		sshServer.runWaitGroup.Add(1)
+		server.runWaitGroup.Add(1)
 		go func(listener *sshListener) {
 		go func(listener *sshListener) {
-			defer sshServer.runWaitGroup.Done()
+			defer server.runWaitGroup.Done()
 
 
-			sshServer.runListener(
-				listener.Listener, listener.tunnelProtocol)
+			log.WithContextFields(
+				LogFields{
+					"localAddress":   listener.localAddress,
+					"tunnelProtocol": listener.tunnelProtocol,
+				}).Info("running")
+
+			server.sshServer.runListener(
+				listener.Listener,
+				server.listenerError,
+				listener.tunnelProtocol)
 
 
 			log.WithContextFields(
 			log.WithContextFields(
 				LogFields{
 				LogFields{
 					"localAddress":   listener.localAddress,
 					"localAddress":   listener.localAddress,
 					"tunnelProtocol": listener.tunnelProtocol,
 					"tunnelProtocol": listener.tunnelProtocol,
-				}).Info("stopping")
+				}).Info("stopped")
 
 
 		}(listener)
 		}(listener)
 	}
 	}
 
 
-	if config.RunLoadMonitor() {
-		sshServer.runWaitGroup.Add(1)
-		go func() {
-			defer sshServer.runWaitGroup.Done()
-			sshServer.runLoadMonitor()
-		}()
-	}
-
-	err = nil
+	var err error
 	select {
 	select {
-	case <-sshServer.shutdownBroadcast:
-	case err = <-sshServer.listenerError:
+	case <-server.shutdownBroadcast:
+	case err = <-server.listenerError:
 	}
 	}
 
 
 	for _, listener := range listeners {
 	for _, listener := range listeners {
 		listener.Close()
 		listener.Close()
 	}
 	}
-	sshServer.stopClients()
-	sshServer.runWaitGroup.Wait()
+	server.sshServer.stopClients()
+	server.runWaitGroup.Wait()
 
 
 	log.WithContext().Info("stopped")
 	log.WithContext().Info("stopped")
 
 
@@ -164,8 +180,6 @@ type sshClientID uint64
 
 
 type sshServer struct {
 type sshServer struct {
 	config            *Config
 	config            *Config
-	runWaitGroup      *sync.WaitGroup
-	listenerError     chan error
 	shutdownBroadcast <-chan struct{}
 	shutdownBroadcast <-chan struct{}
 	sshHostKey        ssh.Signer
 	sshHostKey        ssh.Signer
 	nextClientID      sshClientID
 	nextClientID      sshClientID
@@ -174,69 +188,96 @@ type sshServer struct {
 	clients           map[sshClientID]*sshClient
 	clients           map[sshClientID]*sshClient
 }
 }
 
 
+func newSSHServer(
+	config *Config,
+	shutdownBroadcast <-chan struct{}) (*sshServer, error) {
+
+	privateKey, err := ssh.ParseRawPrivateKey([]byte(config.SSHPrivateKey))
+	if err != nil {
+		return nil, psiphon.ContextError(err)
+	}
+
+	// TODO: use cert (ssh.NewCertSigner) for anti-fingerprint?
+	signer, err := ssh.NewSignerFromKey(privateKey)
+	if err != nil {
+		return nil, psiphon.ContextError(err)
+	}
+
+	return &sshServer{
+		config:            config,
+		shutdownBroadcast: shutdownBroadcast,
+		sshHostKey:        signer,
+		nextClientID:      1,
+		clients:           make(map[sshClientID]*sshClient),
+	}, nil
+}
+
+// runListener is intended to run an a goroutine; it blocks
+// running a particular listener. If an unrecoverable error
+// occurs, it will send the error to the listenerError channel.
 func (sshServer *sshServer) runListener(
 func (sshServer *sshServer) runListener(
-	listener net.Listener, tunnelProtocol string) {
+	listener net.Listener,
+	listenerError chan<- error,
+	tunnelProtocol string) {
 
 
-	for {
-		conn, err := listener.Accept()
+	handleClient := func(clientConn net.Conn) {
+		// process each client connection concurrently
+		go sshServer.handleClient(tunnelProtocol, clientConn)
+	}
 
 
-		if err == nil && tunnelProtocol == psiphon.TUNNEL_PROTOCOL_OBFUSCATED_SSH {
-			conn, err = psiphon.NewObfuscatedSshConn(
-				psiphon.OBFUSCATION_CONN_MODE_SERVER,
-				conn,
-				sshServer.config.ObfuscatedSSHKey)
-		}
+	// Note: when exiting due to a unrecoverable error, be sure
+	// to try to send the error to listenerError so that the outer
+	// TunnelServer.Run will properly shut down instead of remaining
+	// running.
 
 
-		select {
-		case <-sshServer.shutdownBroadcast:
-			if err == nil {
-				conn.Close()
-			}
-			return
-		default:
-		}
+	if psiphon.TunnelProtocolUsesMeekHTTP(tunnelProtocol) ||
+		psiphon.TunnelProtocolUsesMeekHTTPS(tunnelProtocol) {
 
 
+		meekServer, err := NewMeekServer(
+			sshServer.config,
+			listener,
+			psiphon.TunnelProtocolUsesMeekHTTPS(tunnelProtocol),
+			handleClient,
+			sshServer.shutdownBroadcast)
 		if err != nil {
 		if err != nil {
-			if e, ok := err.(net.Error); ok && e.Temporary() {
-				log.WithContextFields(LogFields{"error": err}).Error("accept failed")
-				// Temporary error, keep running
-				continue
-			}
-
 			select {
 			select {
-			case sshServer.listenerError <- psiphon.ContextError(err):
+			case listenerError <- psiphon.ContextError(err):
 			default:
 			default:
 			}
 			}
-
 			return
 			return
 		}
 		}
 
 
-		// process each client connection concurrently
-		go sshServer.handleClient(tunnelProtocol, conn)
-	}
-}
+		meekServer.Run()
 
 
-func (sshServer *sshServer) runLoadMonitor() {
-	ticker := time.NewTicker(
-		time.Duration(sshServer.config.LoadMonitorPeriodSeconds) * time.Second)
-	defer ticker.Stop()
-	for {
-		select {
-		case <-sshServer.shutdownBroadcast:
-			return
-		case <-ticker.C:
-			var memStats runtime.MemStats
-			runtime.ReadMemStats(&memStats)
-			fields := LogFields{
-				"goroutines":    runtime.NumGoroutine(),
-				"memAlloc":      memStats.Alloc,
-				"memTotalAlloc": memStats.TotalAlloc,
-				"memSysAlloc":   memStats.Sys,
+	} else {
+
+		for {
+			conn, err := listener.Accept()
+
+			select {
+			case <-sshServer.shutdownBroadcast:
+				if err == nil {
+					conn.Close()
+				}
+				return
+			default:
 			}
 			}
-			for tunnelProtocol, count := range sshServer.countClients() {
-				fields[tunnelProtocol] = count
+
+			if err != nil {
+				if e, ok := err.(net.Error); ok && e.Temporary() {
+					log.WithContextFields(LogFields{"error": err}).Error("accept failed")
+					// Temporary error, keep running
+					continue
+				}
+
+				select {
+				case listenerError <- psiphon.ContextError(err):
+				default:
+				}
+				return
 			}
 			}
-			log.WithContextFields(fields).Info("load")
+
+			handleClient(conn)
 		}
 		}
 	}
 	}
 }
 }
@@ -270,16 +311,26 @@ func (sshServer *sshServer) unregisterClient(clientID sshClientID) {
 	}
 	}
 }
 }
 
 
-func (sshServer *sshServer) countClients() map[string]int {
+func (sshServer *sshServer) getLoadStats() map[string]map[string]int64 {
 
 
 	sshServer.clientsMutex.Lock()
 	sshServer.clientsMutex.Lock()
 	defer sshServer.clientsMutex.Unlock()
 	defer sshServer.clientsMutex.Unlock()
 
 
-	counts := make(map[string]int)
+	loadStats := make(map[string]map[string]int64)
 	for _, client := range sshServer.clients {
 	for _, client := range sshServer.clients {
-		counts[client.tunnelProtocol] += 1
-	}
-	return counts
+		if loadStats[client.tunnelProtocol] == nil {
+			loadStats[client.tunnelProtocol] = make(map[string]int64)
+		}
+		// Note: can't sum trafficState.peakConcurrentPortForwardCount to get a global peak
+		loadStats[client.tunnelProtocol]["CurrentClients"] += 1
+		client.Lock()
+		loadStats[client.tunnelProtocol]["CurrentTCPPortForwards"] += client.tcpTrafficState.concurrentPortForwardCount
+		loadStats[client.tunnelProtocol]["TotalTCPPortForwards"] += client.tcpTrafficState.totalPortForwardCount
+		loadStats[client.tunnelProtocol]["CurrentUDPPortForwards"] += client.udpTrafficState.concurrentPortForwardCount
+		loadStats[client.tunnelProtocol]["TotalUDPPortForwards"] += client.udpTrafficState.totalPortForwardCount
+		client.Unlock()
+	}
+	return loadStats
 }
 }
 
 
 func (sshServer *sshServer) stopClients() {
 func (sshServer *sshServer) stopClients() {
@@ -304,22 +355,27 @@ func (sshServer *sshServer) handleClient(tunnelProtocol string, clientConn net.C
 		geoIPData,
 		geoIPData,
 		sshServer.config.GetTrafficRules(geoIPData.Country))
 		sshServer.config.GetTrafficRules(geoIPData.Country))
 
 
-	// Wrap the base client connection with an IdleTimeoutConn which will terminate
-	// the connection if no data is received before the deadline. This timeout is
-	// in effect for the entire duration of the SSH connection. Clients must actively
-	// use the connection or send SSH keep alive requests to keep the connection
-	// active.
+	// Wrap the base client connection with an ActivityMonitoredConn which will
+	// terminate the connection if no data is received before the deadline. This
+	// timeout is in effect for the entire duration of the SSH connection. Clients
+	// must actively use the connection or send SSH keep alive requests to keep
+	// the connection active.
 
 
-	var conn net.Conn
-
-	conn = psiphon.NewIdleTimeoutConn(clientConn, SSH_CONNECTION_READ_DEADLINE, false)
+	clientConn = psiphon.NewActivityMonitoredConn(
+		clientConn,
+		SSH_CONNECTION_READ_DEADLINE,
+		false,
+		nil)
 
 
 	// Further wrap the connection in a rate limiting ThrottledConn.
 	// Further wrap the connection in a rate limiting ThrottledConn.
 
 
-	conn = psiphon.NewThrottledConn(
-		conn,
-		int64(sshClient.trafficRules.LimitDownstreamBytesPerSecond),
-		int64(sshClient.trafficRules.LimitUpstreamBytesPerSecond))
+	rateLimits := sshClient.trafficRules.GetRateLimits(tunnelProtocol)
+	clientConn = psiphon.NewThrottledConn(
+		clientConn,
+		rateLimits.DownstreamUnlimitedBytes,
+		int64(rateLimits.DownstreamBytesPerSecond),
+		rateLimits.UpstreamUnlimitedBytes,
+		int64(rateLimits.UpstreamBytesPerSecond))
 
 
 	// Run the initial [obfuscated] SSH handshake in a goroutine so we can both
 	// Run the initial [obfuscated] SSH handshake in a goroutine so we can both
 	// respect shutdownBroadcast and implement a specific handshake timeout.
 	// respect shutdownBroadcast and implement a specific handshake timeout.
@@ -350,17 +406,30 @@ func (sshServer *sshServer) handleClient(tunnelProtocol string, clientConn net.C
 		}
 		}
 		sshServerConfig.AddHostKey(sshServer.sshHostKey)
 		sshServerConfig.AddHostKey(sshServer.sshHostKey)
 
 
-		sshConn, channels, requests, err :=
-			ssh.NewServerConn(conn, sshServerConfig)
+		result := &sshNewServerConnResult{}
+
+		// Wrap the connection in an SSH deobfuscator when required.
 
 
-		resultChannel <- &sshNewServerConnResult{
-			conn:     conn,
-			sshConn:  sshConn,
-			channels: channels,
-			requests: requests,
-			err:      err,
+		if psiphon.TunnelProtocolUsesObfuscatedSSH(tunnelProtocol) {
+			// Note: NewObfuscatedSshConn blocks on network I/O
+			// TODO: ensure this won't block shutdown
+			conn, result.err = psiphon.NewObfuscatedSshConn(
+				psiphon.OBFUSCATION_CONN_MODE_SERVER,
+				clientConn,
+				sshServer.config.ObfuscatedSSHKey)
+			if result.err != nil {
+				result.err = psiphon.ContextError(result.err)
+			}
+		}
+
+		if result.err == nil {
+			result.sshConn, result.channels, result.requests, result.err =
+				ssh.NewServerConn(conn, sshServerConfig)
 		}
 		}
-	}(conn)
+
+		resultChannel <- result
+
+	}(clientConn)
 
 
 	var result *sshNewServerConnResult
 	var result *sshNewServerConnResult
 	select {
 	select {
@@ -368,13 +437,16 @@ func (sshServer *sshServer) handleClient(tunnelProtocol string, clientConn net.C
 	case <-sshServer.shutdownBroadcast:
 	case <-sshServer.shutdownBroadcast:
 		// Close() will interrupt an ongoing handshake
 		// Close() will interrupt an ongoing handshake
 		// TODO: wait for goroutine to exit before returning?
 		// TODO: wait for goroutine to exit before returning?
-		conn.Close()
+		clientConn.Close()
 		return
 		return
 	}
 	}
 
 
 	if result.err != nil {
 	if result.err != nil {
-		conn.Close()
-		log.WithContextFields(LogFields{"error": result.err}).Warning("handshake failed")
+		clientConn.Close()
+		// This is a Debug log due to noise. The handshake often fails due to I/O
+		// errors as clients frequently interrupt connections in progress when
+		// client-side load balancing completes a connection to a different server.
+		log.WithContextFields(LogFields{"error": result.err}).Debug("handshake failed")
 		return
 		return
 	}
 	}
 
 
@@ -384,7 +456,7 @@ func (sshServer *sshServer) handleClient(tunnelProtocol string, clientConn net.C
 
 
 	clientID, ok := sshServer.registerClient(sshClient)
 	clientID, ok := sshServer.registerClient(sshClient)
 	if !ok {
 	if !ok {
-		conn.Close()
+		clientConn.Close()
 		log.WithContext().Warning("register failed")
 		log.WithContext().Warning("register failed")
 		return
 		return
 	}
 	}
@@ -393,6 +465,8 @@ func (sshServer *sshServer) handleClient(tunnelProtocol string, clientConn net.C
 	go ssh.DiscardRequests(result.requests)
 	go ssh.DiscardRequests(result.requests)
 
 
 	sshClient.handleChannels(result.channels)
 	sshClient.handleChannels(result.channels)
+
+	// TODO: clientConn.Close()?
 }
 }
 
 
 type sshClient struct {
 type sshClient struct {
@@ -408,15 +482,16 @@ type sshClient struct {
 	tcpTrafficState         *trafficState
 	tcpTrafficState         *trafficState
 	udpTrafficState         *trafficState
 	udpTrafficState         *trafficState
 	channelHandlerWaitGroup *sync.WaitGroup
 	channelHandlerWaitGroup *sync.WaitGroup
+	tcpPortForwardLRU       *psiphon.LRUConns
 	stopBroadcast           chan struct{}
 	stopBroadcast           chan struct{}
 }
 }
 
 
 type trafficState struct {
 type trafficState struct {
 	bytesUp                        int64
 	bytesUp                        int64
 	bytesDown                      int64
 	bytesDown                      int64
-	portForwardCount               int64
 	concurrentPortForwardCount     int64
 	concurrentPortForwardCount     int64
 	peakConcurrentPortForwardCount int64
 	peakConcurrentPortForwardCount int64
+	totalPortForwardCount          int64
 }
 }
 
 
 func newSshClient(
 func newSshClient(
@@ -430,10 +505,94 @@ func newSshClient(
 		tcpTrafficState:         &trafficState{},
 		tcpTrafficState:         &trafficState{},
 		udpTrafficState:         &trafficState{},
 		udpTrafficState:         &trafficState{},
 		channelHandlerWaitGroup: new(sync.WaitGroup),
 		channelHandlerWaitGroup: new(sync.WaitGroup),
+		tcpPortForwardLRU:       psiphon.NewLRUConns(),
 		stopBroadcast:           make(chan struct{}),
 		stopBroadcast:           make(chan struct{}),
 	}
 	}
 }
 }
 
 
+func (sshClient *sshClient) passwordCallback(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
+	var sshPasswordPayload struct {
+		SessionId   string `json:"SessionId"`
+		SshPassword string `json:"SshPassword"`
+	}
+	err := json.Unmarshal(password, &sshPasswordPayload)
+	if err != nil {
+		return nil, psiphon.ContextError(fmt.Errorf("invalid password payload for %q", conn.User()))
+	}
+
+	userOk := (subtle.ConstantTimeCompare(
+		[]byte(conn.User()), []byte(sshClient.sshServer.config.SSHUserName)) == 1)
+
+	passwordOk := (subtle.ConstantTimeCompare(
+		[]byte(sshPasswordPayload.SshPassword), []byte(sshClient.sshServer.config.SSHPassword)) == 1)
+
+	if !userOk || !passwordOk {
+		return nil, psiphon.ContextError(fmt.Errorf("invalid password for %q", conn.User()))
+	}
+
+	psiphonSessionID := sshPasswordPayload.SessionId
+
+	sshClient.Lock()
+	sshClient.psiphonSessionID = psiphonSessionID
+	geoIPData := sshClient.geoIPData
+	sshClient.Unlock()
+
+	if sshClient.sshServer.config.UseRedis() {
+		err = UpdateRedisForLegacyPsiWeb(psiphonSessionID, geoIPData)
+		if err != nil {
+			log.WithContextFields(LogFields{
+				"psiphonSessionID": psiphonSessionID,
+				"error":            err}).Warning("UpdateRedisForLegacyPsiWeb failed")
+			// Allow the connection to proceed; legacy psi_web will not get accurate GeoIP values.
+		}
+	}
+
+	return nil, nil
+}
+
+func (sshClient *sshClient) authLogCallback(conn ssh.ConnMetadata, method string, err error) {
+	if err != nil {
+		if sshClient.sshServer.config.UseFail2Ban() {
+			clientIPAddress := psiphon.IPAddressFromAddr(conn.RemoteAddr())
+			if clientIPAddress != "" {
+				LogFail2Ban(clientIPAddress)
+			}
+		}
+		log.WithContextFields(LogFields{"error": err, "method": method}).Debug("authentication failed")
+	} else {
+		log.WithContextFields(LogFields{"error": err, "method": method}).Debug("authentication success")
+	}
+}
+
+func (sshClient *sshClient) stop() {
+
+	sshClient.sshConn.Close()
+	sshClient.sshConn.Wait()
+
+	close(sshClient.stopBroadcast)
+	sshClient.channelHandlerWaitGroup.Wait()
+
+	sshClient.Lock()
+	log.WithContextFields(
+		LogFields{
+			"startTime":                         sshClient.startTime,
+			"duration":                          time.Now().Sub(sshClient.startTime),
+			"psiphonSessionID":                  sshClient.psiphonSessionID,
+			"country":                           sshClient.geoIPData.Country,
+			"city":                              sshClient.geoIPData.City,
+			"ISP":                               sshClient.geoIPData.ISP,
+			"bytesUpTCP":                        sshClient.tcpTrafficState.bytesUp,
+			"bytesDownTCP":                      sshClient.tcpTrafficState.bytesDown,
+			"peakConcurrentPortForwardCountTCP": sshClient.tcpTrafficState.peakConcurrentPortForwardCount,
+			"totalPortForwardCountTCP":          sshClient.tcpTrafficState.totalPortForwardCount,
+			"bytesUpUDP":                        sshClient.udpTrafficState.bytesUp,
+			"bytesDownUDP":                      sshClient.udpTrafficState.bytesDown,
+			"peakConcurrentPortForwardCountUDP": sshClient.udpTrafficState.peakConcurrentPortForwardCount,
+			"totalPortForwardCountUDP":          sshClient.udpTrafficState.totalPortForwardCount,
+		}).Info("tunnel closed")
+	sshClient.Unlock()
+}
+
 func (sshClient *sshClient) handleChannels(channels <-chan ssh.NewChannel) {
 func (sshClient *sshClient) handleChannels(channels <-chan ssh.NewChannel) {
 	for newChannel := range channels {
 	for newChannel := range channels {
 
 
@@ -478,8 +637,8 @@ func (sshClient *sshClient) handleNewPortForwardChannel(newChannel ssh.NewChanne
 
 
 	// Intercept TCP port forwards to a specified udpgw server and handle directly.
 	// Intercept TCP port forwards to a specified udpgw server and handle directly.
 	// TODO: also support UDP explicitly, e.g. with a custom "direct-udp" channel type?
 	// TODO: also support UDP explicitly, e.g. with a custom "direct-udp" channel type?
-	isUDPChannel := sshClient.sshServer.config.UdpgwServerAddress != "" &&
-		sshClient.sshServer.config.UdpgwServerAddress ==
+	isUDPChannel := sshClient.sshServer.config.UDPInterceptUdpgwServerAddress != "" &&
+		sshClient.sshServer.config.UDPInterceptUdpgwServerAddress ==
 			fmt.Sprintf("%s:%d",
 			fmt.Sprintf("%s:%d",
 				directTcpipExtraData.HostToConnect,
 				directTcpipExtraData.HostToConnect,
 				directTcpipExtraData.PortToConnect)
 				directTcpipExtraData.PortToConnect)
@@ -496,7 +655,7 @@ func (sshClient *sshClient) isPortForwardPermitted(
 	port int, allowPorts []int, denyPorts []int) bool {
 	port int, allowPorts []int, denyPorts []int) bool {
 
 
 	// TODO: faster lookup?
 	// TODO: faster lookup?
-	if allowPorts != nil {
+	if len(allowPorts) > 0 {
 		for _, allowPort := range allowPorts {
 		for _, allowPort := range allowPorts {
 			if port == allowPort {
 			if port == allowPort {
 				return true
 				return true
@@ -504,7 +663,7 @@ func (sshClient *sshClient) isPortForwardPermitted(
 		}
 		}
 		return false
 		return false
 	}
 	}
-	if denyPorts != nil {
+	if len(denyPorts) > 0 {
 		for _, denyPort := range denyPorts {
 		for _, denyPort := range denyPorts {
 			if port == denyPort {
 			if port == denyPort {
 				return false
 				return false
@@ -520,7 +679,7 @@ func (sshClient *sshClient) isPortForwardLimitExceeded(
 	limitExceeded := false
 	limitExceeded := false
 	if maxPortForwardCount > 0 {
 	if maxPortForwardCount > 0 {
 		sshClient.Lock()
 		sshClient.Lock()
-		limitExceeded = state.portForwardCount >= int64(maxPortForwardCount)
+		limitExceeded = state.concurrentPortForwardCount >= int64(maxPortForwardCount)
 		sshClient.Unlock()
 		sshClient.Unlock()
 	}
 	}
 	return limitExceeded
 	return limitExceeded
@@ -530,11 +689,11 @@ func (sshClient *sshClient) openedPortForward(
 	state *trafficState) {
 	state *trafficState) {
 
 
 	sshClient.Lock()
 	sshClient.Lock()
-	state.portForwardCount += 1
 	state.concurrentPortForwardCount += 1
 	state.concurrentPortForwardCount += 1
 	if state.concurrentPortForwardCount > state.peakConcurrentPortForwardCount {
 	if state.concurrentPortForwardCount > state.peakConcurrentPortForwardCount {
 		state.peakConcurrentPortForwardCount = state.concurrentPortForwardCount
 		state.peakConcurrentPortForwardCount = state.concurrentPortForwardCount
 	}
 	}
+	state.totalPortForwardCount += 1
 	sshClient.Unlock()
 	sshClient.Unlock()
 }
 }
 
 
@@ -565,8 +724,12 @@ func (sshClient *sshClient) handleTCPChannel(
 
 
 	var bytesUp, bytesDown int64
 	var bytesUp, bytesDown int64
 	sshClient.openedPortForward(sshClient.tcpTrafficState)
 	sshClient.openedPortForward(sshClient.tcpTrafficState)
-	defer sshClient.closedPortForward(
-		sshClient.tcpTrafficState, atomic.LoadInt64(&bytesUp), atomic.LoadInt64(&bytesDown))
+	defer func() {
+		sshClient.closedPortForward(
+			sshClient.tcpTrafficState,
+			atomic.LoadInt64(&bytesUp),
+			atomic.LoadInt64(&bytesDown))
+	}()
 
 
 	// TOCTOU note: important to increment the port forward count (via
 	// TOCTOU note: important to increment the port forward count (via
 	// openPortForward) _before_ checking isPortForwardLimitExceeded
 	// openPortForward) _before_ checking isPortForwardLimitExceeded
@@ -578,11 +741,43 @@ func (sshClient *sshClient) handleTCPChannel(
 		sshClient.tcpTrafficState,
 		sshClient.tcpTrafficState,
 		sshClient.trafficRules.MaxTCPPortForwardCount) {
 		sshClient.trafficRules.MaxTCPPortForwardCount) {
 
 
-		sshClient.rejectNewChannel(
-			newChannel, ssh.Prohibited, "maximum port forward limit exceeded")
-		return
+		// Close the oldest TCP port forward. CloseOldest() closes
+		// the conn and the port forward's goroutine will complete
+		// the cleanup asynchronously.
+		//
+		// Some known limitations:
+		//
+		// - Since CloseOldest() closes the upstream socket but does not
+		//   clean up all resources associated with the port forward. These
+		//   include the goroutine(s) relaying traffic as well as the SSH
+		//   channel. Closing the socket will interrupt the goroutines which
+		//   will then complete the cleanup. But, since the full cleanup is
+		//   asynchronous, there exists a possibility that a client can consume
+		//   more than max port forward resources -- just not upstream sockets.
+		//
+		// - An LRU list entry for this port forward is not added until
+		//   after the dial completes, but the port forward is counted
+		//   towards max limits. This means many dials in progress will
+		//   put established connections in jeopardy.
+		//
+		// - We're closing the oldest open connection _before_ successfully
+		//   dialing the new port forward. This means we are potentially
+		//   discarding a good connection to make way for a failed connection.
+		//   We cannot simply dial first and still maintain a limit on
+		//   resources used, so to address this we'd need to add some
+		//   accounting for connections still establishing.
+
+		sshClient.tcpPortForwardLRU.CloseOldest()
+
+		log.WithContextFields(
+			LogFields{
+				"maxCount": sshClient.trafficRules.MaxTCPPortForwardCount,
+			}).Debug("closed LRU TCP port forward")
 	}
 	}
 
 
+	// Dial the target remote address. This is done in a goroutine to
+	// ensure the shutdown signal is handled immediately.
+
 	remoteAddr := fmt.Sprintf("%s:%d", hostToConnect, portToConnect)
 	remoteAddr := fmt.Sprintf("%s:%d", hostToConnect, portToConnect)
 
 
 	log.WithContextFields(LogFields{"remoteAddr": remoteAddr}).Debug("dialing")
 	log.WithContextFields(LogFields{"remoteAddr": remoteAddr}).Debug("dialing")
@@ -615,9 +810,25 @@ func (sshClient *sshClient) handleTCPChannel(
 		return
 		return
 	}
 	}
 
 
+	// The upstream TCP port forward connection has been established. Schedule
+	// some cleanup and notify the SSH client that the channel is accepted.
+
 	fwdConn := result.conn
 	fwdConn := result.conn
 	defer fwdConn.Close()
 	defer fwdConn.Close()
 
 
+	lruEntry := sshClient.tcpPortForwardLRU.Add(fwdConn)
+	defer lruEntry.Remove()
+
+	// ActivityMonitoredConn monitors the TCP port forward I/O and updates
+	// its LRU status. ActivityMonitoredConn also times out read on the port
+	// forward if both reads and writes have been idle for the specified
+	// duration.
+	fwdConn = psiphon.NewActivityMonitoredConn(
+		fwdConn,
+		time.Duration(sshClient.trafficRules.IdleTCPPortForwardTimeoutMilliseconds)*time.Millisecond,
+		true,
+		lruEntry)
+
 	fwdChannel, requests, err := newChannel.Accept()
 	fwdChannel, requests, err := newChannel.Accept()
 	if err != nil {
 	if err != nil {
 		log.WithContextFields(LogFields{"error": err}).Warning("accept new channel failed")
 		log.WithContextFields(LogFields{"error": err}).Warning("accept new channel failed")
@@ -628,39 +839,35 @@ func (sshClient *sshClient) handleTCPChannel(
 
 
 	log.WithContextFields(LogFields{"remoteAddr": remoteAddr}).Debug("relaying")
 	log.WithContextFields(LogFields{"remoteAddr": remoteAddr}).Debug("relaying")
 
 
-	// When idle port forward traffic rules are in place, wrap fwdConn
-	// in an IdleTimeoutConn configured to reset idle on writes as well
-	// as read. This ensures the port forward idle timeout only happens
-	// when both upstream and downstream directions are are idle.
-
-	if sshClient.trafficRules.IdlePortForwardTimeoutMilliseconds > 0 {
-		fwdConn = psiphon.NewIdleTimeoutConn(
-			fwdConn,
-			time.Duration(sshClient.trafficRules.IdlePortForwardTimeoutMilliseconds)*time.Millisecond,
-			true)
-	}
+	// Relay channel to forwarded connection.
 
 
-	// relay channel to forwarded connection
 	// TODO: relay errors to fwdChannel.Stderr()?
 	// TODO: relay errors to fwdChannel.Stderr()?
-	// TODO: use a low-memory io.Copy?
-
 	relayWaitGroup := new(sync.WaitGroup)
 	relayWaitGroup := new(sync.WaitGroup)
 	relayWaitGroup.Add(1)
 	relayWaitGroup.Add(1)
 	go func() {
 	go func() {
 		defer relayWaitGroup.Done()
 		defer relayWaitGroup.Done()
-		bytes, err := io.Copy(fwdChannel, fwdConn)
+		// io.Copy allocates a 32K temporary buffer, and each port forward relay uses
+		// two of these buffers; using io.CopyBuffer with a smaller buffer reduces the
+		// overall memory footprint.
+		bytes, err := io.CopyBuffer(
+			fwdChannel, fwdConn, make([]byte, SSH_TCP_PORT_FORWARD_COPY_BUFFER_SIZE))
 		atomic.AddInt64(&bytesDown, bytes)
 		atomic.AddInt64(&bytesDown, bytes)
 		if err != nil && err != io.EOF {
 		if err != nil && err != io.EOF {
 			// Debug since errors such as "connection reset by peer" occur during normal operation
 			// Debug since errors such as "connection reset by peer" occur during normal operation
 			log.WithContextFields(LogFields{"error": err}).Debug("downstream TCP relay failed")
 			log.WithContextFields(LogFields{"error": err}).Debug("downstream TCP relay failed")
 		}
 		}
+		// Interrupt upstream io.Copy when downstream is shutting down.
+		// TODO: this is done to quickly cleanup the port forward when
+		// fwdConn has a read timeout, but is it clean -- upstream may still
+		// be flowing?
+		fwdChannel.Close()
 	}()
 	}()
-	bytes, err := io.Copy(fwdConn, fwdChannel)
+	bytes, err := io.CopyBuffer(
+		fwdConn, fwdChannel, make([]byte, SSH_TCP_PORT_FORWARD_COPY_BUFFER_SIZE))
 	atomic.AddInt64(&bytesUp, bytes)
 	atomic.AddInt64(&bytesUp, bytes)
 	if err != nil && err != io.EOF {
 	if err != nil && err != io.EOF {
 		log.WithContextFields(LogFields{"error": err}).Debug("upstream TCP relay failed")
 		log.WithContextFields(LogFields{"error": err}).Debug("upstream TCP relay failed")
 	}
 	}
-
 	// Shutdown special case: fwdChannel will be closed and return EOF when
 	// Shutdown special case: fwdChannel will be closed and return EOF when
 	// the SSH connection is closed, but we need to explicitly close fwdConn
 	// the SSH connection is closed, but we need to explicitly close fwdConn
 	// to interrupt the downstream io.Copy, which may be blocked on a
 	// to interrupt the downstream io.Copy, which may be blocked on a
@@ -675,86 +882,3 @@ func (sshClient *sshClient) handleTCPChannel(
 			"bytesUp":    atomic.LoadInt64(&bytesUp),
 			"bytesUp":    atomic.LoadInt64(&bytesUp),
 			"bytesDown":  atomic.LoadInt64(&bytesDown)}).Debug("exiting")
 			"bytesDown":  atomic.LoadInt64(&bytesDown)}).Debug("exiting")
 }
 }
-
-func (sshClient *sshClient) passwordCallback(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
-	var sshPasswordPayload struct {
-		SessionId   string `json:"SessionId"`
-		SshPassword string `json:"SshPassword"`
-	}
-	err := json.Unmarshal(password, &sshPasswordPayload)
-	if err != nil {
-		return nil, psiphon.ContextError(fmt.Errorf("invalid password payload for %q", conn.User()))
-	}
-
-	userOk := (subtle.ConstantTimeCompare(
-		[]byte(conn.User()), []byte(sshClient.sshServer.config.SSHUserName)) == 1)
-
-	passwordOk := (subtle.ConstantTimeCompare(
-		[]byte(sshPasswordPayload.SshPassword), []byte(sshClient.sshServer.config.SSHPassword)) == 1)
-
-	if !userOk || !passwordOk {
-		return nil, psiphon.ContextError(fmt.Errorf("invalid password for %q", conn.User()))
-	}
-
-	psiphonSessionID := sshPasswordPayload.SessionId
-
-	sshClient.Lock()
-	sshClient.psiphonSessionID = psiphonSessionID
-	geoIPData := sshClient.geoIPData
-	sshClient.Unlock()
-
-	if sshClient.sshServer.config.UseRedis() {
-		err = UpdateRedisForLegacyPsiWeb(psiphonSessionID, geoIPData)
-		if err != nil {
-			log.WithContextFields(LogFields{
-				"psiphonSessionID": psiphonSessionID,
-				"error":            err}).Warning("UpdateRedisForLegacyPsiWeb failed")
-			// Allow the connection to proceed; legacy psi_web will not get accurate GeoIP values.
-		}
-	}
-
-	return nil, nil
-}
-
-func (sshClient *sshClient) authLogCallback(conn ssh.ConnMetadata, method string, err error) {
-	if err != nil {
-		if sshClient.sshServer.config.UseFail2Ban() {
-			clientIPAddress := psiphon.IPAddressFromAddr(conn.RemoteAddr())
-			if clientIPAddress != "" {
-				LogFail2Ban(clientIPAddress)
-			}
-		}
-		log.WithContextFields(LogFields{"error": err, "method": method}).Warning("authentication failed")
-	} else {
-		log.WithContextFields(LogFields{"error": err, "method": method}).Info("authentication success")
-	}
-}
-
-func (sshClient *sshClient) stop() {
-
-	sshClient.sshConn.Close()
-	sshClient.sshConn.Wait()
-
-	close(sshClient.stopBroadcast)
-	sshClient.channelHandlerWaitGroup.Wait()
-
-	sshClient.Lock()
-	log.WithContextFields(
-		LogFields{
-			"startTime":                         sshClient.startTime,
-			"duration":                          time.Now().Sub(sshClient.startTime),
-			"psiphonSessionID":                  sshClient.psiphonSessionID,
-			"country":                           sshClient.geoIPData.Country,
-			"city":                              sshClient.geoIPData.City,
-			"ISP":                               sshClient.geoIPData.ISP,
-			"bytesUpTCP":                        sshClient.tcpTrafficState.bytesUp,
-			"bytesDownTCP":                      sshClient.tcpTrafficState.bytesDown,
-			"portForwardCountTCP":               sshClient.tcpTrafficState.portForwardCount,
-			"peakConcurrentPortForwardCountTCP": sshClient.tcpTrafficState.peakConcurrentPortForwardCount,
-			"bytesUpUDP":                        sshClient.udpTrafficState.bytesUp,
-			"bytesDownUDP":                      sshClient.udpTrafficState.bytesDown,
-			"portForwardCountUDP":               sshClient.udpTrafficState.portForwardCount,
-			"peakConcurrentPortForwardCountUDP": sshClient.udpTrafficState.peakConcurrentPortForwardCount,
-		}).Info("tunnel closed")
-	sshClient.Unlock()
-}

+ 41 - 34
psiphon/server/udpChannel.go → psiphon/server/udp.go

@@ -24,7 +24,6 @@ import (
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 	"io"
 	"io"
-	"math"
 	"net"
 	"net"
 	"strconv"
 	"strconv"
 	"sync"
 	"sync"
@@ -75,6 +74,7 @@ func (sshClient *sshClient) handleUDPChannel(newChannel ssh.NewChannel) {
 		sshClient:      sshClient,
 		sshClient:      sshClient,
 		sshChannel:     sshChannel,
 		sshChannel:     sshChannel,
 		portForwards:   make(map[uint16]*udpPortForward),
 		portForwards:   make(map[uint16]*udpPortForward),
+		portForwardLRU: psiphon.NewLRUConns(),
 		relayWaitGroup: new(sync.WaitGroup),
 		relayWaitGroup: new(sync.WaitGroup),
 	}
 	}
 	multiplexer.run()
 	multiplexer.run()
@@ -86,6 +86,7 @@ type udpPortForwardMultiplexer struct {
 	portForwardsMutex sync.Mutex
 	portForwardsMutex sync.Mutex
 	portForwards      map[uint16]*udpPortForward
 	portForwards      map[uint16]*udpPortForward
 	relayWaitGroup    *sync.WaitGroup
 	relayWaitGroup    *sync.WaitGroup
+	portForwardLRU    *psiphon.LRUConns
 }
 }
 
 
 func (mux *udpPortForwardMultiplexer) run() {
 func (mux *udpPortForwardMultiplexer) run() {
@@ -158,10 +159,18 @@ func (mux *udpPortForwardMultiplexer) run() {
 				mux.sshClient.tcpTrafficState,
 				mux.sshClient.tcpTrafficState,
 				mux.sshClient.trafficRules.MaxUDPPortForwardCount) {
 				mux.sshClient.trafficRules.MaxUDPPortForwardCount) {
 
 
-				// When the UDP port forward limit is exceeded, we
-				// select the least recently used (read from or written
-				// to) port forward and discard it.
-				mux.closeLeastRecentlyUsedPortForward()
+				// Close the oldest UDP port forward. CloseOldest() closes
+				// the conn and the port forward's goroutine will complete
+				// the cleanup asynchronously.
+				//
+				// See LRU comment in handleTCPChannel() for a known
+				// limitations regarding CloseOldest().
+				mux.portForwardLRU.CloseOldest()
+
+				log.WithContextFields(
+					LogFields{
+						"maxCount": mux.sshClient.trafficRules.MaxUDPPortForwardCount,
+					}).Debug("closed LRU UDP port forward")
 			}
 			}
 
 
 			dialIP := net.IP(message.remoteIP)
 			dialIP := net.IP(message.remoteIP)
@@ -178,7 +187,7 @@ func (mux *udpPortForwardMultiplexer) run() {
 					"connID":     message.connID}).Debug("dialing")
 					"connID":     message.connID}).Debug("dialing")
 
 
 			// TODO: on EADDRNOTAVAIL, temporarily suspend new clients
 			// TODO: on EADDRNOTAVAIL, temporarily suspend new clients
-			updConn, err := net.DialUDP(
+			udpConn, err := net.DialUDP(
 				"udp", nil, &net.UDPAddr{IP: dialIP, Port: dialPort})
 				"udp", nil, &net.UDPAddr{IP: dialIP, Port: dialPort})
 			if err != nil {
 			if err != nil {
 				mux.sshClient.closedPortForward(mux.sshClient.udpTrafficState, 0, 0)
 				mux.sshClient.closedPortForward(mux.sshClient.udpTrafficState, 0, 0)
@@ -186,13 +195,25 @@ func (mux *udpPortForwardMultiplexer) run() {
 				continue
 				continue
 			}
 			}
 
 
+			lruEntry := mux.portForwardLRU.Add(udpConn)
+
+			// ActivityMonitoredConn monitors the TCP port forward I/O and updates
+			// its LRU status. ActivityMonitoredConn also times out read on the port
+			// forward if both reads and writes have been idle for the specified
+			// duration.
+			conn := psiphon.NewActivityMonitoredConn(
+				udpConn,
+				time.Duration(mux.sshClient.trafficRules.IdleUDPPortForwardTimeoutMilliseconds)*time.Millisecond,
+				true,
+				lruEntry)
+
 			portForward = &udpPortForward{
 			portForward = &udpPortForward{
 				connID:       message.connID,
 				connID:       message.connID,
 				preambleSize: message.preambleSize,
 				preambleSize: message.preambleSize,
 				remoteIP:     message.remoteIP,
 				remoteIP:     message.remoteIP,
 				remotePort:   message.remotePort,
 				remotePort:   message.remotePort,
-				conn:         updConn,
-				lastActivity: time.Now().UnixNano(),
+				conn:         conn,
+				lruEntry:     lruEntry,
 				bytesUp:      0,
 				bytesUp:      0,
 				bytesDown:    0,
 				bytesDown:    0,
 				mux:          mux,
 				mux:          mux,
@@ -201,8 +222,6 @@ func (mux *udpPortForwardMultiplexer) run() {
 			mux.portForwards[portForward.connID] = portForward
 			mux.portForwards[portForward.connID] = portForward
 			mux.portForwardsMutex.Unlock()
 			mux.portForwardsMutex.Unlock()
 
 
-			// TODO: timeout inactive UDP port forwards
-
 			// relayDownstream will call sshClient.closedPortForward()
 			// relayDownstream will call sshClient.closedPortForward()
 			mux.relayWaitGroup.Add(1)
 			mux.relayWaitGroup.Add(1)
 			go portForward.relayDownstream()
 			go portForward.relayDownstream()
@@ -216,7 +235,9 @@ func (mux *udpPortForwardMultiplexer) run() {
 			// The port forward's goroutine will complete cleanup
 			// The port forward's goroutine will complete cleanup
 			portForward.conn.Close()
 			portForward.conn.Close()
 		}
 		}
-		atomic.StoreInt64(&portForward.lastActivity, time.Now().UnixNano())
+
+		portForward.lruEntry.Touch()
+
 		atomic.AddInt64(&portForward.bytesUp, int64(len(message.packet)))
 		atomic.AddInt64(&portForward.bytesUp, int64(len(message.packet)))
 	}
 	}
 
 
@@ -232,30 +253,13 @@ func (mux *udpPortForwardMultiplexer) run() {
 	mux.relayWaitGroup.Wait()
 	mux.relayWaitGroup.Wait()
 }
 }
 
 
-func (mux *udpPortForwardMultiplexer) closeLeastRecentlyUsedPortForward() {
-	// TODO: use "container/list" and avoid a linear scan?
-	mux.portForwardsMutex.Lock()
-	oldestActivity := int64(math.MaxInt64)
-	var oldestPortForward *udpPortForward
-	for _, nextPortForward := range mux.portForwards {
-		if nextPortForward.lastActivity < oldestActivity {
-			oldestPortForward = nextPortForward
-		}
-	}
-	if oldestPortForward != nil {
-		// The port forward's goroutine will complete cleanup
-		oldestPortForward.conn.Close()
-	}
-	mux.portForwardsMutex.Unlock()
-}
-
 func (mux *udpPortForwardMultiplexer) transparentDNSAddress(
 func (mux *udpPortForwardMultiplexer) transparentDNSAddress(
 	dialIP net.IP, dialPort int) (net.IP, int) {
 	dialIP net.IP, dialPort int) (net.IP, int) {
 
 
-	if mux.sshClient.sshServer.config.DNSServerAddress != "" {
-		// Note: DNSServerAddress is validated in LoadConfig
+	if mux.sshClient.sshServer.config.UDPForwardDNSServerAddress != "" {
+		// Note: UDPForwardDNSServerAddress is validated in LoadConfig
 		host, portStr, _ := net.SplitHostPort(
 		host, portStr, _ := net.SplitHostPort(
-			mux.sshClient.sshServer.config.DNSServerAddress)
+			mux.sshClient.sshServer.config.UDPForwardDNSServerAddress)
 		dialIP = net.ParseIP(host)
 		dialIP = net.ParseIP(host)
 		dialPort, _ = strconv.Atoi(portStr)
 		dialPort, _ = strconv.Atoi(portStr)
 	}
 	}
@@ -273,8 +277,8 @@ type udpPortForward struct {
 	preambleSize int
 	preambleSize int
 	remoteIP     []byte
 	remoteIP     []byte
 	remotePort   uint16
 	remotePort   uint16
-	conn         *net.UDPConn
-	lastActivity int64
+	conn         net.Conn
+	lruEntry     *psiphon.LRUConnsEntry
 	bytesUp      int64
 	bytesUp      int64
 	bytesDown    int64
 	bytesDown    int64
 	mux          *udpPortForwardMultiplexer
 	mux          *udpPortForwardMultiplexer
@@ -326,12 +330,15 @@ func (portForward *udpPortForward) relayDownstream() {
 			break
 			break
 		}
 		}
 
 
-		atomic.StoreInt64(&portForward.lastActivity, time.Now().UnixNano())
+		portForward.lruEntry.Touch()
+
 		atomic.AddInt64(&portForward.bytesDown, int64(packetSize))
 		atomic.AddInt64(&portForward.bytesDown, int64(packetSize))
 	}
 	}
 
 
 	portForward.mux.removePortForward(portForward.connID)
 	portForward.mux.removePortForward(portForward.connID)
 
 
+	portForward.lruEntry.Remove()
+
 	portForward.conn.Close()
 	portForward.conn.Close()
 
 
 	bytesUp := atomic.LoadInt64(&portForward.bytesUp)
 	bytesUp := atomic.LoadInt64(&portForward.bytesUp)

+ 128 - 0
psiphon/server/utils.go

@@ -0,0 +1,128 @@
+/*
+ * Copyright (c) 2016, 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 server
+
+import (
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/sha1"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"encoding/pem"
+	"math/big"
+	"time"
+
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon"
+)
+
+// GenerateWebServerCertificate creates a self-signed web server certificate,
+// using the specified host name (commonName).
+// This is primarily intended for use by MeekServer to generate on-the-fly,
+// self-signed TLS certificates for fronted HTTPS mode. In this case, the nature
+// of the certificate is non-circumvention; it only has to be acceptable to the
+// front CDN making connections to meek.
+// The same certificates are used for unfronted HTTPS meek. In this case, the
+// certificates may be a fingerprint used to detect Psiphon servers or traffic.
+// TODO: more effort to mitigate fingerprinting these certificates.
+//
+// In addition, GenerateWebServerCertificate is used by GenerateConfig to create
+// Psiphon web server certificates for test/example configurations. If these Psiphon
+// web server certificates are used in production, the same caveats about
+// fingerprints apply.
+func GenerateWebServerCertificate(commonName string) (string, string, error) {
+
+	// Based on https://golang.org/src/crypto/tls/generate_cert.go
+	// TODO: use other key types: anti-fingerprint by varying params
+
+	rsaKey, err := rsa.GenerateKey(rand.Reader, 2048)
+	if err != nil {
+		return "", "", psiphon.ContextError(err)
+	}
+
+	// Validity period is ~10 years, starting some number of ~months
+	// back in the last year.
+
+	age, err := psiphon.MakeSecureRandomInt(12)
+	if err != nil {
+		return "", "", psiphon.ContextError(err)
+	}
+	age += 1
+	validityPeriod := 10 * 365 * 24 * time.Hour
+	notBefore := time.Now().Add(time.Duration(-age) * 30 * 24 * time.Hour).UTC()
+	notAfter := notBefore.Add(validityPeriod).UTC()
+
+	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
+	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
+	if err != nil {
+		return "", "", psiphon.ContextError(err)
+	}
+
+	publicKeyBytes, err := x509.MarshalPKIXPublicKey(rsaKey.Public())
+	if err != nil {
+		return "", "", psiphon.ContextError(err)
+	}
+	// as per RFC3280 sec. 4.2.1.2
+	subjectKeyID := sha1.Sum(publicKeyBytes)
+
+	var subject pkix.Name
+	if commonName != "" {
+		subject = pkix.Name{CommonName: commonName}
+	}
+
+	template := x509.Certificate{
+		SerialNumber:          serialNumber,
+		Subject:               subject,
+		NotBefore:             notBefore,
+		NotAfter:              notAfter,
+		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
+		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
+		BasicConstraintsValid: true,
+		IsCA:         true,
+		SubjectKeyId: subjectKeyID[:],
+		MaxPathLen:   1,
+		Version:      2,
+	}
+
+	derCert, err := x509.CreateCertificate(
+		rand.Reader,
+		&template,
+		&template,
+		rsaKey.Public(),
+		rsaKey)
+	if err != nil {
+		return "", "", psiphon.ContextError(err)
+	}
+
+	webServerCertificate := pem.EncodeToMemory(
+		&pem.Block{
+			Type:  "CERTIFICATE",
+			Bytes: derCert,
+		},
+	)
+
+	webServerPrivateKey := pem.EncodeToMemory(
+		&pem.Block{
+			Type:  "RSA PRIVATE KEY",
+			Bytes: x509.MarshalPKCS1PrivateKey(rsaKey),
+		},
+	)
+
+	return string(webServerCertificate), string(webServerPrivateKey), nil
+}

+ 27 - 0
psiphon/server/webService.go → psiphon/server/webServer.go

@@ -54,6 +54,7 @@ func RunWebServer(config *Config, shutdownBroadcast <-chan struct{}) error {
 	serveMux.HandleFunc("/handshake", webServer.handshakeHandler)
 	serveMux.HandleFunc("/handshake", webServer.handshakeHandler)
 	serveMux.HandleFunc("/connected", webServer.connectedHandler)
 	serveMux.HandleFunc("/connected", webServer.connectedHandler)
 	serveMux.HandleFunc("/status", webServer.statusHandler)
 	serveMux.HandleFunc("/status", webServer.statusHandler)
+	serveMux.HandleFunc("/client_verification", webServer.clientVerificationHandler)
 
 
 	certificate, err := tls.X509KeyPair(
 	certificate, err := tls.X509KeyPair(
 		[]byte(config.WebServerCertificate),
 		[]byte(config.WebServerCertificate),
@@ -245,3 +246,29 @@ func (webServer *webServer) statusHandler(w http.ResponseWriter, r *http.Request
 
 
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 }
 }
+
+func (webServer *webServer) clientVerificationHandler(w http.ResponseWriter, r *http.Request) {
+
+	if !webServer.checkWebServerSecret(r) {
+		// TODO: log more details?
+		log.WithContext().Warning("checkWebServerSecret failed")
+		// TODO: psi_web does NotFound in this case
+		w.WriteHeader(http.StatusForbidden)
+		return
+	}
+
+	// TODO: validate; proper log
+	log.WithContextFields(LogFields{"queryParams": r.URL.Query()}).Info("client_verification")
+
+	// TODO: use json.NewDecoder(r.Body)? But will that handle bogus extra data in request body?
+	requestBody, err := ioutil.ReadAll(r.Body)
+	if err != nil {
+		w.WriteHeader(http.StatusInternalServerError)
+		return
+	}
+
+	// TODO: parse payload; validate; proper logs
+	log.WithContextFields(LogFields{"payload": string(requestBody)}).Info("client_verification payload")
+
+	w.WriteHeader(http.StatusOK)
+}

+ 18 - 0
psiphon/serverEntry.go

@@ -90,6 +90,24 @@ const (
 	SERVER_ENTRY_SOURCE_TARGET    ServerEntrySource = "TARGET"
 	SERVER_ENTRY_SOURCE_TARGET    ServerEntrySource = "TARGET"
 )
 )
 
 
+func TunnelProtocolUsesSSH(protocol string) bool {
+	return true
+}
+
+func TunnelProtocolUsesObfuscatedSSH(protocol string) bool {
+	return protocol != TUNNEL_PROTOCOL_SSH
+}
+
+func TunnelProtocolUsesMeekHTTP(protocol string) bool {
+	return protocol == TUNNEL_PROTOCOL_UNFRONTED_MEEK ||
+		protocol == TUNNEL_PROTOCOL_FRONTED_MEEK_HTTP
+}
+
+func TunnelProtocolUsesMeekHTTPS(protocol string) bool {
+	return protocol == TUNNEL_PROTOCOL_FRONTED_MEEK ||
+		protocol == TUNNEL_PROTOCOL_UNFRONTED_MEEK_HTTPS
+}
+
 // GetCapability returns the server capability corresponding
 // GetCapability returns the server capability corresponding
 // to the protocol.
 // to the protocol.
 func GetCapability(protocol string) string {
 func GetCapability(protocol string) string {

+ 14 - 69
psiphon/utils.go

@@ -17,41 +17,10 @@
  *
  *
  */
  */
 
 
-/*
-Copyright (c) 2012 The Go Authors. All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are
-met:
-
-   * Redistributions of source code must retain the above copyright
-notice, this list of conditions and the following disclaimer.
-   * Redistributions in binary form must reproduce the above
-copyright notice, this list of conditions and the following disclaimer
-in the documentation and/or other materials provided with the
-distribution.
-   * Neither the name of Google Inc. nor the names of its
-contributors may be used to endorse or promote products derived from
-this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-*/
-
 package psiphon
 package psiphon
 
 
 import (
 import (
 	"crypto/rand"
 	"crypto/rand"
-	"crypto/tls"
 	"crypto/x509"
 	"crypto/x509"
 	"encoding/base64"
 	"encoding/base64"
 	"encoding/hex"
 	"encoding/hex"
@@ -59,7 +28,6 @@ import (
 	"fmt"
 	"fmt"
 	"math/big"
 	"math/big"
 	"net"
 	"net"
-	"net/http"
 	"net/url"
 	"net/url"
 	"os"
 	"os"
 	"runtime"
 	"runtime"
@@ -121,7 +89,7 @@ func MakeSecureRandomBytes(length int) ([]byte, error) {
 
 
 // MakeSecureRandomPadding selects a random padding length in the indicated
 // MakeSecureRandomPadding selects a random padding length in the indicated
 // range and returns a random byte array of the selected length.
 // range and returns a random byte array of the selected length.
-// In the unlikely case where an  underlying MakeRandom functions fails,
+// In the unlikely case where an underlying MakeRandom functions fails,
 // the padding is length 0.
 // the padding is length 0.
 func MakeSecureRandomPadding(minLength, maxLength int) []byte {
 func MakeSecureRandomPadding(minLength, maxLength int) []byte {
 	var padding []byte
 	var padding []byte
@@ -151,9 +119,9 @@ func MakeRandomPeriod(min, max time.Duration) (duration time.Duration) {
 	return
 	return
 }
 }
 
 
-// MakeRandomString returns a hex encoded random string. byteLength
-// specifies the pre-encoded data length.
-func MakeRandomString(byteLength int) (string, error) {
+// MakeRandomStringHex returns a hex encoded random string.
+// byteLength specifies the pre-encoded data length.
+func MakeRandomStringHex(byteLength int) (string, error) {
 	bytes, err := MakeSecureRandomBytes(byteLength)
 	bytes, err := MakeSecureRandomBytes(byteLength)
 	if err != nil {
 	if err != nil {
 		return "", ContextError(err)
 		return "", ContextError(err)
@@ -161,6 +129,16 @@ func MakeRandomString(byteLength int) (string, error) {
 	return hex.EncodeToString(bytes), nil
 	return hex.EncodeToString(bytes), nil
 }
 }
 
 
+// MakeRandomStringBase64 returns a base64 encoded random string.
+// byteLength specifies the pre-encoded data length.
+func MakeRandomStringBase64(byteLength int) (string, error) {
+	bytes, err := MakeSecureRandomBytes(byteLength)
+	if err != nil {
+		return "", ContextError(err)
+	}
+	return base64.RawURLEncoding.EncodeToString(bytes), nil
+}
+
 func DecodeCertificate(encodedCertificate string) (certificate *x509.Certificate, err error) {
 func DecodeCertificate(encodedCertificate string) (certificate *x509.Certificate, err error) {
 	derEncodedCertificate, err := base64.StdEncoding.DecodeString(encodedCertificate)
 	derEncodedCertificate, err := base64.StdEncoding.DecodeString(encodedCertificate)
 	if err != nil {
 	if err != nil {
@@ -297,36 +275,3 @@ func TruncateTimestampToHour(timestamp string) string {
 	}
 	}
 	return t.Truncate(1 * time.Hour).Format(time.RFC3339)
 	return t.Truncate(1 * time.Hour).Format(time.RFC3339)
 }
 }
-
-// HTTPSServer is a wrapper around http.Server which adds the
-// ServeTLS function.
-type HTTPSServer struct {
-	http.Server
-}
-
-// ServeTLS is a offers the equivalent interface as http.Serve.
-// The http package has both ListenAndServe and ListenAndServeTLS higher-
-// level interfaces, but only Serve (not TLS) offers a lower-level interface that
-// allows the caller to keep a refererence to the Listener, allowing for external
-// shutdown. ListenAndServeTLS also requires the TLS cert and key to be in files
-// and we avoid that here.
-// tcpKeepAliveListener is used in http.ListenAndServeTLS but not exported,
-// so we use a copy from https://golang.org/src/net/http/server.go.
-func (server *HTTPSServer) ServeTLS(listener net.Listener) error {
-	tlsListener := tls.NewListener(tcpKeepAliveListener{listener.(*net.TCPListener)}, server.TLSConfig)
-	return server.Serve(tlsListener)
-}
-
-type tcpKeepAliveListener struct {
-	*net.TCPListener
-}
-
-func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
-	tc, err := ln.AcceptTCP()
-	if err != nil {
-		return
-	}
-	tc.SetKeepAlive(true)
-	tc.SetKeepAlivePeriod(3 * time.Minute)
-	return tc, nil
-}