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

Test refactoring
- Move ServerEntry and related routines to "protocol"
- Move Obfuscator and ObfuscatedSSHConn to "common"
- Add unit tests for obfuscation
- "server" package no longer references "psiphon",
which resolves testing import cycles
- Make server_test output less noisy: don't log
server_load as frequently, test client verification
only as much as necessary
- Fix server_test shutdown issue related to not
always stopping client

Rod Hynes 9 лет назад
Родитель
Сommit
edbf21bfe4

+ 2 - 0
.travis.yml

@@ -11,11 +11,13 @@ script:
 - cd psiphon
 - go test -race -v ./common
 - go test -race -v ./common/osl
+- go test -race -v ./common/protocol
 - go test -race -v ./transferstats
 - go test -race -v ./server
 - go test -race -v
 - go test -v -covermode=count -coverprofile=common.coverprofile ./common
 - go test -v -covermode=count -coverprofile=osl.coverprofile ./common/osl
+- go test -v -covermode=count -coverprofile=protocol.coverprofile ./common/protocol
 - go test -v -covermode=count -coverprofile=transferstats.coverprofile ./transferstats
 - go test -v -covermode=count -coverprofile=server.coverprofile ./server
 - go test -v -covermode=count -coverprofile=psiphon.coverprofile

+ 1 - 1
ConsoleClient/main.go

@@ -142,7 +142,7 @@ func main() {
 				return
 			}
 			// TODO: stream embedded server list data? also, the cast makes an unnecessary copy of a large buffer?
-			serverEntries, err := psiphon.DecodeAndValidateServerEntryList(
+			serverEntries, err := protocol.DecodeAndValidateServerEntryList(
 				string(serverEntryList),
 				common.GetCurrentTimestamp(),
 				protocol.SERVER_ENTRY_SOURCE_EMBEDDED)

+ 1 - 1
MobileLibrary/psi/psi.go

@@ -83,7 +83,7 @@ func Start(
 		return fmt.Errorf("error initializing datastore: %s", err)
 	}
 
-	serverEntries, err := psiphon.DecodeAndValidateServerEntryList(
+	serverEntries, err := protocol.DecodeAndValidateServerEntryList(
 		embeddedServerEntryList,
 		common.GetCurrentTimestamp(),
 		protocol.SERVER_ENTRY_SOURCE_EMBEDDED)

+ 2 - 2
Server/main.go

@@ -27,7 +27,7 @@ import (
 	"strconv"
 	"strings"
 
-	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon"
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/server"
 )
 
@@ -111,7 +111,7 @@ func main() {
 
 		if generateServerNetworkInterface != "" {
 			var err error
-			serverIPaddress, err = psiphon.GetInterfaceIPAddress(generateServerNetworkInterface)
+			serverIPaddress, err = common.GetInterfaceIPAddress(generateServerNetworkInterface)
 			if err != nil {
 				fmt.Printf("generate failed: %s\n", err)
 				os.Exit(1)

+ 4 - 6
psiphon/networkInterface.go → psiphon/common/networkInterface.go

@@ -17,13 +17,11 @@
  *
  */
 
-package psiphon
+package common
 
 import (
 	"errors"
 	"net"
-
-	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 )
 
 // Take in an interface name ("lo", "eth0", "any") passed from either
@@ -43,12 +41,12 @@ func GetInterfaceIPAddress(listenInterface string) (string, error) {
 	} else {
 		availableInterfaces, err := net.InterfaceByName(listenInterface)
 		if err != nil {
-			return "", common.ContextError(err)
+			return "", ContextError(err)
 		}
 
 		addrs, err := availableInterfaces.Addrs()
 		if err != nil {
-			return "", common.ContextError(err)
+			return "", ContextError(err)
 		}
 		for _, addr := range addrs {
 			iptype := addr.(*net.IPNet)
@@ -64,6 +62,6 @@ func GetInterfaceIPAddress(listenInterface string) (string, error) {
 		}
 	}
 
-	return "", common.ContextError(errors.New("Could not find IP address of specified interface"))
+	return "", ContextError(errors.New("Could not find IP address of specified interface"))
 
 }

+ 25 - 27
psiphon/obfuscatedSshConn.go → psiphon/common/obfuscatedSshConn.go

@@ -17,7 +17,7 @@
  *
  */
 
-package psiphon
+package common
 
 import (
 	"bytes"
@@ -25,8 +25,6 @@ import (
 	"errors"
 	"io"
 	"net"
-
-	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 )
 
 const (
@@ -117,7 +115,7 @@ func NewObfuscatedSshConn(
 	if mode == OBFUSCATION_CONN_MODE_CLIENT {
 		obfuscator, err = NewClientObfuscator(&ObfuscatorConfig{Keyword: obfuscationKeyword})
 		if err != nil {
-			return nil, common.ContextError(err)
+			return nil, ContextError(err)
 		}
 		readDeobfuscate = obfuscator.ObfuscateServerToClient
 		writeObfuscate = obfuscator.ObfuscateClientToServer
@@ -128,7 +126,7 @@ func NewObfuscatedSshConn(
 			conn, &ObfuscatorConfig{Keyword: obfuscationKeyword})
 		if err != nil {
 			// TODO: readForver() equivilent
-			return nil, common.ContextError(err)
+			return nil, ContextError(err)
 		}
 		readDeobfuscate = obfuscator.ObfuscateClientToServer
 		writeObfuscate = obfuscator.ObfuscateServerToClient
@@ -163,7 +161,7 @@ func (conn *ObfuscatedSshConn) Write(buffer []byte) (n int, err error) {
 	}
 	err = conn.transformAndWrite(buffer)
 	if err != nil {
-		return 0, common.ContextError(err)
+		return 0, ContextError(err)
 	}
 	// Reports that we wrote all the bytes
 	// (althogh we may have buffered some or all)
@@ -220,7 +218,7 @@ func (conn *ObfuscatedSshConn) readAndTransform(buffer []byte) (n int, err error
 				conn.readBuffer, err = readSshIdentificationLine(
 					conn.Conn, conn.readDeobfuscate)
 				if err != nil {
-					return 0, common.ContextError(err)
+					return 0, ContextError(err)
 				}
 				if bytes.HasPrefix(conn.readBuffer, []byte("SSH-")) {
 					break
@@ -237,7 +235,7 @@ func (conn *ObfuscatedSshConn) readAndTransform(buffer []byte) (n int, err error
 			conn.readBuffer, isMsgNewKeys, err = readSshPacket(
 				conn.Conn, conn.readDeobfuscate)
 			if err != nil {
-				return 0, common.ContextError(err)
+				return 0, ContextError(err)
 			}
 
 			if isMsgNewKeys {
@@ -249,7 +247,7 @@ func (conn *ObfuscatedSshConn) readAndTransform(buffer []byte) (n int, err error
 		nextState = OBFUSCATION_READ_STATE_FINISHED
 
 	case OBFUSCATION_READ_STATE_FINISHED:
-		return 0, common.ContextError(errors.New("invalid read state"))
+		return 0, ContextError(errors.New("invalid read state"))
 	}
 
 	n = copy(buffer, conn.readBuffer)
@@ -308,18 +306,18 @@ func (conn *ObfuscatedSshConn) transformAndWrite(buffer []byte) (err error) {
 	if conn.writeState == OBFUSCATION_WRITE_STATE_CLIENT_SEND_SEED_MESSAGE {
 		_, err = conn.Conn.Write(conn.obfuscator.SendSeedMessage())
 		if err != nil {
-			return common.ContextError(err)
+			return ContextError(err)
 		}
 		conn.writeState = OBFUSCATION_WRITE_STATE_IDENTIFICATION_LINE
 	} else if conn.writeState == OBFUSCATION_WRITE_STATE_SERVER_SEND_IDENTIFICATION_LINE_PADDING {
 		padding, err := makeServerIdentificationLinePadding()
 		if err != nil {
-			return common.ContextError(err)
+			return ContextError(err)
 		}
 		conn.writeObfuscate(padding)
 		_, err = conn.Conn.Write(padding)
 		if err != nil {
-			return common.ContextError(err)
+			return ContextError(err)
 		}
 		conn.writeState = OBFUSCATION_WRITE_STATE_IDENTIFICATION_LINE
 	}
@@ -338,21 +336,21 @@ func (conn *ObfuscatedSshConn) transformAndWrite(buffer []byte) (err error) {
 		var hasMsgNewKeys bool
 		conn.writeBuffer, sendBuffer, hasMsgNewKeys, err = extractSshPackets(conn.writeBuffer)
 		if err != nil {
-			return common.ContextError(err)
+			return ContextError(err)
 		}
 		if hasMsgNewKeys {
 			conn.writeState = OBFUSCATION_WRITE_STATE_FINISHED
 		}
 
 	case OBFUSCATION_WRITE_STATE_FINISHED:
-		return common.ContextError(errors.New("invalid write state"))
+		return ContextError(errors.New("invalid write state"))
 	}
 
 	if sendBuffer != nil {
 		conn.writeObfuscate(sendBuffer)
 		_, err := conn.Conn.Write(sendBuffer)
 		if err != nil {
-			return common.ContextError(err)
+			return ContextError(err)
 		}
 	}
 
@@ -360,7 +358,7 @@ func (conn *ObfuscatedSshConn) transformAndWrite(buffer []byte) (err error) {
 		// After SSH_MSG_NEWKEYS, any remaining bytes are un-obfuscated
 		_, err := conn.Conn.Write(conn.writeBuffer)
 		if err != nil {
-			return common.ContextError(err)
+			return ContextError(err)
 		}
 		// The buffer memory is no longer used
 		conn.writeBuffer = nil
@@ -378,7 +376,7 @@ func readSshIdentificationLine(
 	for len(readBuffer) < SSH_MAX_SERVER_LINE_LENGTH {
 		_, err := io.ReadFull(conn, oneByte[:])
 		if err != nil {
-			return nil, common.ContextError(err)
+			return nil, ContextError(err)
 		}
 		deobfuscate(oneByte[:])
 		readBuffer = append(readBuffer, oneByte[0])
@@ -388,7 +386,7 @@ func readSshIdentificationLine(
 		}
 	}
 	if !validLine {
-		return nil, common.ContextError(errors.New("invalid identification line"))
+		return nil, ContextError(errors.New("invalid identification line"))
 	}
 	return readBuffer, nil
 }
@@ -399,18 +397,18 @@ func readSshPacket(
 	prefix := make([]byte, SSH_PACKET_PREFIX_LENGTH)
 	_, err := io.ReadFull(conn, prefix)
 	if err != nil {
-		return nil, false, common.ContextError(err)
+		return nil, false, ContextError(err)
 	}
 	deobfuscate(prefix)
 	packetLength, _, payloadLength, messageLength := getSshPacketPrefix(prefix)
 	if packetLength > SSH_MAX_PACKET_LENGTH {
-		return nil, false, common.ContextError(errors.New("ssh packet length too large"))
+		return nil, false, ContextError(errors.New("ssh packet length too large"))
 	}
 	readBuffer := make([]byte, messageLength)
 	copy(readBuffer, prefix)
 	_, err = io.ReadFull(conn, readBuffer[len(prefix):])
 	if err != nil {
-		return nil, false, common.ContextError(err)
+		return nil, false, ContextError(err)
 	}
 	deobfuscate(readBuffer[len(prefix):])
 	isMsgNewKeys := false
@@ -426,9 +424,9 @@ func readSshPacket(
 // From the original patch to sshd.c:
 // https://bitbucket.org/psiphon/psiphon-circumvention-system/commits/f40865ce624b680be840dc2432283c8137bd896d
 func makeServerIdentificationLinePadding() ([]byte, error) {
-	paddingLength, err := common.MakeSecureRandomInt(OBFUSCATE_MAX_PADDING - 2) // 2 = CRLF
+	paddingLength, err := MakeSecureRandomInt(OBFUSCATE_MAX_PADDING - 2) // 2 = CRLF
 	if err != nil {
-		return nil, common.ContextError(err)
+		return nil, ContextError(err)
 	}
 	paddingLength += 2
 	padding := make([]byte, paddingLength)
@@ -492,14 +490,14 @@ func extractSshPackets(writeBuffer []byte) ([]byte, []byte, bool, error) {
 		possiblePaddings := (SSH_MAX_PADDING_LENGTH - paddingLength) / SSH_PADDING_MULTIPLE
 		if possiblePaddings > 0 {
 			// selectedPadding is integer in range [0, possiblePaddings)
-			selectedPadding, err := common.MakeSecureRandomInt(possiblePaddings)
+			selectedPadding, err := MakeSecureRandomInt(possiblePaddings)
 			if err != nil {
-				return nil, nil, false, common.ContextError(err)
+				return nil, nil, false, ContextError(err)
 			}
 			extraPaddingLength := selectedPadding * SSH_PADDING_MULTIPLE
-			extraPadding, err := common.MakeSecureRandomBytes(extraPaddingLength)
+			extraPadding, err := MakeSecureRandomBytes(extraPaddingLength)
 			if err != nil {
-				return nil, nil, false, common.ContextError(err)
+				return nil, nil, false, ContextError(err)
 			}
 			setSshPacketPrefix(
 				packetBuffer, packetLength+extraPaddingLength, paddingLength+extraPaddingLength)

+ 27 - 29
psiphon/obfuscator.go → psiphon/common/obfuscator.go

@@ -17,7 +17,7 @@
  *
  */
 
-package psiphon
+package common
 
 import (
 	"bytes"
@@ -26,8 +26,6 @@ import (
 	"encoding/binary"
 	"errors"
 	"io"
-
-	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 )
 
 const (
@@ -60,14 +58,14 @@ type ObfuscatorConfig struct {
 func NewClientObfuscator(
 	config *ObfuscatorConfig) (obfuscator *Obfuscator, err error) {
 
-	seed, err := common.MakeSecureRandomBytes(OBFUSCATE_SEED_LENGTH)
+	seed, err := MakeSecureRandomBytes(OBFUSCATE_SEED_LENGTH)
 	if err != nil {
-		return nil, common.ContextError(err)
+		return nil, ContextError(err)
 	}
 
 	clientToServerCipher, serverToClientCipher, err := initObfuscatorCiphers(seed, config)
 	if err != nil {
-		return nil, common.ContextError(err)
+		return nil, ContextError(err)
 	}
 
 	maxPadding := OBFUSCATE_MAX_PADDING
@@ -77,7 +75,7 @@ func NewClientObfuscator(
 
 	seedMessage, err := makeSeedMessage(maxPadding, seed, clientToServerCipher)
 	if err != nil {
-		return nil, common.ContextError(err)
+		return nil, ContextError(err)
 	}
 
 	return &Obfuscator{
@@ -94,7 +92,7 @@ func NewServerObfuscator(
 	clientToServerCipher, serverToClientCipher, err := readSeedMessage(
 		clientReader, config)
 	if err != nil {
-		return nil, common.ContextError(err)
+		return nil, ContextError(err)
 	}
 
 	return &Obfuscator{
@@ -125,22 +123,22 @@ func initObfuscatorCiphers(
 
 	clientToServerKey, err := deriveKey(seed, []byte(config.Keyword), []byte(OBFUSCATE_CLIENT_TO_SERVER_IV))
 	if err != nil {
-		return nil, nil, common.ContextError(err)
+		return nil, nil, ContextError(err)
 	}
 
 	serverToClientKey, err := deriveKey(seed, []byte(config.Keyword), []byte(OBFUSCATE_SERVER_TO_CLIENT_IV))
 	if err != nil {
-		return nil, nil, common.ContextError(err)
+		return nil, nil, ContextError(err)
 	}
 
 	clientToServerCipher, err := rc4.NewCipher(clientToServerKey)
 	if err != nil {
-		return nil, nil, common.ContextError(err)
+		return nil, nil, ContextError(err)
 	}
 
 	serverToClientCipher, err := rc4.NewCipher(serverToClientKey)
 	if err != nil {
-		return nil, nil, common.ContextError(err)
+		return nil, nil, ContextError(err)
 	}
 
 	return clientToServerCipher, serverToClientCipher, nil
@@ -158,37 +156,37 @@ func deriveKey(seed, keyword, iv []byte) ([]byte, error) {
 		digest = h.Sum(nil)
 	}
 	if len(digest) < OBFUSCATE_KEY_LENGTH {
-		return nil, common.ContextError(errors.New("insufficient bytes for obfuscation key"))
+		return nil, ContextError(errors.New("insufficient bytes for obfuscation key"))
 	}
 	return digest[0:OBFUSCATE_KEY_LENGTH], nil
 }
 
 func makeSeedMessage(maxPadding int, seed []byte, clientToServerCipher *rc4.Cipher) ([]byte, error) {
 	// paddingLength is integer in range [0, maxPadding]
-	paddingLength, err := common.MakeSecureRandomInt(maxPadding + 1)
+	paddingLength, err := MakeSecureRandomInt(maxPadding + 1)
 	if err != nil {
-		return nil, common.ContextError(err)
+		return nil, ContextError(err)
 	}
-	padding, err := common.MakeSecureRandomBytes(paddingLength)
+	padding, err := MakeSecureRandomBytes(paddingLength)
 	if err != nil {
-		return nil, common.ContextError(err)
+		return nil, ContextError(err)
 	}
 	buffer := new(bytes.Buffer)
 	err = binary.Write(buffer, binary.BigEndian, seed)
 	if err != nil {
-		return nil, common.ContextError(err)
+		return nil, ContextError(err)
 	}
 	err = binary.Write(buffer, binary.BigEndian, uint32(OBFUSCATE_MAGIC_VALUE))
 	if err != nil {
-		return nil, common.ContextError(err)
+		return nil, ContextError(err)
 	}
 	err = binary.Write(buffer, binary.BigEndian, uint32(paddingLength))
 	if err != nil {
-		return nil, common.ContextError(err)
+		return nil, ContextError(err)
 	}
 	err = binary.Write(buffer, binary.BigEndian, padding)
 	if err != nil {
-		return nil, common.ContextError(err)
+		return nil, ContextError(err)
 	}
 	seedMessage := buffer.Bytes()
 	clientToServerCipher.XORKeyStream(seedMessage[len(seed):], seedMessage[len(seed):])
@@ -201,18 +199,18 @@ func readSeedMessage(
 	seed := make([]byte, OBFUSCATE_SEED_LENGTH)
 	_, err := io.ReadFull(clientReader, seed)
 	if err != nil {
-		return nil, nil, common.ContextError(err)
+		return nil, nil, ContextError(err)
 	}
 
 	clientToServerCipher, serverToClientCipher, err := initObfuscatorCiphers(seed, config)
 	if err != nil {
-		return nil, nil, common.ContextError(err)
+		return nil, nil, ContextError(err)
 	}
 
 	fixedLengthFields := make([]byte, 8) // 4 bytes each for magic value and padding length
 	_, err = io.ReadFull(clientReader, fixedLengthFields)
 	if err != nil {
-		return nil, nil, common.ContextError(err)
+		return nil, nil, ContextError(err)
 	}
 
 	clientToServerCipher.XORKeyStream(fixedLengthFields, fixedLengthFields)
@@ -222,25 +220,25 @@ func readSeedMessage(
 	var magicValue, paddingLength int32
 	err = binary.Read(buffer, binary.BigEndian, &magicValue)
 	if err != nil {
-		return nil, nil, common.ContextError(err)
+		return nil, nil, ContextError(err)
 	}
 	err = binary.Read(buffer, binary.BigEndian, &paddingLength)
 	if err != nil {
-		return nil, nil, common.ContextError(err)
+		return nil, nil, ContextError(err)
 	}
 
 	if magicValue != OBFUSCATE_MAGIC_VALUE {
-		return nil, nil, common.ContextError(errors.New("invalid magic value"))
+		return nil, nil, ContextError(errors.New("invalid magic value"))
 	}
 
 	if paddingLength < 0 || paddingLength > OBFUSCATE_MAX_PADDING {
-		return nil, nil, common.ContextError(errors.New("invalid padding length"))
+		return nil, nil, ContextError(errors.New("invalid padding length"))
 	}
 
 	padding := make([]byte, paddingLength)
 	_, err = io.ReadFull(clientReader, padding)
 	if err != nil {
-		return nil, nil, common.ContextError(err)
+		return nil, nil, ContextError(err)
 	}
 
 	clientToServerCipher.XORKeyStream(padding, padding)

+ 161 - 0
psiphon/common/obfuscator_test.go

@@ -0,0 +1,161 @@
+/*
+ * 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 common
+
+import (
+	"bytes"
+	"crypto/rand"
+	"crypto/rsa"
+	"errors"
+	"net"
+	"testing"
+	"time"
+
+	"github.com/Psiphon-Inc/crypto/ssh"
+)
+
+func TestObfuscator(t *testing.T) {
+
+	keyword, _ := MakeRandomStringHex(32)
+
+	config := &ObfuscatorConfig{
+		Keyword:    keyword,
+		MaxPadding: 256,
+	}
+
+	client, err := NewClientObfuscator(config)
+	if err != nil {
+		t.Fatalf("NewClientObfuscator failed: %s", err)
+	}
+
+	seedMessage := client.SendSeedMessage()
+
+	server, err := NewServerObfuscator(bytes.NewReader(seedMessage), config)
+	if err != nil {
+		t.Fatalf("NewServerObfuscator failed: %s", err)
+	}
+
+	clientMessage := []byte("client hello")
+
+	b := append([]byte(nil), clientMessage...)
+	client.ObfuscateClientToServer(b)
+	server.ObfuscateClientToServer(b)
+
+	if !bytes.Equal(clientMessage, b) {
+		t.Fatalf("unexpected client message")
+	}
+
+	serverMessage := []byte("server hello")
+
+	b = append([]byte(nil), serverMessage...)
+	client.ObfuscateServerToClient(b)
+	server.ObfuscateServerToClient(b)
+
+	if !bytes.Equal(serverMessage, b) {
+		t.Fatalf("unexpected client message")
+	}
+}
+
+func TestObfuscatedSSHConn(t *testing.T) {
+
+	keyword, _ := MakeRandomStringHex(32)
+
+	serverAddress := "127.0.0.1:2222"
+
+	listener, err := net.Listen("tcp", serverAddress)
+	if err != nil {
+		t.Fatalf("Listen failed: %s", err)
+	}
+
+	rsaKey, err := rsa.GenerateKey(rand.Reader, 2048)
+	if err != nil {
+		t.Fatalf("GenerateKey failed: %s", err)
+	}
+
+	hostKey, err := ssh.NewSignerFromKey(rsaKey)
+	if err != nil {
+		t.Fatalf("NewSignerFromKey failed: %s", err)
+	}
+
+	sshCertChecker := &ssh.CertChecker{
+		HostKeyFallback: func(addr string, remote net.Addr, publicKey ssh.PublicKey) error {
+			if !bytes.Equal(hostKey.PublicKey().Marshal(), publicKey.Marshal()) {
+				return errors.New("unexpected host public key")
+			}
+			return nil
+		},
+	}
+
+	result := make(chan error, 1)
+
+	go func() {
+
+		conn, err := listener.Accept()
+
+		if err == nil {
+			conn, err = NewObfuscatedSshConn(
+				OBFUSCATION_CONN_MODE_SERVER, conn, keyword)
+		}
+
+		if err == nil {
+			config := &ssh.ServerConfig{
+				NoClientAuth: true,
+			}
+			config.AddHostKey(hostKey)
+
+			_, _, _, err = ssh.NewServerConn(conn, config)
+		}
+
+		if err != nil {
+			select {
+			case result <- err:
+			default:
+			}
+		}
+	}()
+
+	go func() {
+
+		conn, err := net.DialTimeout("tcp", serverAddress, 5*time.Second)
+
+		if err == nil {
+			conn, err = NewObfuscatedSshConn(
+				OBFUSCATION_CONN_MODE_CLIENT, conn, keyword)
+		}
+
+		if err == nil {
+			config := &ssh.ClientConfig{
+				HostKeyCallback: sshCertChecker.CheckHostKey,
+			}
+			_, _, _, err = ssh.NewClientConn(conn, "", config)
+		}
+
+		// Sends nil on success
+		select {
+		case result <- err:
+		default:
+		}
+	}()
+
+	err = <-result
+	if err != nil {
+		t.Fatalf("obfuscated SSH handshake failed: %s", err)
+	}
+}

+ 6 - 9
psiphon/serverEntry.go → psiphon/common/protocol/serverEntry.go

@@ -17,7 +17,7 @@
  *
  */
 
-package psiphon
+package protocol
 
 import (
 	"bytes"
@@ -29,7 +29,6 @@ import (
 	"strings"
 
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
-	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
 )
 
 // ServerEntry represents a Psiphon server. It contains information
@@ -83,7 +82,7 @@ func (serverEntry *ServerEntry) SupportsProtocol(protocol string) bool {
 // by the ServerEntry's capabilities.
 func (serverEntry *ServerEntry) GetSupportedProtocols() []string {
 	supportedProtocols := make([]string, 0)
-	for _, protocol := range protocol.SupportedTunnelProtocols {
+	for _, protocol := range SupportedTunnelProtocols {
 		if serverEntry.SupportsProtocol(protocol) {
 			supportedProtocols = append(supportedProtocols, protocol)
 		}
@@ -115,16 +114,16 @@ func (serverEntry *ServerEntry) DisableImpairedProtocols(impairedProtocols []str
 // SupportsSSHAPIRequests returns true when the server supports
 // SSH API requests.
 func (serverEntry *ServerEntry) SupportsSSHAPIRequests() bool {
-	return common.Contains(serverEntry.Capabilities, protocol.CAPABILITY_SSH_API_REQUESTS)
+	return common.Contains(serverEntry.Capabilities, CAPABILITY_SSH_API_REQUESTS)
 }
 
 func (serverEntry *ServerEntry) GetUntunneledWebRequestPorts() []string {
 	ports := make([]string, 0)
-	if common.Contains(serverEntry.Capabilities, protocol.CAPABILITY_UNTUNNELED_WEB_API_REQUESTS) {
+	if common.Contains(serverEntry.Capabilities, CAPABILITY_UNTUNNELED_WEB_API_REQUESTS) {
 		// Server-side configuration quirk: there's a port forward from
 		// port 443 to the web server, which we can try, except on servers
 		// running FRONTED_MEEK, which listens on port 443.
-		if !serverEntry.SupportsProtocol(protocol.TUNNEL_PROTOCOL_FRONTED_MEEK) {
+		if !serverEntry.SupportsProtocol(TUNNEL_PROTOCOL_FRONTED_MEEK) {
 			ports = append(ports, "443")
 		}
 		ports = append(ports, serverEntry.WebServerPort)
@@ -196,9 +195,6 @@ func ValidateServerEntry(serverEntry *ServerEntry) error {
 	ipAddr := net.ParseIP(serverEntry.IpAddress)
 	if ipAddr == nil {
 		errMsg := fmt.Sprintf("server entry has invalid IpAddress: '%s'", serverEntry.IpAddress)
-		// Some callers skip invalid server entries without propagating
-		// the error mesage, so issue a notice.
-		NoticeAlert(errMsg)
 		return common.ContextError(errors.New(errMsg))
 	}
 	return nil
@@ -226,6 +222,7 @@ func DecodeAndValidateServerEntryList(
 
 		if ValidateServerEntry(serverEntry) != nil {
 			// Skip this entry and continue with the next one
+			// TODO: invoke a logging callback
 			continue
 		}
 

+ 3 - 4
psiphon/serverEntry_test.go → psiphon/common/protocol/serverEntry_test.go

@@ -17,14 +17,13 @@
  *
  */
 
-package psiphon
+package protocol
 
 import (
 	"encoding/hex"
 	"testing"
 
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
-	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
 )
 
 const (
@@ -44,7 +43,7 @@ func TestDecodeAndValidateServerEntryList(t *testing.T) {
 		hex.EncodeToString([]byte(_INVALID_MALFORMED_IP_ADDRESS_SERVER_ENTRY))
 
 	serverEntries, err := DecodeAndValidateServerEntryList(
-		testEncodedServerEntryList, common.GetCurrentTimestamp(), protocol.SERVER_ENTRY_SOURCE_EMBEDDED)
+		testEncodedServerEntryList, common.GetCurrentTimestamp(), SERVER_ENTRY_SOURCE_EMBEDDED)
 	if err != nil {
 		t.Error(err.Error())
 		t.FailNow()
@@ -67,7 +66,7 @@ func TestInvalidServerEntries(t *testing.T) {
 	for _, testCase := range testCases {
 		encodedServerEntry := hex.EncodeToString([]byte(testCase))
 		serverEntry, err := DecodeServerEntry(
-			encodedServerEntry, common.GetCurrentTimestamp(), protocol.SERVER_ENTRY_SOURCE_EMBEDDED)
+			encodedServerEntry, common.GetCurrentTimestamp(), SERVER_ENTRY_SOURCE_EMBEDDED)
 		if err != nil {
 			t.Error(err.Error())
 		}

+ 5 - 3
psiphon/controller.go

@@ -69,7 +69,7 @@ type Controller struct {
 }
 
 type candidateServerEntry struct {
-	serverEntry                *ServerEntry
+	serverEntry                *protocol.ServerEntry
 	isServerAffinityCandidate  bool
 	adjustedEstablishStartTime monotime.Time
 }
@@ -163,7 +163,7 @@ func (controller *Controller) Run(shutdownBroadcast <-chan struct{}) {
 
 	// Start components
 
-	listenIP, err := GetInterfaceIPAddress(controller.config.ListenInterface)
+	listenIP, err := common.GetInterfaceIPAddress(controller.config.ListenInterface)
 	if err != nil {
 		NoticeError("error getting listener IP: %s", err)
 		return
@@ -876,7 +876,9 @@ func (controller *Controller) getNextActiveTunnel() (tunnel *Tunnel) {
 
 // isActiveTunnelServerEntry is used to check if there's already
 // an existing tunnel to a candidate server.
-func (controller *Controller) isActiveTunnelServerEntry(serverEntry *ServerEntry) bool {
+func (controller *Controller) isActiveTunnelServerEntry(
+	serverEntry *protocol.ServerEntry) bool {
+
 	controller.tunnelMutex.Lock()
 	defer controller.tunnelMutex.Unlock()
 	for _, activeTunnel := range controller.tunnels {

+ 17 - 17
psiphon/dataStore.go

@@ -169,12 +169,12 @@ func checkInitDataStore() {
 // overwritten; otherwise, the existing record is unchanged.
 // If the server entry data is malformed, an alert notice is issued and
 // the entry is skipped; no error is returned.
-func StoreServerEntry(serverEntry *ServerEntry, replaceIfExists bool) error {
+func StoreServerEntry(serverEntry *protocol.ServerEntry, replaceIfExists bool) error {
 	checkInitDataStore()
 
 	// Server entries should already be validated before this point,
 	// so instead of skipping we fail with an error.
-	err := ValidateServerEntry(serverEntry)
+	err := protocol.ValidateServerEntry(serverEntry)
 	if err != nil {
 		return common.ContextError(errors.New("invalid server entry"))
 	}
@@ -196,7 +196,7 @@ func StoreServerEntry(serverEntry *ServerEntry, replaceIfExists bool) error {
 		existingServerEntryValid := false
 		existingData := serverEntries.Get([]byte(serverEntry.IpAddress))
 		if existingData != nil {
-			existingServerEntry := new(ServerEntry)
+			existingServerEntry := new(protocol.ServerEntry)
 			if json.Unmarshal(existingData, existingServerEntry) == nil {
 				existingServerEntryValid = true
 			}
@@ -239,7 +239,7 @@ func StoreServerEntry(serverEntry *ServerEntry, replaceIfExists bool) error {
 // Shuffling is performed on imported server entrues as part of client-side
 // load balancing.
 // There is an independent transaction for each entry insert/update.
-func StoreServerEntries(serverEntries []*ServerEntry, replaceIfExists bool) error {
+func StoreServerEntries(serverEntries []*protocol.ServerEntry, replaceIfExists bool) error {
 	checkInitDataStore()
 
 	for index := len(serverEntries) - 1; index > 0; index-- {
@@ -365,7 +365,7 @@ func insertRankedServerEntry(tx *bolt.Tx, serverEntryId string, position int) er
 	return nil
 }
 
-func serverEntrySupportsProtocol(serverEntry *ServerEntry, protocol string) bool {
+func serverEntrySupportsProtocol(serverEntry *protocol.ServerEntry, protocol string) bool {
 	// Note: for meek, the capabilities are FRONTED-MEEK and UNFRONTED-MEEK
 	// and the additonal OSSH service is assumed to be available internally.
 	requiredCapability := strings.TrimSuffix(protocol, "-OSSH")
@@ -382,7 +382,7 @@ type ServerEntryIterator struct {
 	serverEntryIndex            int
 	isTargetServerEntryIterator bool
 	hasNextTargetServerEntry    bool
-	targetServerEntry           *ServerEntry
+	targetServerEntry           *protocol.ServerEntry
 }
 
 // NewServerEntryIterator creates a new ServerEntryIterator
@@ -409,7 +409,7 @@ func NewServerEntryIterator(config *Config) (iterator *ServerEntryIterator, err
 
 // newTargetServerEntryIterator is a helper for initializing the TargetServerEntry case
 func newTargetServerEntryIterator(config *Config) (iterator *ServerEntryIterator, err error) {
-	serverEntry, err := DecodeServerEntry(
+	serverEntry, err := protocol.DecodeServerEntry(
 		config.TargetServerEntry, common.GetCurrentTimestamp(), protocol.SERVER_ENTRY_SOURCE_TARGET)
 	if err != nil {
 		return nil, err
@@ -513,7 +513,7 @@ func (iterator *ServerEntryIterator) Close() {
 
 // Next returns the next server entry, by rank, for a ServerEntryIterator.
 // Returns nil with no error when there is no next item.
-func (iterator *ServerEntryIterator) Next() (serverEntry *ServerEntry, err error) {
+func (iterator *ServerEntryIterator) Next() (serverEntry *protocol.ServerEntry, err error) {
 	defer func() {
 		if err != nil {
 			iterator.Close()
@@ -562,7 +562,7 @@ func (iterator *ServerEntryIterator) Next() (serverEntry *ServerEntry, err error
 			continue
 		}
 
-		serverEntry = new(ServerEntry)
+		serverEntry = new(protocol.ServerEntry)
 		err = json.Unmarshal(data, serverEntry)
 		if err != nil {
 			// In case of data corruption or a bug causing this condition,
@@ -586,7 +586,7 @@ func (iterator *ServerEntryIterator) Next() (serverEntry *ServerEntry, err error
 // which have a single meekFrontingDomain and not a meekFrontingAddresses array.
 // By copying this one meekFrontingDomain into meekFrontingAddresses, this client effectively
 // uses that single value as legacy clients do.
-func MakeCompatibleServerEntry(serverEntry *ServerEntry) *ServerEntry {
+func MakeCompatibleServerEntry(serverEntry *protocol.ServerEntry) *protocol.ServerEntry {
 	if len(serverEntry.MeekFrontingAddresses) == 0 && serverEntry.MeekFrontingDomain != "" {
 		serverEntry.MeekFrontingAddresses =
 			append(serverEntry.MeekFrontingAddresses, serverEntry.MeekFrontingDomain)
@@ -595,13 +595,13 @@ func MakeCompatibleServerEntry(serverEntry *ServerEntry) *ServerEntry {
 	return serverEntry
 }
 
-func scanServerEntries(scanner func(*ServerEntry)) error {
+func scanServerEntries(scanner func(*protocol.ServerEntry)) error {
 	err := singleton.db.View(func(tx *bolt.Tx) error {
 		bucket := tx.Bucket([]byte(serverEntriesBucket))
 		cursor := bucket.Cursor()
 
 		for key, value := cursor.First(); key != nil; key, value = cursor.Next() {
-			serverEntry := new(ServerEntry)
+			serverEntry := new(protocol.ServerEntry)
 			err := json.Unmarshal(value, serverEntry)
 			if err != nil {
 				// In case of data corruption or a bug causing this condition,
@@ -624,13 +624,13 @@ func scanServerEntries(scanner func(*ServerEntry)) error {
 
 // CountServerEntries returns a count of stored servers for the
 // specified region and protocol.
-func CountServerEntries(region, protocol string) int {
+func CountServerEntries(region, tunnelProtocol string) int {
 	checkInitDataStore()
 
 	count := 0
-	err := scanServerEntries(func(serverEntry *ServerEntry) {
+	err := scanServerEntries(func(serverEntry *protocol.ServerEntry) {
 		if (region == "" || serverEntry.Region == region) &&
-			(protocol == "" || serverEntrySupportsProtocol(serverEntry, protocol)) {
+			(tunnelProtocol == "" || serverEntrySupportsProtocol(serverEntry, tunnelProtocol)) {
 			count += 1
 		}
 	})
@@ -649,7 +649,7 @@ func ReportAvailableRegions() {
 	checkInitDataStore()
 
 	regions := make(map[string]bool)
-	err := scanServerEntries(func(serverEntry *ServerEntry) {
+	err := scanServerEntries(func(serverEntry *protocol.ServerEntry) {
 		regions[serverEntry.Region] = true
 	})
 
@@ -676,7 +676,7 @@ func GetServerEntryIpAddresses() (ipAddresses []string, err error) {
 	checkInitDataStore()
 
 	ipAddresses = make([]string, 0)
-	err = scanServerEntries(func(serverEntry *ServerEntry) {
+	err = scanServerEntries(func(serverEntry *protocol.ServerEntry) {
 		ipAddresses = append(ipAddresses, serverEntry.IpAddress)
 	})
 

+ 3 - 3
psiphon/meekConn.go

@@ -578,7 +578,7 @@ func (meek *MeekConn) roundTrip(sendPayload []byte) (io.ReadCloser, error) {
 
 		request.Header.Set("Content-Type", "application/octet-stream")
 
-		// Set additional headers to the HTTP request using the same method we use for adding 
+		// Set additional headers to the HTTP request using the same method we use for adding
 		// custom headers to HTTP proxy requests
 		for name, value := range meek.additionalHeaders {
 			// hack around special case of "Host" header
@@ -705,8 +705,8 @@ func makeMeekCookie(meekConfig *MeekConfig) (cookie *http.Cookie, err error) {
 	copy(encryptedCookie[32:], box)
 
 	// Obfuscate the encrypted data
-	obfuscator, err := NewClientObfuscator(
-		&ObfuscatorConfig{Keyword: meekConfig.MeekObfuscatedKey, MaxPadding: MEEK_COOKIE_MAX_PADDING})
+	obfuscator, err := common.NewClientObfuscator(
+		&common.ObfuscatorConfig{Keyword: meekConfig.MeekObfuscatedKey, MaxPadding: MEEK_COOKIE_MAX_PADDING})
 	if err != nil {
 		return nil, common.ContextError(err)
 	}

+ 6 - 2
psiphon/migrateDataStore.go

@@ -21,11 +21,15 @@
 
 package psiphon
 
+import (
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
+)
+
 // Stub function to return an empty list for non-Windows builds
-func prepareMigrationEntries(config *Config) []*ServerEntry {
+func prepareMigrationEntries(config *Config) []*protocol.ServerEntry {
 	return nil
 }
 
 // Stub function to return immediately for non-Windows builds
-func migrateEntries(serverEntries []*ServerEntry, legacyDataStoreFilename string) {
+func migrateEntries(serverEntries []*protocol.ServerEntry, legacyDataStoreFilename string) {
 }

+ 4 - 3
psiphon/migrateDataStore_windows.go

@@ -28,6 +28,7 @@ import (
 
 	_ "github.com/Psiphon-Inc/go-sqlite3"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
 )
 
 var legacyDb *sql.DB
@@ -84,7 +85,7 @@ func prepareMigrationEntries(config *Config) []*ServerEntry {
 // migrateEntries calls the BoltDB data store method to shuffle
 // and store an array of server entries (StoreServerEntries)
 // Failing to migrate entries, or delete the legacy file is never fatal
-func migrateEntries(serverEntries []*ServerEntry, legacyDataStoreFilename string) {
+func migrateEntries(serverEntries []*protocol.ServerEntry, legacyDataStoreFilename string) {
 	checkInitDataStore()
 
 	err := StoreServerEntries(serverEntries, false)
@@ -149,7 +150,7 @@ func (iterator *legacyServerEntryIterator) Close() {
 
 // Next returns the next server entry, by rank, for a legacyServerEntryIterator.
 // Returns nil with no error when there is no next item.
-func (iterator *legacyServerEntryIterator) Next() (serverEntry *ServerEntry, err error) {
+func (iterator *legacyServerEntryIterator) Next() (serverEntry *protocol.ServerEntry, err error) {
 	defer func() {
 		if err != nil {
 			iterator.Close()
@@ -170,7 +171,7 @@ func (iterator *legacyServerEntryIterator) Next() (serverEntry *ServerEntry, err
 	if err != nil {
 		return nil, common.ContextError(err)
 	}
-	serverEntry = new(ServerEntry)
+	serverEntry = new(protocol.ServerEntry)
 	err = json.Unmarshal(data, serverEntry)
 	if err != nil {
 		return nil, common.ContextError(err)

+ 1 - 1
psiphon/remoteServerList.go

@@ -363,7 +363,7 @@ func unpackRemoteServerListFile(
 
 func storeServerEntries(serverList string) error {
 
-	serverEntries, err := DecodeAndValidateServerEntryList(
+	serverEntries, err := protocol.DecodeAndValidateServerEntryList(
 		serverList,
 		common.GetCurrentTimestamp(),
 		protocol.SERVER_ENTRY_SOURCE_REMOTE)

+ 4 - 8
psiphon/remoteServerList_test.go

@@ -19,9 +19,6 @@
 
 package psiphon
 
-// Note: disabled until import cycle can be resolved
-
-/*
 import (
 	"bytes"
 	"crypto/md5"
@@ -61,7 +58,7 @@ func TestObfuscatedRemoteServerLists(t *testing.T) {
 
 	serverIPaddress := ""
 	for _, interfaceName := range []string{"eth0", "en0"} {
-		serverIPaddress, err = GetInterfaceIPAddress(interfaceName)
+		serverIPaddress, err = common.GetInterfaceIPAddress(interfaceName)
 		if err == nil {
 			break
 		}
@@ -74,8 +71,8 @@ func TestObfuscatedRemoteServerLists(t *testing.T) {
 		&server.GenerateConfigParams{
 			ServerIPAddress:      serverIPaddress,
 			EnableSSHAPIRequests: true,
-			WebServerPort:        8000,
-			TunnelProtocolPorts:  map[string]int{"OSSH": 4000},
+			WebServerPort:        8001,
+			TunnelProtocolPorts:  map[string]int{"OSSH": 4001},
 		})
 	if err != nil {
 		t.Fatalf("error generating server config: %s", err)
@@ -184,7 +181,7 @@ func TestObfuscatedRemoteServerLists(t *testing.T) {
 	// run mock remote server list host
 	//
 
-	remoteServerListHostAddress := net.JoinHostPort(serverIPaddress, "8080")
+	remoteServerListHostAddress := net.JoinHostPort(serverIPaddress, "8081")
 
 	// The common remote server list fetches will 404
 	remoteServerListURL := fmt.Sprintf("http://%s/server_list_compressed", remoteServerListHostAddress)
@@ -371,4 +368,3 @@ func TestObfuscatedRemoteServerLists(t *testing.T) {
 		}
 	}
 }
-*/

+ 4 - 5
psiphon/server/config.go

@@ -34,7 +34,6 @@ import (
 
 	"github.com/Psiphon-Inc/crypto/nacl/box"
 	"github.com/Psiphon-Inc/crypto/ssh"
-	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
 )
@@ -570,8 +569,8 @@ func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, []byte, error
 		capabilities = append(capabilities, protocol.CAPABILITY_UNTUNNELED_WEB_API_REQUESTS)
 	}
 
-	for protocol, _ := range params.TunnelProtocolPorts {
-		capabilities = append(capabilities, psiphon.GetCapability(protocol))
+	for tunnelProtocol, _ := range params.TunnelProtocolPorts {
+		capabilities = append(capabilities, protocol.GetCapability(tunnelProtocol))
 	}
 
 	sshPort := params.TunnelProtocolPorts["SSH"]
@@ -600,7 +599,7 @@ func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, []byte, error
 		strippedWebServerCertificate = strings.Join(lines[1:len(lines)-2], "")
 	}
 
-	serverEntry := &psiphon.ServerEntry{
+	serverEntry := &protocol.ServerEntry{
 		IpAddress:                     params.ServerIPAddress,
 		WebServerPort:                 serverEntryWebServerPort,
 		WebServerSecret:               webServerSecret,
@@ -621,7 +620,7 @@ func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, []byte, error
 		MeekFrontingDisableSNI:        false,
 	}
 
-	encodedServerEntry, err := psiphon.EncodeServerEntry(serverEntry)
+	encodedServerEntry, err := protocol.EncodeServerEntry(serverEntry)
 	if err != nil {
 		return nil, nil, nil, common.ContextError(err)
 	}

+ 2 - 3
psiphon/server/meek.go

@@ -35,7 +35,6 @@ import (
 
 	"github.com/Psiphon-Inc/crypto/nacl/box"
 	"github.com/Psiphon-Inc/goarista/monotime"
-	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 )
 
@@ -531,9 +530,9 @@ func getMeekCookiePayload(support *SupportServices, cookieValue string) ([]byte,
 
 	reader := bytes.NewReader(decodedValue[:])
 
-	obfuscator, err := psiphon.NewServerObfuscator(
+	obfuscator, err := common.NewServerObfuscator(
 		reader,
-		&psiphon.ObfuscatorConfig{Keyword: support.Config.MeekObfuscatedKey})
+		&common.ObfuscatorConfig{Keyword: support.Config.MeekObfuscatedKey})
 	if err != nil {
 		return nil, common.ContextError(err)
 	}

+ 30 - 13
psiphon/server/server_test.go

@@ -73,6 +73,7 @@ func TestSSH(t *testing.T) {
 			enableSSHAPIRequests: true,
 			doHotReload:          false,
 			denyTrafficRules:     false,
+			doClientVerification: true,
 			doTunneledWebRequest: true,
 			doTunneledNTPRequest: true,
 		})
@@ -85,6 +86,7 @@ func TestOSSH(t *testing.T) {
 			enableSSHAPIRequests: true,
 			doHotReload:          false,
 			denyTrafficRules:     false,
+			doClientVerification: false,
 			doTunneledWebRequest: true,
 			doTunneledNTPRequest: true,
 		})
@@ -97,6 +99,7 @@ func TestUnfrontedMeek(t *testing.T) {
 			enableSSHAPIRequests: true,
 			doHotReload:          false,
 			denyTrafficRules:     false,
+			doClientVerification: false,
 			doTunneledWebRequest: true,
 			doTunneledNTPRequest: true,
 		})
@@ -109,6 +112,7 @@ func TestUnfrontedMeekHTTPS(t *testing.T) {
 			enableSSHAPIRequests: true,
 			doHotReload:          false,
 			denyTrafficRules:     false,
+			doClientVerification: false,
 			doTunneledWebRequest: true,
 			doTunneledNTPRequest: true,
 		})
@@ -121,6 +125,7 @@ func TestWebTransportAPIRequests(t *testing.T) {
 			enableSSHAPIRequests: false,
 			doHotReload:          false,
 			denyTrafficRules:     false,
+			doClientVerification: true,
 			doTunneledWebRequest: true,
 			doTunneledNTPRequest: true,
 		})
@@ -133,6 +138,7 @@ func TestHotReload(t *testing.T) {
 			enableSSHAPIRequests: true,
 			doHotReload:          true,
 			denyTrafficRules:     false,
+			doClientVerification: false,
 			doTunneledWebRequest: true,
 			doTunneledNTPRequest: true,
 		})
@@ -145,6 +151,7 @@ func TestDenyTrafficRules(t *testing.T) {
 			enableSSHAPIRequests: true,
 			doHotReload:          true,
 			denyTrafficRules:     true,
+			doClientVerification: false,
 			doTunneledWebRequest: true,
 			doTunneledNTPRequest: true,
 		})
@@ -157,6 +164,7 @@ func TestTCPOnlySLOK(t *testing.T) {
 			enableSSHAPIRequests: true,
 			doHotReload:          false,
 			denyTrafficRules:     false,
+			doClientVerification: false,
 			doTunneledWebRequest: true,
 			doTunneledNTPRequest: false,
 		})
@@ -169,6 +177,7 @@ func TestUDPOnlySLOK(t *testing.T) {
 			enableSSHAPIRequests: true,
 			doHotReload:          false,
 			denyTrafficRules:     false,
+			doClientVerification: false,
 			doTunneledWebRequest: false,
 			doTunneledNTPRequest: true,
 		})
@@ -179,6 +188,7 @@ type runServerConfig struct {
 	enableSSHAPIRequests bool
 	doHotReload          bool
 	denyTrafficRules     bool
+	doClientVerification bool
 	doTunneledWebRequest bool
 	doTunneledNTPRequest bool
 }
@@ -211,7 +221,7 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 	var err error
 	serverIPaddress := ""
 	for _, interfaceName := range []string{"eth0", "en0"} {
-		serverIPaddress, err = psiphon.GetInterfaceIPAddress(interfaceName)
+		serverIPaddress, err = common.GetInterfaceIPAddress(interfaceName)
 		if err == nil {
 			break
 		}
@@ -254,10 +264,6 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 	serverConfig["OSLConfigFilename"] = oslConfigFilename
 	serverConfig["LogLevel"] = "error"
 
-	// 1 second is the minimum period; should be small enough to emit a log during the
-	// test run, but not guaranteed
-	serverConfig["LoadMonitorPeriodSeconds"] = 1
-
 	serverConfigJSON, _ = json.Marshal(serverConfig)
 
 	// run server
@@ -315,6 +321,11 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 		// handler below.
 	}
 
+	// Exercise server_load logging
+	p, _ := os.FindProcess(os.Getpid())
+	p.Signal(syscall.SIGUSR2)
+	time.Sleep(1 * time.Second)
+
 	// connect to server with client
 
 	// TODO: currently, TargetServerEntry only works with one tunnel
@@ -326,7 +337,7 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 	// Note: calling LoadConfig ensures all *int config fields are initialized
 	clientConfigJSON := `
     {
-        "ClientPlatform" : "Android",
+        "ClientPlatform" : "Windows",
         "ClientVersion" : "0",
         "SponsorId" : "0",
         "PropagationChannelId" : "0",
@@ -345,6 +356,10 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 	clientConfig.LocalHttpProxyPort = localHTTPProxyPort
 	clientConfig.EmitSLOKs = true
 
+	if runConfig.doClientVerification {
+		clientConfig.ClientPlatform = "Android"
+	}
+
 	clientConfig.DataStoreDirectory = testDataDirName
 	err = psiphon.InitDataStore(clientConfig)
 	if err != nil {
@@ -405,7 +420,7 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 		defer controllerWaitGroup.Done()
 		controller.Run(controllerShutdownBroadcast)
 	}()
-	stopClient := func() {
+	defer func() {
 		close(controllerShutdownBroadcast)
 
 		shutdownTimeout := time.NewTimer(20 * time.Second)
@@ -421,7 +436,7 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 		case <-shutdownTimeout.C:
 			t.Fatalf("controller shutdown timeout exceeded")
 		}
-	}
+	}()
 
 	// Test: tunnels must be established, and correct homepage
 	// must be received, within 30 seconds
@@ -435,8 +450,11 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 
 	waitOnNotification(t, tunnelsEstablished, timeoutSignal, "tunnel establish timeout exceeded")
 	waitOnNotification(t, homepageReceived, timeoutSignal, "homepage received timeout exceeded")
-	waitOnNotification(t, verificationRequired, timeoutSignal, "verification required timeout exceeded")
-	waitOnNotification(t, verificationCompleted, timeoutSignal, "verification completed timeout exceeded")
+
+	if runConfig.doClientVerification {
+		waitOnNotification(t, verificationRequired, timeoutSignal, "verification required timeout exceeded")
+		waitOnNotification(t, verificationCompleted, timeoutSignal, "verification completed timeout exceeded")
+	}
 
 	if runConfig.doTunneledWebRequest {
 
@@ -474,11 +492,10 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 		}
 	}
 
-	// Test: stop client to trigger a final status request and receive SLOK payload
-
-	stopClient()
+	// Test: await SLOK payload
 
 	if !runConfig.denyTrafficRules {
+		time.Sleep(1 * time.Second)
 		waitOnNotification(t, slokSeeded, timeoutSignal, "SLOK seeded timeout exceeded")
 	}
 }

+ 2 - 3
psiphon/server/tunnelServer.go

@@ -33,7 +33,6 @@ import (
 
 	"github.com/Psiphon-Inc/crypto/ssh"
 	"github.com/Psiphon-Inc/goarista/monotime"
-	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/osl"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
@@ -706,8 +705,8 @@ func (sshClient *sshClient) run(clientConn net.Conn) {
 		if protocol.TunnelProtocolUsesObfuscatedSSH(sshClient.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,
+			conn, result.err = common.NewObfuscatedSshConn(
+				common.OBFUSCATION_CONN_MODE_SERVER,
 				conn,
 				sshClient.sshServer.support.Config.ObfuscatedSSHKey)
 			if result.err != nil {

+ 4 - 3
psiphon/serverApi.go

@@ -183,14 +183,14 @@ func (serverContext *ServerContext) doHandshakeRequest() error {
 	serverContext.clientRegion = handshakeResponse.ClientRegion
 	NoticeClientRegion(serverContext.clientRegion)
 
-	var decodedServerEntries []*ServerEntry
+	var decodedServerEntries []*protocol.ServerEntry
 
 	// Store discovered server entries
 	// We use the server's time, as it's available here, for the server entry
 	// timestamp since this is more reliable than the client time.
 	for _, encodedServerEntry := range handshakeResponse.EncodedServerList {
 
-		serverEntry, err := DecodeServerEntry(
+		serverEntry, err := protocol.DecodeServerEntry(
 			encodedServerEntry,
 			common.TruncateTimestampToHour(handshakeResponse.ServerTimestamp),
 			protocol.SERVER_ENTRY_SOURCE_DISCOVERY)
@@ -198,9 +198,10 @@ func (serverContext *ServerContext) doHandshakeRequest() error {
 			return common.ContextError(err)
 		}
 
-		err = ValidateServerEntry(serverEntry)
+		err = protocol.ValidateServerEntry(serverEntry)
 		if err != nil {
 			// Skip this entry and continue with the next one
+			NoticeAlert("invalid server entry: %s", err)
 			continue
 		}
 

+ 10 - 8
psiphon/tunnel.go

@@ -73,7 +73,7 @@ type Tunnel struct {
 	untunneledDialConfig         *DialConfig
 	isDiscarded                  bool
 	isClosed                     bool
-	serverEntry                  *ServerEntry
+	serverEntry                  *protocol.ServerEntry
 	serverContext                *ServerContext
 	protocol                     string
 	conn                         *common.ActivityMonitoredConn
@@ -120,7 +120,7 @@ func EstablishTunnel(
 	untunneledDialConfig *DialConfig,
 	sessionId string,
 	pendingConns *common.Conns,
-	serverEntry *ServerEntry,
+	serverEntry *protocol.ServerEntry,
 	adjustedEstablishStartTime monotime.Time,
 	tunnelOwner TunnelOwner) (tunnel *Tunnel, err error) {
 
@@ -372,7 +372,9 @@ func (conn *TunneledConn) Close() error {
 }
 
 // selectProtocol is a helper that picks the tunnel protocol
-func selectProtocol(config *Config, serverEntry *ServerEntry) (selectedProtocol string, err error) {
+func selectProtocol(
+	config *Config, serverEntry *protocol.ServerEntry) (selectedProtocol string, err error) {
+
 	// TODO: properly handle protocols (e.g. FRONTED-MEEK-OSSH) vs. capabilities (e.g., {FRONTED-MEEK, OSSH})
 	// for now, the code is simply assuming that MEEK capabilities imply OSSH capability.
 	if config.TunnelProtocol != "" {
@@ -404,7 +406,7 @@ func selectProtocol(config *Config, serverEntry *ServerEntry) (selectedProtocol
 // selectFrontingParameters is a helper which selects/generates meek fronting
 // parameters where the server entry provides multiple options or patterns.
 func selectFrontingParameters(
-	serverEntry *ServerEntry) (frontingAddress, frontingHost string, err error) {
+	serverEntry *protocol.ServerEntry) (frontingAddress, frontingHost string, err error) {
 
 	if len(serverEntry.MeekFrontingAddressesRegex) > 0 {
 
@@ -447,7 +449,7 @@ func selectFrontingParameters(
 // selected meek tunnel protocol.
 func initMeekConfig(
 	config *Config,
-	serverEntry *ServerEntry,
+	serverEntry *protocol.ServerEntry,
 	selectedProtocol,
 	sessionId string) (*MeekConfig, error) {
 
@@ -543,7 +545,7 @@ type dialResult struct {
 func dialSsh(
 	config *Config,
 	pendingConns *common.Conns,
-	serverEntry *ServerEntry,
+	serverEntry *protocol.ServerEntry,
 	selectedProtocol,
 	sessionId string) (*dialResult, error) {
 
@@ -637,8 +639,8 @@ func dialSsh(
 	// Add obfuscated SSH layer
 	var sshConn net.Conn = throttledConn
 	if useObfuscatedSsh {
-		sshConn, err = NewObfuscatedSshConn(
-			OBFUSCATION_CONN_MODE_CLIENT, throttledConn, serverEntry.SshObfuscatedKey)
+		sshConn, err = common.NewObfuscatedSshConn(
+			common.OBFUSCATION_CONN_MODE_CLIENT, throttledConn, serverEntry.SshObfuscatedKey)
 		if err != nil {
 			return nil, common.ContextError(err)
 		}