From 5ef0cfc2725f8b0ced646ad6349e324a34b4a1fc Mon Sep 17 00:00:00 2001 From: Amir Khan Date: Tue, 4 Jul 2023 12:45:04 -0400 Subject: [PATCH 1/4] Added OSSH prefix split delay --- .../common/obfuscator/obfuscatedSshConn.go | 90 ++++++++++-- psiphon/common/obfuscator/obfuscator.go | 128 +++++++++++------- psiphon/common/obfuscator/obfuscator_test.go | 73 ++++++---- psiphon/common/parameters/obfuscator.go | 51 +++++++ psiphon/common/parameters/parameters.go | 13 +- psiphon/common/tactics/tactics.go | 2 +- psiphon/common/transforms/transforms.go | 14 +- psiphon/config.go | 27 +++- psiphon/dialParameters.go | 46 ++----- psiphon/meekConn.go | 2 +- psiphon/server/tunnelServer.go | 10 ++ psiphon/tunnel.go | 1 + 12 files changed, 320 insertions(+), 137 deletions(-) create mode 100644 psiphon/common/parameters/obfuscator.go diff --git a/psiphon/common/obfuscator/obfuscatedSshConn.go b/psiphon/common/obfuscator/obfuscatedSshConn.go index 38235eb40..7770009cf 100644 --- a/psiphon/common/obfuscator/obfuscatedSshConn.go +++ b/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) } diff --git a/psiphon/common/obfuscator/obfuscator.go b/psiphon/common/obfuscator/obfuscator.go index 23dd2413f..77e326593 100644 --- a/psiphon/common/obfuscator/obfuscator.go +++ b/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. diff --git a/psiphon/common/obfuscator/obfuscator_test.go b/psiphon/common/obfuscator/obfuscator_test.go index 854296822..2269582c3 100644 --- a/psiphon/common/obfuscator/obfuscator_test.go +++ b/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 diff --git a/psiphon/common/parameters/obfuscator.go b/psiphon/common/parameters/obfuscator.go new file mode 100644 index 000000000..15b5243fe --- /dev/null +++ b/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 +} diff --git a/psiphon/common/parameters/parameters.go b/psiphon/common/parameters/parameters.go index b69935565..f76c98bc2 100755 --- a/psiphon/common/parameters/parameters.go +++ b/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 diff --git a/psiphon/common/tactics/tactics.go b/psiphon/common/tactics/tactics.go index de637dcfd..a72bb7c4a 100755 --- a/psiphon/common/tactics/tactics.go +++ b/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...) diff --git a/psiphon/common/transforms/transforms.go b/psiphon/common/transforms/transforms.go index f909c445b..593aec64b 100644 --- a/psiphon/common/transforms/transforms.go +++ b/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. diff --git a/psiphon/config.go b/psiphon/config.go index 16dbca315..e79d65482 100755 --- a/psiphon/config.go +++ b/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) } diff --git a/psiphon/dialParameters.go b/psiphon/dialParameters.go index 773ce792c..bc33ce901 100644 --- a/psiphon/dialParameters.go +++ b/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 - } -} diff --git a/psiphon/meekConn.go b/psiphon/meekConn.go index a8622cd83..7385300d1 100644 --- a/psiphon/meekConn.go +++ b/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:]) diff --git a/psiphon/server/tunnelServer.go b/psiphon/server/tunnelServer.go index 24928222a..657a1b99a 100644 --- a/psiphon/server/tunnelServer.go +++ b/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, diff --git a/psiphon/tunnel.go b/psiphon/tunnel.go index 13c4e9489..d642eb901 100644 --- a/psiphon/tunnel.go +++ b/psiphon/tunnel.go @@ -984,6 +984,7 @@ func dialTunnel( dialParams.ObfuscatorPaddingSeed, dialParams.OSSHObfuscatorSeedTransformerParameters, dialParams.OSSHPrefixSpec, + dialParams.OSSHPrefixSplitConfig, &obfuscatedSSHMinPadding, &obfuscatedSSHMaxPadding) if err != nil { From 2b70d9552022538905b05b185b250854c52cd157 Mon Sep 17 00:00:00 2001 From: Amir Khan Date: Wed, 5 Jul 2023 16:51:41 -0400 Subject: [PATCH 2/4] Added OSSHPrefixEnableFragmentor parameter --- psiphon/common/fragmentor/fragmentor.go | 9 +++++++++ psiphon/common/net.go | 5 +++-- psiphon/common/obfuscator/obfuscatedSshConn.go | 6 ++++++ psiphon/common/parameters/parameters.go | 2 ++ psiphon/config.go | 10 ++++++++++ psiphon/dialParameters.go | 9 ++++++++- psiphon/server/listener.go | 2 +- psiphon/server/meek.go | 8 ++++++-- psiphon/server/tunnelServer.go | 16 ++++++++++++---- 9 files changed, 57 insertions(+), 10 deletions(-) diff --git a/psiphon/common/fragmentor/fragmentor.go b/psiphon/common/fragmentor/fragmentor.go index 4f3577a04..18565c4af 100644 --- a/psiphon/common/fragmentor/fragmentor.go +++ b/psiphon/common/fragmentor/fragmentor.go @@ -273,6 +273,15 @@ func (c *Conn) GetReplay() (*prng.Seed, bool) { return seed, c.isReplay } +// Stops the fragmentor from fragmenting any further writes. +func (c *Conn) Stop() { + + c.writeMutex.Lock() + defer c.writeMutex.Unlock() + + c.bytesToFragment = 0 +} + func (c *Conn) Write(buffer []byte) (int, error) { c.writeMutex.Lock() diff --git a/psiphon/common/net.go b/psiphon/common/net.go index 2962e7f50..d0dd37f8c 100644 --- a/psiphon/common/net.go +++ b/psiphon/common/net.go @@ -72,11 +72,12 @@ type UnderlyingTCPAddrSource interface { GetUnderlyingTCPAddrs() (*net.TCPAddr, *net.TCPAddr, bool) } -// FragmentorReplayAccessor defines the interface for accessing replay properties +// FragmentorAccessor defines the interface for accessing properties // of a fragmentor Conn. -type FragmentorReplayAccessor interface { +type FragmentorAccessor interface { SetReplay(*prng.PRNG) GetReplay() (*prng.Seed, bool) + Stop() } // HTTPRoundTripper is an adapter that allows using a function as a diff --git a/psiphon/common/obfuscator/obfuscatedSshConn.go b/psiphon/common/obfuscator/obfuscatedSshConn.go index 7770009cf..683f18f69 100644 --- a/psiphon/common/obfuscator/obfuscatedSshConn.go +++ b/psiphon/common/obfuscator/obfuscatedSshConn.go @@ -278,6 +278,12 @@ func NewServerObfuscatedSSHConn( irregularLogger) } +// IsOSSHPrefixedStream returns true if client wrote a prefix to the Obfuscated SSH stream, +// or the server read a prefixed Obfuscated SSH stream. +func (conn *ObfuscatedSSHConn) IsOSSHPrefixStream() bool { + return conn.obfuscator.osshPrefixHeader != nil +} + // GetDerivedPRNG creates a new PRNG with a seed derived from the // ObfuscatedSSHConn padding seed and distinguished by the salt, which should // be a unique identifier for each usage context. diff --git a/psiphon/common/parameters/parameters.go b/psiphon/common/parameters/parameters.go index f76c98bc2..916606659 100755 --- a/psiphon/common/parameters/parameters.go +++ b/psiphon/common/parameters/parameters.go @@ -343,6 +343,7 @@ const ( OSSHPrefixProbability = "OSSHPrefixProbability" OSSHPrefixSplitMinDelay = "OSSHPrefixSplitMinDelay" OSSHPrefixSplitMaxDelay = "OSSHPrefixSplitMaxDelay" + OSSHPrefixEnableFragmentor = "OSSHPrefixEnableFragmentor" ServerOSSHPrefixSpecs = "ServerOSSHPrefixSpecs" ) @@ -727,6 +728,7 @@ var defaultParameters = map[string]struct { 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)}, + OSSHPrefixEnableFragmentor: {value: false}, ServerOSSHPrefixSpecs: {value: transforms.Specs{}, flags: serverSideOnly}, } diff --git a/psiphon/config.go b/psiphon/config.go index e79d65482..9c8061329 100755 --- a/psiphon/config.go +++ b/psiphon/config.go @@ -846,6 +846,7 @@ type Config struct { OSSHPrefixProbability *float64 OSSHPrefixSplitMinDelayMilliseconds *int OSSHPrefixSplitMaxDelayMilliseconds *int + OSSHPrefixEnableFragmentor *bool // params is the active parameters.Parameters with defaults, config values, // and, optionally, tactics applied. @@ -2001,6 +2002,10 @@ func (config *Config) makeConfigParameters() map[string]interface{} { applyParameters[parameters.OSSHPrefixSplitMaxDelay] = fmt.Sprintf("%dms", *config.OSSHPrefixSplitMaxDelayMilliseconds) } + if config.OSSHPrefixEnableFragmentor != nil { + applyParameters[parameters.OSSHPrefixEnableFragmentor] = *config.OSSHPrefixEnableFragmentor + } + // When adding new config dial parameters that may override tactics, also // update setDialParametersHash. @@ -2512,6 +2517,11 @@ func (config *Config) setDialParametersHash() { binary.Write(hash, binary.LittleEndian, int64(*config.OSSHPrefixSplitMaxDelayMilliseconds)) } + if config.OSSHPrefixEnableFragmentor != nil { + hash.Write([]byte("OSSHPrefixEnableFragmentor")) + binary.Write(hash, binary.LittleEndian, *config.OSSHPrefixEnableFragmentor) + } + config.dialParametersHash = hash.Sum(nil) } diff --git a/psiphon/dialParameters.go b/psiphon/dialParameters.go index bc33ce901..1d6da2316 100644 --- a/psiphon/dialParameters.go +++ b/psiphon/dialParameters.go @@ -1076,6 +1076,13 @@ func MakeDialParameters( return IPs, nil } + // Fragmentor configuration. + // Note: fragmentorConfig is nil if fragmentor is disabled for prefixed OSSH. + fragmentorConfig := fragmentor.NewUpstreamConfig(p, dialParams.TunnelProtocol, dialParams.FragmentorSeed) + if !p.Bool(parameters.OSSHPrefixEnableFragmentor) && dialParams.OSSHPrefixSpec != nil { + fragmentorConfig = nil + } + dialParams.dialConfig = &DialConfig{ DiagnosticID: serverEntry.GetDiagnosticID(), UpstreamProxyURL: config.UpstreamProxyURL, @@ -1085,7 +1092,7 @@ func MakeDialParameters( IPv6Synthesizer: config.IPv6Synthesizer, ResolveIP: resolveIP, TrustedCACertificatesFilename: config.TrustedCACertificatesFilename, - FragmentorConfig: fragmentor.NewUpstreamConfig(p, dialParams.TunnelProtocol, dialParams.FragmentorSeed), + FragmentorConfig: fragmentorConfig, UpstreamProxyErrorCallback: upstreamProxyErrorCallback, } diff --git a/psiphon/server/listener.go b/psiphon/server/listener.go index 71a8ff31e..31cd20c1e 100644 --- a/psiphon/server/listener.go +++ b/psiphon/server/listener.go @@ -147,7 +147,7 @@ func (listener *TacticsListener) accept() (net.Conn, error) { conn) if doReplay && replaySeed != nil { - conn.(common.FragmentorReplayAccessor).SetReplay( + conn.(common.FragmentorAccessor).SetReplay( prng.NewPRNGWithSeed(replaySeed)) } } diff --git a/psiphon/server/meek.go b/psiphon/server/meek.go index 4161def5a..354aed9ce 100644 --- a/psiphon/server/meek.go +++ b/psiphon/server/meek.go @@ -1656,7 +1656,7 @@ func (conn *meekConn) SetReplay(PRNG *prng.PRNG) { } } - fragmentor, ok := underlyingConn.(common.FragmentorReplayAccessor) + fragmentor, ok := underlyingConn.(common.FragmentorAccessor) if ok { fragmentor.SetReplay(PRNG) } @@ -1680,13 +1680,17 @@ func (conn *meekConn) GetReplay() (*prng.Seed, bool) { } } - fragmentor, ok := underlyingConn.(common.FragmentorReplayAccessor) + fragmentor, ok := underlyingConn.(common.FragmentorAccessor) if ok { return fragmentor.GetReplay() } return nil, false } +func (conn *meekConn) Stop() { + // No-op +} + // pumpReads causes goroutines blocking on meekConn.Read() to read // from the specified reader. This function blocks until the reader // is fully consumed or the meekConn is closed. A read buffer allows diff --git a/psiphon/server/tunnelServer.go b/psiphon/server/tunnelServer.go index 657a1b99a..e4ef786db 100644 --- a/psiphon/server/tunnelServer.go +++ b/psiphon/server/tunnelServer.go @@ -1792,7 +1792,7 @@ func (sshClient *sshClient) run( if isReplayCandidate { getFragmentorSeed := func() *prng.Seed { - fragmentor, ok := baseConn.(common.FragmentorReplayAccessor) + fragmentor, ok := baseConn.(common.FragmentorAccessor) if ok { fragmentorSeed, _ := fragmentor.GetReplay() return fragmentorSeed @@ -1831,7 +1831,7 @@ func (sshClient *sshClient) run( replayedFragmentation := false if sshClient.tunnelProtocol != protocol.TUNNEL_PROTOCOL_OBFUSCATED_SSH { - fragmentor, ok := baseConn.(common.FragmentorReplayAccessor) + fragmentor, ok := baseConn.(common.FragmentorAccessor) if ok { _, replayedFragmentation = fragmentor.GetReplay() } @@ -1916,9 +1916,11 @@ func (sshClient *sshClient) run( "ServerTacticsParametersCache.Get failed") } + var osshPrefixEnableFragmentor bool = false var serverOsshPrefixSpecs transforms.Specs = nil var serverOsshPrefixSplitConfig *obfuscator.OSSHPrefixSplitConfig = nil if !p.IsNil() { + osshPrefixEnableFragmentor = p.Bool(parameters.OSSHPrefixEnableFragmentor) serverOsshPrefixSpecs = p.ProtocolTransformSpecs(parameters.ServerOSSHPrefixSpecs) serverOsshPrefixSplitConfig, err = parameters.NewOSSHPrefixSplitConfig(p) @@ -1961,7 +1963,7 @@ func (sshClient *sshClient) run( // ssh.NewServerConn to ensure fragmentor is seeded before downstream bytes // are written. if err == nil && sshClient.tunnelProtocol == protocol.TUNNEL_PROTOCOL_OBFUSCATED_SSH { - fragmentor, ok := baseConn.(common.FragmentorReplayAccessor) + fragmentor, ok := baseConn.(common.FragmentorAccessor) if ok { var fragmentorPRNG *prng.PRNG fragmentorPRNG, err = result.obfuscatedSSHConn.GetDerivedPRNG("server-side-fragmentor") @@ -1970,6 +1972,12 @@ func (sshClient *sshClient) run( } else { fragmentor.SetReplay(fragmentorPRNG) } + + // Stops the fragmentor if disabled for prefixed OSSH streams. + if !osshPrefixEnableFragmentor && result.obfuscatedSSHConn.IsOSSHPrefixStream() { + fragmentor.Stop() + } + } } } @@ -2078,7 +2086,7 @@ func (sshClient *sshClient) run( replayMetrics := make(LogFields) replayedFragmentation := false - fragmentor, ok := baseConn.(common.FragmentorReplayAccessor) + fragmentor, ok := baseConn.(common.FragmentorAccessor) if ok { _, replayedFragmentation = fragmentor.GetReplay() } From edbd10b28f55c009cf033760f1d75e311b2f331f Mon Sep 17 00:00:00 2001 From: Amir Khan Date: Thu, 6 Jul 2023 12:47:56 -0400 Subject: [PATCH 3/4] Added end-to-end testing of OSSH prefixes --- psiphon/server/server_test.go | 72 ++++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/psiphon/server/server_test.go b/psiphon/server/server_test.go index c680504e2..a912520f7 100644 --- a/psiphon/server/server_test.go +++ b/psiphon/server/server_test.go @@ -50,6 +50,7 @@ import ( "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol" "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/quic" "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/tactics" + "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/transforms" "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/values" "github.com/miekg/dns" "golang.org/x/net/proxy" @@ -196,6 +197,33 @@ func TestFragmentedOSSH(t *testing.T) { }) } +func TestPrefixedOSSH(t *testing.T) { + runServer(t, + &runServerConfig{ + tunnelProtocol: "OSSH", + enableSSHAPIRequests: true, + doHotReload: false, + doDefaultSponsorID: false, + denyTrafficRules: false, + requireAuthorization: true, + omitAuthorization: false, + doTunneledWebRequest: true, + doTunneledNTPRequest: true, + applyPrefix: true, + forceFragmenting: false, + forceLivenessTest: false, + doPruneServerEntries: false, + doDanglingTCPConn: true, + doPacketManipulation: false, + doBurstMonitor: false, + doSplitTunnel: false, + limitQUICVersions: false, + doDestinationBytes: false, + doChangeBytesConfig: false, + doLogHostProvider: true, + }) +} + func TestUnfrontedMeek(t *testing.T) { runServer(t, &runServerConfig{ @@ -842,6 +870,7 @@ type runServerConfig struct { omitAuthorization bool doTunneledWebRequest bool doTunneledNTPRequest bool + applyPrefix bool forceFragmenting bool forceLivenessTest bool doPruneServerEntries bool @@ -1005,7 +1034,9 @@ func runServer(t *testing.T, runConfig *runServerConfig) { propagationChannelID, livenessTestSize, runConfig.doBurstMonitor, - runConfig.doDestinationBytes) + runConfig.doDestinationBytes, + runConfig.applyPrefix, + ) } blocklistFilename := filepath.Join(testDataDirName, "blocklist.csv") @@ -1289,6 +1320,22 @@ func runServer(t *testing.T, runConfig *runServerConfig) { applyParameters := make(map[string]interface{}) + if runConfig.applyPrefix { + + applyParameters[parameters.OSSHPrefixSpecs] = transforms.Specs{ + "TEST": {{"", "\x00{24}"}}, + } + applyParameters[parameters.OSSHPrefixScopedSpecNames] = transforms.ScopedSpecNames{ + "": {"TEST"}, + } + applyParameters[parameters.OSSHPrefixProbability] = 1.0 + applyParameters[parameters.OSSHPrefixSplitMinDelay] = 1 * time.Millisecond + applyParameters[parameters.OSSHPrefixSplitMaxDelay] = 10 * time.Millisecond + + applyParameters[parameters.OSSHPrefixEnableFragmentor] = runConfig.forceFragmenting + + } + if runConfig.forceFragmenting { applyParameters[parameters.FragmentorLimitProtocols] = protocol.TunnelProtocols{runConfig.tunnelProtocol} applyParameters[parameters.FragmentorProbability] = 1.0 @@ -1498,6 +1545,7 @@ func runServer(t *testing.T, runConfig *runServerConfig) { propagationChannelID, livenessTestSize, runConfig.doBurstMonitor, + false, false) p, _ := os.FindProcess(os.Getpid()) @@ -2025,6 +2073,13 @@ func checkExpectedServerTunnelLogFields( } } + if runConfig.applyPrefix { + + if fields["ossh_prefix"] == nil || fmt.Sprintf("%s", fields["ossh_prefix"]) == "" { + return fmt.Errorf("missing expected field 'ossh_prefix'") + } + } + if runConfig.forceFragmenting { for _, name := range []string{ @@ -2802,7 +2857,8 @@ func paveTacticsConfigFile( propagationChannelID string, livenessTestSize int, doBurstMonitor bool, - doDestinationBytes bool) { + doDestinationBytes bool, + applyOsshPrefix bool) { // Setting LimitTunnelProtocols passively exercises the // server-side LimitTunnelProtocols enforcement. @@ -2818,6 +2874,7 @@ func paveTacticsConfigFile( "Parameters" : { %s %s + %s "LimitTunnelProtocols" : ["%s"], "FragmentorLimitProtocols" : ["%s"], "FragmentorProbability" : 1.0, @@ -2905,11 +2962,22 @@ func paveTacticsConfigFile( `, testGeoIPASN) } + osshPrefix := "" + if applyOsshPrefix { + osshPrefix = ` + "ServerOSSHPrefixSpecs": { + "TEST": [["", "\\x00{20}"]], + }, + "OSSHPrefixEnableFragmentor": true, + ` + } + tacticsConfigJSON := fmt.Sprintf( tacticsConfigJSONFormat, tacticsRequestPublicKey, tacticsRequestPrivateKey, tacticsRequestObfuscatedKey, burstParameters, destinationBytesParameters, + osshPrefix, tunnelProtocol, tunnelProtocol, tunnelProtocol, From a3235f1ae9da47d5fe5ed8775ab3bb0344e27e4c Mon Sep 17 00:00:00 2001 From: Amir Khan Date: Thu, 6 Jul 2023 14:51:20 -0400 Subject: [PATCH 4/4] Fixes based on feedback --- psiphon/common/fragmentor/fragmentor.go | 2 +- psiphon/common/net.go | 2 +- .../common/obfuscator/obfuscatedSshConn.go | 33 +++++++++--- psiphon/common/obfuscator/obfuscator.go | 15 +++++- psiphon/common/obfuscator/obfuscator_test.go | 1 - psiphon/common/parameters/obfuscator.go | 51 ------------------- psiphon/dialParameters.go | 49 +++++++++++++++++- psiphon/server/meek.go | 7 ++- psiphon/server/tunnelServer.go | 25 +++++---- 9 files changed, 108 insertions(+), 77 deletions(-) delete mode 100644 psiphon/common/parameters/obfuscator.go diff --git a/psiphon/common/fragmentor/fragmentor.go b/psiphon/common/fragmentor/fragmentor.go index 18565c4af..ff86cbd72 100644 --- a/psiphon/common/fragmentor/fragmentor.go +++ b/psiphon/common/fragmentor/fragmentor.go @@ -274,7 +274,7 @@ func (c *Conn) GetReplay() (*prng.Seed, bool) { } // Stops the fragmentor from fragmenting any further writes. -func (c *Conn) Stop() { +func (c *Conn) StopFragmenting() { c.writeMutex.Lock() defer c.writeMutex.Unlock() diff --git a/psiphon/common/net.go b/psiphon/common/net.go index d0dd37f8c..05eb83a59 100644 --- a/psiphon/common/net.go +++ b/psiphon/common/net.go @@ -77,7 +77,7 @@ type UnderlyingTCPAddrSource interface { type FragmentorAccessor interface { SetReplay(*prng.PRNG) GetReplay() (*prng.Seed, bool) - Stop() + StopFragmenting() } // HTTPRoundTripper is an adapter that allows using a function as a diff --git a/psiphon/common/obfuscator/obfuscatedSshConn.go b/psiphon/common/obfuscator/obfuscatedSshConn.go index 683f18f69..200b8050c 100644 --- a/psiphon/common/obfuscator/obfuscatedSshConn.go +++ b/psiphon/common/obfuscator/obfuscatedSshConn.go @@ -178,11 +178,10 @@ func NewObfuscatedSSHConn( // NewServerObfuscator reads a seed message from conn obfuscator, err = NewServerObfuscator( &ObfuscatorConfig{ - Keyword: obfuscationKeyword, - ServerPrefixSpecs: serverPrefixSepcs, - OSSHPrefixSplitConfig: osshPrefixSplitConfig, - SeedHistory: seedHistory, - IrregularLogger: irregularLogger, + Keyword: obfuscationKeyword, + ServerPrefixSpecs: serverPrefixSepcs, + SeedHistory: seedHistory, + IrregularLogger: irregularLogger, }, common.IPAddressFromAddr(conn.RemoteAddr()), conn) @@ -259,7 +258,6 @@ func NewServerObfuscatedSSHConn( obfuscationKeyword string, seedHistory *SeedHistory, serverPrefixSpecs transforms.Specs, - osshPrefixSplitConfig *OSSHPrefixSplitConfig, irregularLogger func( clientIP string, err error, @@ -272,7 +270,7 @@ func NewServerObfuscatedSSHConn( nil, nil, nil, serverPrefixSpecs, - osshPrefixSplitConfig, + nil, nil, nil, seedHistory, irregularLogger) @@ -295,6 +293,27 @@ func (conn *ObfuscatedSSHConn) GetDerivedPRNG(salt string) (*prng.PRNG, error) { return conn.obfuscator.GetDerivedPRNG(salt) } +// SetOSSHPrefixSplitConfig sets the OSSHPrefixSplitConfig for the server. +// This must be called before any data is written. +func (conn *ObfuscatedSSHConn) SetOSSHPrefixSplitConfig(minDelay, maxDelay time.Duration) error { + if conn.mode != OBFUSCATION_CONN_MODE_SERVER { + return errors.TraceNew("SetOSSHPrefixSplitConfig() is only valid for server connections") + } + if conn.writeState != OBFUSCATION_WRITE_STATE_SERVER_SEND_PREFIX_AND_IDENTIFICATION_LINE_PADDING { + return errors.TraceNew("SetOSSHPrefixSplitConfig() must be called before any data is written") + } + seed, err := conn.obfuscator.GetDerivedPRNGSeed("obfuscated-ssh-prefix-split") + if err != nil { + return errors.Trace(err) + } + conn.obfuscator.osshPrefixSplitConfig = &OSSHPrefixSplitConfig{ + Seed: seed, + MinDelay: minDelay, + MaxDelay: maxDelay, + } + return nil +} + // GetMetrics implements the common.MetricsSource interface. func (conn *ObfuscatedSSHConn) GetMetrics() common.LogFields { logFields := make(common.LogFields) diff --git a/psiphon/common/obfuscator/obfuscator.go b/psiphon/common/obfuscator/obfuscator.go index 77e326593..2b919698d 100644 --- a/psiphon/common/obfuscator/obfuscator.go +++ b/psiphon/common/obfuscator/obfuscator.go @@ -256,7 +256,20 @@ func NewServerObfuscator( // client, so derived PRNGs may be used to replay sequences post-initial // obfuscator message. func (obfuscator *Obfuscator) GetDerivedPRNG(salt string) (*prng.PRNG, error) { - return prng.NewPRNGWithSaltedSeed(obfuscator.paddingPRNGSeed, salt) + seed, err := prng.NewPRNGWithSaltedSeed(obfuscator.paddingPRNGSeed, salt) + return seed, errors.Trace(err) +} + +// GetDerivedPRNGSeed creates a new PRNG seed derived from the obfuscator +// padding seed and distinguished by the salt, which should be a unique +// identifier for each usage context. +// +// For NewServerObfuscator, the obfuscator padding seed is obtained from the +// client, so derived seeds may be used to replay sequences post-initial +// obfuscator message. +func (obfuscator *Obfuscator) GetDerivedPRNGSeed(salt string) (*prng.Seed, error) { + seed, err := prng.NewSaltedSeed(obfuscator.paddingPRNGSeed, salt) + return seed, errors.Trace(err) } // GetPaddingLength returns the client seed message padding length. Only valid diff --git a/psiphon/common/obfuscator/obfuscator_test.go b/psiphon/common/obfuscator/obfuscator_test.go index 2269582c3..3ea64a910 100644 --- a/psiphon/common/obfuscator/obfuscator_test.go +++ b/psiphon/common/obfuscator/obfuscator_test.go @@ -743,7 +743,6 @@ func obfuscatedSSHConnTestHelper( keyword, NewSeedHistory(nil), serverPrefixSpecs, - nil, func(_ string, err error, logFields common.LogFields) { t.Logf("IrregularLogger: %s %+v", err, logFields) }) diff --git a/psiphon/common/parameters/obfuscator.go b/psiphon/common/parameters/obfuscator.go deleted file mode 100644 index 15b5243fe..000000000 --- a/psiphon/common/parameters/obfuscator.go +++ /dev/null @@ -1,51 +0,0 @@ -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 -} diff --git a/psiphon/dialParameters.go b/psiphon/dialParameters.go index 1d6da2316..a22a04fa1 100644 --- a/psiphon/dialParameters.go +++ b/psiphon/dialParameters.go @@ -887,12 +887,12 @@ func MakeDialParameters( if err != nil { return nil, errors.Trace(err) } - prefixSpec, err := parameters.NewOSSHPrefixSpecParameters(p, strconv.Itoa(dialPortNumber)) + prefixSpec, err := makeOSSHPrefixSpecParameters(p, strconv.Itoa(dialPortNumber)) if err != nil { return nil, errors.Trace(err) } - splitConfig, err := parameters.NewOSSHPrefixSplitConfig(p) + splitConfig, err := makeOSSHPrefixSplitConfig(p) if err != nil { return nil, errors.Trace(err) } @@ -1640,3 +1640,48 @@ 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 + } +} + +func makeOSSHPrefixSplitConfig(p parameters.ParametersAccessor) (*obfuscator.OSSHPrefixSplitConfig, error) { + + minDelay := p.Duration(parameters.OSSHPrefixSplitMinDelay) + maxDelay := p.Duration(parameters.OSSHPrefixSplitMaxDelay) + + seed, err := prng.NewSeed() + if err != nil { + return nil, errors.Trace(err) + } + + return &obfuscator.OSSHPrefixSplitConfig{ + Seed: seed, + MinDelay: minDelay, + MaxDelay: maxDelay, + }, nil +} diff --git a/psiphon/server/meek.go b/psiphon/server/meek.go index 354aed9ce..24e4a1768 100644 --- a/psiphon/server/meek.go +++ b/psiphon/server/meek.go @@ -1687,8 +1687,11 @@ func (conn *meekConn) GetReplay() (*prng.Seed, bool) { return nil, false } -func (conn *meekConn) Stop() { - // No-op +func (conn *meekConn) StopFragmenting() { + fragmentor, ok := conn.firstUnderlyingConn.(common.FragmentorAccessor) + if ok { + fragmentor.StopFragmenting() + } } // pumpReads causes goroutines blocking on meekConn.Read() to read diff --git a/psiphon/server/tunnelServer.go b/psiphon/server/tunnelServer.go index e4ef786db..b6988eb62 100644 --- a/psiphon/server/tunnelServer.go +++ b/psiphon/server/tunnelServer.go @@ -1918,18 +1918,12 @@ func (sshClient *sshClient) run( var osshPrefixEnableFragmentor bool = false var serverOsshPrefixSpecs transforms.Specs = nil - var serverOsshPrefixSplitConfig *obfuscator.OSSHPrefixSplitConfig = nil + var minDelay, maxDelay time.Duration if !p.IsNil() { osshPrefixEnableFragmentor = p.Bool(parameters.OSSHPrefixEnableFragmentor) 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") - } - + minDelay = p.Duration(parameters.OSSHPrefixSplitMinDelay) + maxDelay = p.Duration(parameters.OSSHPrefixSplitMaxDelay) // Allow garbage collection. p.Close() } @@ -1941,7 +1935,6 @@ 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, @@ -1958,6 +1951,16 @@ func (sshClient *sshClient) run( conn = result.obfuscatedSSHConn } + // Set the OSSH prefix split config. + if err == nil && result.obfuscatedSSHConn.IsOSSHPrefixStream() { + err = result.obfuscatedSSHConn.SetOSSHPrefixSplitConfig(minDelay, maxDelay) + // Log error, but continue. + if err != nil { + log.WithTraceFields(LogFields{"error": errors.Trace(err)}).Warning( + "SetOSSHPrefixSplitConfig failed") + } + } + // Seed the fragmentor, when present, with seed derived from initial // obfuscator message. See tactics.Listener.Accept. This must preceed // ssh.NewServerConn to ensure fragmentor is seeded before downstream bytes @@ -1975,7 +1978,7 @@ func (sshClient *sshClient) run( // Stops the fragmentor if disabled for prefixed OSSH streams. if !osshPrefixEnableFragmentor && result.obfuscatedSSHConn.IsOSSHPrefixStream() { - fragmentor.Stop() + fragmentor.StopFragmenting() } }