Просмотр исходного кода

Added "connected reporter" component

* Tunnels no longer make individual "connected" requests.

* Controller runs reporter which makes one connected request
to represent the whole run. This ensures we don't overcount unique
users due to multiple tunnels sending the same last_connected.

* Connected requests are repeated after a long time period. This
ensures we don't undercount unique users when a run exceeds the
minimum unique user calculation time period.

* Moved the status jitter logic to be with other API code.
Rod Hynes 11 лет назад
Родитель
Сommit
17645bd461

+ 5 - 1
ConsoleClient/psiphonClient.go

@@ -113,7 +113,11 @@ func main() {
 
 	// Run Psiphon
 
-	controller := psiphon.NewController(config)
+	controller, err := psiphon.NewController(config)
+	if err != nil {
+		log.Fatalf("error creating controller: %s", err)
+	}
+
 	controllerStopSignal := make(chan struct{}, 1)
 	shutdownBroadcast := make(chan struct{})
 	controllerWaitGroup := new(sync.WaitGroup)

+ 81 - 13
psiphon/controller.go

@@ -35,6 +35,7 @@ import (
 // route traffic through the tunnels.
 type Controller struct {
 	config                    *Config
+	sessionId                 string
 	componentFailureSignal    chan struct{}
 	shutdownBroadcast         chan struct{}
 	runWaitGroup              *sync.WaitGroup
@@ -43,6 +44,7 @@ type Controller struct {
 	tunnelMutex               sync.Mutex
 	tunnels                   []*Tunnel
 	nextTunnel                int
+	startedConnectedReporter  bool
 	isEstablishing            bool
 	establishWaitGroup        *sync.WaitGroup
 	stopEstablishingBroadcast chan struct{}
@@ -52,9 +54,18 @@ type Controller struct {
 }
 
 // NewController initializes a new controller.
-func NewController(config *Config) (controller *Controller) {
+func NewController(config *Config) (controller *Controller, err error) {
+
+	// Generate a session ID for the Psiphon server API. This session ID is
+	// used across all tunnels established by the controller.
+	sessionId, err := MakeSessionId()
+	if err != nil {
+		return nil, ContextError(err)
+	}
+
 	return &Controller{
-		config: config,
+		config:    config,
+		sessionId: sessionId,
 		// componentFailureSignal receives a signal from a component (including socks and
 		// http local proxies) if they unexpectedly fail. Senders should not block.
 		// A buffer allows at least one stop signal to be sent before there is a receiver.
@@ -63,13 +74,14 @@ func NewController(config *Config) (controller *Controller) {
 		runWaitGroup:           new(sync.WaitGroup),
 		// establishedTunnels and failedTunnels buffer sizes are large enough to
 		// receive full pools of tunnels without blocking. Senders should not block.
-		establishedTunnels:      make(chan *Tunnel, config.TunnelPoolSize),
-		failedTunnels:           make(chan *Tunnel, config.TunnelPoolSize),
-		tunnels:                 make([]*Tunnel, 0),
-		isEstablishing:          false,
-		establishPendingConns:   new(Conns),
-		fetchRemotePendingConns: new(Conns),
-	}
+		establishedTunnels:       make(chan *Tunnel, config.TunnelPoolSize),
+		failedTunnels:            make(chan *Tunnel, config.TunnelPoolSize),
+		tunnels:                  make([]*Tunnel, 0),
+		startedConnectedReporter: false,
+		isEstablishing:           false,
+		establishPendingConns:    new(Conns),
+		fetchRemotePendingConns:  new(Conns),
+	}, nil
 }
 
 // Run executes the controller. It launches components and then monitors
@@ -77,6 +89,7 @@ func NewController(config *Config) (controller *Controller) {
 // controller.
 // The components include:
 // - the periodic remote server list fetcher
+// - the connected reporter
 // - the tunnel manager
 // - a local SOCKS proxy that port forwards through the pool of tunnels
 // - a local HTTP proxy that port forwards through the pool of tunnels
@@ -97,6 +110,8 @@ func (controller *Controller) Run(shutdownBroadcast <-chan struct{}) {
 	}
 	defer httpProxy.Close()
 
+	/// Note: the connected reporter isn't started until a tunnel is established
+
 	controller.runWaitGroup.Add(2)
 	go controller.remoteServerListFetcher()
 	go controller.runTunnels()
@@ -141,9 +156,9 @@ loop:
 		var duration time.Duration
 		if err != nil {
 			Notice(NOTICE_ALERT, "failed to fetch remote server list: %s", err)
-			duration = FETCH_REMOTE_SERVER_LIST_RETRY_TIMEOUT
+			duration = FETCH_REMOTE_SERVER_LIST_RETRY_PERIOD
 		} else {
-			duration = FETCH_REMOTE_SERVER_LIST_STALE_TIMEOUT
+			duration = FETCH_REMOTE_SERVER_LIST_STALE_PERIOD
 		}
 		timeout := time.After(duration)
 		select {
@@ -157,6 +172,49 @@ loop:
 	Notice(NOTICE_INFO, "exiting remote server list fetcher")
 }
 
+// connectedReporter sends periodic "connected" requests to the Psiphon API.
+// These requests are for server-side unique user stats calculation. See the
+// comment in DoConnectedRequest for a description of the request mechanism.
+// To ensure we don't over- or under-count unique users, only one connected
+// request is made across all simultaneous multi-tunnels; and the connected
+// request is repeated periodically.
+func (controller *Controller) connectedReporter() {
+	defer controller.runWaitGroup.Done()
+loop:
+	for {
+
+		// Pick any active tunnel and make the next connected request. No error
+		// is logged if there's no active tunnel, as that's not an unexpected condition.
+		reported := false
+		tunnel := controller.getNextActiveTunnel()
+		if tunnel != nil {
+			err := tunnel.session.DoConnectedRequest()
+			if err == nil {
+				reported = true
+			} else {
+				Notice(NOTICE_ALERT, "failed to make connected request: %s", err)
+			}
+		}
+
+		// Schedule the next connected request and wait.
+		var duration time.Duration
+		if reported {
+			duration = PSIPHON_API_CONNECTED_REQUEST_PERIOD
+		} else {
+			duration = PSIPHON_API_CONNECTED_REQUEST_RETRY_PERIOD
+		}
+		timeout := time.After(duration)
+		select {
+		case <-timeout:
+			// Make another connected request
+		case <-controller.shutdownBroadcast:
+			break loop
+		}
+	}
+
+	Notice(NOTICE_INFO, "exiting connected reporter")
+}
+
 // runTunnels is the controller tunnel management main loop. It starts and stops
 // establishing tunnels based on the target tunnel pool size and the current size
 // of the pool. Tunnels are established asynchronously using worker goroutines.
@@ -190,8 +248,8 @@ loop:
 		case failedTunnel := <-controller.failedTunnels:
 			Notice(NOTICE_ALERT, "tunnel failed: %s", failedTunnel.serverEntry.IpAddress)
 			controller.terminateTunnel(failedTunnel)
-			// Note: only this goroutine may call startEstablishing/stopEstablishing and access
-			// isEstablishing.
+			// Concurrency note: only this goroutine may call startEstablishing/stopEstablishing
+			// and access isEstablishing.
 			if !controller.isEstablishing {
 				controller.startEstablishing()
 			}
@@ -209,6 +267,15 @@ loop:
 				controller.stopEstablishing()
 			}
 
+			// Start the connected reporter after the first tunnel is established.
+			// Concurrency note: only this goroutine may access startedConnectedReporter.
+			// isEstablishing.
+			if !controller.startedConnectedReporter {
+				controller.startedConnectedReporter = true
+				controller.runWaitGroup.Add(1)
+				go controller.connectedReporter()
+			}
+
 		case <-controller.shutdownBroadcast:
 			break loop
 		}
@@ -486,6 +553,7 @@ loop:
 
 		tunnel, err := EstablishTunnel(
 			controller.config,
+			controller.sessionId,
 			controller.establishPendingConns,
 			serverEntry,
 			controller) // TunnelOwner

+ 22 - 18
psiphon/defaults.go

@@ -24,22 +24,26 @@ import (
 )
 
 const (
-	VERSION                                  = "0.0.5"
-	DATA_STORE_FILENAME                      = "psiphon.db"
-	CONNECTION_WORKER_POOL_SIZE              = 10
-	TUNNEL_POOL_SIZE                         = 1
-	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
-	ESTABLISH_TUNNEL_TIMEOUT                 = 60 * time.Second
-	ESTABLISH_TUNNEL_PAUSE_PERIOD            = 10 * time.Second
-	PORT_FORWARD_FAILURE_THRESHOLD           = 10
-	HTTP_PROXY_ORIGIN_SERVER_TIMEOUT         = 15 * time.Second
-	HTTP_PROXY_MAX_IDLE_CONNECTIONS_PER_HOST = 50
-	FETCH_REMOTE_SERVER_LIST_TIMEOUT         = 10 * time.Second
-	FETCH_REMOTE_SERVER_LIST_RETRY_TIMEOUT   = 5 * time.Second
-	FETCH_REMOTE_SERVER_LIST_STALE_TIMEOUT   = 6 * time.Hour
-	PSIPHON_API_CLIENT_SESSION_ID_LENGTH     = 16
-	PSIPHON_API_SERVER_TIMEOUT               = 20 * time.Second
+	VERSION                                    = "0.0.5"
+	DATA_STORE_FILENAME                        = "psiphon.db"
+	CONNECTION_WORKER_POOL_SIZE                = 10
+	TUNNEL_POOL_SIZE                           = 1
+	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
+	ESTABLISH_TUNNEL_TIMEOUT                   = 60 * time.Second
+	ESTABLISH_TUNNEL_PAUSE_PERIOD              = 10 * time.Second
+	PORT_FORWARD_FAILURE_THRESHOLD             = 10
+	HTTP_PROXY_ORIGIN_SERVER_TIMEOUT           = 15 * time.Second
+	HTTP_PROXY_MAX_IDLE_CONNECTIONS_PER_HOST   = 50
+	FETCH_REMOTE_SERVER_LIST_TIMEOUT           = 10 * time.Second
+	FETCH_REMOTE_SERVER_LIST_RETRY_PERIOD      = 5 * time.Second
+	FETCH_REMOTE_SERVER_LIST_STALE_PERIOD      = 6 * time.Hour
+	PSIPHON_API_CLIENT_SESSION_ID_LENGTH       = 16
+	PSIPHON_API_SERVER_TIMEOUT                 = 20 * time.Second
+	PSIPHON_API_STATUS_REQUEST_PERIOD_MIN      = 5 * time.Minute
+	PSIPHON_API_STATUS_REQUEST_PERIOD_MAX      = 10 * time.Minute
+	PSIPHON_API_CONNECTED_REQUEST_PERIOD       = 24 * time.Hour
+	PSIPHON_API_CONNECTED_REQUEST_RETRY_PERIOD = 5 * time.Second
 )

+ 1 - 1
psiphon/obfuscatedSshConn.go

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2014, Psiphon Inc.
+ * Copyright (c) 2015, Psiphon Inc.
  * All rights reserved.
  *
  * This program is free software: you can redistribute it and/or modify

+ 1 - 1
psiphon/obfuscator.go

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2014, Psiphon Inc.
+ * Copyright (c) 2015, Psiphon Inc.
  * All rights reserved.
  *
  * This program is free software: you can redistribute it and/or modify

+ 67 - 57
psiphon/serverApi.go

@@ -30,6 +30,7 @@ import (
 	"net"
 	"net/http"
 	"strconv"
+	"time"
 )
 
 // Session is a utility struct which holds all of the data associated
@@ -45,9 +46,11 @@ type Session struct {
 }
 
 // MakeSessionId creates a new session ID. Making the session ID is not done
-// in NewSession as the transport needs to send the ID in the SSH credentials
-// before the tunnel is established; and NewSession performs a handshake on
-// an established tunnel.
+// in NewSession because:
+// (1) the transport needs to send the ID in the SSH credentials before the tunnel
+//     is established and NewSession performs a handshake on an established tunnel.
+// (2) the same session ID is used across multi-tunnel controller runs, where each
+//     tunnel has its own Session instance.
 func MakeSessionId() (sessionId string, err error) {
 	randomId, err := MakeSecureRandomBytes(PSIPHON_API_CLIENT_SESSION_ID_LENGTH)
 	if err != nil {
@@ -56,10 +59,10 @@ func MakeSessionId() (sessionId string, err error) {
 	return hex.EncodeToString(randomId), nil
 }
 
-// NewSession makes tunnelled handshake and connected requests to the
+// NewSession makes the tunnelled handshake request to the
 // Psiphon server and returns a Session struct, initialized with the
 // session ID, for use with subsequent Psiphon server API requests (e.g.,
-// periodic status requests).
+// periodic connected and status requests).
 func NewSession(config *Config, tunnel *Tunnel, sessionId string) (session *Session, err error) {
 
 	psiphonHttpsClient, err := makePsiphonHttpsClient(tunnel)
@@ -72,22 +75,52 @@ func NewSession(config *Config, tunnel *Tunnel, sessionId string) (session *Sess
 		psiphonHttpsClient: psiphonHttpsClient,
 		statsServerId:      tunnel.serverEntry.IpAddress,
 	}
-	// Sending two seperate requests is a legacy from when the handshake was
-	// performed before a tunnel was established and the connect was performed
-	// within the established tunnel. Here we perform both requests back-to-back
-	// inside the tunnel.
+
 	err = session.doHandshakeRequest()
 	if err != nil {
 		return nil, ContextError(err)
 	}
-	err = session.doConnectedRequest()
-	if err != nil {
-		return nil, ContextError(err)
-	}
 
 	return session, nil
 }
 
+// DoConnectedRequest performs the connected API request. This request is
+// used for statistics. The server returns a last_connected token for
+// the client to store and send next time it connects. This token is
+// a timestamp (using the server clock, and should be rounded to the
+// nearest hour) which is used to determine when a connection represents
+// a unique user for a time period.
+func (session *Session) DoConnectedRequest() error {
+	const DATA_STORE_LAST_CONNECTED_KEY = "lastConnected"
+	lastConnected, err := GetKeyValue(DATA_STORE_LAST_CONNECTED_KEY)
+	if err != nil {
+		return ContextError(err)
+	}
+	if lastConnected == "" {
+		lastConnected = "None"
+	}
+	url := session.buildRequestUrl(
+		"connected",
+		&ExtraParam{"session_id", session.sessionId},
+		&ExtraParam{"last_connected", lastConnected})
+	responseBody, err := session.doGetRequest(url)
+	if err != nil {
+		return ContextError(err)
+	}
+	var response struct {
+		connectedTimestamp string `json:connected_timestamp`
+	}
+	err = json.Unmarshal(responseBody, &response)
+	if err != nil {
+		return ContextError(err)
+	}
+	err = SetKeyValue(DATA_STORE_LAST_CONNECTED_KEY, response.connectedTimestamp)
+	if err != nil {
+		return ContextError(err)
+	}
+	return nil
+}
+
 // ServerID provides a unique identifier for the server the session connects to.
 // This ID is consistent between multiple sessions/tunnels connected to that server.
 func (session *Session) StatsServerID() string {
@@ -99,23 +132,37 @@ 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 {
+		Notice(NOTICE_ALERT, "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.
-// final should be true if this is the last such request before disconnecting.
-func (session *Session) DoStatusRequest(statsPayload json.Marshaler, final bool) error {
+func (session *Session) DoStatusRequest(statsPayload json.Marshaler) error {
 	statsPayloadJSON, err := json.Marshal(statsPayload)
 	if err != nil {
 		return ContextError(err)
 	}
 
-	connected := "1"
-	if final {
-		connected = "0"
-	}
+	// "connected" is a legacy parameter. This client does not report when
+	// it has disconnected.
 
 	url := session.buildRequestUrl(
 		"status",
 		&ExtraParam{"session_id", session.sessionId},
-		&ExtraParam{"connected", connected})
+		&ExtraParam{"connected", "1"})
 
 	err = session.doPostRequest(url, "application/json", bytes.NewReader(statsPayloadJSON))
 	if err != nil {
@@ -199,43 +246,6 @@ func (session *Session) doHandshakeRequest() error {
 	return nil
 }
 
-// doConnectedRequest performs the connected API request. This request is
-// used for statistics. The server returns a last_connected token for
-// the client to store and send next time it connects. This token is
-// a timestamp (using the server clock, and should be rounded to the
-// nearest hour) which is used to determine when a new connection is
-// a unique user for a time period.
-func (session *Session) doConnectedRequest() error {
-	const DATA_STORE_LAST_CONNECTED_KEY = "lastConnected"
-	lastConnected, err := GetKeyValue(DATA_STORE_LAST_CONNECTED_KEY)
-	if err != nil {
-		return ContextError(err)
-	}
-	if lastConnected == "" {
-		lastConnected = "None"
-	}
-	url := session.buildRequestUrl(
-		"connected",
-		&ExtraParam{"session_id", session.sessionId},
-		&ExtraParam{"last_connected", lastConnected})
-	responseBody, err := session.doGetRequest(url)
-	if err != nil {
-		return ContextError(err)
-	}
-	var response struct {
-		connectedTimestamp string `json:connected_timestamp`
-	}
-	err = json.Unmarshal(responseBody, &response)
-	if err != nil {
-		return ContextError(err)
-	}
-	err = SetKeyValue(DATA_STORE_LAST_CONNECTED_KEY, response.connectedTimestamp)
-	if err != nil {
-		return ContextError(err)
-	}
-	return nil
-}
-
 // doGetRequest makes a tunneled HTTPS request and returns the response body.
 func (session *Session) doGetRequest(requestUrl string) (responseBody []byte, err error) {
 	response, err := session.psiphonHttpsClient.Get(requestUrl)

+ 0 - 18
psiphon/stats_collector.go

@@ -23,7 +23,6 @@ import (
 	"encoding/base64"
 	"encoding/json"
 	"sync"
-	"time"
 )
 
 // TODO: Stats for a server are only removed when they are sent in a status
@@ -102,23 +101,6 @@ func recordStat(stat *statsUpdate) {
 	//fmt.Println("server:", stat.serverID, "host:", stat.hostname, "sent:", storedHostStats.numBytesSent, "received:", storedHostStats.numBytesReceived)
 }
 
-// NextSendPeriod returns the amount of time that should be waited before the
-// next time stats are sent.
-func NextSendPeriod() (duration time.Duration) {
-	defaultStatsSendDuration := 5 * 60 * 1000 // 5 minutes in millis
-
-	// We include a random component to make the stats send less fingerprintable.
-	jitter, err := MakeSecureRandomInt(defaultStatsSendDuration)
-
-	// In case of error we're just going to use zero jitter.
-	if err != nil {
-		Notice(NOTICE_ALERT, "stats.NextSendPeriod: MakeSecureRandomInt failed")
-	}
-
-	duration = time.Duration(defaultStatsSendDuration+jitter) * time.Millisecond
-	return
-}
-
 // Implement the json.Marshaler interface
 func (ss serverStats) MarshalJSON() ([]byte, error) {
 	out := make(map[string]interface{})

+ 4 - 3
psiphon/stats_test.go

@@ -83,11 +83,12 @@ func makeStatsDialer(serverID string, regexps *Regexps) func(network, addr strin
 	}
 }
 
-func (suite *StatsTestSuite) Test_NextSendPeriod() {
-	res1 := NextSendPeriod()
+// TODO: move out of stats test suite
+func (suite *StatsTestSuite) Test_NextStatusRequestPeriod() {
+	res1 := NextStatusRequestPeriod()
 	suite.True(res1 > time.Duration(0), "duration should not be zero")
 
-	res2 := NextSendPeriod()
+	res2 := NextStatusRequestPeriod()
 	suite.NotEqual(res1, res2, "duration should have randomness difference between calls")
 }
 

+ 17 - 19
psiphon/tunnel.go

@@ -95,6 +95,7 @@ type Tunnel struct {
 // server capabilities is used.
 func EstablishTunnel(
 	config *Config,
+	sessionId string,
 	pendingConns *Conns,
 	serverEntry *ServerEntry,
 	tunnelOwner TunnelOwner) (tunnel *Tunnel, err error) {
@@ -106,14 +107,6 @@ func EstablishTunnel(
 	Notice(NOTICE_INFO, "connecting to %s in region %s using %s",
 		serverEntry.IpAddress, serverEntry.Region, selectedProtocol)
 
-	// Generate a session Id for the Psiphon server API. This is generated now so
-	// that it can be sent with the SSH password payload, which helps the server
-	// associate client geo location, used in server API stats, with the session ID.
-	sessionId, err := MakeSessionId()
-	if err != nil {
-		return nil, ContextError(err)
-	}
-
 	// Build transport layers and establish SSH connection
 	conn, closedSignal, sshClient, err := dialSsh(
 		config, pendingConns, serverEntry, selectedProtocol, sessionId)
@@ -143,7 +136,12 @@ func EstablishTunnel(
 		// of failure reports without blocking. Senders can drop failures without blocking.
 		portForwardFailures: make(chan int, config.PortForwardFailureThreshold)}
 
-	// Create a new Psiphon API session for this tunnel
+	// Create a new Psiphon API session for this tunnel. This includes performing
+	// a handshake request. If the handshake fails, this establishment fails.
+	//
+	// TODO: as long as the servers are not enforcing that a client perform a handshake,
+	// proceed with this tunnel as long as at least one previous handhake succeeded?
+	//
 	Notice(NOTICE_INFO, "starting session for %s", tunnel.serverEntry.IpAddress)
 	tunnel.session, err = NewSession(config, tunnel, sessionId)
 	if err != nil {
@@ -392,8 +390,8 @@ func dialSsh(
 	return conn, closedSignal, sshClient, nil
 }
 
-// operateTunnel periodically sends stats updates to the Psiphon API and
-// monitors the tunnel for failures:
+// operateTunnel periodically sends status requests (traffic stats updates updates)
+// to the Psiphon API; and monitors the tunnel for failures:
 //
 // 1. Overall tunnel failure: the tunnel sends a signal to the ClosedSignal
 // channel on keep-alive failure and other transport I/O errors. In case
@@ -419,8 +417,8 @@ func dialSsh(
 func (tunnel *Tunnel) operateTunnel(config *Config, tunnelOwner TunnelOwner) {
 	defer tunnel.operateWaitGroup.Done()
 
-	// Note: not using a Ticker since NextSendPeriod() is not a fixed time period
-	statsTimer := time.NewTimer(NextSendPeriod())
+	// Note: not using a Ticker since NextStatusRequestPeriod() is not a fixed time period
+	statsTimer := time.NewTimer(NextStatusRequestPeriod())
 	defer statsTimer.Stop()
 
 	sshKeepAliveTicker := time.NewTicker(TUNNEL_SSH_KEEP_ALIVE_PERIOD)
@@ -430,8 +428,8 @@ func (tunnel *Tunnel) operateTunnel(config *Config, tunnelOwner TunnelOwner) {
 	for err == nil {
 		select {
 		case <-statsTimer.C:
-			sendStats(tunnel, false)
-			statsTimer.Reset(NextSendPeriod())
+			sendStats(tunnel)
+			statsTimer.Reset(NextStatusRequestPeriod())
 
 		case <-sshKeepAliveTicker.C:
 			_, _, err := tunnel.sshClient.SendRequest("keepalive@openssh.com", true, nil)
@@ -451,8 +449,8 @@ func (tunnel *Tunnel) operateTunnel(config *Config, tunnelOwner TunnelOwner) {
 			err = errors.New("tunnel closed unexpectedly")
 
 		case <-tunnel.shutdownOperateBroadcast:
-			// Send final stats
-			sendStats(tunnel, true)
+			// Attempt to send any remaining stats
+			sendStats(tunnel)
 			Notice(NOTICE_INFO, "shutdown operate tunnel")
 			return
 		}
@@ -465,10 +463,10 @@ func (tunnel *Tunnel) operateTunnel(config *Config, tunnelOwner TunnelOwner) {
 }
 
 // sendStats is a helper for sending session stats to the server.
-func sendStats(tunnel *Tunnel, final bool) {
+func sendStats(tunnel *Tunnel) {
 	payload := GetForServer(tunnel.serverEntry.IpAddress)
 	if payload != nil {
-		err := tunnel.session.DoStatusRequest(payload, final)
+		err := tunnel.session.DoStatusRequest(payload)
 		if err != nil {
 			Notice(NOTICE_ALERT, "DoStatusRequest failed for %s: %s", tunnel.serverEntry.IpAddress, err)
 			PutBack(tunnel.serverEntry.IpAddress, payload)

+ 10 - 3
psiphon/utils.go

@@ -41,13 +41,20 @@ func Contains(list []string, target string) bool {
 }
 
 // MakeSecureRandomInt is a helper function that wraps
-// crypto/rand.Int, which returns a uniform random value in [0, max).
+// MakeSecureRandomInt64.
 func MakeSecureRandomInt(max int) (int, error) {
-	randomInt, err := rand.Int(rand.Reader, big.NewInt(int64(max)))
+	randomInt, err := MakeSecureRandomInt64(int64(max))
+	return int(randomInt), err
+}
+
+// MakeSecureRandomInt64 is a helper function that wraps
+// crypto/rand.Int, which returns a uniform random value in [0, max).
+func MakeSecureRandomInt64(max int64) (int64, error) {
+	randomInt, err := rand.Int(rand.Reader, big.NewInt(max))
 	if err != nil {
 		return 0, ContextError(err)
 	}
-	return int(randomInt.Uint64()), nil
+	return randomInt.Int64(), nil
 }
 
 // MakeSecureRandomBytes is a helper function that wraps