Browse Source

Add random time jitter and padding to SSH keep alives.

* Added helpers MakeRandomPeriod and MakeSecureRandomPadding,
used to generate random request time period jitters and request
size padding, respectively.

* Both SSH keep alive and status requests use theses helpers.

* Randomized period and padding is to make periodic network
events less of a fingerprint.
Rod Hynes 11 years ago
parent
commit
20395bcc02
4 changed files with 63 additions and 40 deletions
  1. 3 1
      psiphon/defaults.go
  2. 1 32
      psiphon/serverApi.go
  3. 26 7
      psiphon/tunnel.go
  4. 33 0
      psiphon/utils.go

+ 3 - 1
psiphon/defaults.go

@@ -31,7 +31,9 @@ const (
 	TUNNEL_CONNECT_TIMEOUT                       = 15 * time.Second
 	TUNNEL_READ_TIMEOUT                          = 0 * time.Second
 	TUNNEL_WRITE_TIMEOUT                         = 5 * time.Second
-	TUNNEL_SSH_KEEP_ALIVE_PERIOD                 = 60 * time.Second
+	TUNNEL_SSH_KEEP_ALIVE_PAYLOAD_MAX_BYTES      = 256
+	TUNNEL_SSH_KEEP_ALIVE_PERIOD_MIN             = 60 * time.Second
+	TUNNEL_SSH_KEEP_ALIVE_PERIOD_MAX             = 120 * time.Second
 	ESTABLISH_TUNNEL_TIMEOUT                     = 60 * time.Second
 	ESTABLISH_TUNNEL_PAUSE_PERIOD                = 10 * time.Second
 	PORT_FORWARD_FAILURE_THRESHOLD               = 10

+ 1 - 32
psiphon/serverApi.go

@@ -31,7 +31,6 @@ import (
 	"net"
 	"net/http"
 	"strconv"
-	"time"
 )
 
 // Session is a utility struct which holds all of the data associated
@@ -133,23 +132,6 @@ func (session *Session) StatsRegexps() *Regexps {
 	return session.statsRegexps
 }
 
-// NextStatusRequestPeriod returns the amount of time that should be waited before the
-// next time stats are sent. The next wait time is picked at random, from a range,
-// to make the stats send less fingerprintable.
-func NextStatusRequestPeriod() (duration time.Duration) {
-	jitter, err := MakeSecureRandomInt64(
-		PSIPHON_API_STATUS_REQUEST_PERIOD_MAX.Nanoseconds() -
-			PSIPHON_API_STATUS_REQUEST_PERIOD_MIN.Nanoseconds())
-
-	// In case of error we're just going to use zero jitter.
-	if err != nil {
-		NoticeAlert("NextStatusRequestPeriod: make jitter failed")
-	}
-
-	duration = PSIPHON_API_STATUS_REQUEST_PERIOD_MIN + time.Duration(jitter)
-	return
-}
-
 // DoStatusRequest makes a /status request to the server, sending session stats.
 func (session *Session) DoStatusRequest(statsPayload json.Marshaler) error {
 	statsPayloadJSON, err := json.Marshal(statsPayload)
@@ -159,20 +141,7 @@ func (session *Session) DoStatusRequest(statsPayload json.Marshaler) error {
 
 	// Add a random amount of padding to help prevent stats updates from being
 	// a predictable size (which often happens when the connection is quiet).
-	var padding []byte
-	paddingSize, err := MakeSecureRandomInt(PSIPHON_API_STATUS_REQUEST_PADDING_MAX_BYTES)
-	// In case of randomness fail, we're going to proceed with zero padding.
-	// TODO: Is this okay?
-	if err != nil {
-		NoticeAlert("DoStatusRequest: MakeSecureRandomInt failed")
-		padding = make([]byte, 0)
-	} else {
-		padding, err = MakeSecureRandomBytes(paddingSize)
-		if err != nil {
-			NoticeAlert("DoStatusRequest: MakeSecureRandomBytes failed")
-			padding = make([]byte, 0)
-		}
-	}
+	padding := MakeSecureRandomPadding(0, PSIPHON_API_STATUS_REQUEST_PADDING_MAX_BYTES)
 
 	// "connected" is a legacy parameter. This client does not report when
 	// it has disconnected.

+ 26 - 7
psiphon/tunnel.go

@@ -438,23 +438,42 @@ func dialSsh(
 func (tunnel *Tunnel) operateTunnel(config *Config, tunnelOwner TunnelOwner) {
 	defer tunnel.operateWaitGroup.Done()
 
-	// Note: not using a Ticker since NextStatusRequestPeriod() is not a fixed time period
-	statsTimer := time.NewTimer(NextStatusRequestPeriod())
+	// The next status request and ssh keep alive times are picked at random,
+	// from a range, to make the resulting traffic less fingerprintable,
+	// especially when then tunnel is otherwise idle.
+	// Note: not using Tickers since these are not fixed time periods.
+
+	nextStatusRequestPeriod := func() time.Duration {
+		return MakeRandomPeriod(
+			PSIPHON_API_STATUS_REQUEST_PERIOD_MIN,
+			PSIPHON_API_STATUS_REQUEST_PERIOD_MAX)
+	}
+	nextSshKeepAlivePeriod := func() time.Duration {
+		return MakeRandomPeriod(
+			TUNNEL_SSH_KEEP_ALIVE_PERIOD_MIN,
+			TUNNEL_SSH_KEEP_ALIVE_PERIOD_MAX)
+	}
+
+	statsTimer := time.NewTimer(nextStatusRequestPeriod())
 	defer statsTimer.Stop()
 
-	sshKeepAliveTicker := time.NewTicker(TUNNEL_SSH_KEEP_ALIVE_PERIOD)
-	defer sshKeepAliveTicker.Stop()
+	sshKeepAliveTimer := time.NewTimer(nextSshKeepAlivePeriod())
+	defer sshKeepAliveTimer.Stop()
 
 	var err error
 	for err == nil {
 		select {
 		case <-statsTimer.C:
 			sendStats(tunnel)
-			statsTimer.Reset(NextStatusRequestPeriod())
+			statsTimer.Reset(nextStatusRequestPeriod())
 
-		case <-sshKeepAliveTicker.C:
-			_, _, err := tunnel.sshClient.SendRequest("keepalive@openssh.com", true, nil)
+		case <-sshKeepAliveTimer.C:
+			// Random padding to frustrate fingerprinting
+			_, _, err := tunnel.sshClient.SendRequest(
+				"keepalive@openssh.com", true,
+				MakeSecureRandomPadding(0, TUNNEL_SSH_KEEP_ALIVE_PAYLOAD_MAX_BYTES))
 			err = fmt.Errorf("ssh keep alive failed: %s", err)
+			sshKeepAliveTimer.Reset(nextSshKeepAlivePeriod())
 
 		case failures := <-tunnel.portForwardFailures:
 			// Note: no mutex on portForwardFailureTotal; only referenced here

+ 33 - 0
psiphon/utils.go

@@ -33,6 +33,7 @@ import (
 	"runtime"
 	"strings"
 	"sync"
+	"time"
 )
 
 // Contains is a helper function that returns true
@@ -77,6 +78,38 @@ func MakeSecureRandomBytes(length int) ([]byte, error) {
 	return randomBytes, nil
 }
 
+// MakeSecureRandomPadding selects a random padding length in the indicated
+// range and returns a random byte array of the selected length.
+// In the unlikely case where an  underlying MakeRandom functions fails,
+// the padding is length 0.
+func MakeSecureRandomPadding(minLength, maxLength int) []byte {
+	var padding []byte
+	paddingSize, err := MakeSecureRandomInt(maxLength - minLength)
+	if err != nil {
+		NoticeAlert("MakeSecureRandomPadding: MakeSecureRandomInt failed")
+		return make([]byte, 0)
+	}
+	paddingSize += minLength
+	padding, err = MakeSecureRandomBytes(paddingSize)
+	if err != nil {
+		NoticeAlert("MakeSecureRandomPadding: MakeSecureRandomBytes failed")
+		return make([]byte, 0)
+	}
+	return padding
+}
+
+// MakeRandomPeriod returns a random duration, within a given range.
+// In the unlikely case where an  underlying MakeRandom functions fails,
+// the period is the minimum.
+func MakeRandomPeriod(min, max time.Duration) (duration time.Duration) {
+	period, err := MakeSecureRandomInt64(max.Nanoseconds() - min.Nanoseconds())
+	if err != nil {
+		NoticeAlert("NextRandomRangePeriod: MakeSecureRandomInt64 failed")
+	}
+	duration = min + time.Duration(period)
+	return
+}
+
 func DecodeCertificate(encodedCertificate string) (certificate *x509.Certificate, err error) {
 	derEncodedCertificate, err := base64.StdEncoding.DecodeString(encodedCertificate)
 	if err != nil {