|
|
@@ -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
|