Browse Source

Meek support and related refactoring (in progress)
* Code reorganization: TunnelServer is now the core server, and
it layers various protocol listeners on top of SSH tunneling.
* Renamed some source files.
* Implement all meek protocol variants. Adapted meek-server.go
with changes to plug a net.Conn into tunnel server, along with
other cleanups.
* Automated testing covers new protocols.
* Added sudo Travis "go test" since server needs to bind to
privileged ports (will this work?)
* GenerateConfig covers new protocols. Emphasized that
GenerateConfig is primarily for testing and illustrative
purposes. Related: removed port command-line params from main.
* Move load monitor to outer RunServices.
* Add more stats to load monitor.
* Fix: blocking NewObfuscatedSshConn call was in the wrong
place.

Rod Hynes 9 years ago
parent
commit
3276dc8e0c

+ 1 - 1
.travis.yml

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

+ 13 - 28
Server/main.go

@@ -26,6 +26,7 @@ import (
 	"os"
 	"strings"
 
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/server"
 )
 
@@ -33,7 +34,6 @@ func main() {
 
 	var generateServerIPaddress, generateServerNetworkInterface string
 	var generateConfigFilename, generateServerEntryFilename string
-	var generateWebServerPort, generateSSHServerPort, generateObfuscatedSSHServerPort int
 	var runConfigFilenames stringListFlag
 
 	flag.StringVar(
@@ -52,7 +52,7 @@ func main() {
 		&generateServerNetworkInterface,
 		"interface",
 		"",
-		"generate server entry with this `network-interface`")
+		"generate with server IP address from this `network-interface`")
 
 	flag.StringVar(
 		&generateServerIPaddress,
@@ -60,24 +60,6 @@ func main() {
 		server.DEFAULT_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(
 		&runConfigFilenames,
 		"config",
@@ -101,19 +83,22 @@ func main() {
 		os.Exit(1)
 	} 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 {
 			fmt.Errorf("generate failed: %s", err)
 			os.Exit(1)
 		}
+
 		err = ioutil.WriteFile(generateConfigFilename, configFileContents, 0600)
 		if err != nil {
 			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 {
 		return nil, ContextError(err)
 	}
@@ -642,7 +642,7 @@ type meekCookieData struct {
 // all consequent HTTP requests
 // In unfronted meek mode, the cookie is visible over the adversary network, so the
 // 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
 	serverAddress := meekConfig.PsiphonServerAddress

+ 64 - 0
psiphon/net.go

@@ -17,6 +17,37 @@
  *
  */
 
+// 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
 
 import (
@@ -582,6 +613,39 @@ func IPAddressFromAddr(addr net.Addr) string {
 	return ipAddress
 }
 
+// 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
+}
+
 // 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

+ 7 - 8
psiphon/obfuscator.go

@@ -26,7 +26,6 @@ import (
 	"encoding/binary"
 	"errors"
 	"io"
-	"net"
 )
 
 const (
@@ -86,12 +85,12 @@ func NewClientObfuscator(
 }
 
 // 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(
-	clientConn net.Conn, config *ObfuscatorConfig) (obfuscator *Obfuscator, err error) {
+	clientReader io.Reader, config *ObfuscatorConfig) (obfuscator *Obfuscator, err error) {
 
 	clientToServerCipher, serverToClientCipher, err := readSeedMessage(
-		clientConn, config)
+		clientReader, config)
 	if err != nil {
 		return nil, ContextError(err)
 	}
@@ -195,10 +194,10 @@ func makeSeedMessage(maxPadding int, seed []byte, clientToServerCipher *rc4.Ciph
 }
 
 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)
-	_, err := io.ReadFull(clientConn, seed)
+	_, err := io.ReadFull(clientReader, seed)
 	if err != nil {
 		return nil, nil, ContextError(err)
 	}
@@ -209,7 +208,7 @@ func readSeedMessage(
 	}
 
 	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 {
 		return nil, nil, ContextError(err)
 	}
@@ -237,7 +236,7 @@ func readSeedMessage(
 	}
 
 	padding := make([]byte, paddingLength)
-	_, err = io.ReadFull(clientConn, padding)
+	_, err = io.ReadFull(clientReader, padding)
 	if err != nil {
 		return nil, nil, ContextError(err)
 	}

+ 222 - 236
psiphon/server/config.go

@@ -28,41 +28,34 @@ import (
 	"encoding/pem"
 	"errors"
 	"fmt"
-	"math/big"
 	"net"
 	"strconv"
 	"strings"
 	"time"
 
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon"
+	"golang.org/x/crypto/nacl/box"
 	"golang.org/x/crypto/ssh"
 )
 
 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_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)
@@ -107,6 +100,10 @@ type Config struct {
 	// performed.
 	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 string
 
@@ -126,40 +123,82 @@ type Config struct {
 	// authenticate itself to clients.
 	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
-	// both the SSH and Obfuscated SSH servers.
+	// all protocols, run by this server instance, which use SSH.
 	SSHPrivateKey string
 
 	// 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
 
 	// 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
 
 	// 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
 
-	// 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
-	// SSH protocol.
+	// SSH protocol. The same secret key is used for all protocols,
+	// run by this server instance, which use Obfuscated SSH.
 	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
 	// no regional-specific rules are set.
@@ -171,21 +210,9 @@ type Config struct {
 	// is one or more space delimited ISO 3166-1 alpha-2 country codes.
 	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
 	// 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.
 	LoadMonitorPeriodSeconds int
 }
@@ -247,16 +274,6 @@ func (config *Config) RunWebServer() bool {
 	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.
 func (config *Config) RunLoadMonitor() bool {
 	return config.LoadMonitorPeriodSeconds > 0
@@ -320,18 +337,38 @@ func LoadConfig(configJSONs [][]byte) (*Config, error) {
 			"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 {
@@ -345,91 +382,53 @@ func LoadConfig(configJSONs [][]byte) (*Config, error) {
 		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
 }
 
-// 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
 // generates all necessary secrets and key material, which are emitted in
 // 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
 
-	webServerPort := params.WebServerPort
-	if webServerPort == 0 {
-		webServerPort = DEFAULT_WEB_SERVER_PORT
+	webServerPort := 8080
+
+	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 {
 		return nil, nil, psiphon.ContextError(err)
 	}
 
-	webServerCertificate, webServerPrivateKey, err := generateWebServerCertificate()
+	discoveryValueHMACKey, err := psiphon.MakeRandomStringBase64(DISCOVERY_VALUE_KEY_BYTE_LENGTH)
 	if err != nil {
 		return nil, nil, psiphon.ContextError(err)
 	}
 
 	// SSH config
 
-	sshServerPort := params.SSHServerPort
-	if sshServerPort == 0 {
-		sshServerPort = DEFAULT_SSH_SERVER_PORT
-	}
-
 	// TODO: use other key types: anti-fingerprint by varying params
-
 	rsaKey, err := rsa.GenerateKey(rand.Reader, SSH_RSA_HOST_KEY_BITS)
 	if err != nil {
 		return nil, nil, psiphon.ContextError(err)
@@ -449,14 +448,14 @@ func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, error) {
 
 	sshPublicKey := signer.PublicKey()
 
-	sshUserNameSuffix, err := psiphon.MakeRandomString(SSH_USERNAME_SUFFIX_BYTE_LENGTH)
+	sshUserNameSuffix, err := psiphon.MakeRandomStringHex(SSH_USERNAME_SUFFIX_BYTE_LENGTH)
 	if err != nil {
 		return nil, nil, psiphon.ContextError(err)
 	}
 
 	sshUserName := "psiphon_" + sshUserNameSuffix
 
-	sshPassword, err := psiphon.MakeRandomString(SSH_PASSWORD_BYTE_LENGTH)
+	sshPassword, err := psiphon.MakeRandomStringHex(SSH_PASSWORD_BYTE_LENGTH)
 	if err != nil {
 		return nil, nil, psiphon.ContextError(err)
 	}
@@ -466,38 +465,77 @@ func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, error) {
 
 	// 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)
 	}
 
-	obfuscatedSSHKey, err := psiphon.MakeRandomString(SSH_OBFUSCATED_KEY_BYTE_LENGTH)
+	// Meek config
+
+	meekCookieEncryptionPublicKey, meekCookieEncryptionPrivateKey, err :=
+		box.GenerateKey(rand.Reader)
+	if err != nil {
+		return nil, nil, psiphon.ContextError(err)
+	}
+
+	meekObfuscatedKey, err := psiphon.MakeRandomStringHex(SSH_OBFUSCATED_KEY_BYTE_LENGTH)
 	if err != nil {
 		return nil, nil, psiphon.ContextError(err)
 	}
 
 	// 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 := 8080
+
 	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",
+		UDPInterceptUdpgwServerAddress: "127.0.0.1:7300",
+		MeekCookieEncryptionPrivateKey: base64.RawStdEncoding.EncodeToString(meekCookieEncryptionPrivateKey[:]),
+		MeekObfuscatedKey:              meekObfuscatedKey,
+		MeekCertificateCommonName:      "www.example.org",
+		MeekProhibitedHeaders:          nil,
+		MeekProxyForwardedForHeaders:   []string{"X-Forwarded-For"},
+		DefaultTrafficRules: TrafficRules{
+			LimitDownstreamBytesPerSecond:      0,
+			LimitUpstreamBytesPerSecond:        0,
+			IdlePortForwardTimeoutMilliseconds: 0,
+			MaxTCPPortForwardCount:             256,
+			MaxUDPPortForwardCount:             32,
+			AllowTCPPorts:                      nil,
+			AllowUDPPorts:                      nil,
+			DenyTCPPorts:                       nil,
+			DenyUDPPorts:                       nil,
+		},
+		LoadMonitorPeriodSeconds: 300,
 	}
 
 	encodedConfig, err := json.MarshalIndent(config, "\n", "    ")
@@ -512,21 +550,33 @@ func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, error) {
 	capabilities := []string{
 		psiphon.GetCapability(psiphon.TUNNEL_PROTOCOL_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{
-		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.RawStdEncoding.EncodeToString(meekCookieEncryptionPublicKey[:]),
+		MeekObfuscatedKey:             meekObfuscatedKey,
+		MeekFrontingHosts:             []string{serverIPaddress},
+		MeekFrontingAddresses:         []string{serverIPaddress},
+		MeekFrontingDisableSNI:        false,
 	}
 
 	encodedServerEntry, err := psiphon.EncodeServerEntry(serverEntry)
@@ -536,67 +586,3 @@ func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, error) {
 
 	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
-}

+ 750 - 0
psiphon/server/meek.go

@@ -0,0 +1,750 @@
+/*
+ * 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"
+	"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,
+		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.Serve(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 {
+		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 {
+		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
+	lastSeen            time.Time
+}
+
+func (session *meekSession) touch() {
+	session.lastSeen = time.Now()
+}
+
+func (session *meekSession) expired() bool {
+	return time.Since(session.lastSeen) > 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{}
+	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{}),
+		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 n == len(buffer) && err == nil {
+		// 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
+	} else {
+		// Assumes readerResult won't block.
+		conn.readResult <- err
+	}
+
+	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)
+			if err != nil {
+				// Assumes that writeResult won't block.
+				conn.writeResult <- err
+				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()
+
+	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 err := <-conn.writeResult:
+			return n, err
+		case <-conn.closeBroadcast:
+			return n, io.EOF
+		}
+	}
+	return n, nil
+}
+
+// Close closes the meekConn. This will interrupt any blocked
+// Read, Write, PumpReads, and PumpWrites.
+func (conn *meekConn) Close() error {
+	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"))
+}

+ 21 - 1
psiphon/server/server_test.go

@@ -41,7 +41,27 @@ func TestMain(m *testing.M) {
 	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
 

+ 64 - 9
psiphon/server/services.go

@@ -26,7 +26,9 @@ package server
 import (
 	"os"
 	"os/signal"
+	"runtime"
 	"sync"
+	"time"
 
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon"
 )
@@ -67,23 +69,28 @@ func RunServices(encodedConfigs [][]byte) error {
 	shutdownBroadcast := make(chan struct{})
 	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)
 		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)
 		go func() {
 			defer waitGroup.Done()
-			err := RunSSHServer(config, shutdownBroadcast)
+			err := RunWebServer(config, shutdownBroadcast)
 			select {
 			case errors <- err:
 			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
 	systemStopSignal := make(chan os.Signal, 1)
 	signal.Notify(systemStopSignal, os.Interrupt, os.Kill)
@@ -109,3 +128,39 @@ func RunServices(encodedConfigs [][]byte) error {
 
 	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")
+		}
+	}
+}

+ 215 - 154
psiphon/server/sshService.go → psiphon/server/tunnelServer.go

@@ -26,7 +26,6 @@ import (
 	"fmt"
 	"io"
 	"net"
-	"runtime"
 	"sync"
 	"sync/atomic"
 	"time"
@@ -35,46 +34,66 @@ import (
 	"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 {
-		return psiphon.ContextError(err)
+		return nil, psiphon.ContextError(err)
 	}
 
-	sshServer := &sshServer{
+	return &TunnelServer{
 		config:            config,
 		runWaitGroup:      new(sync.WaitGroup),
 		listenerError:     make(chan error),
 		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 {
 		net.Listener
@@ -82,78 +101,75 @@ func RunSSHServer(
 		tunnelProtocol string
 	}
 
-	var listeners []*sshListener
+	// First bind all listeners; once all are successful,
+	// start accepting connections on each.
 
-	if config.RunSSHServer() {
-		listeners = append(listeners, &sshListener{
-			localAddress: fmt.Sprintf(
-				"%s:%d", config.ServerIPAddress, config.SSHServerPort),
-			tunnelProtocol: psiphon.TUNNEL_PROTOCOL_SSH,
-		})
-	}
+	var listeners []*sshListener
 
-	if config.RunObfuscatedSSHServer() {
-		listeners = append(listeners, &sshListener{
-			localAddress: fmt.Sprintf(
-				"%s:%d", config.ServerIPAddress, config.ObfuscatedSSHServerPort),
-			tunnelProtocol: psiphon.TUNNEL_PROTOCOL_OBFUSCATED_SSH,
-		})
-	}
+	for tunnelProtocol, listenPort := range server.config.TunnelProtocolPorts {
 
-	// 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 {
-			for j := 0; j < i; j++ {
-				listener.Listener.Close()
+			for _, existingListener := range listeners {
+				existingListener.Listener.Close()
 			}
 			return psiphon.ContextError(err)
 		}
+
 		log.WithContextFields(
 			LogFields{
-				"localAddress":   listener.localAddress,
-				"tunnelProtocol": listener.tunnelProtocol,
+				"localAddress":   localAddress,
+				"tunnelProtocol": tunnelProtocol,
 			}).Info("listening")
+
+		listeners = append(
+			listeners,
+			&sshListener{
+				Listener:       listener,
+				localAddress:   localAddress,
+				tunnelProtocol: tunnelProtocol,
+			})
 	}
 
 	for _, listener := range listeners {
-		sshServer.runWaitGroup.Add(1)
+		server.runWaitGroup.Add(1)
 		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(
 				LogFields{
 					"localAddress":   listener.localAddress,
 					"tunnelProtocol": listener.tunnelProtocol,
-				}).Info("stopping")
+				}).Info("stopped")
 
 		}(listener)
 	}
 
-	if config.RunLoadMonitor() {
-		sshServer.runWaitGroup.Add(1)
-		go func() {
-			defer sshServer.runWaitGroup.Done()
-			sshServer.runLoadMonitor()
-		}()
-	}
-
-	err = nil
+	var err error
 	select {
-	case <-sshServer.shutdownBroadcast:
-	case err = <-sshServer.listenerError:
+	case <-server.shutdownBroadcast:
+	case err = <-server.listenerError:
 	}
 
 	for _, listener := range listeners {
 		listener.Close()
 	}
-	sshServer.stopClients()
-	sshServer.runWaitGroup.Wait()
+	server.sshServer.stopClients()
+	server.runWaitGroup.Wait()
 
 	log.WithContext().Info("stopped")
 
@@ -164,8 +180,6 @@ type sshClientID uint64
 
 type sshServer struct {
 	config            *Config
-	runWaitGroup      *sync.WaitGroup
-	listenerError     chan error
 	shutdownBroadcast <-chan struct{}
 	sshHostKey        ssh.Signer
 	nextClientID      sshClientID
@@ -174,69 +188,96 @@ type sshServer struct {
 	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(
-	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 e, ok := err.(net.Error); ok && e.Temporary() {
-				log.WithContextFields(LogFields{"error": err}).Error("accept failed")
-				// Temporary error, keep running
-				continue
-			}
-
 			select {
-			case sshServer.listenerError <- psiphon.ContextError(err):
+			case listenerError <- psiphon.ContextError(err):
 			default:
 			}
-
 			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,23 @@ 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()
 	defer sshServer.clientsMutex.Unlock()
 
-	counts := make(map[string]int)
+	loadStats := make(map[string]map[string]int64)
 	for _, client := range sshServer.clients {
-		counts[client.tunnelProtocol] += 1
-	}
-	return counts
+		// Note: can't sum trafficState.peakConcurrentPortForwardCount to get a global peak
+		loadStats[client.tunnelProtocol]["CurrentClients"] += 1
+		client.Lock()
+		loadStats[client.tunnelProtocol]["CurrentCPPortForwards"] += 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() {
@@ -310,14 +358,12 @@ func (sshServer *sshServer) handleClient(tunnelProtocol string, clientConn net.C
 	// 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.NewIdleTimeoutConn(clientConn, SSH_CONNECTION_READ_DEADLINE, false)
 
 	// Further wrap the connection in a rate limiting ThrottledConn.
 
-	conn = psiphon.NewThrottledConn(
-		conn,
+	clientConn = psiphon.NewThrottledConn(
+		clientConn,
 		int64(sshClient.trafficRules.LimitDownstreamBytesPerSecond),
 		int64(sshClient.trafficRules.LimitUpstreamBytesPerSecond))
 
@@ -350,17 +396,30 @@ func (sshServer *sshServer) handleClient(tunnelProtocol string, clientConn net.C
 		}
 		sshServerConfig.AddHostKey(sshServer.sshHostKey)
 
-		sshConn, channels, requests, err :=
-			ssh.NewServerConn(conn, sshServerConfig)
+		result := &sshNewServerConnResult{}
 
-		resultChannel <- &sshNewServerConnResult{
-			conn:     conn,
-			sshConn:  sshConn,
-			channels: channels,
-			requests: requests,
-			err:      err,
+		// Wrap the connection in an SSH deobfuscator when required.
+
+		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
 	select {
@@ -368,12 +427,12 @@ func (sshServer *sshServer) handleClient(tunnelProtocol string, clientConn net.C
 	case <-sshServer.shutdownBroadcast:
 		// Close() will interrupt an ongoing handshake
 		// TODO: wait for goroutine to exit before returning?
-		conn.Close()
+		clientConn.Close()
 		return
 	}
 
 	if result.err != nil {
-		conn.Close()
+		clientConn.Close()
 		log.WithContextFields(LogFields{"error": result.err}).Warning("handshake failed")
 		return
 	}
@@ -384,7 +443,7 @@ func (sshServer *sshServer) handleClient(tunnelProtocol string, clientConn net.C
 
 	clientID, ok := sshServer.registerClient(sshClient)
 	if !ok {
-		conn.Close()
+		clientConn.Close()
 		log.WithContext().Warning("register failed")
 		return
 	}
@@ -393,6 +452,8 @@ func (sshServer *sshServer) handleClient(tunnelProtocol string, clientConn net.C
 	go ssh.DiscardRequests(result.requests)
 
 	sshClient.handleChannels(result.channels)
+
+	// TODO: clientConn.Close()?
 }
 
 type sshClient struct {
@@ -414,9 +475,9 @@ type sshClient struct {
 type trafficState struct {
 	bytesUp                        int64
 	bytesDown                      int64
-	portForwardCount               int64
 	concurrentPortForwardCount     int64
 	peakConcurrentPortForwardCount int64
+	totalPortForwardCount          int64
 }
 
 func newSshClient(
@@ -478,8 +539,8 @@ func (sshClient *sshClient) handleNewPortForwardChannel(newChannel ssh.NewChanne
 
 	// 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?
-	isUDPChannel := sshClient.sshServer.config.UdpgwServerAddress != "" &&
-		sshClient.sshServer.config.UdpgwServerAddress ==
+	isUDPChannel := sshClient.sshServer.config.UDPInterceptUdpgwServerAddress != "" &&
+		sshClient.sshServer.config.UDPInterceptUdpgwServerAddress ==
 			fmt.Sprintf("%s:%d",
 				directTcpipExtraData.HostToConnect,
 				directTcpipExtraData.PortToConnect)
@@ -496,7 +557,7 @@ func (sshClient *sshClient) isPortForwardPermitted(
 	port int, allowPorts []int, denyPorts []int) bool {
 
 	// TODO: faster lookup?
-	if allowPorts != nil {
+	if len(allowPorts) > 0 {
 		for _, allowPort := range allowPorts {
 			if port == allowPort {
 				return true
@@ -504,7 +565,7 @@ func (sshClient *sshClient) isPortForwardPermitted(
 		}
 		return false
 	}
-	if denyPorts != nil {
+	if len(denyPorts) > 0 {
 		for _, denyPort := range denyPorts {
 			if port == denyPort {
 				return false
@@ -520,7 +581,7 @@ func (sshClient *sshClient) isPortForwardLimitExceeded(
 	limitExceeded := false
 	if maxPortForwardCount > 0 {
 		sshClient.Lock()
-		limitExceeded = state.portForwardCount >= int64(maxPortForwardCount)
+		limitExceeded = state.concurrentPortForwardCount >= int64(maxPortForwardCount)
 		sshClient.Unlock()
 	}
 	return limitExceeded
@@ -530,11 +591,11 @@ func (sshClient *sshClient) openedPortForward(
 	state *trafficState) {
 
 	sshClient.Lock()
-	state.portForwardCount += 1
 	state.concurrentPortForwardCount += 1
 	if state.concurrentPortForwardCount > state.peakConcurrentPortForwardCount {
 		state.peakConcurrentPortForwardCount = state.concurrentPortForwardCount
 	}
+	state.totalPortForwardCount += 1
 	sshClient.Unlock()
 }
 
@@ -749,12 +810,12 @@ func (sshClient *sshClient) stop() {
 			"ISP":                               sshClient.geoIPData.ISP,
 			"bytesUpTCP":                        sshClient.tcpTrafficState.bytesUp,
 			"bytesDownTCP":                      sshClient.tcpTrafficState.bytesDown,
-			"portForwardCountTCP":               sshClient.tcpTrafficState.portForwardCount,
 			"peakConcurrentPortForwardCountTCP": sshClient.tcpTrafficState.peakConcurrentPortForwardCount,
+			"totalPortForwardCountTCP":          sshClient.tcpTrafficState.totalPortForwardCount,
 			"bytesUpUDP":                        sshClient.udpTrafficState.bytesUp,
 			"bytesDownUDP":                      sshClient.udpTrafficState.bytesDown,
-			"portForwardCountUDP":               sshClient.udpTrafficState.portForwardCount,
 			"peakConcurrentPortForwardCountUDP": sshClient.udpTrafficState.peakConcurrentPortForwardCount,
+			"totalPortForwardCountUDP":          sshClient.udpTrafficState.totalPortForwardCount,
 		}).Info("tunnel closed")
 	sshClient.Unlock()
 }

+ 3 - 3
psiphon/server/udpChannel.go → psiphon/server/udp.go

@@ -252,10 +252,10 @@ func (mux *udpPortForwardMultiplexer) closeLeastRecentlyUsedPortForward() {
 func (mux *udpPortForwardMultiplexer) transparentDNSAddress(
 	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(
-			mux.sshClient.sshServer.config.DNSServerAddress)
+			mux.sshClient.sshServer.config.UDPForwardDNSServerAddress)
 		dialIP = net.ParseIP(host)
 		dialPort, _ = strconv.Atoi(portStr)
 	}

+ 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
+}

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


+ 18 - 0
psiphon/serverEntry.go

@@ -90,6 +90,24 @@ const (
 	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
 // to the protocol.
 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
 
 import (
 	"crypto/rand"
-	"crypto/tls"
 	"crypto/x509"
 	"encoding/base64"
 	"encoding/hex"
@@ -59,7 +28,6 @@ import (
 	"fmt"
 	"math/big"
 	"net"
-	"net/http"
 	"net/url"
 	"os"
 	"runtime"
@@ -121,7 +89,7 @@ func MakeSecureRandomBytes(length int) ([]byte, error) {
 
 // MakeSecureRandomPadding selects a random padding length in the indicated
 // 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.
 func MakeSecureRandomPadding(minLength, maxLength int) []byte {
 	var padding []byte
@@ -151,9 +119,9 @@ func MakeRandomPeriod(min, max time.Duration) (duration time.Duration) {
 	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)
 	if err != nil {
 		return "", ContextError(err)
@@ -161,6 +129,16 @@ func MakeRandomString(byteLength int) (string, error) {
 	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) {
 	derEncodedCertificate, err := base64.StdEncoding.DecodeString(encodedCertificate)
 	if err != nil {
@@ -297,36 +275,3 @@ func TruncateTimestampToHour(timestamp string) string {
 	}
 	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
-}