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

Replace PrioritizeTunnelProtocols with InitialLimitTunnelProtocols

- InitialLimitTunnelProtocols is strict, like LimitTunnelProtocols,
  which achieves the desired effect in most use cases: attempting
  _only_ specific protocols for a temporary phase.

- LimitTunnelProtocols applies only after the InitialLimitTunnelProtocols
  phase is complete, creating two distinct phases.

- PrioritizeTunnelProtocols parameters are retained in ClientParameters
  allowing distribution to older clients.

- Refactored/simplified protocol selection, candidate count and
  available region reporting.

- Fix: [Initial]LimitTunnelProtocols coin flip is performed once and
  the result is used/applied consistently within a single
  establishment. Previously, coin flips were made each time
  LimitTunnelProtocols was referenced.
Rod Hynes 7 лет назад
Родитель
Сommit
cd13d9071d

+ 1 - 3
ConsoleClient/main.go

@@ -275,9 +275,7 @@ func main() {
 			}
 		}()
 
-		limitTunnelProtocols := config.GetClientParameters().TunnelProtocols(parameters.LimitTunnelProtocols)
-		if psiphon.CountServerEntries(
-			config.UseUpstreamProxy(), config.EgressRegion, limitTunnelProtocols) == 0 {
+		if psiphon.CountServerEntries(config.UseUpstreamProxy(), config.EgressRegion, nil) == 0 {
 			embeddedServerListWaitGroup.Wait()
 		} else {
 			defer embeddedServerListWaitGroup.Wait()

+ 12 - 7
psiphon/common/parameters/clientParameters.go

@@ -85,6 +85,9 @@ const (
 	PrioritizeTunnelProtocolsProbability       = "PrioritizeTunnelProtocolsProbability"
 	PrioritizeTunnelProtocols                  = "PrioritizeTunnelProtocols"
 	PrioritizeTunnelProtocolsCandidateCount    = "PrioritizeTunnelProtocolsCandidateCount"
+	InitialLimitTunnelProtocolsProbability     = "InitialLimitTunnelProtocolsProbability"
+	InitialLimitTunnelProtocols                = "InitialLimitTunnelProtocols"
+	InitialLimitTunnelProtocolsCandidateCount  = "InitialLimitTunnelProtocolsCandidateCount"
 	LimitTunnelProtocolsProbability            = "LimitTunnelProtocolsProbability"
 	LimitTunnelProtocols                       = "LimitTunnelProtocols"
 	LimitTLSProfilesProbability                = "LimitTLSProfilesProbability"
@@ -209,16 +212,18 @@ var defaultClientParameters = map[string]struct {
 	TunnelPortForwardDialTimeout:             {value: 10 * time.Second, minimum: 1 * time.Millisecond, flags: useNetworkLatencyMultiplier},
 	TunnelRateLimits:                         {value: common.RateLimits{}},
 
-	// PrioritizeTunnelProtocolsCandidateCount should be set to at least
-	// ConnectionWorkerPoolSize in order to use only priotitized protocols in
-	// the first establishment round. Even then, this will only happen if the
-	// client has sufficient candidates supporting the prioritized protocols.
-
+	// PrioritizeTunnelProtocols parameters are obsoleted by InitialLimitTunnelProtocols.
+	// TODO: remove once no longer required for older clients.
 	PrioritizeTunnelProtocolsProbability:    {value: 1.0, minimum: 0.0},
 	PrioritizeTunnelProtocols:               {value: protocol.TunnelProtocols{}},
 	PrioritizeTunnelProtocolsCandidateCount: {value: 10, minimum: 0},
-	LimitTunnelProtocolsProbability:         {value: 1.0, minimum: 0.0},
-	LimitTunnelProtocols:                    {value: protocol.TunnelProtocols{}},
+
+	InitialLimitTunnelProtocolsProbability:    {value: 1.0, minimum: 0.0},
+	InitialLimitTunnelProtocols:               {value: protocol.TunnelProtocols{}},
+	InitialLimitTunnelProtocolsCandidateCount: {value: 0, minimum: 0},
+
+	LimitTunnelProtocolsProbability: {value: 1.0, minimum: 0.0},
+	LimitTunnelProtocols:            {value: protocol.TunnelProtocols{}},
 
 	LimitTLSProfilesProbability: {value: 1.0, minimum: 0.0},
 	LimitTLSProfiles:            {value: protocol.TLSProfiles{}},

+ 11 - 11
psiphon/common/parameters/clientParameters_test.go

@@ -125,10 +125,10 @@ func TestOverrides(t *testing.T) {
 	applyParameters[ConnectionWorkerPoolSize] = newConnectionWorkerPoolSize
 
 	// Above minimum, should apply
-	defaultPrioritizeTunnelProtocolsCandidateCount := defaultClientParameters[PrioritizeTunnelProtocolsCandidateCount].value.(int)
-	minimumPrioritizeTunnelProtocolsCandidateCount := defaultClientParameters[PrioritizeTunnelProtocolsCandidateCount].minimum.(int)
-	newPrioritizeTunnelProtocolsCandidateCount := minimumPrioritizeTunnelProtocolsCandidateCount + 1
-	applyParameters[PrioritizeTunnelProtocolsCandidateCount] = newPrioritizeTunnelProtocolsCandidateCount
+	defaultInitialLimitTunnelProtocolsCandidateCount := defaultClientParameters[InitialLimitTunnelProtocolsCandidateCount].value.(int)
+	minimumInitialLimitTunnelProtocolsCandidateCount := defaultClientParameters[InitialLimitTunnelProtocolsCandidateCount].minimum.(int)
+	newInitialLimitTunnelProtocolsCandidateCount := minimumInitialLimitTunnelProtocolsCandidateCount + 1
+	applyParameters[InitialLimitTunnelProtocolsCandidateCount] = newInitialLimitTunnelProtocolsCandidateCount
 
 	p, err := NewClientParameters(nil)
 	if err != nil {
@@ -151,12 +151,12 @@ func TestOverrides(t *testing.T) {
 		t.Fatalf("GetInt returned unexpected ConnectionWorkerPoolSize: %d", v)
 	}
 
-	v = p.Get().Int(PrioritizeTunnelProtocolsCandidateCount)
-	if v != defaultPrioritizeTunnelProtocolsCandidateCount {
-		t.Fatalf("GetInt returned unexpected PrioritizeTunnelProtocolsCandidateCount: %d", v)
+	v = p.Get().Int(InitialLimitTunnelProtocolsCandidateCount)
+	if v != defaultInitialLimitTunnelProtocolsCandidateCount {
+		t.Fatalf("GetInt returned unexpected InitialLimitTunnelProtocolsCandidateCount: %d", v)
 	}
 
-	// Skip on error; should skip ConnectionWorkerPoolSize and apply PrioritizeTunnelProtocolsCandidateCount
+	// Skip on error; should skip ConnectionWorkerPoolSize and apply InitialLimitTunnelProtocolsCandidateCount
 
 	counts, err := p.Set(tag, true, applyParameters)
 	if err != nil {
@@ -172,9 +172,9 @@ func TestOverrides(t *testing.T) {
 		t.Fatalf("GetInt returned unexpected ConnectionWorkerPoolSize: %d", v)
 	}
 
-	v = p.Get().Int(PrioritizeTunnelProtocolsCandidateCount)
-	if v != newPrioritizeTunnelProtocolsCandidateCount {
-		t.Fatalf("GetInt returned unexpected PrioritizeTunnelProtocolsCandidateCount: %d", v)
+	v = p.Get().Int(InitialLimitTunnelProtocolsCandidateCount)
+	if v != newInitialLimitTunnelProtocolsCandidateCount {
+		t.Fatalf("GetInt returned unexpected InitialLimitTunnelProtocolsCandidateCount: %d", v)
 	}
 }
 

+ 28 - 9
psiphon/config.go

@@ -127,17 +127,31 @@ type Config struct {
 	// TunnelProtocol indicates which protocol to use. For the default, "",
 	// all protocols are used.
 	//
-	// Deprecated: Use TunnelProtocols. When TunnelProtocols is not nil, this
-	// parameter is ignored.
+	// Deprecated: Use LimitTunnelProtocols. When LimitTunnelProtocols is not
+	// nil, this parameter is ignored.
 	TunnelProtocol string
 
-	// TunnelProtocols indicates which protocols to use. Valid values include:
-	// "SSH", "OSSH", "UNFRONTED-MEEK-OSSH", "UNFRONTED-MEEK-HTTPS-OSSH",
-	// "UNFRONTED-MEEK-SESSION-TICKET-OSSH", "FRONTED-MEEK-OSSH", "FRONTED-
-	// MEEK-HTTP-OSSH".
+	// LimitTunnelProtocols indicates which protocols to use. Valid values
+	// include: "SSH", "OSSH", "UNFRONTED-MEEK-OSSH", "UNFRONTED-MEEK-HTTPS-
+	// OSSH", "UNFRONTED-MEEK-SESSION-TICKET-OSSH", "FRONTED-MEEK-OSSH",
+	// "FRONTED- MEEK-HTTP-OSSH", "QUIC-OSSH".
 	//
 	// For the default, an empty list, all protocols are used.
-	TunnelProtocols []string
+	LimitTunnelProtocols []string
+
+	// InitialLimitTunnelProtocols is an optional initial phase of limited
+	// protocols for the first InitialLimitTunnelProtocolsCandidateCount
+	// candidates; after these candidates, LimitTunnelProtocols applies.
+	//
+	// For the default, an empty list, InitialLimitTunnelProtocols is off.
+	InitialLimitTunnelProtocols []string
+
+	// InitialLimitTunnelProtocolsCandidateCount is the number of candidates
+	// to which InitialLimitTunnelProtocols is applied instead of
+	// LimitTunnelProtocols.
+	//
+	// For the default, 0, InitialLimitTunnelProtocols is off.
+	InitialLimitTunnelProtocolsCandidateCount int
 
 	// EstablishTunnelTimeoutSeconds specifies a time limit after which to
 	// halt the core tunnel controller if no tunnel has been established. The
@@ -793,12 +807,17 @@ func (config *Config) makeConfigParameters() map[string]interface{} {
 		applyParameters[parameters.NetworkLatencyMultiplier] = config.NetworkLatencyMultiplier
 	}
 
-	if len(config.TunnelProtocols) > 0 {
-		applyParameters[parameters.LimitTunnelProtocols] = protocol.TunnelProtocols(config.TunnelProtocols)
+	if len(config.LimitTunnelProtocols) > 0 {
+		applyParameters[parameters.LimitTunnelProtocols] = protocol.TunnelProtocols(config.LimitTunnelProtocols)
 	} else if config.TunnelProtocol != "" {
 		applyParameters[parameters.LimitTunnelProtocols] = protocol.TunnelProtocols{config.TunnelProtocol}
 	}
 
+	if len(config.InitialLimitTunnelProtocols) > 0 && config.InitialLimitTunnelProtocolsCandidateCount > 0 {
+		applyParameters[parameters.InitialLimitTunnelProtocols] = protocol.TunnelProtocols(config.InitialLimitTunnelProtocols)
+		applyParameters[parameters.InitialLimitTunnelProtocolsCandidateCount] = config.InitialLimitTunnelProtocolsCandidateCount
+	}
+
 	if config.EstablishTunnelTimeoutSeconds != nil {
 		applyParameters[parameters.EstablishTunnelTimeout] = fmt.Sprintf("%ds", *config.EstablishTunnelTimeoutSeconds)
 	}

+ 120 - 38
psiphon/controller.go

@@ -57,6 +57,7 @@ type Controller struct {
 	nextTunnel                              int
 	startedConnectedReporter                bool
 	isEstablishing                          bool
+	establishLimitTunnelProtocolsState      *limitTunnelProtocolsState
 	concurrentEstablishTunnelsMutex         sync.Mutex
 	concurrentEstablishTunnels              int
 	concurrentIntensiveEstablishTunnels     int
@@ -77,13 +78,6 @@ type Controller struct {
 	packetTunnelTransport                   *PacketTunnelTransport
 }
 
-type candidateServerEntry struct {
-	serverEntry                *protocol.ServerEntry
-	isServerAffinityCandidate  bool
-	usePriorityProtocol        bool
-	adjustedEstablishStartTime monotime.Time
-}
-
 // NewController initializes a new controller.
 func NewController(config *Config) (controller *Controller, err error) {
 
@@ -970,6 +964,70 @@ func (controller *Controller) DirectDial(remoteAddr string) (conn net.Conn, err
 	return DialTCP(controller.runCtx, remoteAddr, controller.untunneledDialConfig)
 }
 
+type limitTunnelProtocolsState struct {
+	useUpstreamProxy      bool
+	initialProtocols      protocol.TunnelProtocols
+	initialCandidateCount int
+	protocols             protocol.TunnelProtocols
+}
+
+func (l *limitTunnelProtocolsState) isInitialCandidate(
+	excludeIntensive bool, serverEntry *protocol.ServerEntry) bool {
+
+	return len(l.initialProtocols) > 0 && l.initialCandidateCount > 0 &&
+		len(serverEntry.GetSupportedProtocols(l.useUpstreamProxy, l.initialProtocols, excludeIntensive)) > 0
+}
+
+func (l *limitTunnelProtocolsState) isCandidate(
+	excludeIntensive bool, serverEntry *protocol.ServerEntry) bool {
+
+	return len(l.protocols) == 0 ||
+		len(serverEntry.GetSupportedProtocols(l.useUpstreamProxy, l.protocols, excludeIntensive)) > 0
+}
+
+var errNoProtocolSupported = errors.New("server does not support any required protocol(s)")
+
+func (l *limitTunnelProtocolsState) selectProtocol(
+	candidateIndex int, excludeIntensive bool, serverEntry *protocol.ServerEntry) (string, error) {
+
+	limitProtocols := l.protocols
+
+	if len(l.initialProtocols) > 0 && l.initialCandidateCount > candidateIndex {
+		limitProtocols = l.initialProtocols
+	}
+
+	candidateProtocols := serverEntry.GetSupportedProtocols(
+		l.useUpstreamProxy,
+		limitProtocols,
+		excludeIntensive)
+
+	if len(candidateProtocols) == 0 {
+		return "", errNoProtocolSupported
+	}
+
+	// Pick at random from the supported protocols. This ensures that we'll
+	// eventually try all possible protocols. Depending on network
+	// configuration, it may be the case that some protocol is only available
+	// through multi-capability servers, and a simpler ranked preference of
+	// protocols could lead to that protocol never being selected.
+
+	index, err := common.MakeSecureRandomInt(len(candidateProtocols))
+	if err != nil {
+		return "", common.ContextError(err)
+	}
+	selectedProtocol := candidateProtocols[index]
+
+	return selectedProtocol, nil
+
+}
+
+type candidateServerEntry struct {
+	serverEntry                *protocol.ServerEntry
+	isServerAffinityCandidate  bool
+	candidateIndex             int
+	adjustedEstablishStartTime monotime.Time
+}
+
 // startEstablishing creates a pool of worker goroutines which will
 // attempt to establish tunnels to candidate servers. The candidates
 // are generated by another goroutine.
@@ -1074,25 +1132,43 @@ func (controller *Controller) launchEstablishing() {
 		}
 	}
 
-	// Unconditionally report available egress regions. After a fresh install,
-	// the outer client may not have a list of regions to display, so we
-	// always report here. Other events that trigger ReportAvailableRegions,
-	// are not guaranteed to occur.
-	//
-	// This report is delayed until after tactics are likely to be applied, as
-	// tactics can impact the list of available regions; this avoids a
-	// ReportAvailableRegions reporting too many regions, followed shortly by
-	// a ReportAvailableRegions reporting fewer regions. That sequence could
-	// cause issues in the outer client UI.
+	// LimitTunnelProtocols and ConnectionWorkerPoolSize may be set by
+	// tactics.
 
-	ReportAvailableRegions(controller.config)
+	// Initial- and LimitTunnelProtocols are set once per establishment, for
+	// consistent application of related probabilities (applied by
+	// ClientParametersSnapshot.TunnelProtocols). The
+	// establishLimitTunnelProtocolsState field must be read-only after this
+	// point, allowing concurrent reads by establishment workers.
 
-	// The ConnectionWorkerPoolSize may be set by tactics.
+	p := controller.config.clientParameters.Get()
 
-	size := controller.config.clientParameters.Get().Int(
+	controller.establishLimitTunnelProtocolsState = &limitTunnelProtocolsState{
+		useUpstreamProxy:      controller.config.UseUpstreamProxy(),
+		initialProtocols:      p.TunnelProtocols(parameters.InitialLimitTunnelProtocols),
+		initialCandidateCount: p.Int(parameters.InitialLimitTunnelProtocolsCandidateCount),
+		protocols:             p.TunnelProtocols(parameters.LimitTunnelProtocols),
+	}
+
+	workerPoolSize := controller.config.clientParameters.Get().Int(
 		parameters.ConnectionWorkerPoolSize)
 
-	for i := 0; i < size; i++ {
+	p = nil
+
+	// Report available egress regions. After a fresh install, the outer
+	// client may not have a list of regions to display; and
+	// LimitTunnelProtocols may reduce the number of available regions.
+	//
+	// This report is delayed until after tactics are likely to be applied;
+	// this avoids a ReportAvailableRegions reporting too many regions,
+	// followed shortly by a ReportAvailableRegions reporting fewer regions.
+	// That sequence could cause issues in the outer client UI.
+
+	ReportAvailableRegions(
+		controller.config,
+		controller.establishLimitTunnelProtocolsState)
+
+	for i := 0; i < workerPoolSize; i++ {
 		controller.establishWaitGroup.Add(1)
 		go controller.establishTunnelWorker()
 	}
@@ -1370,11 +1446,11 @@ func (controller *Controller) establishCandidateGenerator() {
 		close(controller.serverAffinityDoneBroadcast)
 	}
 
-	candidateCount := 0
+	candidateIndex := 0
 
 loop:
 	// Repeat until stopped
-	for i := 0; ; i++ {
+	for {
 
 		networkWaitStartTime := monotime.Now()
 
@@ -1386,6 +1462,22 @@ loop:
 
 		networkWaitDuration += monotime.Since(networkWaitStartTime)
 
+		// For diagnostics, emits counts of the number of known server
+		// entries that satisfy both the egress region and tunnel protocol
+		// requirements (excluding excludeIntensive logic).
+		// Counts may change during establishment due to remote server
+		// list fetches, etc.
+
+		initialCount, count := CountServerEntriesWithLimits(
+			controller.config.UseUpstreamProxy(),
+			controller.config.EgressRegion,
+			controller.establishLimitTunnelProtocolsState)
+		NoticeCandidateServers(
+			controller.config.EgressRegion,
+			controller.establishLimitTunnelProtocolsState,
+			initialCount,
+			count)
+
 		// Send each iterator server entry to the establish workers
 		startTime := monotime.Now()
 		for {
@@ -1405,15 +1497,6 @@ loop:
 				continue
 			}
 
-			// Use a prioritized tunnel protocol for the first
-			// PrioritizeTunnelProtocolsCandidateCount candidates.
-			// This facility can be used to favor otherwise slower
-			// protocols.
-
-			prioritizeCandidateCount := controller.config.clientParameters.Get().Int(
-				parameters.PrioritizeTunnelProtocolsCandidateCount)
-			usePriorityProtocol := candidateCount < prioritizeCandidateCount
-
 			// adjustedEstablishStartTime is establishStartTime shifted
 			// to exclude time spent waiting for network connectivity.
 
@@ -1422,7 +1505,7 @@ loop:
 			candidate := &candidateServerEntry{
 				serverEntry:                serverEntry,
 				isServerAffinityCandidate:  isServerAffinityCandidate,
-				usePriorityProtocol:        usePriorityProtocol,
+				candidateIndex:             candidateIndex,
 				adjustedEstablishStartTime: adjustedEstablishStartTime,
 			}
 
@@ -1435,7 +1518,7 @@ loop:
 			// TODO: here we could generate multiple candidates from the
 			// server entry when there are many MeekFrontingAddresses.
 
-			candidateCount++
+			candidateIndex++
 
 			select {
 			case controller.candidateServerEntries <- candidate:
@@ -1601,11 +1684,10 @@ loop:
 		}
 		controller.concurrentEstablishTunnelsMutex.Unlock()
 
-		selectedProtocol, err := selectProtocol(
-			controller.config,
-			candidateServerEntry.serverEntry,
+		selectedProtocol, err := controller.establishLimitTunnelProtocolsState.selectProtocol(
+			candidateServerEntry.candidateIndex,
 			excludeIntensive,
-			candidateServerEntry.usePriorityProtocol)
+			candidateServerEntry.serverEntry)
 
 		if err == errNoProtocolSupported {
 			// selectProtocol returns errNoProtocolSupported when the server

+ 2 - 2
psiphon/controller_test.go

@@ -463,7 +463,7 @@ func controllerRun(t *testing.T, runConfig *controllerRunConfig) {
 	modifyConfig["UpgradeDownloadFilename"] = filepath.Join(testDataDirName, "upgrade")
 
 	if runConfig.protocol != "" {
-		modifyConfig["TunnelProtocols"] = protocol.TunnelProtocols{runConfig.protocol}
+		modifyConfig["LimitTunnelProtocols"] = protocol.TunnelProtocols{runConfig.protocol}
 	}
 
 	// Override client retry throttle values to speed up automated
@@ -551,7 +551,7 @@ func controllerRun(t *testing.T, runConfig *controllerRunConfig) {
 	}
 	defer CloseDataStore()
 
-	serverEntryCount := CountServerEntries(config.UseUpstreamProxy(), "", nil)
+	serverEntryCount := CountServerEntries()
 
 	if runConfig.expectNoServerEntries && serverEntryCount > 0 {
 		// TODO: replace expectNoServerEntries with resetServerEntries

+ 44 - 41
psiphon/dataStore.go

@@ -634,28 +634,6 @@ func (iterator *ServerEntryIterator) Reset() error {
 		return nil
 	}
 
-	// For diagnostics, it's useful to count the number of known server
-	// entries that satisfy both the egress region and tunnel protocol
-	// requirements (excluding excludeIntensive logic).
-
-	// TODO: for isTacticsServerEntryIterator, emit tactics candidate count.
-
-	if !iterator.isTacticsServerEntryIterator {
-		limitTunnelProtocols := iterator.config.clientParameters.Get().TunnelProtocols(
-			parameters.LimitTunnelProtocols)
-
-		count := CountServerEntries(
-			iterator.config.UseUpstreamProxy(), iterator.config.EgressRegion, limitTunnelProtocols)
-		NoticeCandidateServers(iterator.config.EgressRegion, limitTunnelProtocols, count)
-
-		// LimitTunnelProtocols may have changed since the last ReportAvailableRegions,
-		// and now there may be no servers with the required capabilities in the
-		// selected region. ReportAvailableRegions will signal this to the client.
-		if count == 0 {
-			ReportAvailableRegions(iterator.config)
-		}
-	}
-
 	// This query implements the Psiphon server candidate selection
 	// algorithm: the first TunnelPoolSize server candidates are in rank
 	// (priority) order, to favor previously successful servers; then the
@@ -846,18 +824,11 @@ func scanServerEntries(scanner func(*protocol.ServerEntry)) error {
 	return nil
 }
 
-// CountServerEntries returns a count of stored servers for the
-// specified region and tunnel protocols.
-func CountServerEntries(useUpstreamProxy bool, region string, limitTunnelProtocols []string) int {
+// CountServerEntries returns a count of stored server entries.
+func CountServerEntries() int {
 	count := 0
-	err := scanServerEntries(func(serverEntry *protocol.ServerEntry) {
-		if (region == "" || serverEntry.Region == region) &&
-			(len(limitTunnelProtocols) == 0 ||
-				// When CountServerEntries is called only limitTunnelProtocols
-				// is known; excludeIntensive may not apply.
-				len(serverEntry.GetSupportedProtocols(useUpstreamProxy, limitTunnelProtocols, false)) > 0) {
-			count += 1
-		}
+	err := scanServerEntries(func(_ *protocol.ServerEntry) {
+		count += 1
 	})
 
 	if err != nil {
@@ -868,19 +839,51 @@ func CountServerEntries(useUpstreamProxy bool, region string, limitTunnelProtoco
 	return count
 }
 
+// CountServerEntriesWithLimits returns a count of stored server entries for
+// the specified region and tunnel protocol limits.
+func CountServerEntriesWithLimits(
+	useUpstreamProxy bool, region string, limitState *limitTunnelProtocolsState) (int, int) {
+
+	// When CountServerEntriesWithLimits is called only
+	// limitTunnelProtocolState is fixed; excludeIntensive is transitory.
+	excludeIntensive := false
+
+	initialCount := 0
+	count := 0
+	err := scanServerEntries(func(serverEntry *protocol.ServerEntry) {
+		if region == "" || serverEntry.Region == region {
+
+			if limitState.isInitialCandidate(excludeIntensive, serverEntry) {
+				initialCount += 1
+			}
+
+			if limitState.isCandidate(excludeIntensive, serverEntry) {
+				count += 1
+			}
+
+		}
+	})
+
+	if err != nil {
+		NoticeAlert("CountServerEntriesWithLimits failed: %s", err)
+		return 0, 0
+	}
+
+	return initialCount, count
+}
+
 // ReportAvailableRegions prints a notice with the available egress regions.
-func ReportAvailableRegions(config *Config) {
+func ReportAvailableRegions(config *Config, limitState *limitTunnelProtocolsState) {
 
-	limitTunnelProtocols := config.clientParameters.Get().TunnelProtocols(
-		parameters.LimitTunnelProtocols)
+	// When ReportAvailableRegions is called only
+	// limitTunnelProtocolState is fixed; excludeIntensive is transitory.
+	excludeIntensive := false
 
 	regions := make(map[string]bool)
 	err := scanServerEntries(func(serverEntry *protocol.ServerEntry) {
-		if len(limitTunnelProtocols) == 0 ||
-			// When ReportAvailableRegions is called only limitTunnelProtocols
-			// is known; excludeIntensive may not apply.
-			len(serverEntry.GetSupportedProtocols(
-				config.UseUpstreamProxy(), limitTunnelProtocols, false)) > 0 {
+
+		if limitState.isInitialCandidate(excludeIntensive, serverEntry) ||
+			limitState.isCandidate(excludeIntensive, serverEntry) {
 
 			regions[serverEntry.Region] = true
 		}

+ 10 - 2
psiphon/notice.go

@@ -373,11 +373,19 @@ func NoticeUserLog(message string) {
 }
 
 // NoticeCandidateServers is how many possible servers are available for the selected region and protocols
-func NoticeCandidateServers(region string, protocols []string, count int) {
+func NoticeCandidateServers(
+	region string,
+	limitState *limitTunnelProtocolsState,
+	initialCount int,
+	count int) {
+
 	singletonNoticeLogger.outputNotice(
 		"CandidateServers", noticeIsDiagnostic,
 		"region", region,
-		"protocols", protocols,
+		"initialLimitTunnelProtocols", limitState.initialProtocols,
+		"initialLimitTunnelProtocolsCandidateCount", limitState.initialCandidateCount,
+		"limitTunnelProtocols", limitState.protocols,
+		"initialCount", initialCount,
 		"count", count)
 }
 

+ 1 - 1
psiphon/remoteServerList_test.go

@@ -213,7 +213,7 @@ func testObfuscatedRemoteServerLists(t *testing.T, omitMD5Sums bool) {
 	}
 	defer CloseDataStore()
 
-	if CountServerEntries(false, "", nil) > 0 {
+	if CountServerEntries() > 0 {
 		t.Fatalf("unexpected server entries")
 	}
 

+ 1 - 1
psiphon/server/server_test.go

@@ -566,7 +566,7 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
         "DisableRemoteServerListFetcher" : true,
         "EstablishTunnelPausePeriodSeconds" : 1,
         "ConnectionWorkerPoolSize" : %d,
-        "TunnelProtocols" : ["%s"]
+        "LimitTunnelProtocols" : ["%s"]
         %s
     }`, numTunnels, runConfig.tunnelProtocol, jsonNetworkID)
 

+ 0 - 51
psiphon/tunnel.go

@@ -526,57 +526,6 @@ func (conn *TunneledConn) Close() error {
 	return conn.Conn.Close()
 }
 
-var errNoProtocolSupported = errors.New("server does not support any required protocol(s)")
-
-// selectProtocol is a helper that picks the tunnel protocol
-func selectProtocol(
-	config *Config,
-	serverEntry *protocol.ServerEntry,
-	excludeIntensive bool,
-	usePriorityProtocol bool) (selectedProtocol string, err error) {
-
-	candidateProtocols := serverEntry.GetSupportedProtocols(
-		config.UseUpstreamProxy(),
-		config.clientParameters.Get().TunnelProtocols(parameters.LimitTunnelProtocols),
-		excludeIntensive)
-	if len(candidateProtocols) == 0 {
-		return "", errNoProtocolSupported
-	}
-
-	// Select a prioritized protocols when indicated. If no prioritized
-	// protocol is available, proceed with selecting any other protocol.
-
-	if usePriorityProtocol {
-		prioritizeProtocols := config.clientParameters.Get().TunnelProtocols(
-			parameters.PrioritizeTunnelProtocols)
-		if len(prioritizeProtocols) > 0 {
-			protocols := make([]string, 0)
-			for _, protocol := range candidateProtocols {
-				if common.Contains(prioritizeProtocols, protocol) {
-					protocols = append(protocols, protocol)
-				}
-			}
-			if len(protocols) > 0 {
-				candidateProtocols = protocols
-			}
-		}
-	}
-
-	// Pick at random from the supported protocols. This ensures that we'll
-	// eventually try all possible protocols. Depending on network
-	// configuration, it may be the case that some protocol is only available
-	// through multi-capability servers, and a simpler ranked preference of
-	// protocols could lead to that protocol never being selected.
-
-	index, err := common.MakeSecureRandomInt(len(candidateProtocols))
-	if err != nil {
-		return "", common.ContextError(err)
-	}
-	selectedProtocol = candidateProtocols[index]
-
-	return selectedProtocol, nil
-}
-
 // selectFrontingParameters is a helper which selects/generates meek fronting
 // parameters where the server entry provides multiple options or patterns.
 func selectFrontingParameters(