Jelajahi Sumber

Merge remote-tracking branch 'upstream/master'

Eugene Fryntov 10 tahun lalu
induk
melakukan
a946056ac9

+ 13 - 56
README.md

@@ -21,7 +21,7 @@ Setup
 * Go 1.4 (or higher) is required.
 * Go 1.4 (or higher) is required.
 * This project builds and runs on recent versions of Windows, Linux, and Mac OS X.
 * This project builds and runs on recent versions of Windows, Linux, and Mac OS X.
 * Note that the `psiphon` package is imported using the absolute path `github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon`; without further local configuration, `go` will use this version of the code and not the local copy in the repository.
 * Note that the `psiphon` package is imported using the absolute path `github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon`; without further local configuration, `go` will use this version of the code and not the local copy in the repository.
-* In this repository, run `go build` to make the `psiphon-tunnel-core` binary.
+* In this repository, run `go build` in `ConsoleClient` to make the `ConsoleClient` binary, a console Psiphon client application.
   * Build versioning info may be configured as follows, and passed to `go build` in the `-ldflags` argument:
   * Build versioning info may be configured as follows, and passed to `go build` in the `-ldflags` argument:
 
 
     ```
     ```
@@ -35,66 +35,23 @@ Setup
     "
     "
     ```
     ```
 
 
-* Run `./psiphon-tunnel-core --config psiphon.config` where the config file looks like this:
+* Run `./ConsoleClient --config psiphon.config` where the config file looks like this:
 
 
-<!--BEGIN-SAMPLE-CONFIG-->
-    ```
-    {
-        "PropagationChannelId" : "<placeholder>",
-        "SponsorId" : "<placeholder>",
-        "RemoteServerListUrl" : "",
-        "RemoteServerListSignaturePublicKey" : "",
-        "DataStoreDirectory" : "",
-        "DataStoreTempDirectory" : "",
-        "LogFilename" : "",
-        "LocalHttpProxyPort" : 0,
-        "LocalSocksProxyPort" : 0,
-        "EgressRegion" : "",
-        "TunnelProtocol" : "",
-        "ConnectionWorkerPoolSize" : 10,
-        "TunnelPoolSize" : 1,
-        "PortForwardFailureThreshold" : 10,
-        "UpstreamProxyUrl" : ""
-    }
-    ```
-<!--END-SAMPLE-CONFIG-->
+  <!--BEGIN-SAMPLE-CONFIG-->
+  ```
+  {
+      "PropagationChannelId" : "<placeholder>",
+      "SponsorId" : "<placeholder>",
+      "LocalHttpProxyPort" : 8080,
+      "LocalSocksProxyPort" : 1080
+  }
+  ```
+  <!--END-SAMPLE-CONFIG-->
 
 
+* Config file parameters are [documented here](https://godoc.org/github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon#Config).
 * Replace each `<placeholder>` with a value from your Psiphon network. The Psiphon server-side stack is open source and can be found in our  [Psiphon 3 repository](https://bitbucket.org/psiphon/psiphon-circumvention-system). If you would like to use the Psiphon Inc. network, contact <developer-support@psiphon.ca>.
 * Replace each `<placeholder>` with a value from your Psiphon network. The Psiphon server-side stack is open source and can be found in our  [Psiphon 3 repository](https://bitbucket.org/psiphon/psiphon-circumvention-system). If you would like to use the Psiphon Inc. network, contact <developer-support@psiphon.ca>.
 * The project builds and runs on Android. See the [AndroidLibrary README](AndroidLibrary/README.md) for more information about building the Go component, and the [AndroidApp README](AndroidApp/README.md) for a sample Android app that uses it.
 * The project builds and runs on Android. See the [AndroidLibrary README](AndroidLibrary/README.md) for more information about building the Go component, and the [AndroidApp README](AndroidApp/README.md) for a sample Android app that uses it.
 
 
-Roadmap
---------------------------------------------------------------------------------
-
-### TODO (short-term)
-
-* sometimes fails to promptly detect loss of connection after device sleep
-* requirements for integrating with Windows client
-  * split tunnel support
-  * resumable download of client upgrades
-* Android app
-  * open home pages
-* log noise
-  * "use of closed network connection"
-  * 'Unsolicited response received on idle HTTP channel starting with "H"'
-
-### TODO (future)
-
-* meek enhancements
-  * address this: https://trac.torproject.org/projects/tor/wiki/doc/meek#HowtolooklikebrowserHTTPS (new Go client is equivilent to current Windows client, but differs from current Android client which uses the same Android HTTPS stack used by regular apps)
-* SSH compression
-* preemptive reconnect functionality
-  * unfronted meek almost makes this obsolete, since meek sessions survive underlying
-     HTTP transport socket disconnects. The client could prefer unfronted meek protocol
-     when handshake returns a preemptive_reconnect_lifetime_milliseconds.
-  * could also be accomplished with TunnelPoolSize > 1 and staggering the establishment times
-* implement local traffic stats (e.g., to display bytes sent/received)
-* more formal control interface (w/ event messages)?
-* support upgrading core only
-* try multiple protocols for each server (currently only tries one protocol per server)
-* support a config pushed by the network
-  * server can push preferred/optimized settings; client should prefer over defaults
-  * e.g., etablish worker pool size; tunnel pool size
-
 Licensing
 Licensing
 --------------------------------------------------------------------------------
 --------------------------------------------------------------------------------
 
 

+ 19 - 21
SampleApps/Psibot/app/src/main/java/ca/psiphon/PsiphonTunnel.java

@@ -206,7 +206,6 @@ public class PsiphonTunnel extends Psi.PsiphonProvider.Stub {
                 socksServerAddress,
                 socksServerAddress,
                 udpgwServerAddress,
                 udpgwServerAddress,
                 true);
                 true);
-        mTunFd = null;
         mHostService.onDiagnosticMessage("routing through tunnel");
         mHostService.onDiagnosticMessage("routing through tunnel");
 
 
         // TODO: should double-check tunnel routing; see:
         // TODO: should double-check tunnel routing; see:
@@ -221,10 +220,8 @@ public class PsiphonTunnel extends Psi.PsiphonProvider.Stub {
             }
             }
             mTunFd = null;
             mTunFd = null;
         }
         }
-        if (mRoutingThroughTunnel) {
-            stopTun2Socks();
-            mRoutingThroughTunnel = false;
-        }
+        waitStopTun2Socks();
+        mRoutingThroughTunnel = false;
     }
     }
     
     
     //----------------------------------------------------------------------------------------------
     //----------------------------------------------------------------------------------------------
@@ -294,7 +291,7 @@ public class PsiphonTunnel extends Psi.PsiphonProvider.Stub {
         // Load settings from the raw resource JSON config file and
         // Load settings from the raw resource JSON config file and
         // update as necessary. Then write JSON to disk for the Go client.
         // update as necessary. Then write JSON to disk for the Go client.
         JSONObject json = new JSONObject(mHostService.getPsiphonConfig());
         JSONObject json = new JSONObject(mHostService.getPsiphonConfig());
-
+        
         // On Android, these directories must be set to the app private storage area.
         // On Android, these directories must be set to the app private storage area.
         // The Psiphon library won't be able to use its current working directory
         // The Psiphon library won't be able to use its current working directory
         // and the standard temporary directories do not exist.
         // and the standard temporary directories do not exist.
@@ -307,6 +304,9 @@ public class PsiphonTunnel extends Psi.PsiphonProvider.Stub {
         // Continue to run indefinitely until connected
         // Continue to run indefinitely until connected
         json.put("EstablishTunnelTimeoutSeconds", 0);
         json.put("EstablishTunnelTimeoutSeconds", 0);
 
 
+        // This parameter is for stats reporting
+        json.put("TunnelWholeDevice", isVpnMode ? 1 : 0);
+
         // Enable tunnel auto-reconnect after a threshold number of port
         // Enable tunnel auto-reconnect after a threshold number of port
         // forward failures. By default, this mechanism is disabled in
         // forward failures. By default, this mechanism is disabled in
         // tunnel-core due to the chance of false positives due to
         // tunnel-core due to the chance of false positives due to
@@ -319,12 +319,6 @@ public class PsiphonTunnel extends Psi.PsiphonProvider.Stub {
 
 
         json.put("EmitBytesTransferred", true);
         json.put("EmitBytesTransferred", true);
 
 
-        json.put("UseIndistinguishableTLS", true);
-
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
-            json.put("SystemCACertificateDirectory", "/system/etc/security/cacerts");
-        }
-
         if (mLocalSocksProxyPort != 0) {
         if (mLocalSocksProxyPort != 0) {
             // When mLocalSocksProxyPort is set, tun2socks is already configured
             // When mLocalSocksProxyPort is set, tun2socks is already configured
             // to use that port value. So we force use of the same port.
             // to use that port value. So we force use of the same port.
@@ -332,6 +326,15 @@ public class PsiphonTunnel extends Psi.PsiphonProvider.Stub {
             // has no effect with restartPsiphon(), a full stop() is necessary.
             // has no effect with restartPsiphon(), a full stop() is necessary.
             json.put("LocalSocksProxyPort", mLocalSocksProxyPort);
             json.put("LocalSocksProxyPort", mLocalSocksProxyPort);
         }
         }
+        
+        json.put("UseIndistinguishableTLS", true);
+
+        // TODO: doesn't work due to OpenSSL version incompatibility; try using
+        // the KeyStore API to build a local copy of trusted CAs cert files.
+        //
+        //if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+        //    json.put("SystemCACertificateDirectory", "/system/etc/security/cacerts");
+        //}
 
 
         return json.toString();
         return json.toString();
     }
     }
@@ -348,9 +351,7 @@ public class PsiphonTunnel extends Psi.PsiphonProvider.Stub {
             if (noticeType.equals("Tunnels")) {
             if (noticeType.equals("Tunnels")) {
                 int count = notice.getJSONObject("data").getInt("count");
                 int count = notice.getJSONObject("data").getInt("count");
                 if (count > 0) {
                 if (count > 0) {
-                    if (mTunFd != null) {
-                        routeThroughTunnel();
-                    }
+                    routeThroughTunnel();
                     mHostService.onConnected();
                     mHostService.onConnected();
                 } else {
                 } else {
                     mHostService.onConnecting();
                     mHostService.onConnecting();
@@ -425,12 +426,11 @@ public class PsiphonTunnel extends Psi.PsiphonProvider.Stub {
             final String socksServerAddress,
             final String socksServerAddress,
             final String udpgwServerAddress,
             final String udpgwServerAddress,
             final boolean udpgwTransparentDNS) {
             final boolean udpgwTransparentDNS) {
-        stopTun2Socks();
         mTun2SocksThread = new Thread(new Runnable() {
         mTun2SocksThread = new Thread(new Runnable() {
             @Override
             @Override
             public void run() {
             public void run() {
                 runTun2Socks(
                 runTun2Socks(
-                        vpnInterfaceFileDescriptor.detachFd(),
+                        vpnInterfaceFileDescriptor.getFd(),
                         vpnInterfaceMTU,
                         vpnInterfaceMTU,
                         vpnIpAddress,
                         vpnIpAddress,
                         vpnNetMask,
                         vpnNetMask,
@@ -443,10 +443,10 @@ public class PsiphonTunnel extends Psi.PsiphonProvider.Stub {
         mHostService.onDiagnosticMessage("tun2socks started");
         mHostService.onDiagnosticMessage("tun2socks started");
     }
     }
 
 
-    private void stopTun2Socks() {
+    private void waitStopTun2Socks() {
         if (mTun2SocksThread != null) {
         if (mTun2SocksThread != null) {
-            terminateTun2Socks();
             try {
             try {
+                // Assumes mTunFd has been closed, which signals tun2socks to exit
                 mTun2SocksThread.join();
                 mTun2SocksThread.join();
             } catch (InterruptedException e) {
             } catch (InterruptedException e) {
                 Thread.currentThread().interrupt();
                 Thread.currentThread().interrupt();
@@ -470,8 +470,6 @@ public class PsiphonTunnel extends Psi.PsiphonProvider.Stub {
             String udpgwServerAddress,
             String udpgwServerAddress,
             int udpgwTransparentDNS);
             int udpgwTransparentDNS);
 
 
-    private native static void terminateTun2Socks();
-
     static {
     static {
         System.loadLibrary("tun2socks");
         System.loadLibrary("tun2socks");
     }
     }

+ 182 - 33
psiphon/config.go

@@ -33,7 +33,7 @@ const (
 	CONNECTION_WORKER_POOL_SIZE                  = 10
 	CONNECTION_WORKER_POOL_SIZE                  = 10
 	TUNNEL_POOL_SIZE                             = 1
 	TUNNEL_POOL_SIZE                             = 1
 	TUNNEL_CONNECT_TIMEOUT                       = 15 * time.Second
 	TUNNEL_CONNECT_TIMEOUT                       = 15 * time.Second
-	TUNNEL_OPERATE_SHUTDOWN_TIMEOUT              = 2 * time.Second
+	TUNNEL_OPERATE_SHUTDOWN_TIMEOUT              = 500 * time.Millisecond
 	TUNNEL_PORT_FORWARD_DIAL_TIMEOUT             = 10 * time.Second
 	TUNNEL_PORT_FORWARD_DIAL_TIMEOUT             = 10 * time.Second
 	TUNNEL_SSH_KEEP_ALIVE_PAYLOAD_MAX_BYTES      = 256
 	TUNNEL_SSH_KEEP_ALIVE_PAYLOAD_MAX_BYTES      = 256
 	TUNNEL_SSH_KEEP_ALIVE_PERIOD_MIN             = 60 * time.Second
 	TUNNEL_SSH_KEEP_ALIVE_PERIOD_MIN             = 60 * time.Second
@@ -66,40 +66,189 @@ const (
 // params, these params are int pointers. nil means no param was supplied
 // params, these params are int pointers. nil means no param was supplied
 // so use the default; a non-nil pointer to 0 means no timeout.
 // so use the default; a non-nil pointer to 0 means no timeout.
 
 
+// Config is the Psiphon configuration specified by the application. This
+// configuration controls the behavior of the core tunnel functionality.
 type Config struct {
 type Config struct {
-	LogFilename                         string
-	DataStoreDirectory                  string
-	DataStoreTempDirectory              string
-	PropagationChannelId                string
-	SponsorId                           string
-	RemoteServerListUrl                 string
-	RemoteServerListSignaturePublicKey  string
-	ClientVersion                       string
-	ClientPlatform                      string
-	TunnelWholeDevice                   int
-	EgressRegion                        string
-	TunnelProtocol                      string
-	EstablishTunnelTimeoutSeconds       *int
-	LocalSocksProxyPort                 int
-	LocalHttpProxyPort                  int
-	ConnectionWorkerPoolSize            int
-	TunnelPoolSize                      int
-	PortForwardFailureThreshold         int
-	UpstreamProxyUrl                    string
-	NetworkConnectivityChecker          NetworkConnectivityChecker
-	DeviceBinder                        DeviceBinder
-	DnsServerGetter                     DnsServerGetter
-	TargetServerEntry                   string
-	DisableApi                          bool
-	DisableRemoteServerListFetcher      bool
-	SplitTunnelRoutesUrlFormat          string
+	// LogFilename specifies a file to receive event notices (JSON format)
+	// By default, notices are emitted to stdout.
+	LogFilename string
+
+	// DataStoreDirectory is the directory in which to store the persistent
+	// database, which contains information such as server entries.
+	// By default, current working directory.
+	DataStoreDirectory string
+
+	// DataStoreTempDirectory is the directory in which to store temporary
+	// work files associated with the persistent database.
+	// This parameter is deprecated and may be removed.
+	DataStoreTempDirectory string
+
+	// PropagationChannelId is a string identifier which indicates how the
+	// Psiphon client was distributed. This parameter is required.
+	// This value is supplied by and depends on the Psiphon Network, and is
+	// typically embedded in the client binary.
+	PropagationChannelId string
+
+	// PropagationChannelId is a string identifier which indicates who
+	// is sponsoring this Psiphon client. One purpose of this value is to
+	// determine the home pages for display. This parameter is required.
+	// This value is supplied by and depends on the Psiphon Network, and is
+	// typically embedded in the client binary.
+	SponsorId string
+
+	// RemoteServerListUrl is a URL which specifies a location to fetch
+	// out-of-band server entries. This facility is used when a tunnel cannot
+	// be established to known servers.
+	// This value is supplied by and depends on the Psiphon Network, and is
+	// typically embedded in the client binary.
+	RemoteServerListUrl string
+
+	// RemoteServerListSignaturePublicKey specifies a public key that's
+	// used to authenticate the remote server list payload.
+	// This value is supplied by and depends on the Psiphon Network, and is
+	// typically embedded in the client binary.
+	RemoteServerListSignaturePublicKey string
+
+	// ClientVersion is the client version number that the client reports
+	// to the server. The version number refers to the host client application,
+	// not the core tunnel library. One purpose of this value is to enable
+	// automatic updates.
+	// This value is supplied by and depends on the Psiphon Network, and is
+	// typically embedded in the client binary.
+	ClientVersion string
+
+	// ClientPlatform is the client platform ("Windows", "Android", etc.) that
+	// the client reports to the server.
+	ClientPlatform string
+
+	// TunnelWholeDevice is a flag that is passed through to the handshake
+	// request for stats purposes. Set to 1 when the host application is tunneling
+	// the whole device, 0 otherwise.
+	TunnelWholeDevice int
+
+	// EgressRegion is a ISO 3166-1 alpha-2 country code which indicates which
+	// country to egress from. For the default, "", the best performing server
+	// in any country is selected.
+	EgressRegion string
+
+	// TunnelProtocol indicates which protocol to use. Valid values include:
+	// "SSH", "OSSH", "UNFRONTED-MEEK-OSSH", "FRONTED-MEEK-OSSH". For the default,
+	// "", the best performing protocol is used.
+	TunnelProtocol string
+
+	// EstablishTunnelTimeoutSeconds specifies a time limit after which to halt
+	// the core tunnel controller if no tunnel has been established. By default,
+	// the controller will keep trying indefinitely.
+	EstablishTunnelTimeoutSeconds *int
+
+	// LocalSocksProxyPort specifies a port number for the local SOCKS proxy
+	// running at 127.0.0.1. For the default value, 0, the system selects a free
+	// port (a notice reporting the selected port is emitted).
+	LocalSocksProxyPort int
+
+	// LocalHttpProxyPort specifies a port number for the local HTTP proxy
+	// running at 127.0.0.1. For the default value, 0, the system selects a free
+	// port (a notice reporting the selected port is emitted).
+	LocalHttpProxyPort int
+
+	// ConnectionWorkerPoolSize specifies how many connection attempts to attempt
+	// in parallel. The default, 0, uses CONNECTION_WORKER_POOL_SIZE which is
+	// recommended.
+	ConnectionWorkerPoolSize int
+
+	// TunnelPoolSize specifies how many tunnels to run in parallel. Port forwards
+	// are multiplexed over multiple tunnels. The default, 0, uses TUNNEL_POOL_SIZE
+	// which is recommended.
+	TunnelPoolSize int
+
+	// PortForwardFailureThreshold specifies a threshold number of port forward
+	// failures (failure to connect, or I/O failure) after which the tunnel is
+	// considered to be degraded and a re-establish is launched. This facility
+	// can suffer from false positives, especially when the host client is running
+	// in configuration where domain name resolution is done as part of the port
+	// forward (as opposed to tunneling UDP, for example). The default is 0, off.
+	PortForwardFailureThreshold int
+
+	// UpstreamProxyUrl is a URL specifying an upstream proxy to use for all
+	// outbound connections. The URL should include proxy type and authentication
+	// information, as required. See example URLs here:
+	// https://github.com/Psiphon-Labs/psiphon-tunnel-core/tree/master/psiphon/upstreamproxy
+	UpstreamProxyUrl string
+
+	// NetworkConnectivityChecker is an interface that enables the core tunnel to call
+	// into the host application to check for network connectivity. This parameter is
+	// only applicable to library deployments.
+	NetworkConnectivityChecker NetworkConnectivityChecker
+
+	// DeviceBinder is an interface that enables the core tunnel to call
+	// into the host application to bind sockets to specific devices. This is used
+	// for VPN routing exclusion. This parameter is only applicable to library
+	// deployments.
+	DeviceBinder DeviceBinder
+
+	// DnsServerGetter is an interface that enables the core tunnel to call
+	// into the host application to discover the native network DNS server settings.
+	// This parameter is only applicable to library deployments.
+	DnsServerGetter DnsServerGetter
+
+	// TargetServerEntry is an encoded server entry. When specified, this server entry
+	// is used exclusively and all other known servers are ignored.
+	TargetServerEntry string
+
+	// DisableApi disables Psiphon server API calls including handshake, connected,
+	// status, etc. This is used for special case temporary tunnels (Windows VPN mode).
+	DisableApi bool
+
+	// DisableRemoteServerListFetcher disables fetching remote server lists. This is
+	// used for special case temporary tunnels.
+	DisableRemoteServerListFetcher bool
+
+	// SplitTunnelRoutesUrlFormat is an URL which specifies the location of a routes
+	// file to use for split tunnel mode. The URL must include a placeholder for the
+	// client region to be supplied. Split tunnel mode uses the routes file to classify
+	// port forward destinations as foreign or domestic and does not tunnel domestic
+	// destinations. Split tunnel mode is on when all the SplitTunnel parameters are
+	// supplied.
+	// This value is supplied by and depends on the Psiphon Network, and is
+	// typically embedded in the client binary.
+	SplitTunnelRoutesUrlFormat string
+
+	// SplitTunnelRoutesSignaturePublicKey specifies a public key that's
+	// used to authenticate the split tunnel routes payload.
+	// This value is supplied by and depends on the Psiphon Network, and is
+	// typically embedded in the client binary.
 	SplitTunnelRoutesSignaturePublicKey string
 	SplitTunnelRoutesSignaturePublicKey string
-	SplitTunnelDnsServer                string
-	UpgradeDownloadUrl                  string
-	UpgradeDownloadFilename             string
-	EmitBytesTransferred                bool
-	UseIndistinguishableTLS             bool
-	SystemCACertificateDirectory        string
+
+	// SplitTunnelDnsServer specifies a DNS server to use when resolving port
+	// forward target domain names to IP addresses for classification. The DNS
+	// server must support TCP requests.
+	SplitTunnelDnsServer string
+
+	// UpgradeDownloadUrl specifies a URL from which to download a host client upgrade
+	// file, when one is available. The core tunnel controller provides a resumable
+	// download facility which downloads this resource and emits a notice when complete.
+	// This value is supplied by and depends on the Psiphon Network, and is
+	// typically embedded in the client binary.
+	UpgradeDownloadUrl string
+
+	// UpgradeDownloadFilename is the local target filename for an upgrade download.
+	// This parameter is required when UpgradeDownloadUrl is specified.
+	UpgradeDownloadFilename string
+
+	// EmitBytesTransferred indicates whether to emit periodic notices showing
+	// bytes sent and received.
+	EmitBytesTransferred bool
+
+	// UseIndistinguishableTLS enables use of an alternative TLS stack with a less
+	// distinct fingerprint (ClientHello content) than the stock Go TLS. This
+	// parameter is only supported on platforms built with OpenSSL.
+	UseIndistinguishableTLS bool
+
+	// SystemCACertificateDirectory specifies a directory containing OpenSSL-format
+	// CA certificate files (OpenSSL 1.0.1+ format). When specified, this enables
+	// use of indistinguishable TLS for HTTPS requests that require typical (system
+	// CA) server authentication.
+	SystemCACertificateDirectory string
 }
 }
 
 
 // LoadConfig parses and validates a JSON format Psiphon config JSON
 // LoadConfig parses and validates a JSON format Psiphon config JSON

+ 2 - 2
psiphon/controller.go

@@ -772,7 +772,7 @@ func (controller *Controller) establishCandidateGenerator(impairedProtocols []st
 
 
 loop:
 loop:
 	// Repeat until stopped
 	// Repeat until stopped
-	for {
+	for i := 0; ; i++ {
 
 
 		if !WaitForNetworkConnectivity(
 		if !WaitForNetworkConnectivity(
 			controller.config.NetworkConnectivityChecker,
 			controller.config.NetworkConnectivityChecker,
@@ -783,7 +783,7 @@ loop:
 
 
 		// Send each iterator server entry to the establish workers
 		// Send each iterator server entry to the establish workers
 		startTime := time.Now()
 		startTime := time.Now()
-		for i := 0; ; i++ {
+		for {
 			serverEntry, err := iterator.Next()
 			serverEntry, err := iterator.Next()
 			if err != nil {
 			if err != nil {
 				NoticeAlert("failed to get next candidate: %s", err)
 				NoticeAlert("failed to get next candidate: %s", err)

+ 0 - 1
psiphon/dataStore_alt.go

@@ -416,7 +416,6 @@ func (iterator *ServerEntryIterator) Reset() error {
 		return ContextError(err)
 		return ContextError(err)
 	}
 	}
 
 
-	rand.Seed(int64(time.Now().Nanosecond()))
 	for i := len(serverEntryIds) - 1; i > iterator.shuffleHeadLength-1; i-- {
 	for i := len(serverEntryIds) - 1; i > iterator.shuffleHeadLength-1; i-- {
 		j := rand.Intn(i)
 		j := rand.Intn(i)
 		serverEntryIds[i], serverEntryIds[j] = serverEntryIds[j], serverEntryIds[i]
 		serverEntryIds[i], serverEntryIds[j] = serverEntryIds[j], serverEntryIds[i]

+ 4 - 2
psiphon/httpProxy.go

@@ -65,6 +65,8 @@ type HttpProxy struct {
 	stopListeningBroadcast chan struct{}
 	stopListeningBroadcast chan struct{}
 }
 }
 
 
+var _HTTP_PROXY_TYPE = "HTTP"
+
 // NewHttpProxy initializes and runs a new HTTP proxy server.
 // NewHttpProxy initializes and runs a new HTTP proxy server.
 func NewHttpProxy(
 func NewHttpProxy(
 	config *Config,
 	config *Config,
@@ -225,7 +227,7 @@ func (proxy *HttpProxy) httpConnectHandler(localConn net.Conn, target string) (e
 	if err != nil {
 	if err != nil {
 		return ContextError(err)
 		return ContextError(err)
 	}
 	}
-	Relay(localConn, remoteConn)
+	LocalProxyRelay(_HTTP_PROXY_TYPE, localConn, remoteConn)
 	return nil
 	return nil
 }
 }
 
 
@@ -396,7 +398,7 @@ func (proxy *HttpProxy) serve() {
 	default:
 	default:
 		if err != nil {
 		if err != nil {
 			proxy.tunneler.SignalComponentFailure()
 			proxy.tunneler.SignalComponentFailure()
-			NoticeAlert("%s", ContextError(err))
+			NoticeLocalProxyError(_HTTP_PROXY_TYPE, ContextError(err))
 		}
 		}
 	}
 	}
 	NoticeInfo("HTTP proxy stopped")
 	NoticeInfo("HTTP proxy stopped")

+ 7 - 4
psiphon/net.go

@@ -20,6 +20,7 @@
 package psiphon
 package psiphon
 
 
 import (
 import (
+	"fmt"
 	"io"
 	"io"
 	"net"
 	"net"
 	"reflect"
 	"reflect"
@@ -152,21 +153,23 @@ func (conns *Conns) CloseAll() {
 	conns.conns = make(map[net.Conn]bool)
 	conns.conns = make(map[net.Conn]bool)
 }
 }
 
 
-// Relay sends to remoteConn bytes received from localConn,
+// LocalProxyRelay sends to remoteConn bytes received from localConn,
 // and sends to localConn bytes received from remoteConn.
 // and sends to localConn bytes received from remoteConn.
-func Relay(localConn, remoteConn net.Conn) {
+func LocalProxyRelay(proxyType string, localConn, remoteConn net.Conn) {
 	copyWaitGroup := new(sync.WaitGroup)
 	copyWaitGroup := new(sync.WaitGroup)
 	copyWaitGroup.Add(1)
 	copyWaitGroup.Add(1)
 	go func() {
 	go func() {
 		defer copyWaitGroup.Done()
 		defer copyWaitGroup.Done()
 		_, err := io.Copy(localConn, remoteConn)
 		_, err := io.Copy(localConn, remoteConn)
 		if err != nil {
 		if err != nil {
-			NoticeAlert("Relay failed: %s", ContextError(err))
+			err = fmt.Errorf("Relay failed: %s", ContextError(err))
+			NoticeLocalProxyError(proxyType, err)
 		}
 		}
 	}()
 	}()
 	_, err := io.Copy(remoteConn, localConn)
 	_, err := io.Copy(remoteConn, localConn)
 	if err != nil {
 	if err != nil {
-		NoticeAlert("Relay failed: %s", ContextError(err))
+		err = fmt.Errorf("Relay failed: %s", ContextError(err))
+		NoticeLocalProxyError(proxyType, err)
 	}
 	}
 	copyWaitGroup.Wait()
 	copyWaitGroup.Wait()
 }
 }

+ 72 - 3
psiphon/notice.go

@@ -26,6 +26,7 @@ import (
 	"io"
 	"io"
 	"log"
 	"log"
 	"os"
 	"os"
+	"strings"
 	"sync"
 	"sync"
 	"time"
 	"time"
 )
 )
@@ -104,9 +105,13 @@ func NoticeCandidateServers(region, protocol string, count int) {
 	outputNotice("CandidateServers", false, "region", region, "protocol", protocol, "count", count)
 	outputNotice("CandidateServers", false, "region", region, "protocol", protocol, "count", count)
 }
 }
 
 
-// NoticeAvailableEgressRegions is what regions are available for egress from
+// NoticeAvailableEgressRegions is what regions are available for egress from.
+// Consecutive reports of the same list of regions are suppressed.
 func NoticeAvailableEgressRegions(regions []string) {
 func NoticeAvailableEgressRegions(regions []string) {
-	outputNotice("AvailableEgressRegions", false, "regions", regions)
+	repetitionMessage := strings.Join(regions, "")
+	outputRepetitiveNotice(
+		"AvailableEgressRegions", repetitionMessage, 0,
+		"AvailableEgressRegions", false, "regions", regions)
 }
 }
 
 
 // NoticeConnectingServer is details on a connection attempt
 // NoticeConnectingServer is details on a connection attempt
@@ -183,7 +188,7 @@ func NoticeSplitTunnelRegion(region string) {
 // NoticeUpstreamProxyError reports an error when connecting to an upstream proxy. The
 // NoticeUpstreamProxyError reports an error when connecting to an upstream proxy. The
 // user may have input, for example, an incorrect address or incorrect credentials.
 // user may have input, for example, an incorrect address or incorrect credentials.
 func NoticeUpstreamProxyError(err error) {
 func NoticeUpstreamProxyError(err error) {
-	outputNotice("UpstreamProxyError", true, "message", fmt.Sprintf("%s", err))
+	outputNotice("UpstreamProxyError", true, "message", err.Error())
 }
 }
 
 
 // NoticeClientUpgradeDownloaded indicates that a client upgrade download
 // NoticeClientUpgradeDownloaded indicates that a client upgrade download
@@ -198,6 +203,70 @@ func NoticeBytesTransferred(sent, received int64) {
 	outputNotice("BytesTransferred", false, "sent", sent, "received", received)
 	outputNotice("BytesTransferred", false, "sent", sent, "received", received)
 }
 }
 
 
+// NoticeLocalProxyError reports a local proxy error message. Repetitive
+// errors for a given proxy type are suppressed.
+func NoticeLocalProxyError(proxyType string, err error) {
+
+	// For repeats, only consider the base error message, which is
+	// the root error that repeats (the full error often contains
+	// different specific values, e.g., local port numbers, but
+	// the same repeating root).
+	// Assumes error format of ContextError.
+	repetitionMessage := err.Error()
+	index := strings.LastIndex(repetitionMessage, ": ")
+	if index != -1 {
+		repetitionMessage = repetitionMessage[index+2:]
+	}
+
+	outputRepetitiveNotice(
+		"LocalProxyError"+proxyType, repetitionMessage, 1,
+		"LocalProxyError", false, "message", err.Error())
+}
+
+type repetitiveNoticeState struct {
+	message string
+	repeats int
+}
+
+var repetitiveNoticeMutex sync.Mutex
+var repetitiveNoticeStates = make(map[string]*repetitiveNoticeState)
+
+// outputRepetitiveNotice conditionally outputs a notice. Used for noticies which
+// often repeat in noisy bursts. For a repeat limit of N, the notice is emitted
+// with a "repeats" count on consecutive repeats up to the limit and then suppressed
+// until the repetitionMessage differs.
+func outputRepetitiveNotice(
+	repetitionKey, repetitionMessage string, repeatLimit int,
+	noticeType string, showUser bool, args ...interface{}) {
+
+	repetitiveNoticeMutex.Lock()
+	defer repetitiveNoticeMutex.Unlock()
+
+	state, ok := repetitiveNoticeStates[repetitionKey]
+	if !ok {
+		state = new(repetitiveNoticeState)
+		repetitiveNoticeStates[repetitionKey] = state
+	}
+
+	emit := true
+	if repetitionMessage != state.message {
+		state.message = repetitionMessage
+		state.repeats = 0
+	} else {
+		state.repeats += 1
+		if state.repeats > repeatLimit {
+			emit = false
+		}
+	}
+
+	if emit {
+		if state.repeats > 0 {
+			args = append(args, "repeats", state.repeats)
+		}
+		outputNotice(noticeType, showUser, args...)
+	}
+}
+
 type noticeObject struct {
 type noticeObject struct {
 	NoticeType string          `json:"noticeType"`
 	NoticeType string          `json:"noticeType"`
 	Data       json.RawMessage `json:"data"`
 	Data       json.RawMessage `json:"data"`

+ 4 - 2
psiphon/socksProxy.go

@@ -39,6 +39,8 @@ type SocksProxy struct {
 	stopListeningBroadcast chan struct{}
 	stopListeningBroadcast chan struct{}
 }
 }
 
 
+var _SOCKS_PROXY_TYPE = "SOCKS"
+
 // NewSocksProxy initializes a new SOCKS server. It begins listening for
 // NewSocksProxy initializes a new SOCKS server. It begins listening for
 // connections, starts a goroutine that runs an accept loop, and returns
 // connections, starts a goroutine that runs an accept loop, and returns
 // leaving the accept loop running.
 // leaving the accept loop running.
@@ -89,7 +91,7 @@ func (proxy *SocksProxy) socksConnectionHandler(localConn *socks.SocksConn) (err
 	if err != nil {
 	if err != nil {
 		return ContextError(err)
 		return ContextError(err)
 	}
 	}
-	Relay(localConn, remoteConn)
+	LocalProxyRelay(_SOCKS_PROXY_TYPE, localConn, remoteConn)
 	return nil
 	return nil
 }
 }
 
 
@@ -121,7 +123,7 @@ loop:
 		go func() {
 		go func() {
 			err := proxy.socksConnectionHandler(socksConnection)
 			err := proxy.socksConnectionHandler(socksConnection)
 			if err != nil {
 			if err != nil {
-				NoticeAlert("%s", ContextError(err))
+				NoticeLocalProxyError(_SOCKS_PROXY_TYPE, ContextError(err))
 			}
 			}
 		}()
 		}()
 	}
 	}

+ 2 - 5
psiphon/tunnel.go

@@ -30,7 +30,7 @@ import (
 	"sync"
 	"sync"
 	"time"
 	"time"
 
 
-        "github.com/Psiphon-Inc/goregen"
+	regen "github.com/Psiphon-Inc/goregen"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/transferstats"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/transferstats"
 	"golang.org/x/crypto/ssh"
 	"golang.org/x/crypto/ssh"
 )
 )
@@ -280,10 +280,7 @@ func (conn *TunneledConn) Write(buffer []byte) (n int, err error) {
 
 
 func (conn *TunneledConn) Close() error {
 func (conn *TunneledConn) Close() error {
 	if conn.downstreamConn != nil {
 	if conn.downstreamConn != nil {
-		err := conn.downstreamConn.Close()
-		if err != nil {
-			NoticeAlert("downstreamConn.Close() error: %s", ContextError(err))
-		}
+		conn.downstreamConn.Close()
 	}
 	}
 	return conn.Conn.Close()
 	return conn.Conn.Close()
 }
 }