Преглед изворни кода

Add Obfuscated SSH mode to SSH server

Rod Hynes пре 10 година
родитељ
комит
2dff225e4a
4 измењених фајлова са 160 додато и 51 уклоњено
  1. 56 26
      psiphon/server/config.go
  2. 95 22
      psiphon/server/sshService.go
  3. 8 2
      psiphon/serverEntry.go
  4. 1 1
      psiphon/tunnel.go

+ 56 - 26
psiphon/server/config.go

@@ -49,19 +49,24 @@ const (
 	SSH_PASSWORD_BYTE_LENGTH               = 32
 	SSH_PASSWORD_BYTE_LENGTH               = 32
 	SSH_RSA_HOST_KEY_BITS                  = 2048
 	SSH_RSA_HOST_KEY_BITS                  = 2048
 	DEFAULT_SSH_SERVER_PORT                = 2222
 	DEFAULT_SSH_SERVER_PORT                = 2222
+	SSH_HANDSHAKE_TIMEOUT                  = 30 * time.Second
+	SSH_OBFUSCATED_KEY_BYTE_LENGTH         = 32
+	DEFAULT_OBFUSCATED_SSH_SERVER_PORT     = 3333
 )
 )
 
 
 type Config struct {
 type Config struct {
-	ServerIPAddress      string
-	WebServerPort        int
-	WebServerSecret      string
-	WebServerCertificate string
-	WebServerPrivateKey  string
-	SSHPrivateKey        string
-	SSHServerVersion     string
-	SSHUserName          string
-	SSHPassword          string
-	SSHPort              int
+	ServerIPAddress         string
+	WebServerPort           int
+	WebServerSecret         string
+	WebServerCertificate    string
+	WebServerPrivateKey     string
+	SSHPrivateKey           string
+	SSHServerVersion        string
+	SSHUserName             string
+	SSHPassword             string
+	SSHServerPort           int
+	ObfuscatedSSHKey        string
+	ObfuscatedSSHServerPort int
 }
 }
 
 
 func LoadConfig(configJson []byte) (*Config, error) {
 func LoadConfig(configJson []byte) (*Config, error) {
@@ -73,18 +78,22 @@ func LoadConfig(configJson []byte) (*Config, error) {
 	}
 	}
 
 
 	// TODO: config field validation
 	// TODO: config field validation
+	// TODO: validation case: OSSH requires extra fields
 
 
 	return &config, nil
 	return &config, nil
 }
 }
 
 
 type GenerateConfigParams struct {
 type GenerateConfigParams struct {
-	ServerIPAddress string
-	WebServerPort   int
-	SSHServerPort   int
+	ServerIPAddress         string
+	WebServerPort           int
+	SSHServerPort           int
+	ObfuscatedSSHServerPort int
 }
 }
 
 
 func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, error) {
 func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, error) {
 
 
+	// TODO: support disabling web server or a subset of protocols
+
 	serverIPaddress := params.ServerIPAddress
 	serverIPaddress := params.ServerIPAddress
 	if serverIPaddress == "" {
 	if serverIPaddress == "" {
 		serverIPaddress = DEFAULT_SERVER_IP_ADDRESS
 		serverIPaddress = DEFAULT_SERVER_IP_ADDRESS
@@ -150,17 +159,33 @@ func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, error) {
 	// TODO: vary version string for anti-fingerprint
 	// TODO: vary version string for anti-fingerprint
 	sshServerVersion := "SSH-2.0-Psiphon"
 	sshServerVersion := "SSH-2.0-Psiphon"
 
 
+	// Obfuscated SSH config
+
+	obfuscatedSSHServerPort := params.ObfuscatedSSHServerPort
+	if obfuscatedSSHServerPort == 0 {
+		obfuscatedSSHServerPort = DEFAULT_OBFUSCATED_SSH_SERVER_PORT
+	}
+
+	obfuscatedSSHKey, err := psiphon.MakeRandomString(SSH_OBFUSCATED_KEY_BYTE_LENGTH)
+	if err != nil {
+		return nil, nil, psiphon.ContextError(err)
+	}
+
+	// Assemble config and server entry
+
 	config := &Config{
 	config := &Config{
-		ServerIPAddress:      serverIPaddress,
-		WebServerPort:        webServerPort,
-		WebServerSecret:      webServerSecret,
-		WebServerCertificate: webServerCertificate,
-		WebServerPrivateKey:  webServerPrivateKey,
-		SSHPrivateKey:        string(sshPrivateKey),
-		SSHServerVersion:     sshServerVersion,
-		SSHUserName:          sshUserName,
-		SSHPassword:          sshPassword,
-		SSHPort:              sshServerPort,
+		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,
 	}
 	}
 
 
 	encodedConfig, err := json.Marshal(config)
 	encodedConfig, err := json.Marshal(config)
@@ -172,6 +197,11 @@ func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, error) {
 	lines := strings.Split(webServerCertificate, "\n")
 	lines := strings.Split(webServerCertificate, "\n")
 	strippedWebServerCertificate := strings.Join(lines[1:len(lines)-2], "")
 	strippedWebServerCertificate := strings.Join(lines[1:len(lines)-2], "")
 
 
+	capabilities := []string{
+		psiphon.GetCapability(psiphon.TUNNEL_PROTOCOL_SSH),
+		psiphon.GetCapability(psiphon.TUNNEL_PROTOCOL_OBFUSCATED_SSH),
+	}
+
 	serverEntry := &psiphon.ServerEntry{
 	serverEntry := &psiphon.ServerEntry{
 		IpAddress:            serverIPaddress,
 		IpAddress:            serverIPaddress,
 		WebServerPort:        fmt.Sprintf("%d", webServerPort),
 		WebServerPort:        fmt.Sprintf("%d", webServerPort),
@@ -181,9 +211,9 @@ func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, error) {
 		SshUsername:          sshUserName,
 		SshUsername:          sshUserName,
 		SshPassword:          sshPassword,
 		SshPassword:          sshPassword,
 		SshHostKey:           base64.RawStdEncoding.EncodeToString(sshPublicKey.Marshal()),
 		SshHostKey:           base64.RawStdEncoding.EncodeToString(sshPublicKey.Marshal()),
-		SshObfuscatedPort:    0,
-		SshObfuscatedKey:     "",
-		Capabilities:         []string{"SSH"},
+		SshObfuscatedPort:    obfuscatedSSHServerPort,
+		SshObfuscatedKey:     obfuscatedSSHKey,
+		Capabilities:         capabilities,
 		Region:               "US",
 		Region:               "US",
 	}
 	}
 
 

+ 95 - 22
psiphon/server/sshService.go

@@ -21,28 +21,43 @@ package server
 
 
 import (
 import (
 	"encoding/json"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"fmt"
 	"io"
 	"io"
 	"net"
 	"net"
 	"sync"
 	"sync"
+	"time"
 
 
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon"
 	"golang.org/x/crypto/ssh"
 	"golang.org/x/crypto/ssh"
 )
 )
 
 
+func RunSSHServer(config *Config, shutdownBroadcast <-chan struct{}) error {
+	return runSSHServer(config, false, shutdownBroadcast)
+}
+
+func RunObfuscatedSSHServer(config *Config, shutdownBroadcast <-chan struct{}) error {
+	return runSSHServer(config, true, shutdownBroadcast)
+}
+
 type sshServer struct {
 type sshServer struct {
-	config          *Config
-	sshConfig       *ssh.ServerConfig
-	clientMutex     sync.Mutex
-	stoppingClients bool
-	clients         map[string]ssh.Conn
+	config            *Config
+	useObfuscation    bool
+	shutdownBroadcast <-chan struct{}
+	sshConfig         *ssh.ServerConfig
+	clientMutex       sync.Mutex
+	stoppingClients   bool
+	clients           map[string]ssh.Conn
 }
 }
 
 
-func RunSSHServer(config *Config, shutdownBroadcast <-chan struct{}) error {
+func runSSHServer(
+	config *Config, useObfuscation bool, shutdownBroadcast <-chan struct{}) error {
 
 
 	sshServer := &sshServer{
 	sshServer := &sshServer{
-		config:  config,
-		clients: make(map[string]ssh.Conn),
+		config:            config,
+		useObfuscation:    useObfuscation,
+		shutdownBroadcast: shutdownBroadcast,
+		clients:           make(map[string]ssh.Conn),
 	}
 	}
 
 
 	sshServer.sshConfig = &ssh.ServerConfig{
 	sshServer.sshConfig = &ssh.ServerConfig{
@@ -64,13 +79,21 @@ func RunSSHServer(config *Config, shutdownBroadcast <-chan struct{}) error {
 
 
 	sshServer.sshConfig.AddHostKey(signer)
 	sshServer.sshConfig.AddHostKey(signer)
 
 
+	var serverPort int
+	if useObfuscation {
+		serverPort = config.ObfuscatedSSHServerPort
+	} else {
+		serverPort = config.SSHServerPort
+	}
+
 	listener, err := net.Listen(
 	listener, err := net.Listen(
-		"tcp", fmt.Sprintf("%s:%d", config.ServerIPAddress, config.SSHPort))
+		"tcp", fmt.Sprintf("%s:%d", config.ServerIPAddress, serverPort))
 	if err != nil {
 	if err != nil {
 		return psiphon.ContextError(err)
 		return psiphon.ContextError(err)
 	}
 	}
 
 
-	log.WithContext().Info("starting")
+	log.WithContextFields(
+		LogFields{"useObfuscation": useObfuscation}).Info("starting")
 
 
 	err = nil
 	err = nil
 	errors := make(chan error)
 	errors := make(chan error)
@@ -86,6 +109,9 @@ func RunSSHServer(config *Config, shutdownBroadcast <-chan struct{}) error {
 
 
 			select {
 			select {
 			case <-shutdownBroadcast:
 			case <-shutdownBroadcast:
+				if err == nil {
+					conn.Close()
+				}
 				break loop
 				break loop
 			default:
 			default:
 			}
 			}
@@ -111,7 +137,8 @@ func RunSSHServer(config *Config, shutdownBroadcast <-chan struct{}) error {
 
 
 		sshServer.stopClients()
 		sshServer.stopClients()
 
 
-		log.WithContext().Info("stopped")
+		log.WithContextFields(
+			LogFields{"useObfuscation": useObfuscation}).Info("stopped")
 	}()
 	}()
 
 
 	select {
 	select {
@@ -123,7 +150,8 @@ func RunSSHServer(config *Config, shutdownBroadcast <-chan struct{}) error {
 
 
 	waitGroup.Wait()
 	waitGroup.Wait()
 
 
-	log.WithContext().Info("exiting")
+	log.WithContextFields(
+		LogFields{"useObfuscation": useObfuscation}).Info("exiting")
 
 
 	return err
 	return err
 }
 }
@@ -192,27 +220,72 @@ func (sshServer *sshServer) stopClients() {
 
 
 func (sshServer *sshServer) handleClient(conn net.Conn) {
 func (sshServer *sshServer) handleClient(conn net.Conn) {
 
 
-	// TODO: does this block on SSH handshake (so should be in goroutine)?
-	sshConn, channels, requests, err := ssh.NewServerConn(conn, sshServer.sshConfig)
-	if err != nil {
+	// Run the initial [obfuscated] SSH handshake in a goroutine
+	// so we can both respect shutdownBroadcast and implement a
+	// handshake timeout. The timeout is to reclaim network
+	// resources in case the handshake takes too long.
+
+	type sshNewServerConnResult struct {
+		conn     net.Conn
+		sshConn  *ssh.ServerConn
+		channels <-chan ssh.NewChannel
+		requests <-chan *ssh.Request
+		err      error
+	}
+
+	resultChannel := make(chan *sshNewServerConnResult, 2)
+
+	if SSH_HANDSHAKE_TIMEOUT > 0 {
+		time.AfterFunc(time.Duration(SSH_HANDSHAKE_TIMEOUT), func() {
+			resultChannel <- &sshNewServerConnResult{err: errors.New("ssh handshake timeout")}
+		})
+	}
+
+	go func() {
+		result := &sshNewServerConnResult{}
+		if sshServer.useObfuscation {
+			result.conn, result.err = psiphon.NewObfuscatedSshConn(
+				psiphon.OBFUSCATION_CONN_MODE_SERVER, conn, sshServer.config.ObfuscatedSSHKey)
+		} else {
+			result.conn = conn
+		}
+		if result.err == nil {
+			result.sshConn, result.channels,
+				result.requests, result.err = ssh.NewServerConn(conn, sshServer.sshConfig)
+		}
+		resultChannel <- result
+	}()
+
+	var result *sshNewServerConnResult
+	select {
+	case result = <-resultChannel:
+	case <-sshServer.shutdownBroadcast:
+		// Close() will interrupt an ongoing handshake
+		// TODO: wait for goroutine to exit before returning?
 		conn.Close()
 		conn.Close()
-		log.WithContextFields(LogFields{"error": err}).Warning("establish failed")
 		return
 		return
 	}
 	}
 
 
-	if !sshServer.registerClient(sshConn) {
-		sshConn.Close()
+	if result.err != nil {
+		conn.Close()
+		log.WithContextFields(LogFields{"error": result.err}).Warning("handshake failed")
+		return
+	}
+
+	if !sshServer.registerClient(result.sshConn) {
+		result.sshConn.Close()
 		log.WithContext().Warning("register failed")
 		log.WithContext().Warning("register failed")
 		return
 		return
 	}
 	}
-	defer sshServer.unregisterClient(sshConn)
+	defer sshServer.unregisterClient(result.sshConn)
 
 
 	// TODO: don't record IP; do GeoIP
 	// TODO: don't record IP; do GeoIP
-	log.WithContextFields(LogFields{"remoteAddr": sshConn.RemoteAddr()}).Warning("connection accepted")
+	log.WithContextFields(
+		LogFields{"remoteAddr": result.sshConn.RemoteAddr()}).Warning("connection accepted")
 
 
-	go ssh.DiscardRequests(requests)
+	go ssh.DiscardRequests(result.requests)
 
 
-	for newChannel := range channels {
+	for newChannel := range result.channels {
 
 
 		if newChannel.ChannelType() != "direct-tcpip" {
 		if newChannel.ChannelType() != "direct-tcpip" {
 			sshServer.rejectNewChannel(newChannel, ssh.Prohibited, "unknown or unsupported channel type")
 			sshServer.rejectNewChannel(newChannel, ssh.Prohibited, "unknown or unsupported channel type")

+ 8 - 2
psiphon/serverEntry.go

@@ -90,10 +90,16 @@ const (
 	SERVER_ENTRY_SOURCE_TARGET    ServerEntrySource = "TARGET"
 	SERVER_ENTRY_SOURCE_TARGET    ServerEntrySource = "TARGET"
 )
 )
 
 
+// GetCapability returns the server capability corresponding
+// to the protocol.
+func GetCapability(protocol string) string {
+	return strings.TrimSuffix(protocol, "-OSSH")
+}
+
 // SupportsProtocol returns true if and only if the ServerEntry has
 // SupportsProtocol returns true if and only if the ServerEntry has
 // the necessary capability to support the specified tunnel protocol.
 // the necessary capability to support the specified tunnel protocol.
 func (serverEntry *ServerEntry) SupportsProtocol(protocol string) bool {
 func (serverEntry *ServerEntry) SupportsProtocol(protocol string) bool {
-	requiredCapability := strings.TrimSuffix(protocol, "-OSSH")
+	requiredCapability := GetCapability(protocol)
 	return Contains(serverEntry.Capabilities, requiredCapability)
 	return Contains(serverEntry.Capabilities, requiredCapability)
 }
 }
 
 
@@ -117,7 +123,7 @@ func (serverEntry *ServerEntry) DisableImpairedProtocols(impairedProtocols []str
 	for _, capability := range serverEntry.Capabilities {
 	for _, capability := range serverEntry.Capabilities {
 		omit := false
 		omit := false
 		for _, protocol := range impairedProtocols {
 		for _, protocol := range impairedProtocols {
-			requiredCapability := strings.TrimSuffix(protocol, "-OSSH")
+			requiredCapability := GetCapability(protocol)
 			if capability == requiredCapability {
 			if capability == requiredCapability {
 				omit = true
 				omit = true
 				break
 				break

+ 1 - 1
psiphon/tunnel.go

@@ -607,7 +607,7 @@ func dialSsh(
 	}
 	}
 
 
 	go func() {
 	go func() {
-		// The folowing is adapted from ssh.Dial(), here using a custom conn
+		// The following is adapted from ssh.Dial(), here using a custom conn
 		// The sshAddress is passed through to host key verification callbacks; we don't use it.
 		// The sshAddress is passed through to host key verification callbacks; we don't use it.
 		sshAddress := ""
 		sshAddress := ""
 		sshClientConn, sshChans, sshReqs, err := ssh.NewClientConn(sshConn, sshAddress, sshClientConfig)
 		sshClientConn, sshChans, sshReqs, err := ssh.NewClientConn(sshConn, sshAddress, sshClientConfig)