Browse Source

workaround for incompatibility with extra lines before SSH server identification string

Rod Hynes 11 years ago
parent
commit
0731f2f1ce
2 changed files with 110 additions and 39 deletions
  1. 109 38
      psiphon/obfuscatedSshConn.go
  2. 1 1
      psiphon/tunnel.go

+ 109 - 38
psiphon/obfuscatedSshConn.go

@@ -22,15 +22,17 @@ package psiphon
 import (
 	"bytes"
 	"encoding/binary"
+	"errors"
+	"io"
 	"net"
 )
 
 type ObfuscatedSshState int
 
 const (
-	OBFUSCATION_STATE_SEND_SEED_MESSAGE = iota
-	OBFUSCATION_STATE_IDENTITY_LINE
-	OBFUSCATION_STATE_PACKETS
+	OBFUSCATION_STATE_SEND_CLIENT_SEED_MESSAGE = iota
+	OBFUSCATION_STATE_CLIENT_IDENTIFICATION_LINE
+	OBFUSCATION_STATE_CLIENT_KEX_PACKETS
 	OBFUSCATION_STATE_FINISHED
 )
 
@@ -46,9 +48,11 @@ const (
 // sent by the client).
 type ObfuscatedSshConn struct {
 	net.Conn
-	obfuscator    *Obfuscator
-	state         ObfuscatedSshState
-	messageBuffer []byte
+	obfuscator                   *Obfuscator
+	state                        ObfuscatedSshState
+	finishedServerIdentification bool
+	clientMessageBuffer          []byte
+	serverIdentificationBuffer   []byte
 }
 
 // NewObfuscatedSshConn creates a new ObfuscatedSshConn. The underlying
@@ -62,15 +66,20 @@ func NewObfuscatedSshConn(conn net.Conn, obfuscationKeyword string) (*Obfuscated
 	return &ObfuscatedSshConn{
 		Conn:       conn,
 		obfuscator: obfuscator,
-		state:      OBFUSCATION_STATE_SEND_SEED_MESSAGE}, nil
+		state:      OBFUSCATION_STATE_SEND_CLIENT_SEED_MESSAGE,
+		finishedServerIdentification: false}, nil
 }
 
 // Read wraps standard Read, deobfuscating read bytes while in the
 // obfuscating state.
 func (conn *ObfuscatedSshConn) Read(buffer []byte) (n int, err error) {
-	n, err = conn.Conn.Read(buffer)
-	if conn.state != OBFUSCATION_STATE_FINISHED {
-		conn.obfuscator.ObfuscateServerToClient(buffer)
+	if !conn.finishedServerIdentification {
+		n, err = conn.readServerIdentification(buffer)
+	} else {
+		n, err = conn.Conn.Read(buffer)
+		if conn.state != OBFUSCATION_STATE_FINISHED {
+			conn.obfuscator.ObfuscateServerToClient(buffer)
+		}
 	}
 	return
 }
@@ -79,28 +88,90 @@ func (conn *ObfuscatedSshConn) Read(buffer []byte) (n int, err error) {
 // obfuscating state. The plain SSH protocol bytes are parsed to observe
 // the protocol state and set obfuscation state accordingly.
 func (conn *ObfuscatedSshConn) Write(buffer []byte) (n int, err error) {
-	err = conn.updateState(buffer)
-	if err != nil {
-		return
+	if conn.state != OBFUSCATION_STATE_FINISHED {
+		err = conn.updateState(buffer)
+		if err != nil {
+			return
+		}
+		conn.obfuscator.ObfuscateClientToServer(buffer)
 	}
-	conn.obfuscator.ObfuscateClientToServer(buffer)
 	return conn.Conn.Write(buffer)
 }
 
+// readServerIdentification implements a workaround for issues with
+// ssh/transport.go exchangeVersions/readVersion and Psiphon's openssh
+// server.
+//
+// Psiphon's server sends extra lines before the version line, as
+// permitted by http://www.ietf.org/rfc/rfc4253.txt sec 4.2:
+//   The server MAY send other lines of data before sending the
+//   version string. [...] Clients MUST be able to process such lines.
+//
+// A comment in exchangeVersions explains that the go code doesn't
+// support this:
+//   Contrary to the RFC, we do not ignore lines that don't
+//   start with "SSH-2.0-" to make the library usable with
+//   nonconforming servers.
+//
+// In addition, Psiphon's server sends up to 512 characters per extra
+// line. It's not clear that the 255 max string size in sec 4.2 refers
+// to the extra lines as well, but in any case go's code only supports
+// a 255 character lines.
+//
+// When first called, this function reads all the extra lines, discarding
+// them, and then the version string line, retaining it in a buffer so
+// that it can be consumed by subsequent calls (depending on the input
+// buffer size).
+func (conn *ObfuscatedSshConn) readServerIdentification(buffer []byte) (n int, err error) {
+	if conn.serverIdentificationBuffer == nil {
+		for {
+			conn.serverIdentificationBuffer = make([]byte, 0, 512)
+			var readBuffer [1]byte
+			var validLine = false
+			for len(conn.serverIdentificationBuffer) < cap(conn.serverIdentificationBuffer) {
+				_, err := io.ReadFull(conn.Conn, readBuffer[:])
+				if err != nil {
+					return 0, err
+				}
+				conn.obfuscator.ObfuscateServerToClient(readBuffer[:])
+				conn.serverIdentificationBuffer = append(conn.serverIdentificationBuffer, readBuffer[0])
+				if bytes.HasSuffix(conn.serverIdentificationBuffer, []byte("\r\n")) {
+					validLine = true
+					break
+				}
+			}
+			if !validLine {
+				return 0, errors.New("invalid server identity line")
+			}
+			if bytes.HasPrefix(conn.serverIdentificationBuffer, []byte("SSH-")) {
+				break
+			}
+		}
+	}
+	n = copy(buffer, conn.serverIdentificationBuffer)
+	conn.serverIdentificationBuffer = conn.serverIdentificationBuffer[n:]
+	if len(conn.serverIdentificationBuffer) == 0 {
+		conn.serverIdentificationBuffer = nil
+		conn.finishedServerIdentification = true
+	}
+	return n, nil
+}
+
 // updateState transforms the obfucation state. It parses the stream of bytes
 // written by the client, looking for the first SSH_MSG_NEWKEYS packet sent,
 // after which obfuscation is turned off.
 //
-// State OBFUSCATION_STATE_SEND_SEED_MESSAGE: the initial state, when the client
-// has not sent any data. In this state, the seed message is injected into the
-// client output stream.
+// State OBFUSCATION_STATE_SEND_CLIENT_SEED_MESSAGE: the initial state, when
+// the client has not sent any data. In this state, the seed message is
+// injected into the client output stream.
 //
-// State OBFUSCATION_STATE_IDENTITY_LINE: before packets are sent, the client
-// send a line terminated by CRLF: http://www.ietf.org/rfc/rfc4253.txt sec 4.2.
+// State OBFUSCATION_STATE_CLIENT_IDENTIFICATION_LINE: before packets are sent,
+// the client 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.
 //
-// State OBFUSCATION_STATE_PACKETS: follows the binary packet protocol, parsing
-// each packet until the first SSH_MSG_NEWKEYS.
+// State OBFUSCATION_STATE_CLIENT_KEX_PACKETS: follows the binary packet protocol,
+// parsing each packet until the first SSH_MSG_NEWKEYS.
 // http://www.ietf.org/rfc/rfc4253.txt sec 6:
 //     uint32    packet_length
 //     byte      padding_length
@@ -111,51 +182,51 @@ func (conn *ObfuscatedSshConn) Write(buffer []byte) (n int, err error) {
 // http://www.ietf.org/rfc/rfc4253.txt sec 7.3, 12:
 // The payload for SSH_MSG_NEWKEYS is one byte, the packet type, value 21.
 func (conn *ObfuscatedSshConn) updateState(buffer []byte) (err error) {
-	// Use of conn.messageBuffer allows protocol message boundaries to cross Write() calls
+	// Use of conn.clientMessageBuffer allows protocol message boundaries to cross Write() calls
 	switch conn.state {
-	case OBFUSCATION_STATE_SEND_SEED_MESSAGE:
+	case OBFUSCATION_STATE_SEND_CLIENT_SEED_MESSAGE:
 		_, err = conn.Conn.Write(conn.obfuscator.ConsumeSeedMessage())
 		if err != nil {
 			return err
 		}
-		conn.state = OBFUSCATION_STATE_IDENTITY_LINE
-	case OBFUSCATION_STATE_IDENTITY_LINE:
-		conn.messageBuffer = append(conn.messageBuffer, buffer...)
-		line := bytes.SplitN(conn.messageBuffer, []byte("\r\n"), 1)
+		conn.state = OBFUSCATION_STATE_CLIENT_IDENTIFICATION_LINE
+	case OBFUSCATION_STATE_CLIENT_IDENTIFICATION_LINE:
+		conn.clientMessageBuffer = append(conn.clientMessageBuffer, buffer...)
+		line := bytes.SplitN(conn.clientMessageBuffer, []byte("\r\n"), 1)
 		if len(line) > 1 {
 			// TODO: efficiency...?
-			conn.messageBuffer = conn.messageBuffer[len(line[0]):]
-			conn.state = OBFUSCATION_STATE_PACKETS
+			conn.clientMessageBuffer = conn.clientMessageBuffer[len(line[0]):]
+			conn.state = OBFUSCATION_STATE_CLIENT_KEX_PACKETS
 		}
-	case OBFUSCATION_STATE_PACKETS:
+	case OBFUSCATION_STATE_CLIENT_KEX_PACKETS:
 		const SSH_MSG_NEWKEYS = 21
-		conn.messageBuffer = append(conn.messageBuffer, buffer...)
+		conn.clientMessageBuffer = append(conn.clientMessageBuffer, buffer...)
 		const PREFIX_LENGTH = 5 // uint32 + byte
-		for len(conn.messageBuffer) >= PREFIX_LENGTH {
+		for len(conn.clientMessageBuffer) >= PREFIX_LENGTH {
 			// This parsing repeats for a single packet sent over multiple Write() calls
 			var packetLength uint32
-			reader := bytes.NewReader(conn.messageBuffer)
+			reader := bytes.NewReader(conn.clientMessageBuffer)
 			err = binary.Read(reader, binary.BigEndian, &packetLength)
 			if err != nil {
 				return err
 			}
 			// TODO: handle malformed packet [lengths]
-			paddingLength := conn.messageBuffer[PREFIX_LENGTH-1]
+			paddingLength := conn.clientMessageBuffer[PREFIX_LENGTH-1]
 			payloadLength := packetLength - uint32(paddingLength) - 1
 			messageLength := PREFIX_LENGTH + packetLength - 1
-			if uint32(len(conn.messageBuffer)) < messageLength {
+			if uint32(len(conn.clientMessageBuffer)) < messageLength {
 				break
 			}
 			if payloadLength > 1 {
-				packetType := conn.messageBuffer[PREFIX_LENGTH]
+				packetType := conn.clientMessageBuffer[PREFIX_LENGTH]
 				if packetType == SSH_MSG_NEWKEYS {
 					conn.state = OBFUSCATION_STATE_FINISHED
-					conn.messageBuffer = nil
+					conn.clientMessageBuffer = nil
 					break
 				}
 			}
 			// TODO: efficiency...?
-			conn.messageBuffer = conn.messageBuffer[messageLength:]
+			conn.clientMessageBuffer = conn.clientMessageBuffer[messageLength:]
 		}
 	}
 	return nil

+ 1 - 1
psiphon/tunnel.go

@@ -69,7 +69,7 @@ func EstablishTunnel(tunnel *Tunnel) (err error) {
 	// First connect the transport
 	// TODO: meek
 	sshCapable := Contains(tunnel.serverEntry.Capabilities, "SSH")
-	obfuscatedSshCapable := false //Contains(tunnel.serverEntry.Capabilities, "OSSH")
+	obfuscatedSshCapable := Contains(tunnel.serverEntry.Capabilities, "OSSH")
 	if !sshCapable && !obfuscatedSshCapable {
 		return fmt.Errorf("server does not have sufficient capabilities")
 	}