فهرست منبع

Add OSSH padding dial stats and metrics

Rod Hynes 7 سال پیش
والد
کامیت
40e6d127ee

+ 58 - 38
psiphon/common/obfuscator/obfuscatedSshConn.go

@@ -38,11 +38,11 @@ const (
 	SSH_PADDING_MULTIPLE       = 16  // Default cipher block size
 )
 
-// ObfuscatedSshConn wraps a Conn and applies the obfuscated SSH protocol
+// ObfuscatedSSHConn wraps a Conn and applies the obfuscated SSH protocol
 // to the traffic on the connection:
 // https://github.com/brl/obfuscated-openssh/blob/master/README.obfuscation
 //
-// ObfuscatedSshConn is used to add obfuscation to golang's stock ssh
+// ObfuscatedSSHConn is used to add obfuscation to golang's stock "ssh"
 // client and server without modification to that standard library code.
 // The underlying connection must be used for SSH traffic. This code
 // injects the obfuscated seed message, applies obfuscated stream cipher
@@ -54,28 +54,29 @@ const (
 // no synchronization of access to the read/writeBuffers, so concurrent
 // calls to one of Read or Write will result in undefined behavior.
 //
-type ObfuscatedSshConn struct {
+type ObfuscatedSSHConn struct {
 	net.Conn
-	mode            ObfuscatedSshConnMode
+	mode            ObfuscatedSSHConnMode
 	obfuscator      *Obfuscator
 	readDeobfuscate func([]byte)
 	writeObfuscate  func([]byte)
-	readState       ObfuscatedSshReadState
-	writeState      ObfuscatedSshWriteState
+	readState       ObfuscatedSSHReadState
+	writeState      ObfuscatedSSHWriteState
 	readBuffer      *bytes.Buffer
 	writeBuffer     *bytes.Buffer
 	transformBuffer *bytes.Buffer
 	legacyPadding   bool
+	paddingLength   int
 }
 
-type ObfuscatedSshConnMode int
+type ObfuscatedSSHConnMode int
 
 const (
 	OBFUSCATION_CONN_MODE_CLIENT = iota
 	OBFUSCATION_CONN_MODE_SERVER
 )
 
-type ObfuscatedSshReadState int
+type ObfuscatedSSHReadState int
 
 const (
 	OBFUSCATION_READ_STATE_IDENTIFICATION_LINES = iota
@@ -84,7 +85,7 @@ const (
 	OBFUSCATION_READ_STATE_FINISHED
 )
 
-type ObfuscatedSshWriteState int
+type ObfuscatedSSHWriteState int
 
 const (
 	OBFUSCATION_WRITE_STATE_CLIENT_SEND_SEED_MESSAGE = iota
@@ -94,28 +95,27 @@ const (
 	OBFUSCATION_WRITE_STATE_FINISHED
 )
 
-// NewObfuscatedSshConn creates a new ObfuscatedSshConn.
+// NewObfuscatedSSHConn creates a new ObfuscatedSSHConn.
 // The underlying conn must be used for SSH traffic and must have
 // transferred no traffic.
 //
-// In client mode, NewObfuscatedSshConn does not block or initiate network
+// In client mode, NewObfuscatedSSHConn does not block or initiate network
 // I/O. The obfuscation seed message is sent when Write() is first called.
 //
-// In server mode, NewObfuscatedSshConn cannot completely initialize itself
+// In server mode, NewObfuscatedSSHConn cannot completely initialize itself
 // without the seed message from the client to derive obfuscation keys. So
-// NewObfuscatedSshConn blocks on reading the client seed message from the
+// NewObfuscatedSSHConn blocks on reading the client seed message from the
 // underlying conn.
-//
-func NewObfuscatedSshConn(
-	mode ObfuscatedSshConnMode,
+func NewObfuscatedSSHConn(
+	mode ObfuscatedSSHConnMode,
 	conn net.Conn,
 	obfuscationKeyword string,
-	minPadding, maxPadding *int) (*ObfuscatedSshConn, error) {
+	minPadding, maxPadding *int) (*ObfuscatedSSHConn, error) {
 
 	var err error
 	var obfuscator *Obfuscator
 	var readDeobfuscate, writeObfuscate func([]byte)
-	var writeState ObfuscatedSshWriteState
+	var writeState ObfuscatedSSHWriteState
 
 	if mode == OBFUSCATION_CONN_MODE_CLIENT {
 		obfuscator, err = NewClientObfuscator(
@@ -143,7 +143,7 @@ func NewObfuscatedSshConn(
 		writeState = OBFUSCATION_WRITE_STATE_SERVER_SEND_IDENTIFICATION_LINE_PADDING
 	}
 
-	return &ObfuscatedSshConn{
+	return &ObfuscatedSSHConn{
 		Conn:            conn,
 		mode:            mode,
 		obfuscator:      obfuscator,
@@ -154,12 +154,29 @@ func NewObfuscatedSshConn(
 		readBuffer:      new(bytes.Buffer),
 		writeBuffer:     new(bytes.Buffer),
 		transformBuffer: new(bytes.Buffer),
+		paddingLength:   -1,
 	}, nil
 }
 
+// GetMetrics implements the common.MetricsSource interface.
+func (conn *ObfuscatedSSHConn) GetMetrics() common.LogFields {
+	logFields := make(common.LogFields)
+	if conn.mode == OBFUSCATION_CONN_MODE_CLIENT {
+		paddingLength := conn.obfuscator.GetPaddingLength()
+		if paddingLength != -1 {
+			logFields["upstream_ossh_padding"] = paddingLength
+		}
+	} else {
+		if conn.paddingLength != -1 {
+			logFields["downstream_ossh_padding"] = conn.paddingLength
+		}
+	}
+	return logFields
+}
+
 // Read wraps standard Read, transparently applying the obfuscation
 // transformations.
-func (conn *ObfuscatedSshConn) Read(buffer []byte) (int, error) {
+func (conn *ObfuscatedSSHConn) Read(buffer []byte) (int, error) {
 	if conn.readState == OBFUSCATION_READ_STATE_FINISHED {
 		return conn.Conn.Read(buffer)
 	}
@@ -172,7 +189,7 @@ func (conn *ObfuscatedSshConn) Read(buffer []byte) (int, error) {
 
 // Write wraps standard Write, transparently applying the obfuscation
 // transformations.
-func (conn *ObfuscatedSshConn) Write(buffer []byte) (int, error) {
+func (conn *ObfuscatedSSHConn) Write(buffer []byte) (int, error) {
 	if conn.writeState == OBFUSCATION_WRITE_STATE_FINISHED {
 		return conn.Conn.Write(buffer)
 	}
@@ -224,7 +241,7 @@ func (conn *ObfuscatedSshConn) Write(buffer []byte) (int, error) {
 // State OBFUSCATION_READ_STATE_FLUSH: after SSH_MSG_NEWKEYS, no more
 // packets are read by this function, but bytes from the SSH_MSG_NEWKEYS
 // packet may need to be buffered due to partial reading.
-func (conn *ObfuscatedSshConn) readAndTransform(buffer []byte) (int, error) {
+func (conn *ObfuscatedSSHConn) readAndTransform(buffer []byte) (int, error) {
 
 	nextState := conn.readState
 
@@ -233,7 +250,7 @@ func (conn *ObfuscatedSshConn) readAndTransform(buffer []byte) (int, error) {
 		// TODO: only client should accept multiple lines?
 		if conn.readBuffer.Len() == 0 {
 			for {
-				err := readSshIdentificationLine(
+				err := readSSHIdentificationLine(
 					conn.Conn, conn.readDeobfuscate, conn.readBuffer)
 				if err != nil {
 					return 0, common.ContextError(err)
@@ -252,7 +269,7 @@ func (conn *ObfuscatedSshConn) readAndTransform(buffer []byte) (int, error) {
 
 	case OBFUSCATION_READ_STATE_KEX_PACKETS:
 		if conn.readBuffer.Len() == 0 {
-			isMsgNewKeys, err := readSshPacket(
+			isMsgNewKeys, err := readSSHPacket(
 				conn.Conn, conn.readDeobfuscate, conn.readBuffer)
 			if err != nil {
 				return 0, common.ContextError(err)
@@ -306,7 +323,7 @@ func (conn *ObfuscatedSshConn) readAndTransform(buffer []byte) (int, error) {
 // will ignore (http://tools.ietf.org/html/rfc4253#section-4.2).
 //
 // State OBFUSCATION_WRITE_STATE_IDENTIFICATION_LINE: before
-// packets are sent, the ssh peer sends an identification line terminated by CRLF:
+// packets are sent, the SSH peer sends an identification line terminated by CRLF:
 // http://www.ietf.org/rfc/rfc4253.txt sec 4.2.
 // In this state, the CRLF terminator is used to parse message boundaries.
 //
@@ -326,7 +343,7 @@ func (conn *ObfuscatedSshConn) readAndTransform(buffer []byte) (int, error) {
 // padding during the KEX phase as a partial defense against traffic analysis.
 // (The transformer can do this since only the payload and not the padding of
 // these packets is authenticated in the "exchange hash").
-func (conn *ObfuscatedSshConn) transformAndWrite(buffer []byte) error {
+func (conn *ObfuscatedSSHConn) transformAndWrite(buffer []byte) error {
 
 	// The seed message (client) and identification line padding (server)
 	// are injected before any standard SSH traffic.
@@ -341,6 +358,7 @@ func (conn *ObfuscatedSshConn) transformAndWrite(buffer []byte) error {
 		if err != nil {
 			return common.ContextError(err)
 		}
+		conn.paddingLength = len(padding)
 		conn.writeObfuscate(padding)
 		_, err = conn.Conn.Write(padding)
 		if err != nil {
@@ -359,14 +377,14 @@ func (conn *ObfuscatedSshConn) transformAndWrite(buffer []byte) error {
 
 	switch conn.writeState {
 	case OBFUSCATION_WRITE_STATE_IDENTIFICATION_LINE:
-		hasIdentificationLine := extractSshIdentificationLine(
+		hasIdentificationLine := extractSSHIdentificationLine(
 			conn.writeBuffer, conn.transformBuffer)
 		if hasIdentificationLine {
 			conn.writeState = OBFUSCATION_WRITE_STATE_KEX_PACKETS
 		}
 
 	case OBFUSCATION_WRITE_STATE_KEX_PACKETS:
-		hasMsgNewKeys, err := extractSshPackets(
+		hasMsgNewKeys, err := extractSSHPackets(
 			conn.legacyPadding, conn.writeBuffer, conn.transformBuffer)
 		if err != nil {
 			return common.ContextError(err)
@@ -403,7 +421,7 @@ func (conn *ObfuscatedSshConn) transformAndWrite(buffer []byte) error {
 	return nil
 }
 
-func readSshIdentificationLine(
+func readSSHIdentificationLine(
 	conn net.Conn,
 	deobfuscate func([]byte),
 	readBuffer *bytes.Buffer) error {
@@ -430,7 +448,7 @@ func readSshIdentificationLine(
 	return nil
 }
 
-func readSshPacket(
+func readSSHPacket(
 	conn net.Conn,
 	deobfuscate func([]byte),
 	readBuffer *bytes.Buffer) (bool, error) {
@@ -449,7 +467,7 @@ func readSshPacket(
 	prefix := readBuffer.Bytes()[prefixOffset : prefixOffset+SSH_PACKET_PREFIX_LENGTH]
 	deobfuscate(prefix)
 
-	_, _, payloadLength, messageLength, err := getSshPacketPrefix(prefix)
+	_, _, payloadLength, messageLength, err := getSSHPacketPrefix(prefix)
 	if err != nil {
 		return false, common.ContextError(err)
 	}
@@ -480,11 +498,13 @@ 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
 	if err != nil {
 		return nil, common.ContextError(err)
 	}
 	paddingLength += 2
+
 	padding := make([]byte, paddingLength)
 
 	// For backwards compatibility with some clients, send no more than 512 characters
@@ -513,7 +533,7 @@ func makeServerIdentificationLinePadding() ([]byte, error) {
 	return padding, nil
 }
 
-func extractSshIdentificationLine(writeBuffer, transformBuffer *bytes.Buffer) bool {
+func extractSSHIdentificationLine(writeBuffer, transformBuffer *bytes.Buffer) bool {
 	index := bytes.Index(writeBuffer.Bytes(), []byte("\r\n"))
 	if index != -1 {
 		lineLength := index + 2 // + 2 for \r\n
@@ -523,13 +543,13 @@ func extractSshIdentificationLine(writeBuffer, transformBuffer *bytes.Buffer) bo
 	return false
 }
 
-func extractSshPackets(
+func extractSSHPackets(
 	legacyPadding bool, writeBuffer, transformBuffer *bytes.Buffer) (bool, error) {
 
 	hasMsgNewKeys := false
 	for writeBuffer.Len() >= SSH_PACKET_PREFIX_LENGTH {
 
-		packetLength, paddingLength, payloadLength, messageLength, err := getSshPacketPrefix(
+		packetLength, paddingLength, payloadLength, messageLength, err := getSSHPacketPrefix(
 			writeBuffer.Bytes()[:SSH_PACKET_PREFIX_LENGTH])
 		if err != nil {
 			return false, common.ContextError(err)
@@ -593,7 +613,7 @@ func extractSshPackets(
 			return false, common.ContextError(err)
 		}
 
-		setSshPacketPrefix(
+		setSSHPacketPrefix(
 			transformedPacket,
 			packetLength+extraPaddingLength,
 			paddingLength+extraPaddingLength)
@@ -604,12 +624,12 @@ func extractSshPackets(
 	return hasMsgNewKeys, nil
 }
 
-func getSshPacketPrefix(buffer []byte) (int, int, int, int, error) {
+func getSSHPacketPrefix(buffer []byte) (int, int, int, int, error) {
 
 	packetLength := int(binary.BigEndian.Uint32(buffer[0 : SSH_PACKET_PREFIX_LENGTH-1]))
 
 	if packetLength < 1 || packetLength > SSH_MAX_PACKET_LENGTH {
-		return 0, 0, 0, 0, common.ContextError(errors.New("invalid ssh packet length"))
+		return 0, 0, 0, 0, common.ContextError(errors.New("invalid SSH packet length"))
 	}
 
 	paddingLength := int(buffer[SSH_PACKET_PREFIX_LENGTH-1])
@@ -619,7 +639,7 @@ func getSshPacketPrefix(buffer []byte) (int, int, int, int, error) {
 	return packetLength, paddingLength, payloadLength, messageLength, nil
 }
 
-func setSshPacketPrefix(buffer []byte, packetLength, paddingLength int) {
+func setSSHPacketPrefix(buffer []byte, packetLength, paddingLength int) {
 	binary.BigEndian.PutUint32(buffer, uint32(packetLength))
 	buffer[SSH_PACKET_PREFIX_LENGTH-1] = byte(paddingLength)
 }

+ 17 - 8
psiphon/common/obfuscator/obfuscator.go

@@ -45,6 +45,7 @@ const (
 // https://github.com/brl/obfuscated-openssh/blob/master/README.obfuscation
 type Obfuscator struct {
 	seedMessage          []byte
+	paddingLength        int
 	clientToServerCipher *rc4.Cipher
 	serverToClientCipher *rc4.Cipher
 }
@@ -88,13 +89,14 @@ func NewClientObfuscator(
 		maxPadding = *config.MaxPadding
 	}
 
-	seedMessage, err := makeSeedMessage(minPadding, maxPadding, seed, clientToServerCipher)
+	seedMessage, paddingLength, err := makeSeedMessage(minPadding, maxPadding, seed, clientToServerCipher)
 	if err != nil {
 		return nil, common.ContextError(err)
 	}
 
 	return &Obfuscator{
 		seedMessage:          seedMessage,
+		paddingLength:        paddingLength,
 		clientToServerCipher: clientToServerCipher,
 		serverToClientCipher: serverToClientCipher}, nil
 }
@@ -111,10 +113,17 @@ func NewServerObfuscator(
 	}
 
 	return &Obfuscator{
+		paddingLength:        -1,
 		clientToServerCipher: clientToServerCipher,
 		serverToClientCipher: serverToClientCipher}, nil
 }
 
+// GetPaddingLength returns the client seed message padding length. Only valid
+// for NewClientObfuscator.
+func (obfuscator *Obfuscator) GetPaddingLength() int {
+	return obfuscator.paddingLength
+}
+
 // SendSeedMessage returns the seed message created in NewObfuscatorClient,
 // removing the reference so that it may be garbage collected.
 func (obfuscator *Obfuscator) SendSeedMessage() []byte {
@@ -176,31 +185,31 @@ func deriveKey(seed, keyword, iv []byte) ([]byte, error) {
 	return digest[0:OBFUSCATE_KEY_LENGTH], nil
 }
 
-func makeSeedMessage(minPadding, maxPadding int, seed []byte, clientToServerCipher *rc4.Cipher) ([]byte, error) {
+func makeSeedMessage(minPadding, maxPadding int, seed []byte, clientToServerCipher *rc4.Cipher) ([]byte, int, error) {
 	padding, err := common.MakeSecureRandomPadding(minPadding, maxPadding)
 	if err != nil {
-		return nil, common.ContextError(err)
+		return nil, 0, common.ContextError(err)
 	}
 	buffer := new(bytes.Buffer)
 	err = binary.Write(buffer, binary.BigEndian, seed)
 	if err != nil {
-		return nil, common.ContextError(err)
+		return nil, 0, common.ContextError(err)
 	}
 	err = binary.Write(buffer, binary.BigEndian, uint32(OBFUSCATE_MAGIC_VALUE))
 	if err != nil {
-		return nil, common.ContextError(err)
+		return nil, 0, common.ContextError(err)
 	}
 	err = binary.Write(buffer, binary.BigEndian, uint32(len(padding)))
 	if err != nil {
-		return nil, common.ContextError(err)
+		return nil, 0, common.ContextError(err)
 	}
 	err = binary.Write(buffer, binary.BigEndian, padding)
 	if err != nil {
-		return nil, common.ContextError(err)
+		return nil, 0, common.ContextError(err)
 	}
 	seedMessage := buffer.Bytes()
 	clientToServerCipher.XORKeyStream(seedMessage[len(seed):], seedMessage[len(seed):])
-	return seedMessage, nil
+	return seedMessage, len(padding), nil
 }
 
 func readSeedMessage(

+ 2 - 2
psiphon/common/obfuscator/obfuscator_test.go

@@ -113,7 +113,7 @@ func TestObfuscatedSSHConn(t *testing.T) {
 		conn, err := listener.Accept()
 
 		if err == nil {
-			conn, err = NewObfuscatedSshConn(
+			conn, err = NewObfuscatedSSHConn(
 				OBFUSCATION_CONN_MODE_SERVER, conn, keyword, nil, nil)
 		}
 
@@ -139,7 +139,7 @@ func TestObfuscatedSSHConn(t *testing.T) {
 		conn, err := net.DialTimeout("tcp", serverAddress, 5*time.Second)
 
 		if err == nil {
-			conn, err = NewObfuscatedSshConn(
+			conn, err = NewObfuscatedSSHConn(
 				OBFUSCATION_CONN_MODE_CLIENT, conn, keyword, nil, nil)
 		}
 

+ 7 - 0
psiphon/notice.go

@@ -469,6 +469,13 @@ func noticeWithDialStats(noticeType, ipAddress, region, protocol string, dialSta
 		}
 	}
 
+	if dialStats.ObfuscatedSSHConnMetrics != nil {
+		metrics := dialStats.ObfuscatedSSHConnMetrics.GetMetrics()
+		for name, value := range metrics {
+			args = append(args, name, value)
+		}
+	}
+
 	singletonNoticeLogger.outputNotice(
 		noticeType, noticeIsDiagnostic,
 		args...)

+ 20 - 16
psiphon/server/api.go

@@ -308,7 +308,7 @@ var connectedRequestParams = append(
 	[]requestParamSpec{
 		{"session_id", isHexDigits, 0},
 		{"last_connected", isLastConnected, 0},
-		{"establishment_duration", isIntString, requestParamOptional}},
+		{"establishment_duration", isIntString, requestParamOptional | requestParamLogStringAsInt}},
 	baseRequestParams...)
 
 // connectedAPIRequestHandler implements the "connected" API request.
@@ -527,18 +527,19 @@ type requestParamSpec struct {
 }
 
 const (
-	requestParamOptional  = 1
-	requestParamNotLogged = 2
-	requestParamArray     = 4
-	requestParamJSON      = 8
+	requestParamOptional       = 1
+	requestParamNotLogged      = 2
+	requestParamArray          = 4
+	requestParamJSON           = 8
+	requestParamLogStringAsInt = 16
 )
 
 var upstreamFragmentorParams = []requestParamSpec{
-	{"upstream_bytes_fragmented", isIntString, requestParamOptional},
-	{"upstream_min_bytes_written", isIntString, requestParamOptional},
-	{"upstream_max_bytes_written", isIntString, requestParamOptional},
-	{"upstream_min_delayed", isIntString, requestParamOptional},
-	{"upstream_max_delayed", isIntString, requestParamOptional},
+	{"upstream_bytes_fragmented", isIntString, requestParamOptional | requestParamLogStringAsInt},
+	{"upstream_min_bytes_written", isIntString, requestParamOptional | requestParamLogStringAsInt},
+	{"upstream_max_bytes_written", isIntString, requestParamOptional | requestParamLogStringAsInt},
+	{"upstream_min_delayed", isIntString, requestParamOptional | requestParamLogStringAsInt},
+	{"upstream_max_delayed", isIntString, requestParamOptional | requestParamLogStringAsInt},
 }
 
 // baseRequestParams is the list of required and optional
@@ -552,7 +553,7 @@ var baseRequestParams = append(
 		{"client_session_id", isHexDigits, requestParamNotLogged},
 		{"propagation_channel_id", isHexDigits, 0},
 		{"sponsor_id", isHexDigits, 0},
-		{"client_version", isIntString, 0},
+		{"client_version", isIntString, requestParamLogStringAsInt},
 		{"client_platform", isClientPlatform, 0},
 		{"client_build_rev", isHexDigits, requestParamOptional},
 		{"relay_protocol", isRelayProtocol, 0},
@@ -572,9 +573,10 @@ var baseRequestParams = append(
 		{"server_entry_source", isServerEntrySource, requestParamOptional},
 		{"server_entry_timestamp", isISO8601Date, requestParamOptional},
 		{tactics.APPLIED_TACTICS_TAG_PARAMETER_NAME, isAnyString, requestParamOptional},
-		{"dial_port_number", isIntString, requestParamOptional},
+		{"dial_port_number", isIntString, requestParamOptional | requestParamLogStringAsInt},
 		{"quic_version", isAnyString, requestParamOptional},
 		{"quic_dial_sni_address", isAnyString, requestParamOptional},
+		{"upstream_ossh_padding", isIntString, requestParamOptional | requestParamLogStringAsInt},
 	},
 	upstreamFragmentorParams...)
 
@@ -744,9 +746,6 @@ func getRequestLogFields(
 			// - Boolean fields that come into the api as "1"/"0"
 			//   must be logged as actual boolean values
 			switch expectedParam.name {
-			case "client_version", "establishment_duration":
-				intValue, _ := strconv.Atoi(strValue)
-				logFields[expectedParam.name] = intValue
 			case "meek_dial_address":
 				host, _, _ := net.SplitHostPort(strValue)
 				if isIPAddress(nil, host) {
@@ -772,7 +771,12 @@ func getRequestLogFields(
 					logFields[expectedParam.name] = false
 				}
 			default:
-				logFields[expectedParam.name] = strValue
+				if expectedParam.flags&requestParamLogStringAsInt != 0 {
+					intValue, _ := strconv.Atoi(strValue)
+					logFields[expectedParam.name] = intValue
+				} else {
+					logFields[expectedParam.name] = strValue
+				}
 			}
 
 		case []interface{}:

+ 19 - 17
psiphon/server/tunnelServer.go

@@ -1033,13 +1033,6 @@ func (sshClient *sshClient) run(
 		}
 	}()
 
-	// Some conns report additional metrics. Meek conns report resiliency
-	// metrics and fragmentor.Conns report fragmentor configs.
-	//
-	// Limitation: for meek, GetMetrics from underlying fragmentor.Conns
-	// should be called in order to log fragmentor metrics for meek sessions.
-	metricsSource, isMetricsSource := clientConn.(common.MetricsSource)
-
 	// Set initial traffic rules, pre-handshake, based on currently known info.
 	sshClient.setTrafficRules()
 
@@ -1117,14 +1110,13 @@ func (sshClient *sshClient) run(
 		// Wrap the connection in an SSH deobfuscator when required.
 
 		if protocol.TunnelProtocolUsesObfuscatedSSH(sshClient.tunnelProtocol) {
-			// Note: NewObfuscatedSshConn blocks on network I/O
+			// Note: NewObfuscatedSSHConn blocks on network I/O
 			// TODO: ensure this won't block shutdown
-			conn, result.err = obfuscator.NewObfuscatedSshConn(
+			conn, result.err = obfuscator.NewObfuscatedSSHConn(
 				obfuscator.OBFUSCATION_CONN_MODE_SERVER,
 				conn,
 				sshClient.sshServer.support.Config.ObfuscatedSSHKey,
-				nil,
-				nil)
+				nil, nil)
 			if result.err != nil {
 				result.err = common.ContextError(result.err)
 			}
@@ -1188,10 +1180,20 @@ func (sshClient *sshClient) run(
 
 	sshClient.sshServer.unregisterEstablishedClient(sshClient)
 
-	var additionalMetrics LogFields
-	if isMetricsSource {
-		additionalMetrics = LogFields(metricsSource.GetMetrics())
+	// Some conns report additional metrics. Meek conns report resiliency
+	// metrics and fragmentor.Conns report fragmentor configs.
+	//
+	// Limitation: for meek, GetMetrics from underlying fragmentor.Conns
+	// should be called in order to log fragmentor metrics for meek sessions.
+
+	var additionalMetrics []LogFields
+	if metricsSource, ok := clientConn.(common.MetricsSource); ok {
+		additionalMetrics = append(additionalMetrics, LogFields(metricsSource.GetMetrics()))
+	}
+	if metricsSource, ok := sshClient.sshConn.(common.MetricsSource); ok {
+		additionalMetrics = append(additionalMetrics, LogFields(metricsSource.GetMetrics()))
 	}
+
 	sshClient.logTunnel(additionalMetrics)
 
 	// Transfer OSL seed state -- the OSL progress -- from the closing
@@ -1756,7 +1758,7 @@ func (sshClient *sshClient) setUDPChannel(channel ssh.Channel) {
 	sshClient.Unlock()
 }
 
-func (sshClient *sshClient) logTunnel(additionalMetrics LogFields) {
+func (sshClient *sshClient) logTunnel(additionalMetrics []LogFields) {
 
 	// Note: reporting duration based on last confirmed data transfer, which
 	// is reads for sshClient.activityConn.GetActiveDuration(), and not
@@ -1799,8 +1801,8 @@ func (sshClient *sshClient) logTunnel(additionalMetrics LogFields) {
 		sshClient.udpTrafficState.bytesDown
 
 	// Merge in additional metrics from the optional metrics source
-	if additionalMetrics != nil {
-		for name, value := range additionalMetrics {
+	for _, metrics := range additionalMetrics {
+		for name, value := range metrics {
 			// Don't overwrite any basic fields
 			if logFields[name] == nil {
 				logFields[name] = value

+ 7 - 0
psiphon/serverApi.go

@@ -790,6 +790,13 @@ func getBaseAPIParameters(
 		}
 	}
 
+	if dialStats.ObfuscatedSSHConnMetrics != nil {
+		metrics := dialStats.ObfuscatedSSHConnMetrics.GetMetrics()
+		for name, value := range metrics {
+			params[name] = fmt.Sprintf("%s", value)
+		}
+	}
+
 	return params
 }
 

+ 5 - 7
psiphon/tunnel.go

@@ -136,6 +136,7 @@ type DialStats struct {
 	QUICVersion                    string
 	QUICDialSNIAddress             string
 	DialConnMetrics                common.MetricsSource
+	ObfuscatedSSHConnMetrics       common.MetricsSource
 }
 
 // ConnectTunnel first makes a network transport connection to the
@@ -830,7 +831,6 @@ func dialSsh(
 
 	// Note: when SSHClientVersion is "", a default is supplied by the ssh package:
 	// https://godoc.org/golang.org/x/crypto/ssh#ClientConfig
-	useObfuscatedSsh := false
 	var directDialAddress string
 	var QUICVersion string
 	var QUICDialSNIAddress string
@@ -840,17 +840,14 @@ func dialSsh(
 
 	switch selectedProtocol {
 	case protocol.TUNNEL_PROTOCOL_OBFUSCATED_SSH, protocol.TUNNEL_PROTOCOL_TAPDANCE_OBFUSCATED_SSH:
-		useObfuscatedSsh = true
 		directDialAddress = fmt.Sprintf("%s:%d", serverEntry.IpAddress, serverEntry.SshObfuscatedPort)
 
 	case protocol.TUNNEL_PROTOCOL_QUIC_OBFUSCATED_SSH:
-		useObfuscatedSsh = true
 		directDialAddress = fmt.Sprintf("%s:%d", serverEntry.IpAddress, serverEntry.SshObfuscatedQUICPort)
 		QUICVersion = selectQUICVersion(config.clientParameters)
 		QUICDialSNIAddress = fmt.Sprintf("%s:%d", common.GenerateHostName(), serverEntry.SshObfuscatedQUICPort)
 
 	case protocol.TUNNEL_PROTOCOL_MARIONETTE_OBFUSCATED_SSH:
-		useObfuscatedSsh = true
 		directDialAddress = serverEntry.IpAddress
 
 	case protocol.TUNNEL_PROTOCOL_SSH:
@@ -858,7 +855,6 @@ func dialSsh(
 		directDialAddress = fmt.Sprintf("%s:%d", serverEntry.IpAddress, serverEntry.SshPort)
 
 	default:
-		useObfuscatedSsh = true
 		meekConfig, err = initMeekConfig(config, serverEntry, selectedProtocol, sessionId)
 		if err != nil {
 			return nil, common.ContextError(err)
@@ -989,8 +985,8 @@ func dialSsh(
 
 	// Add obfuscated SSH layer
 	var sshConn net.Conn = throttledConn
-	if useObfuscatedSsh {
-		sshConn, err = obfuscator.NewObfuscatedSshConn(
+	if protocol.TunnelProtocolUsesObfuscatedSSH(selectedProtocol) {
+		obfuscatedSSHConn, err := obfuscator.NewObfuscatedSSHConn(
 			obfuscator.OBFUSCATION_CONN_MODE_CLIENT,
 			throttledConn,
 			serverEntry.SshObfuscatedKey,
@@ -999,6 +995,8 @@ func dialSsh(
 		if err != nil {
 			return nil, common.ContextError(err)
 		}
+		sshConn = obfuscatedSSHConn
+		dialStats.ObfuscatedSSHConnMetrics = obfuscatedSSHConn
 	}
 
 	// Now establish the SSH session over the conn transport