Procházet zdrojové kódy

Support more aggressive replay

- Unlimited replay candidates when ReplayCandidateCount set to -1
- Move-to-front in later rounds with ReplayLaterRoundMoveToFrontProbability
- Retain replay parameters for failed tunnels with ReplayRetainFailedProbability
Rod Hynes před 6 roky
rodič
revize
db1ed0cefe

+ 20 - 16
psiphon/common/parameters/clientParameters.go

@@ -204,6 +204,8 @@ const (
 	ReplayLivenessTest                               = "ReplayLivenessTest"
 	ReplayUserAgent                                  = "ReplayUserAgent"
 	ReplayAPIRequestPadding                          = "ReplayAPIRequestPadding"
+	ReplayLaterRoundMoveToFrontProbability           = "ReplayLaterRoundMoveToFrontProbability"
+	ReplayRetainFailedProbability                    = "ReplayRetainFailedProbability"
 	APIRequestUpstreamPaddingMinBytes                = "APIRequestUpstreamPaddingMinBytes"
 	APIRequestUpstreamPaddingMaxBytes                = "APIRequestUpstreamPaddingMaxBytes"
 	APIRequestDownstreamPaddingMinBytes              = "APIRequestDownstreamPaddingMinBytes"
@@ -412,22 +414,24 @@ var defaultClientParameters = map[string]struct {
 	LivenessTestMinDownstreamBytes: {value: 0, minimum: 0},
 	LivenessTestMaxDownstreamBytes: {value: 0, minimum: 0},
 
-	ReplayCandidateCount:        {value: 10, minimum: 0},
-	ReplayDialParametersTTL:     {value: 24 * time.Hour, minimum: time.Duration(0)},
-	ReplayTargetUpstreamBytes:   {value: 0, minimum: 0},
-	ReplayTargetDownstreamBytes: {value: 0, minimum: 0},
-	ReplaySSH:                   {value: true},
-	ReplayObfuscatorPadding:     {value: true},
-	ReplayFragmentor:            {value: true},
-	ReplayTLSProfile:            {value: true},
-	ReplayRandomizedTLSProfile:  {value: true},
-	ReplayFronting:              {value: true},
-	ReplayHostname:              {value: true},
-	ReplayQUICVersion:           {value: true},
-	ReplayObfuscatedQUIC:        {value: true},
-	ReplayLivenessTest:          {value: true},
-	ReplayUserAgent:             {value: true},
-	ReplayAPIRequestPadding:     {value: true},
+	ReplayCandidateCount:                   {value: 10, minimum: -1},
+	ReplayDialParametersTTL:                {value: 24 * time.Hour, minimum: time.Duration(0)},
+	ReplayTargetUpstreamBytes:              {value: 0, minimum: 0},
+	ReplayTargetDownstreamBytes:            {value: 0, minimum: 0},
+	ReplaySSH:                              {value: true},
+	ReplayObfuscatorPadding:                {value: true},
+	ReplayFragmentor:                       {value: true},
+	ReplayTLSProfile:                       {value: true},
+	ReplayRandomizedTLSProfile:             {value: true},
+	ReplayFronting:                         {value: true},
+	ReplayHostname:                         {value: true},
+	ReplayQUICVersion:                      {value: true},
+	ReplayObfuscatedQUIC:                   {value: true},
+	ReplayLivenessTest:                     {value: true},
+	ReplayUserAgent:                        {value: true},
+	ReplayAPIRequestPadding:                {value: true},
+	ReplayLaterRoundMoveToFrontProbability: {value: 0.0, minimum: 0.0},
+	ReplayRetainFailedProbability:          {value: 0.5, minimum: 0.0},
 
 	APIRequestUpstreamPaddingMinBytes:   {value: 0, minimum: 0},
 	APIRequestUpstreamPaddingMaxBytes:   {value: 1024, minimum: 0},

+ 12 - 2
psiphon/config.go

@@ -521,8 +521,10 @@ type Config struct {
 
 	// ReplayCandidateCount and other Replay fields are for
 	// testing purposes.
-	ReplayCandidateCount           *int
-	ReplayDialParametersTTLSeconds *int
+	ReplayCandidateCount                   *int
+	ReplayDialParametersTTLSeconds         *int
+	ReplayLaterRoundMoveToFrontProbability *float64
+	ReplayRetainFailedProbability          *float64
 
 	// clientParameters is the active ClientParameters with defaults, config
 	// values, and, optionally, tactics applied.
@@ -1038,6 +1040,14 @@ func (config *Config) makeConfigParameters() map[string]interface{} {
 		applyParameters[parameters.ReplayDialParametersTTL] = fmt.Sprintf("%ds", *config.ReplayDialParametersTTLSeconds)
 	}
 
+	if config.ReplayLaterRoundMoveToFrontProbability != nil {
+		applyParameters[parameters.ReplayLaterRoundMoveToFrontProbability] = *config.ReplayLaterRoundMoveToFrontProbability
+	}
+
+	if config.ReplayRetainFailedProbability != nil {
+		applyParameters[parameters.ReplayRetainFailedProbability] = *config.ReplayRetainFailedProbability
+	}
+
 	return applyParameters
 }
 

+ 11 - 10
psiphon/controller.go

@@ -1041,7 +1041,7 @@ func (p *protocolSelectionConstraints) canReplay(
 	serverEntry *protocol.ServerEntry,
 	replayProtocol string) bool {
 
-	if connectTunnelCount > p.replayCandidateCount {
+	if p.replayCandidateCount != -1 && connectTunnelCount > p.replayCandidateCount {
 		return false
 	}
 
@@ -1807,21 +1807,22 @@ loop:
 		// entry has a previous, recent successful connection and
 		// tactics/config has not changed.
 		//
-		// In the first round of establishing, ServerEntryIterator will move
-		// potential replay candidates to the front of the iterator after the
-		// random shuffle, which greatly prioritizes previously successful
-		// servers for that round.
+		// In the first round -- and later rounds, with some probability -- of
+		// establishing, ServerEntryIterator will move potential replay candidates
+		// to the front of the iterator after the random shuffle, which greatly
+		// prioritizes previously successful servers for that round.
 		//
 		// As ServerEntryIterator does not unmarshal and validate replay
 		// candidate dial parameters, some potential replay candidates may
 		// have expired or otherwise ineligible dial parameters; in this case
 		// the candidate proceeds without replay.
 		//
-		// The ReplayCandidateCount tactic determines how many candidates may
-		// use replay. After ReplayCandidateCount candidates on any type,
-		// replay or no, replay is skipped. If ReplayCandidateCount exceed the
-		// intial round, replay may still be performed but the iterator no
-		// longer moves potential replay server entries to the front.
+		// The ReplayCandidateCount tactic determines how many candidates may use
+		// replay. After ReplayCandidateCount candidates of any type, replay or no,
+		// replay is skipped. If ReplayCandidateCount exceeds the intial round,
+		// replay may still be performed but the iterator may no longer move
+		// potential replay server entries to the front. When ReplayCandidateCount
+		// is set to -1, unlimited candidates may use replay.
 
 		dialParams, err := MakeDialParameters(
 			controller.config,

+ 10 - 6
psiphon/dataStore.go

@@ -572,18 +572,22 @@ func (iterator *ServerEntryIterator) reset(isInitialRound bool) error {
 			serverEntryIDs[i], serverEntryIDs[j] = serverEntryIDs[j], serverEntryIDs[i]
 		}
 
-		// In the first round only, move _potential_ replay candidates to the
-		// front of the list (excepting the server affinity slot, if any).
-		// This move is post-shuffle so the order is still randomized. To save
-		// the memory overhead of unmarshalling all dial parameters, this
+		// In the first round, or with some probability, move _potential_ replay
+		// candidates to the front of the list (excepting the server affinity slot,
+		// if any). This move is post-shuffle so the order is still randomized. To
+		// save the memory overhead of unmarshalling all dial parameters, this
 		// operation just moves any server with a dial parameter record to the
 		// front. Whether the dial parameter remains valid for replay -- TTL,
 		// tactics/config unchanged, etc. --- is checked later.
 		//
 		// TODO: move only up to parameters.ReplayCandidateCount to front?
 
-		if isInitialRound &&
-			iterator.config.GetClientParameters().Int(parameters.ReplayCandidateCount) > 0 {
+		if (isInitialRound ||
+			iterator.config.GetClientParameters().WeightedCoinFlip(
+				parameters.ReplayLaterRoundMoveToFrontProbability)) &&
+
+			iterator.config.GetClientParameters().Int(
+				parameters.ReplayCandidateCount) != 0 {
 
 			networkID := []byte(iterator.config.GetNetworkID())
 

+ 13 - 6
psiphon/dialParameters.go

@@ -139,7 +139,7 @@ func MakeDialParameters(
 
 	networkID := config.GetNetworkID()
 
-	p := config.clientParameters.Get()
+	p := config.GetClientParameters()
 
 	ttl := p.Duration(parameters.ReplayDialParametersTTL)
 	replaySSH := p.Bool(parameters.ReplaySSH)
@@ -601,17 +601,24 @@ func (dialParams *DialParameters) Succeeded() {
 	}
 }
 
-func (dialParams *DialParameters) Failed() {
+func (dialParams *DialParameters) Failed(config *Config) {
 
 	// When a tunnel fails, and the dial is a replay, clear the stored dial
 	// parameters which are now presumed to be blocked, impaired or otherwise
 	// no longer effective.
 	//
-	// It may be the case that a dial is not using stored dial parameters, and
-	// in this case we retain those dial parameters since they were not
-	// exercised and may still be efective.
+	// It may be the case that a dial is not using stored dial parameters
+	// (!IsReplay), and in this case we retain those dial parameters since they
+	// were not exercised and may still be effective.
+	//
+	// Failed tunnel dial parameters may be retained with a configurable
+	// probability; this is intended to help mitigate false positive failures due
+	// to, e.g., temporary network disruptions or server load limiting.
+
+	if dialParams.IsReplay &&
+		!config.GetClientParameters().WeightedCoinFlip(
+			parameters.ReplayRetainFailedProbability) {
 
-	if dialParams.IsReplay {
 		NoticeInfo("Delete dial parameters for %s", dialParams.ServerEntry.IpAddress)
 		err := DeleteDialParameters(dialParams.ServerEntry.IpAddress, dialParams.NetworkID)
 		if err != nil {

+ 1 - 1
psiphon/dialParameters_test.go

@@ -206,7 +206,7 @@ func runDialParametersAndReplay(t *testing.T, tunnelProtocol string) {
 
 	// Test: no replay after dial reported to fail
 
-	dialParams.Failed()
+	dialParams.Failed(clientConfig)
 
 	dialParams, err = MakeDialParameters(clientConfig, canReplay, selectProtocol, serverEntries[0], false, 0)
 	if err != nil {

+ 2 - 2
psiphon/tunnel.go

@@ -162,7 +162,7 @@ func (tunnel *Tunnel) Activate(
 	activationSucceeded := false
 	defer func() {
 		if !activationSucceeded && ctx.Err() == nil {
-			tunnel.dialParams.Failed()
+			tunnel.dialParams.Failed(tunnel.config)
 			_ = RecordFailedTunnelStat(tunnel.config, tunnel.dialParams, retErr)
 		}
 	}()
@@ -552,7 +552,7 @@ func dialTunnel(
 	baseCtx := ctx
 	defer func() {
 		if !dialSucceeded && baseCtx.Err() == nil {
-			dialParams.Failed()
+			dialParams.Failed(config)
 			_ = RecordFailedTunnelStat(config, dialParams, retErr)
 		}
 	}()