Amir Khan 2 лет назад
Родитель
Сommit
5ef0cfc272

+ 81 - 9
psiphon/common/obfuscator/obfuscatedSshConn.go

@@ -21,11 +21,13 @@ package obfuscator
 
 import (
 	"bytes"
+	"context"
 	"encoding/binary"
 	std_errors "errors"
 	"io"
 	"io/ioutil"
 	"net"
+	"time"
 
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
@@ -60,6 +62,8 @@ const (
 type ObfuscatedSSHConn struct {
 	net.Conn
 	mode            ObfuscatedSSHConnMode
+	runCtx          context.Context
+	stopRunning     context.CancelFunc
 	obfuscator      *Obfuscator
 	readDeobfuscate func([]byte)
 	writeObfuscate  func([]byte)
@@ -129,6 +133,7 @@ func NewObfuscatedSSHConn(
 	obfuscatorSeedTransformerParameters *transforms.ObfuscatorSeedTransformerParameters,
 	clientPrefixSpec *OSSHPrefixSpec,
 	serverPrefixSepcs transforms.Specs,
+	osshPrefixSplitConfig *OSSHPrefixSplitConfig,
 	minPadding, maxPadding *int,
 	seedHistory *SeedHistory,
 	irregularLogger func(
@@ -151,6 +156,7 @@ func NewObfuscatedSSHConn(
 				IsOSSH:                              true,
 				Keyword:                             obfuscationKeyword,
 				ClientPrefixSpec:                    clientPrefixSpec,
+				OSSHPrefixSplitConfig:               osshPrefixSplitConfig,
 				PaddingPRNGSeed:                     obfuscationPaddingPRNGSeed,
 				MinPadding:                          minPadding,
 				MaxPadding:                          maxPadding,
@@ -163,7 +169,7 @@ func NewObfuscatedSSHConn(
 		writeObfuscate = obfuscator.ObfuscateClientToServer
 		writeState = OBFUSCATION_WRITE_STATE_CLIENT_SEND_PREAMBLE
 
-		if obfuscator.prefixHeader != nil {
+		if obfuscator.osshPrefixHeader != nil {
 			// Client expects prefix with terminator from the server.
 			readState = OBFUSCATION_READ_STATE_CLIENT_READ_PREFIX
 		}
@@ -172,10 +178,11 @@ func NewObfuscatedSSHConn(
 		// NewServerObfuscator reads a seed message from conn
 		obfuscator, err = NewServerObfuscator(
 			&ObfuscatorConfig{
-				Keyword:           obfuscationKeyword,
-				ServerPrefixSpecs: serverPrefixSepcs,
-				SeedHistory:       seedHistory,
-				IrregularLogger:   irregularLogger,
+				Keyword:               obfuscationKeyword,
+				ServerPrefixSpecs:     serverPrefixSepcs,
+				OSSHPrefixSplitConfig: osshPrefixSplitConfig,
+				SeedHistory:           seedHistory,
+				IrregularLogger:       irregularLogger,
 			},
 			common.IPAddressFromAddr(conn.RemoteAddr()),
 			conn)
@@ -200,9 +207,13 @@ func NewObfuscatedSSHConn(
 		return nil, errors.Trace(err)
 	}
 
+	runCtx, stopRunning := context.WithCancel(context.Background())
+
 	return &ObfuscatedSSHConn{
 		Conn:            conn,
 		mode:            mode,
+		runCtx:          runCtx,
+		stopRunning:     stopRunning,
 		obfuscator:      obfuscator,
 		readDeobfuscate: readDeobfuscate,
 		writeObfuscate:  writeObfuscate,
@@ -224,6 +235,7 @@ func NewClientObfuscatedSSHConn(
 	obfuscationPaddingPRNGSeed *prng.Seed,
 	obfuscatorSeedTransformerParameters *transforms.ObfuscatorSeedTransformerParameters,
 	prefixSpec *OSSHPrefixSpec,
+	osshPrefixSplitConfig *OSSHPrefixSplitConfig,
 	minPadding, maxPadding *int) (*ObfuscatedSSHConn, error) {
 
 	return NewObfuscatedSSHConn(
@@ -234,6 +246,7 @@ func NewClientObfuscatedSSHConn(
 		obfuscatorSeedTransformerParameters,
 		prefixSpec,
 		nil,
+		osshPrefixSplitConfig,
 		minPadding, maxPadding,
 		nil,
 		nil)
@@ -246,6 +259,7 @@ func NewServerObfuscatedSSHConn(
 	obfuscationKeyword string,
 	seedHistory *SeedHistory,
 	serverPrefixSpecs transforms.Specs,
+	osshPrefixSplitConfig *OSSHPrefixSplitConfig,
 	irregularLogger func(
 		clientIP string,
 		err error,
@@ -258,6 +272,7 @@ func NewServerObfuscatedSSHConn(
 		nil, nil,
 		nil,
 		serverPrefixSpecs,
+		osshPrefixSplitConfig,
 		nil, nil,
 		seedHistory,
 		irregularLogger)
@@ -318,6 +333,11 @@ func (conn *ObfuscatedSSHConn) Write(buffer []byte) (int, error) {
 	return len(buffer), nil
 }
 
+func (conn *ObfuscatedSSHConn) Close() error {
+	conn.stopRunning()
+	return conn.Conn.Close()
+}
+
 // readAndTransform reads and transforms the downstream bytes stream
 // while in an obfucation state. It parses the stream of bytes read
 // looking for the first SSH_MSG_NEWKEYS packet sent from the peer,
@@ -499,9 +519,35 @@ func (conn *ObfuscatedSSHConn) transformAndWrite(buffer []byte) error {
 	// identification line padding (server) are injected before any standard SSH traffic.
 	if conn.writeState == OBFUSCATION_WRITE_STATE_CLIENT_SEND_PREAMBLE {
 
-		preamble := conn.obfuscator.SendPreamble()
+		preamble, prefixLen := conn.obfuscator.SendPreamble()
+
+		// Writes the prefix first, then the rest of the preamble after a delay.
+		_, err := conn.Conn.Write(preamble[:prefixLen])
+		if err != nil {
+			return errors.Trace(err)
+		}
+
+		// Adds random delay defined by OSSH prefix split config.
+		if config := conn.obfuscator.osshPrefixSplitConfig; config != nil {
+			rng := prng.NewPRNGWithSeed(config.Seed)
+			delay := rng.Period(config.MinDelay, config.MaxDelay)
+
+			timer := time.NewTimer(delay)
+
+			var err error
+			select {
+			case <-conn.runCtx.Done():
+				err = conn.runCtx.Err()
+			case <-timer.C:
+			}
+			timer.Stop()
+
+			if err != nil {
+				return errors.Trace(err)
+			}
+		}
 
-		_, err := conn.Conn.Write(preamble)
+		_, err = conn.Conn.Write(preamble[prefixLen:])
 		if err != nil {
 			return errors.Trace(err)
 		}
@@ -512,8 +558,34 @@ func (conn *ObfuscatedSSHConn) transformAndWrite(buffer []byte) error {
 
 		var buffer bytes.Buffer
 
-		if preamble := conn.obfuscator.SendPreamble(); preamble != nil {
-			_, err := buffer.Write(preamble)
+		if preamble, prefixLen := conn.obfuscator.SendPreamble(); preamble != nil {
+			// Prefix bytes are written to the underlying conn immediately, skipping the buffer.
+			_, err := conn.Conn.Write(preamble[:prefixLen])
+			if err != nil {
+				return errors.Trace(err)
+			}
+
+			// Adds random delay defined by OSSH prefix split config.
+			if config := conn.obfuscator.osshPrefixSplitConfig; config != nil {
+				rng := prng.NewPRNGWithSeed(config.Seed)
+				delay := rng.Period(config.MinDelay, config.MaxDelay)
+
+				timer := time.NewTimer(delay)
+
+				var err error
+				select {
+				case <-conn.runCtx.Done():
+					err = conn.runCtx.Err()
+				case <-timer.C:
+				}
+				timer.Stop()
+
+				if err != nil {
+					return errors.Trace(err)
+				}
+			}
+
+			_, err = buffer.Write(preamble[prefixLen:])
 			if err != nil {
 				return errors.Trace(err)
 			}

+ 77 - 51
psiphon/common/obfuscator/obfuscator.go

@@ -27,6 +27,7 @@ import (
 	"encoding/binary"
 	"fmt"
 	"io"
+	"time"
 
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
@@ -67,6 +68,14 @@ type OSSHPrefixHeader struct {
 	SpecName string
 }
 
+// OSSHPrefixSplitConfig are parameters for splitting the
+// preamble into two writes: prefix followed by rest of the preamble.
+type OSSHPrefixSplitConfig struct {
+	Seed     *prng.Seed
+	MinDelay time.Duration
+	MaxDelay time.Duration
+}
+
 // Obfuscator implements the seed message, key derivation, and
 // stream ciphers for:
 // https://github.com/brl/obfuscated-openssh/blob/master/README.obfuscation
@@ -79,9 +88,14 @@ type OSSHPrefixHeader struct {
 type Obfuscator struct {
 	preamble []byte
 
-	// prefixHeader is the prefix header written by the client,
+	// Length of the prefix in the preamble.
+	preambleOSSHPrefixLength int
+
+	// osshPrefixHeader is the prefix header written by the client,
 	// or the prefix header read by the server.
-	prefixHeader *OSSHPrefixHeader
+	osshPrefixHeader *OSSHPrefixHeader
+
+	osshPrefixSplitConfig *OSSHPrefixSplitConfig
 
 	keyword              string
 	paddingLength        int
@@ -97,6 +111,7 @@ type ObfuscatorConfig struct {
 	Keyword                             string
 	ClientPrefixSpec                    *OSSHPrefixSpec
 	ServerPrefixSpecs                   transforms.Specs
+	OSSHPrefixSplitConfig               *OSSHPrefixSplitConfig
 	PaddingPRNGSeed                     *prng.Seed
 	MinPadding                          *int
 	MaxPadding                          *int
@@ -171,7 +186,7 @@ func NewClientObfuscator(
 		maxPadding = *config.MaxPadding
 	}
 
-	preamble, prefixHeader, paddingLength, err := makeClientPreamble(
+	preamble, prefixLen, prefixHeader, paddingLength, err := makeClientPreamble(
 		config.Keyword, config.ClientPrefixSpec,
 		paddingPRNG, minPadding, maxPadding, obfuscatorSeed,
 		clientToServerCipher)
@@ -180,14 +195,16 @@ func NewClientObfuscator(
 	}
 
 	return &Obfuscator{
-		preamble:             preamble,
-		prefixHeader:         prefixHeader,
-		keyword:              config.Keyword,
-		paddingLength:        paddingLength,
-		clientToServerCipher: clientToServerCipher,
-		serverToClientCipher: serverToClientCipher,
-		paddingPRNGSeed:      config.PaddingPRNGSeed,
-		paddingPRNG:          paddingPRNG}, nil
+		preamble:                 preamble,
+		preambleOSSHPrefixLength: prefixLen,
+		osshPrefixHeader:         prefixHeader,
+		osshPrefixSplitConfig:    config.OSSHPrefixSplitConfig,
+		keyword:                  config.Keyword,
+		paddingLength:            paddingLength,
+		clientToServerCipher:     clientToServerCipher,
+		serverToClientCipher:     serverToClientCipher,
+		paddingPRNGSeed:          config.PaddingPRNGSeed,
+		paddingPRNG:              paddingPRNG}, nil
 }
 
 // NewServerObfuscator creates a new Obfuscator, reading a seed message directly
@@ -212,20 +229,22 @@ func NewServerObfuscator(
 		return nil, errors.Trace(err)
 	}
 
-	preamble, err := makeServerPreamble(prefixHeader, config.ServerPrefixSpecs, config.Keyword)
+	preamble, prefixLen, err := makeServerPreamble(prefixHeader, config.ServerPrefixSpecs, config.Keyword)
 	if err != nil {
 		return nil, errors.Trace(err)
 	}
 
 	return &Obfuscator{
-		preamble:             preamble,
-		prefixHeader:         prefixHeader,
-		keyword:              config.Keyword,
-		paddingLength:        -1,
-		clientToServerCipher: clientToServerCipher,
-		serverToClientCipher: serverToClientCipher,
-		paddingPRNGSeed:      paddingPRNGSeed,
-		paddingPRNG:          prng.NewPRNGWithSeed(paddingPRNGSeed),
+		preamble:                 preamble,
+		preambleOSSHPrefixLength: prefixLen,
+		osshPrefixHeader:         prefixHeader,
+		osshPrefixSplitConfig:    config.OSSHPrefixSplitConfig,
+		keyword:                  config.Keyword,
+		paddingLength:            -1,
+		clientToServerCipher:     clientToServerCipher,
+		serverToClientCipher:     serverToClientCipher,
+		paddingPRNGSeed:          paddingPRNGSeed,
+		paddingPRNG:              prng.NewPRNGWithSeed(paddingPRNGSeed),
 	}, nil
 }
 
@@ -248,10 +267,12 @@ func (obfuscator *Obfuscator) GetPaddingLength() int {
 
 // SendPreamble returns the preamble created in NewObfuscatorClient or
 // NewServerObfuscator, removing the reference so that it may be garbage collected.
-func (obfuscator *Obfuscator) SendPreamble() []byte {
+func (obfuscator *Obfuscator) SendPreamble() ([]byte, int) {
 	msg := obfuscator.preamble
+	prefixLen := obfuscator.preambleOSSHPrefixLength
 	obfuscator.preamble = nil
-	return msg
+	obfuscator.preambleOSSHPrefixLength = 0
+	return msg, prefixLen
 }
 
 // ObfuscateClientToServer applies the client RC4 stream to the bytes in buffer.
@@ -341,42 +362,45 @@ func makeClientPreamble(
 	paddingPRNG *prng.PRNG,
 	minPadding, maxPadding int,
 	obfuscatorSeed []byte,
-	clientToServerCipher *rc4.Cipher) ([]byte, *OSSHPrefixHeader, int, error) {
+	clientToServerCipher *rc4.Cipher) ([]byte, int, *OSSHPrefixHeader, int, error) {
 
 	padding := paddingPRNG.Padding(minPadding, maxPadding)
 	buffer := new(bytes.Buffer)
 	magicValueStartIndex := len(obfuscatorSeed)
 
+	prefixLen := 0
+
 	if prefixSpec != nil {
-		// Writes the prefix and terminator to the buffer.
-		prefix, err := makePrefix(prefixSpec, keyword, OBFUSCATE_CLIENT_TO_SERVER_IV)
+		var b []byte
+		var err error
+		b, prefixLen, err = makeTerminatedPrefixWithPadding(prefixSpec, keyword, OBFUSCATE_CLIENT_TO_SERVER_IV)
 		if err != nil {
-			return nil, nil, 0, errors.Trace(err)
+			return nil, 0, nil, 0, errors.Trace(err)
 		}
 
-		_, err = buffer.Write(prefix)
+		_, err = buffer.Write(b)
 		if err != nil {
-			return nil, nil, 0, errors.Trace(err)
+			return nil, 0, nil, 0, errors.Trace(err)
 		}
 
-		magicValueStartIndex += len(prefix)
+		magicValueStartIndex += len(b)
 	}
 
 	err := binary.Write(buffer, binary.BigEndian, obfuscatorSeed)
 	if err != nil {
-		return nil, nil, 0, errors.Trace(err)
+		return nil, 0, nil, 0, errors.Trace(err)
 	}
 	err = binary.Write(buffer, binary.BigEndian, uint32(OBFUSCATE_MAGIC_VALUE))
 	if err != nil {
-		return nil, nil, 0, errors.Trace(err)
+		return nil, 0, nil, 0, errors.Trace(err)
 	}
 	err = binary.Write(buffer, binary.BigEndian, uint32(len(padding)))
 	if err != nil {
-		return nil, nil, 0, errors.Trace(err)
+		return nil, 0, nil, 0, errors.Trace(err)
 	}
 	err = binary.Write(buffer, binary.BigEndian, padding)
 	if err != nil {
-		return nil, nil, 0, errors.Trace(err)
+		return nil, 0, nil, 0, errors.Trace(err)
 	}
 
 	var prefixHeader *OSSHPrefixHeader = nil
@@ -384,7 +408,7 @@ func makeClientPreamble(
 		// Writes the prefix header after the padding.
 		err := prefixSpec.writePrefixHeader(buffer)
 		if err != nil {
-			return nil, nil, 0, errors.Trace(err)
+			return nil, 0, nil, 0, errors.Trace(err)
 		}
 
 		prefixHeader = &OSSHPrefixHeader{
@@ -399,7 +423,7 @@ func makeClientPreamble(
 		preamble[magicValueStartIndex:],
 		preamble[magicValueStartIndex:])
 
-	return preamble, prefixHeader, len(padding), nil
+	return preamble, prefixLen, prefixHeader, len(padding), nil
 }
 
 // makeServerPreamble generates a server preamble (prefix or nil).
@@ -410,10 +434,10 @@ func makeClientPreamble(
 func makeServerPreamble(
 	header *OSSHPrefixHeader,
 	serverSpecs transforms.Specs,
-	keyword string) ([]byte, error) {
+	keyword string) ([]byte, int, error) {
 
 	if header == nil {
-		return nil, nil
+		return nil, 0, nil
 	}
 
 	spec, ok := serverSpecs[header.SpecName]
@@ -424,7 +448,7 @@ func makeServerPreamble(
 
 	seed, err := prng.NewSeed()
 	if err != nil {
-		return nil, errors.Trace(err)
+		return nil, 0, errors.Trace(err)
 	}
 
 	prefixSpec := &OSSHPrefixSpec{
@@ -432,7 +456,7 @@ func makeServerPreamble(
 		Spec: spec,
 		Seed: seed,
 	}
-	return makePrefix(prefixSpec, keyword, OBFUSCATE_SERVER_TO_CLIENT_IV)
+	return makeTerminatedPrefixWithPadding(prefixSpec, keyword, OBFUSCATE_SERVER_TO_CLIENT_IV)
 }
 
 // readPreamble reads the preamble bytes from the client. If it does not detect
@@ -622,12 +646,12 @@ func readPreambleHelper(
 
 // makeTerminator generates a prefix terminator used in finding end of prefix
 // placed before OSSH stream.
-// prefix should be at least PREAMBLE_HEADER_LENGTH bytes and contain enough entropy.
-func makeTerminator(keyword string, prefix []byte, direction string) ([]byte, error) {
+// b should be at least PREAMBLE_HEADER_LENGTH bytes and contain enough entropy.
+func makeTerminator(keyword string, b []byte, direction string) ([]byte, error) {
 
-	// prefix length is at least equal to obfuscator seed message.
-	if len(prefix) < PREAMBLE_HEADER_LENGTH {
-		return nil, errors.TraceNew("prefix too short")
+	// Bytes length is at least equal to obfuscator seed message.
+	if len(b) < PREAMBLE_HEADER_LENGTH {
+		return nil, errors.TraceNew("bytes too short")
 	}
 
 	if (direction != OBFUSCATE_CLIENT_TO_SERVER_IV) &&
@@ -637,7 +661,7 @@ func makeTerminator(keyword string, prefix []byte, direction string) ([]byte, er
 
 	hkdf := hkdf.New(sha256.New,
 		[]byte(keyword),
-		prefix[:PREAMBLE_HEADER_LENGTH],
+		b[:PREAMBLE_HEADER_LENGTH],
 		[]byte(direction))
 
 	terminator := make([]byte, PREFIX_TERMINATOR_LENGTH)
@@ -649,24 +673,26 @@ func makeTerminator(keyword string, prefix []byte, direction string) ([]byte, er
 	return terminator, nil
 }
 
-// makePrefix generates a prefix followed by it's terminator using the given spec.
+// makeTerminatedPrefixWithPadding generates bytes starting with the prefix bytes defiend
+// by spec and ending with the generated terminator.
 // If the generated prefix is shorter than PREAMBLE_HEADER_LENGTH, it is padded
 // with random bytes.
-func makePrefix(spec *OSSHPrefixSpec, keyword, direction string) ([]byte, error) {
+// Returns the generated prefix with teminator, and the length of the prefix if no error.
+func makeTerminatedPrefixWithPadding(spec *OSSHPrefixSpec, keyword, direction string) ([]byte, int, error) {
 
-	prefix, err := spec.Spec.ApplyPrefix(spec.Seed, PREAMBLE_HEADER_LENGTH)
+	prefix, prefixLen, err := spec.Spec.ApplyPrefix(spec.Seed, PREAMBLE_HEADER_LENGTH)
 	if err != nil {
-		return nil, errors.Trace(err)
+		return nil, 0, errors.Trace(err)
 	}
 
 	terminator, err := makeTerminator(keyword, prefix, direction)
 
 	if err != nil {
-		return nil, errors.Trace(err)
+		return nil, 0, errors.Trace(err)
 	}
 	terminatedPrefix := append(prefix, terminator...)
 
-	return terminatedPrefix, nil
+	return terminatedPrefix, prefixLen, nil
 }
 
 // writePrefixHeader writes the prefix header to the given writer.

+ 43 - 30
psiphon/common/obfuscator/obfuscator_test.go

@@ -79,7 +79,7 @@ func TestObfuscator(t *testing.T) {
 		t.Fatalf("NewClientObfuscator failed: %s", err)
 	}
 
-	preamble := client.SendPreamble()
+	preamble, _ := client.SendPreamble()
 
 	server, err := NewServerObfuscator(config, "", bytes.NewReader(preamble))
 	if err != nil {
@@ -113,7 +113,7 @@ func TestObfuscator(t *testing.T) {
 		t.Fatalf("NewClientObfuscator failed: %s", err)
 	}
 
-	preamble = client.SendPreamble()
+	preamble, _ = client.SendPreamble()
 
 	clientIP := "192.168.0.1"
 
@@ -225,7 +225,7 @@ func TestObfuscatorSeedTransformParameters(t *testing.T) {
 				return
 			}
 
-			preamble := client.SendPreamble()
+			preamble, _ := client.SendPreamble()
 
 			if tt.expectedResult == nil {
 
@@ -269,32 +269,38 @@ func TestClientObfuscatorPrefix(t *testing.T) {
 			Spec: transforms.Spec{{"", spec}},
 			Seed: prefixSeed,
 		}
-		b, _ := makePrefix(&prefixSpec, keyword, OBFUSCATE_CLIENT_TO_SERVER_IV)
-		// return the prefix without the terminator
+		b, _, _ := makeTerminatedPrefixWithPadding(&prefixSpec, keyword, OBFUSCATE_CLIENT_TO_SERVER_IV)
+		// Strips the terminator.
 		return b[:len(b)-PREFIX_TERMINATOR_LENGTH]
 	}
 
 	type test struct {
-		name           string
-		prefixSpec     transforms.Spec
-		expectedPrefix []byte
+		name       string
+		prefixSpec transforms.Spec
+		// The expected prefix bytes with padding (if any) and terminator.
+		paddedTerminatedPrefixBytes []byte
+		// Length of the prefix without padding and terminator.
+		prefixLen int
 	}
 
 	tests := []test{
 		{
-			name:           "24 byte prefix",
-			prefixSpec:     transforms.Spec{{"", "\\x00{24}"}},
-			expectedPrefix: bytes.Repeat([]byte{0}, 24),
+			name:                        "24 byte prefix",
+			prefixSpec:                  transforms.Spec{{"", "\\x00{24}"}},
+			paddedTerminatedPrefixBytes: bytes.Repeat([]byte{0}, 24),
+			prefixLen:                   24,
 		},
 		{
-			name:           "long prefix",
-			prefixSpec:     transforms.Spec{{"", "\\x00{1000}\\x00{1000}\\x00{1000}\\x00{1000}"}},
-			expectedPrefix: bytes.Repeat([]byte{0}, 4000),
+			name:                        "long prefix",
+			prefixSpec:                  transforms.Spec{{"", "\\x00{1000}\\x00{1000}\\x00{1000}\\x00{1000}"}},
+			paddedTerminatedPrefixBytes: bytes.Repeat([]byte{0}, 4000),
+			prefixLen:                   4000,
 		},
 		{
-			name:           "short prefix spec",
-			prefixSpec:     transforms.Spec{{"", "\\x00\\x00\\x00\\x00"}},
-			expectedPrefix: generatePrefix("\\x00\\x00\\x00\\x00"),
+			name:                        "short prefix spec",
+			prefixSpec:                  transforms.Spec{{"", "\\x00\\x00\\x00\\x00"}},
+			paddedTerminatedPrefixBytes: generatePrefix("\\x00\\x00\\x00\\x00"),
+			prefixLen:                   4,
 		},
 	}
 
@@ -322,17 +328,23 @@ func TestClientObfuscatorPrefix(t *testing.T) {
 				t.Fatalf("NewClientObfuscator failed: %s", err)
 			}
 
-			preamble := bytes.NewBuffer(client.SendPreamble())
+			preambleBytes, prefixLen := client.SendPreamble()
+			preamble := bytes.NewBuffer(preambleBytes)
 
-			// check prefix
-			prefix := preamble.Next(len(tt.expectedPrefix))
-			if !bytes.Equal(prefix, tt.expectedPrefix) {
+			// check prefix excluding any padding
+			prefix := preamble.Next(prefixLen)
+			if !bytes.Equal(prefix, tt.paddedTerminatedPrefixBytes[:tt.prefixLen]) {
 				t.Fatalf("expected prefix to be all zeros")
 			}
 
+			// skips padding if any
+			if tt.prefixLen < PREAMBLE_HEADER_LENGTH {
+				preamble.Next(PREAMBLE_HEADER_LENGTH - tt.prefixLen)
+			}
+
 			// check terminator
 			terminator := preamble.Next(PREFIX_TERMINATOR_LENGTH)
-			expectedTerminator, err := makeTerminator(keyword, tt.expectedPrefix, OBFUSCATE_CLIENT_TO_SERVER_IV)
+			expectedTerminator, err := makeTerminator(keyword, tt.paddedTerminatedPrefixBytes[:PREAMBLE_HEADER_LENGTH], OBFUSCATE_CLIENT_TO_SERVER_IV)
 			if err != nil {
 				t.Fatalf("makeTerminator failed: %s", err)
 			}
@@ -406,7 +418,7 @@ func TestServerObfuscatorPrefix(t *testing.T) {
 		t.Fatalf("NewClientObfuscator failed: %s", err)
 	}
 
-	preamble := client.SendPreamble()
+	preamble, _ := client.SendPreamble()
 	reader := WrapConnWithSkipReader(newConn(preamble))
 
 	// test server obfuscator
@@ -416,7 +428,7 @@ func TestServerObfuscatorPrefix(t *testing.T) {
 	}
 
 	// check server prefix reply
-	serverPrefix := server.SendPreamble()
+	serverPrefix, _ := server.SendPreamble()
 	if !bytes.Equal(serverPrefix[:serverTermInd], expectedServerPrefix) {
 		t.Fatalf("unexpected server prefix")
 	}
@@ -507,11 +519,11 @@ func TestIrregularConnections(t *testing.T) {
 		t.Fatalf("NewClientObfuscator failed: %s", err)
 	}
 
-	if client.prefixHeader == nil {
+	if client.osshPrefixHeader == nil {
 		t.Fatalf("unexpected nil prefixHeader")
 	}
 
-	preamble := client.SendPreamble()
+	preamble, _ := client.SendPreamble()
 	seed := hex.EncodeToString(preamble[seedInd : seedInd+OBFUSCATE_SEED_LENGTH])
 
 	clientIP := "192.168.0.1"
@@ -522,7 +534,7 @@ func TestIrregularConnections(t *testing.T) {
 	if err != nil {
 		t.Fatalf("NewServerObfuscator failed: %s", err)
 	}
-	if server.prefixHeader == nil {
+	if server.osshPrefixHeader == nil {
 		t.Fatalf("unexpected nil prefixHeader")
 	}
 
@@ -616,7 +628,7 @@ func TestIrregularConnections(t *testing.T) {
 		t.Fatalf("NewClientObfuscator failed: %s", err)
 	}
 
-	preamble = client.SendPreamble()
+	preamble, _ = client.SendPreamble()
 	seedInd = 100 + PREFIX_TERMINATOR_LENGTH
 	preamble[seedInd+OBFUSCATE_SEED_LENGTH] = 0x00 // mutate magic value
 
@@ -639,7 +651,7 @@ func TestIrregularConnections(t *testing.T) {
 		t.Fatalf("NewClientObfuscator failed: %s", err)
 	}
 
-	preamble = client.SendPreamble()
+	preamble, _ = client.SendPreamble()
 	seedInd = 100 + PREFIX_TERMINATOR_LENGTH
 	preamble[seedInd+OBFUSCATE_SEED_LENGTH+4] = 0x00 // mutate padding length
 
@@ -731,6 +743,7 @@ func obfuscatedSSHConnTestHelper(
 				keyword,
 				NewSeedHistory(nil),
 				serverPrefixSpecs,
+				nil,
 				func(_ string, err error, logFields common.LogFields) {
 					t.Logf("IrregularLogger: %s %+v", err, logFields)
 				})
@@ -776,7 +789,7 @@ func obfuscatedSSHConnTestHelper(
 				conn,
 				keyword,
 				paddingPRNGSeed,
-				nil, clientPrefixSpec, nil, nil)
+				nil, clientPrefixSpec, nil, nil, nil)
 		}
 
 		var KEXPRNGSeed *prng.Seed

+ 51 - 0
psiphon/common/parameters/obfuscator.go

@@ -0,0 +1,51 @@
+package parameters
+
+import (
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/obfuscator"
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
+)
+
+func NewOSSHPrefixSpecParameters(p ParametersAccessor, dialPortNumber string) (*obfuscator.OSSHPrefixSpec, error) {
+
+	seed, err := prng.NewSeed()
+	if err != nil {
+		return nil, errors.Trace(err)
+	}
+
+	if !p.WeightedCoinFlip(OSSHPrefixProbability) {
+		return &obfuscator.OSSHPrefixSpec{}, nil
+	}
+
+	specs := p.ProtocolTransformSpecs(OSSHPrefixSpecs)
+	scopedSpecNames := p.ProtocolTransformScopedSpecNames(OSSHPrefixScopedSpecNames)
+
+	name, spec := specs.Select(dialPortNumber, scopedSpecNames)
+
+	if spec == nil {
+		return &obfuscator.OSSHPrefixSpec{}, nil
+	} else {
+		return &obfuscator.OSSHPrefixSpec{
+			Name: name,
+			Spec: spec,
+			Seed: seed,
+		}, nil
+	}
+}
+
+func NewOSSHPrefixSplitConfig(p ParametersAccessor) (*obfuscator.OSSHPrefixSplitConfig, error) {
+
+	seed, err := prng.NewSeed()
+	if err != nil {
+		return nil, errors.Trace(err)
+	}
+
+	minDelay := p.Duration(OSSHPrefixSplitMinDelay)
+	maxDelay := p.Duration(OSSHPrefixSplitMaxDelay)
+
+	return &obfuscator.OSSHPrefixSplitConfig{
+		Seed:     seed,
+		MinDelay: minDelay,
+		MaxDelay: maxDelay,
+	}, nil
+}

+ 8 - 5
psiphon/common/parameters/parameters.go

@@ -341,6 +341,8 @@ const (
 	OSSHPrefixSpecs                                  = "OSSHPrefixSpecs"
 	OSSHPrefixScopedSpecNames                        = "OSSHPrefixScopedSpecNames"
 	OSSHPrefixProbability                            = "OSSHPrefixProbability"
+	OSSHPrefixSplitMinDelay                          = "OSSHPrefixSplitMinDelay"
+	OSSHPrefixSplitMaxDelay                          = "OSSHPrefixSplitMaxDelay"
 	ServerOSSHPrefixSpecs                            = "ServerOSSHPrefixSpecs"
 )
 
@@ -720,11 +722,12 @@ var defaultParameters = map[string]struct {
 	ObfuscatedQUICNonceTransformScopedSpecNames: {value: transforms.ScopedSpecNames{}},
 	ObfuscatedQUICNonceTransformProbability:     {value: 0.0, minimum: 0.0},
 
-	OSSHPrefixSpecs:           {value: transforms.Specs{}},
-	OSSHPrefixScopedSpecNames: {value: transforms.ScopedSpecNames{}},
-	OSSHPrefixProbability:     {value: 0.0, minimum: 0.0},
-
-	ServerOSSHPrefixSpecs: {value: transforms.Specs{}, flags: serverSideOnly},
+	OSSHPrefixSpecs:            {value: transforms.Specs{}},
+	OSSHPrefixScopedSpecNames:  {value: transforms.ScopedSpecNames{}},
+	OSSHPrefixProbability:      {value: 0.0, minimum: 0.0},
+	OSSHPrefixSplitMinDelay:    {value: time.Duration(0), minimum: time.Duration(0)},
+	OSSHPrefixSplitMaxDelay:    {value: time.Duration(0), minimum: time.Duration(0)},
+	ServerOSSHPrefixSpecs:      {value: transforms.Specs{}, flags: serverSideOnly},
 }
 
 // IsServerSideOnly indicates if the parameter specified by name is used

+ 1 - 1
psiphon/common/tactics/tactics.go

@@ -1782,7 +1782,7 @@ func boxPayload(
 		return nil, errors.Trace(err)
 	}
 
-	obfuscatedBox := obfuscator.SendPreamble()
+	obfuscatedBox, _ := obfuscator.SendPreamble()
 	seedLen := len(obfuscatedBox)
 
 	obfuscatedBox = append(obfuscatedBox, box...)

+ 8 - 6
psiphon/common/transforms/transforms.go

@@ -68,7 +68,7 @@ func (specs Specs) Validate(prefixMode bool) error {
 			if len(spec) != 1 || len(spec[0]) != 2 {
 				return errors.TraceNew("prefix mode requires exactly one transform")
 			}
-			_, err := spec.ApplyPrefix(seed, 0)
+			_, _, err := spec.ApplyPrefix(seed, 0)
 			if err != nil {
 				return errors.Trace(err)
 			}
@@ -161,10 +161,10 @@ func (specs Specs) Select(scope string, scopedSpecs ScopedSpecNames) (string, Sp
 //
 // The input seed is used for all random number generation. The same seed can be
 // supplied to produce the same output, for replay.
-func (spec Spec) ApplyPrefix(seed *prng.Seed, minLength int) ([]byte, error) {
+func (spec Spec) ApplyPrefix(seed *prng.Seed, minLength int) ([]byte, int, error) {
 
 	if len(spec) != 1 || len(spec[0]) != 2 {
-		return nil, errors.TraceNew("prefix mode requires exactly one transform")
+		return nil, 0, errors.TraceNew("prefix mode requires exactly one transform")
 	}
 
 	rng := prng.NewPRNGWithSeed(seed)
@@ -175,21 +175,23 @@ func (spec Spec) ApplyPrefix(seed *prng.Seed, minLength int) ([]byte, error) {
 	}
 	gen, err := regen.NewGenerator(spec[0][1], args)
 	if err != nil {
-		return nil, errors.Trace(err)
+		return nil, 0, errors.Trace(err)
 	}
 
 	prefix, err := gen.Generate()
 	if err != nil {
-		return nil, errors.Trace(err)
+		return nil, 0, errors.Trace(err)
 	}
 
+	prefixLen := len(prefix)
+
 	if len(prefix) < minLength {
 		// Add random padding to fill up to minLength.
 		padding := rng.Bytes(minLength - len(prefix))
 		prefix = append(prefix, padding...)
 	}
 
-	return prefix, nil
+	return prefix, prefixLen, nil
 }
 
 // ApplyString applies the Spec to the input string, producing the output string.

+ 24 - 3
psiphon/config.go

@@ -840,9 +840,12 @@ type Config struct {
 	ObfuscatedQUICNonceTransformScopedSpecNames transforms.ScopedSpecNames
 	ObfuscatedQUICNonceTransformProbability     *float64
 
-	OSSHPrefixSpecs           transforms.Specs
-	OSSHPrefixScopedSpecNames transforms.ScopedSpecNames
-	OSSHPrefixProbability     *float64
+	// OSSHPrefix parameters are for testing purposes only.
+	OSSHPrefixSpecs                     transforms.Specs
+	OSSHPrefixScopedSpecNames           transforms.ScopedSpecNames
+	OSSHPrefixProbability               *float64
+	OSSHPrefixSplitMinDelayMilliseconds *int
+	OSSHPrefixSplitMaxDelayMilliseconds *int
 
 	// params is the active parameters.Parameters with defaults, config values,
 	// and, optionally, tactics applied.
@@ -1990,6 +1993,14 @@ func (config *Config) makeConfigParameters() map[string]interface{} {
 		applyParameters[parameters.OSSHPrefixProbability] = *config.OSSHPrefixProbability
 	}
 
+	if config.OSSHPrefixSplitMinDelayMilliseconds != nil {
+		applyParameters[parameters.OSSHPrefixSplitMinDelay] = fmt.Sprintf("%dms", *config.OSSHPrefixSplitMinDelayMilliseconds)
+	}
+
+	if config.OSSHPrefixSplitMaxDelayMilliseconds != nil {
+		applyParameters[parameters.OSSHPrefixSplitMaxDelay] = fmt.Sprintf("%dms", *config.OSSHPrefixSplitMaxDelayMilliseconds)
+	}
+
 	// When adding new config dial parameters that may override tactics, also
 	// update setDialParametersHash.
 
@@ -2491,6 +2502,16 @@ func (config *Config) setDialParametersHash() {
 		binary.Write(hash, binary.LittleEndian, *config.OSSHPrefixProbability)
 	}
 
+	if config.OSSHPrefixSplitMinDelayMilliseconds != nil {
+		hash.Write([]byte("OSSHPrefixSplitMinDelayMilliseconds"))
+		binary.Write(hash, binary.LittleEndian, int64(*config.OSSHPrefixSplitMinDelayMilliseconds))
+	}
+
+	if config.OSSHPrefixSplitMaxDelayMilliseconds != nil {
+		hash.Write([]byte("OSSHPrefixSplitMaxDelayMilliseconds"))
+		binary.Write(hash, binary.LittleEndian, int64(*config.OSSHPrefixSplitMaxDelayMilliseconds))
+	}
+
 	config.dialParametersHash = hash.Sum(nil)
 }
 

+ 15 - 31
psiphon/dialParameters.go

@@ -92,7 +92,8 @@ type DialParameters struct {
 	ObfuscatorPaddingSeed                   *prng.Seed
 	OSSHObfuscatorSeedTransformerParameters *transforms.ObfuscatorSeedTransformerParameters
 
-	OSSHPrefixSpec *obfuscator.OSSHPrefixSpec
+	OSSHPrefixSpec        *obfuscator.OSSHPrefixSpec
+	OSSHPrefixSplitConfig *obfuscator.OSSHPrefixSplitConfig
 
 	FragmentorSeed *prng.Seed
 
@@ -878,20 +879,30 @@ func MakeDialParameters(
 
 		if serverEntry.DisableOSSHPrefix {
 			dialParams.OSSHPrefixSpec = nil
+			dialParams.OSSHPrefixSplitConfig = nil
+
 		} else if !isReplay || !replayOSSHPrefix {
+
 			dialPortNumber, err := serverEntry.GetDialPortNumber(dialParams.TunnelProtocol)
 			if err != nil {
 				return nil, errors.Trace(err)
 			}
-			params, err := makeOSSHPrefixSpecParameters(p, strconv.Itoa(dialPortNumber))
+			prefixSpec, err := parameters.NewOSSHPrefixSpecParameters(p, strconv.Itoa(dialPortNumber))
+			if err != nil {
+				return nil, errors.Trace(err)
+			}
+
+			splitConfig, err := parameters.NewOSSHPrefixSplitConfig(p)
 			if err != nil {
 				return nil, errors.Trace(err)
 			}
 
-			if params.Spec != nil {
-				dialParams.OSSHPrefixSpec = params
+			if prefixSpec.Spec != nil {
+				dialParams.OSSHPrefixSpec = prefixSpec
+				dialParams.OSSHPrefixSplitConfig = splitConfig
 			} else {
 				dialParams.OSSHPrefixSpec = nil
+				dialParams.OSSHPrefixSplitConfig = nil
 			}
 		}
 
@@ -1622,30 +1633,3 @@ func makeSeedTransformerParameters(p parameters.ParametersAccessor,
 		}, nil
 	}
 }
-
-func makeOSSHPrefixSpecParameters(
-	p parameters.ParametersAccessor, dialPortNumber string) (*obfuscator.OSSHPrefixSpec, error) {
-
-	if !p.WeightedCoinFlip(parameters.OSSHPrefixProbability) {
-		return &obfuscator.OSSHPrefixSpec{}, nil
-	}
-
-	specs := p.ProtocolTransformSpecs(parameters.OSSHPrefixSpecs)
-	scopedSpecNames := p.ProtocolTransformScopedSpecNames(parameters.OSSHPrefixScopedSpecNames)
-
-	name, spec := specs.Select(dialPortNumber, scopedSpecNames)
-
-	if spec == nil {
-		return &obfuscator.OSSHPrefixSpec{}, nil
-	} else {
-		seed, err := prng.NewSeed()
-		if err != nil {
-			return nil, errors.Trace(err)
-		}
-		return &obfuscator.OSSHPrefixSpec{
-			Name: name,
-			Spec: spec,
-			Seed: seed,
-		}, nil
-	}
-}

+ 1 - 1
psiphon/meekConn.go

@@ -1679,7 +1679,7 @@ func makeMeekObfuscationValues(
 	if err != nil {
 		return nil, "", 0, 0, 0.0, errors.Trace(err)
 	}
-	obfuscatedCookie := obfuscator.SendPreamble()
+	obfuscatedCookie, _ := obfuscator.SendPreamble()
 	seedLen := len(obfuscatedCookie)
 	obfuscatedCookie = append(obfuscatedCookie, encryptedCookie...)
 	obfuscator.ObfuscateClientToServer(obfuscatedCookie[seedLen:])

+ 10 - 0
psiphon/server/tunnelServer.go

@@ -1917,8 +1917,17 @@ func (sshClient *sshClient) run(
 			}
 
 			var serverOsshPrefixSpecs transforms.Specs = nil
+			var serverOsshPrefixSplitConfig *obfuscator.OSSHPrefixSplitConfig = nil
 			if !p.IsNil() {
 				serverOsshPrefixSpecs = p.ProtocolTransformSpecs(parameters.ServerOSSHPrefixSpecs)
+				serverOsshPrefixSplitConfig, err = parameters.NewOSSHPrefixSplitConfig(p)
+
+				// Log error, but continue.
+				if err != nil {
+					log.WithTraceFields(LogFields{"error": errors.Trace(err)}).Warning(
+						"NewOSSHPrefixSplitConfig failed")
+				}
+
 				// Allow garbage collection.
 				p.Close()
 			}
@@ -1930,6 +1939,7 @@ func (sshClient *sshClient) run(
 				sshClient.sshServer.support.Config.ObfuscatedSSHKey,
 				sshClient.sshServer.obfuscatorSeedHistory,
 				serverOsshPrefixSpecs,
+				serverOsshPrefixSplitConfig,
 				func(clientIP string, err error, logFields common.LogFields) {
 					logIrregularTunnel(
 						sshClient.sshServer.support,

+ 1 - 0
psiphon/tunnel.go

@@ -984,6 +984,7 @@ func dialTunnel(
 			dialParams.ObfuscatorPaddingSeed,
 			dialParams.OSSHObfuscatorSeedTransformerParameters,
 			dialParams.OSSHPrefixSpec,
+			dialParams.OSSHPrefixSplitConfig,
 			&obfuscatedSSHMinPadding,
 			&obfuscatedSSHMaxPadding)
 		if err != nil {