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

Add support for DSL prioritize dial hints

Also:
- Change ReplayIgnoreChangedConfigState to a probability
- Don't ship false values for unconditional boolean dial parameters
Rod Hynes 3 месяцев назад
Родитель
Сommit
6b1105648e

+ 12 - 5
psiphon/common/dsl/api.go

@@ -77,10 +77,17 @@ func (tag ServerEntryTag) String() string {
 	return base64.StdEncoding.EncodeToString(tag)
 	return base64.StdEncoding.EncodeToString(tag)
 }
 }
 
 
-// VersionedServerEntryTag is a server entry tag and version pair.
+// VersionedServerEntryTag is a server entry tag and version pair returned in
+// DiscoverServerEntriesResponse. When the client already has a server entry
+// for the specified tag, the client uses the version field to determine
+// whether the server entry needs to be updated. In addition, this return
+// value includes a PrioritizeDial hint, from the DSL backend, that the
+// server entry is expected to be effective for the client and the client
+// should prioritize the server in establishment scheduling.
 type VersionedServerEntryTag struct {
 type VersionedServerEntryTag struct {
-	Tag     ServerEntryTag `cbor:"1,keyasint,omitempty"`
-	Version int32          `cbor:"2,keyasint,omitempty"`
+	Tag            ServerEntryTag `cbor:"1,keyasint,omitempty"`
+	Version        int32          `cbor:"2,keyasint,omitempty"`
+	PrioritizeDial bool           `cbor:"3,keyasint,omitempty"`
 }
 }
 
 
 // DiscoverServerEntriesResponse is the set of server entries revealed to the
 // DiscoverServerEntriesResponse is the set of server entries revealed to the
@@ -109,8 +116,8 @@ type SourcedServerEntry struct {
 
 
 // GetServerEntriesResponse includes the list of server entries requested by
 // GetServerEntriesResponse includes the list of server entries requested by
 // the client. Each requested tag has a corresponding entry in
 // the client. Each requested tag has a corresponding entry in
-// SourcedServerEntries. When a requested tag is no longer available for
-// distribution, there is a nil/empty entry.
+// SourcedServerEntries, in requested order. When a requested tag is no
+// longer available for distribution, there is a nil/empty entry.
 type GetServerEntriesResponse struct {
 type GetServerEntriesResponse struct {
 	SourcedServerEntries []*SourcedServerEntry `cbor:"1,keyasint,omitempty"`
 	SourcedServerEntries []*SourcedServerEntry `cbor:"1,keyasint,omitempty"`
 }
 }

+ 45 - 2
psiphon/common/dsl/dsl_test.go

@@ -28,6 +28,7 @@ import (
 	"os"
 	"os"
 	"runtime/debug"
 	"runtime/debug"
 	"sync"
 	"sync"
+	"sync/atomic"
 	"testing"
 	"testing"
 	"time"
 	"time"
 
 
@@ -279,6 +280,40 @@ func testDSLs(testConfig *testConfig) error {
 
 
 	// TODO: exercise BaseAPIParameters?
 	// TODO: exercise BaseAPIParameters?
 
 
+	var unexpectedServerEntrySource atomic.Int32
+	var unexpectedServerEntryPrioritizeDial atomic.Int32
+
+	datastoreHasServerEntryWithCheck := func(
+		tag ServerEntryTag,
+		version int,
+		prioritizeDial bool) bool {
+
+		_, expectedPrioritizeDial, err := backend.GetServerEntryProperties(tag.String())
+		if err != nil || prioritizeDial != expectedPrioritizeDial {
+			unexpectedServerEntryPrioritizeDial.Store(1)
+		}
+		return dslClient.DatastoreHasServerEntry(tag, version)
+	}
+
+	datastoreStoreServerEntryWithCheck := func(
+		packedServerEntryFields protocol.PackedServerEntryFields,
+		source string,
+		prioritizeDial bool) error {
+
+		serverEntryFields, _ := protocol.DecodePackedServerEntryFields(packedServerEntryFields)
+		tag := serverEntryFields.GetTag()
+
+		expectedSource, expectedPrioritizeDial, err := backend.GetServerEntryProperties(tag)
+		if err != nil || prioritizeDial != expectedPrioritizeDial {
+			unexpectedServerEntryPrioritizeDial.Store(1)
+		}
+		if err != nil || source != expectedSource {
+			unexpectedServerEntrySource.Store(1)
+		}
+		return errors.Trace(
+			dslClient.DatastoreStoreServerEntry(packedServerEntryFields, source))
+	}
+
 	fetcherConfig := &FetcherConfig{
 	fetcherConfig := &FetcherConfig{
 		Logger: testutils.NewTestLoggerWithComponent("fetcher"),
 		Logger: testutils.NewTestLoggerWithComponent("fetcher"),
 
 
@@ -288,8 +323,8 @@ func testDSLs(testConfig *testConfig) error {
 		DatastoreSetLastFetchTime:      dslClient.DatastoreSetLastFetchTime,
 		DatastoreSetLastFetchTime:      dslClient.DatastoreSetLastFetchTime,
 		DatastoreGetLastActiveOSLsTime: dslClient.DatastoreGetLastActiveOSLsTime,
 		DatastoreGetLastActiveOSLsTime: dslClient.DatastoreGetLastActiveOSLsTime,
 		DatastoreSetLastActiveOSLsTime: dslClient.DatastoreSetLastActiveOSLsTime,
 		DatastoreSetLastActiveOSLsTime: dslClient.DatastoreSetLastActiveOSLsTime,
-		DatastoreHasServerEntry:        dslClient.DatastoreHasServerEntry,
-		DatastoreStoreServerEntry:      dslClient.DatastoreStoreServerEntry,
+		DatastoreHasServerEntry:        datastoreHasServerEntryWithCheck,
+		DatastoreStoreServerEntry:      datastoreStoreServerEntryWithCheck,
 		DatastoreKnownOSLIDs:           dslClient.DatastoreKnownOSLIDs,
 		DatastoreKnownOSLIDs:           dslClient.DatastoreKnownOSLIDs,
 		DatastoreGetOSLState:           dslClient.DatastoreGetOSLState,
 		DatastoreGetOSLState:           dslClient.DatastoreGetOSLState,
 		DatastoreStoreOSLState:         dslClient.DatastoreStoreOSLState,
 		DatastoreStoreOSLState:         dslClient.DatastoreStoreOSLState,
@@ -439,6 +474,14 @@ func testDSLs(testConfig *testConfig) error {
 		return errors.Trace(err)
 		return errors.Trace(err)
 	}
 	}
 
 
+	if unexpectedServerEntrySource.Load() != 0 {
+		return errors.TraceNew("unexpected server entry source")
+	}
+
+	if unexpectedServerEntryPrioritizeDial.Load() != 0 {
+		return errors.TraceNew("unexpected server entry prioritize dial")
+	}
+
 	return nil
 	return nil
 }
 }
 
 

+ 37 - 7
psiphon/common/dsl/fetcher.go

@@ -54,10 +54,14 @@ type FetcherConfig struct {
 
 
 	DatastoreGetLastFetchTime func() (time.Time, error)
 	DatastoreGetLastFetchTime func() (time.Time, error)
 	DatastoreSetLastFetchTime func(time time.Time) error
 	DatastoreSetLastFetchTime func(time time.Time) error
-	DatastoreHasServerEntry   func(tag ServerEntryTag, version int) bool
+	DatastoreHasServerEntry   func(
+		tag ServerEntryTag,
+		version int,
+		prioritizeDial bool) bool
 	DatastoreStoreServerEntry func(
 	DatastoreStoreServerEntry func(
 		serverEntryFields protocol.PackedServerEntryFields,
 		serverEntryFields protocol.PackedServerEntryFields,
-		source string) error
+		source string,
+		prioritizeDial bool) error
 
 
 	DatastoreGetLastActiveOSLsTime func() (time.Time, error)
 	DatastoreGetLastActiveOSLsTime func() (time.Time, error)
 	DatastoreSetLastActiveOSLsTime func(time time.Time) error
 	DatastoreSetLastActiveOSLsTime func(time time.Time) error
@@ -66,7 +70,8 @@ type FetcherConfig struct {
 	DatastoreStoreOSLState         func(ID OSLID, state []byte) error
 	DatastoreStoreOSLState         func(ID OSLID, state []byte) error
 	DatastoreDeleteOSLState        func(ID OSLID) error
 	DatastoreDeleteOSLState        func(ID OSLID) error
 	DatastoreSLOKLookup            osl.SLOKLookup
 	DatastoreSLOKLookup            osl.SLOKLookup
-	DatastoreFatalError            func(error)
+
+	DatastoreFatalError func(error)
 
 
 	RequestTimeout          time.Duration
 	RequestTimeout          time.Duration
 	RequestRetryCount       int
 	RequestRetryCount       int
@@ -221,25 +226,48 @@ func (f *Fetcher) Run(ctx context.Context) error {
 
 
 	storeServerEntriesCount := 0
 	storeServerEntriesCount := 0
 	knownServerEntriesCount := 0
 	knownServerEntriesCount := 0
+	tagCount := len(versionedTags)
 	defer func() {
 	defer func() {
 		// Emit log even if not all fetches succeed.
 		// Emit log even if not all fetches succeed.
 		f.config.Logger.WithTraceFields(common.LogFields{
 		f.config.Logger.WithTraceFields(common.LogFields{
 			"tunneled": f.config.Tunneled,
 			"tunneled": f.config.Tunneled,
-			"tags":     len(versionedTags),
+			"tags":     tagCount,
 			"updated":  storeServerEntriesCount,
 			"updated":  storeServerEntriesCount,
 			"known":    knownServerEntriesCount,
 			"known":    knownServerEntriesCount,
 		}).Info("DSL: fetched server entries")
 		}).Info("DSL: fetched server entries")
 	}()
 	}()
 
 
+	// A subset of versionedTags containing both the tag and prioritize flag
+	// could be used here instead of two slices, but the memory impact of two
+	// slices should be less, considering the DoGarbageCollection can reclaim
+	// versionedTags, and we need a slice of tags (slice of getTags) to send
+	// in GetServerEntries.
+	//
+	// As GetServerEntriesResponse will contain an entry (potentially nil) for
+	// every requested server entry tag, in requested order, each index in
+	// prioritizeDials corresponds both to the same index in getTags and the
+	// sourcedServerEntries returned from doGetServerEntriesRequest.
+
 	var getTags []ServerEntryTag
 	var getTags []ServerEntryTag
+	var prioritizeDials []bool
 	for _, v := range versionedTags {
 	for _, v := range versionedTags {
-		if f.config.DatastoreHasServerEntry(v.Tag, int(v.Version)) {
+
+		hasServerEntry := f.config.DatastoreHasServerEntry(
+			v.Tag,
+			int(v.Version),
+			v.PrioritizeDial)
+
+		if hasServerEntry {
 			knownServerEntriesCount += 1
 			knownServerEntriesCount += 1
 			continue
 			continue
 		}
 		}
 		getTags = append(getTags, v.Tag)
 		getTags = append(getTags, v.Tag)
+		prioritizeDials = append(prioritizeDials, v.PrioritizeDial)
 	}
 	}
 
 
+	// Allow garbage collection.
+	versionedTags = nil
+
 	for len(getTags) > 0 {
 	for len(getTags) > 0 {
 
 
 		// Vary the size of the request and response.
 		// Vary the size of the request and response.
@@ -257,7 +285,7 @@ func (f *Fetcher) Run(ctx context.Context) error {
 			return errors.Trace(err)
 			return errors.Trace(err)
 		}
 		}
 
 
-		for _, sourcedEntry := range sourcedServerEntries {
+		for i, sourcedEntry := range sourcedServerEntries {
 
 
 			if sourcedEntry == nil {
 			if sourcedEntry == nil {
 				// The requested server entry is no longer distributable or
 				// The requested server entry is no longer distributable or
@@ -267,7 +295,8 @@ func (f *Fetcher) Run(ctx context.Context) error {
 
 
 			err := f.config.DatastoreStoreServerEntry(
 			err := f.config.DatastoreStoreServerEntry(
 				sourcedEntry.ServerEntryFields,
 				sourcedEntry.ServerEntryFields,
-				sourcedEntry.Source)
+				sourcedEntry.Source,
+				prioritizeDials[i])
 			if err != nil {
 			if err != nil {
 				return errors.Trace(err)
 				return errors.Trace(err)
 			}
 			}
@@ -280,6 +309,7 @@ func (f *Fetcher) Run(ctx context.Context) error {
 		// Unfetched server entries will be added to the next batch.
 		// Unfetched server entries will be added to the next batch.
 
 
 		getTags = getTags[len(sourcedServerEntries):]
 		getTags = getTags[len(sourcedServerEntries):]
+		prioritizeDials = prioritizeDials[len(sourcedServerEntries):]
 
 
 		f.config.DoGarbageCollection()
 		f.config.DoGarbageCollection()
 	}
 	}

+ 6 - 4
psiphon/common/dsl/shim.go

@@ -92,8 +92,9 @@ func (b *backendTestShim) UnmarshalDiscoverServerEntriesRequest(
 
 
 func (b *backendTestShim) MarshalDiscoverServerEntriesResponse(
 func (b *backendTestShim) MarshalDiscoverServerEntriesResponse(
 	versionedServerEntryTags []*struct {
 	versionedServerEntryTags []*struct {
-		Tag     []byte
-		Version int32
+		Tag            []byte
+		Version        int32
+		PrioritizeDial bool
 	}) (
 	}) (
 
 
 	cborResponse []byte,
 	cborResponse []byte,
@@ -102,8 +103,9 @@ func (b *backendTestShim) MarshalDiscoverServerEntriesResponse(
 	response := &DiscoverServerEntriesResponse{
 	response := &DiscoverServerEntriesResponse{
 		VersionedServerEntryTags: convertSlice[
 		VersionedServerEntryTags: convertSlice[
 			*struct {
 			*struct {
-				Tag     []byte
-				Version int32
+				Tag            []byte
+				Version        int32
+				PrioritizeDial bool
 			}, *VersionedServerEntryTag](versionedServerEntryTags),
 			}, *VersionedServerEntryTag](versionedServerEntryTags),
 	}
 	}
 
 

+ 39 - 33
psiphon/common/parameters/parameters.go

@@ -226,7 +226,7 @@ const (
 	ReplayTargetTunnelDuration                         = "ReplayTargetTunnelDuration"
 	ReplayTargetTunnelDuration                         = "ReplayTargetTunnelDuration"
 	ReplayLaterRoundMoveToFrontProbability             = "ReplayLaterRoundMoveToFrontProbability"
 	ReplayLaterRoundMoveToFrontProbability             = "ReplayLaterRoundMoveToFrontProbability"
 	ReplayRetainFailedProbability                      = "ReplayRetainFailedProbability"
 	ReplayRetainFailedProbability                      = "ReplayRetainFailedProbability"
-	ReplayIgnoreChangedConfigState                     = "ReplayIgnoreChangedConfigState"
+	ReplayIgnoreChangedConfigStateProbability          = "ReplayIgnoreChangedConfigStateProbability"
 	ReplayBPF                                          = "ReplayBPF"
 	ReplayBPF                                          = "ReplayBPF"
 	ReplaySSH                                          = "ReplaySSH"
 	ReplaySSH                                          = "ReplaySSH"
 	ReplayObfuscatorPadding                            = "ReplayObfuscatorPadding"
 	ReplayObfuscatorPadding                            = "ReplayObfuscatorPadding"
@@ -554,12 +554,15 @@ const (
 	DSLFetcherGetLastActiveOSLsTTL                     = "DSLFetcherGetLastActiveOSLsTTL"
 	DSLFetcherGetLastActiveOSLsTTL                     = "DSLFetcherGetLastActiveOSLsTTL"
 	DSLFetcherGetOSLFileSpecsMinCount                  = "DSLFetcherGetOSLFileSpecsMinCount"
 	DSLFetcherGetOSLFileSpecsMinCount                  = "DSLFetcherGetOSLFileSpecsMinCount"
 	DSLFetcherGetOSLFileSpecsMaxCount                  = "DSLFetcherGetOSLFileSpecsMaxCount"
 	DSLFetcherGetOSLFileSpecsMaxCount                  = "DSLFetcherGetOSLFileSpecsMaxCount"
+	DSLPrioritizeDialNewServerEntryProbability         = "DSLPrioritizeDialNewServerEntryProbability"
+	DSLPrioritizeDialExistingServerEntryProbability    = "DSLPrioritizeDialExistingServerEntryProbability"
 
 
 	// Retired parameters
 	// Retired parameters
 
 
 	ReplayRandomizedTLSProfile                = "ReplayRandomizedTLSProfile"
 	ReplayRandomizedTLSProfile                = "ReplayRandomizedTLSProfile"
 	InproxyAllBrokerPublicKeys                = "InproxyAllBrokerPublicKeys"
 	InproxyAllBrokerPublicKeys                = "InproxyAllBrokerPublicKeys"
 	InproxyTunnelProtocolSelectionProbability = "InproxyTunnelProtocolSelectionProbability"
 	InproxyTunnelProtocolSelectionProbability = "InproxyTunnelProtocolSelectionProbability"
+	ReplayIgnoreChangedConfigState            = "ReplayIgnoreChangedConfigState"
 )
 )
 
 
 const (
 const (
@@ -794,38 +797,39 @@ var defaultParameters = map[string]struct {
 	LivenessTestMinDownstreamBytes: {value: 0, minimum: 0},
 	LivenessTestMinDownstreamBytes: {value: 0, minimum: 0},
 	LivenessTestMaxDownstreamBytes: {value: 0, minimum: 0},
 	LivenessTestMaxDownstreamBytes: {value: 0, minimum: 0},
 
 
-	ReplayCandidateCount:                   {value: 10, minimum: -1},
-	ReplayDialParametersTTL:                {value: 24 * time.Hour, minimum: time.Duration(0)},
-	ReplayTargetUpstreamBytes:              {value: 0, minimum: 0},
-	ReplayTargetDownstreamBytes:            {value: 0, minimum: 0},
-	ReplayTargetTunnelDuration:             {value: 1 * time.Second, minimum: time.Duration(0)},
-	ReplayLaterRoundMoveToFrontProbability: {value: 0.0, minimum: 0.0},
-	ReplayRetainFailedProbability:          {value: 0.5, minimum: 0.0},
-	ReplayIgnoreChangedConfigState:         {value: false},
-	ReplayBPF:                              {value: true},
-	ReplaySSH:                              {value: true},
-	ReplayObfuscatorPadding:                {value: true},
-	ReplayFragmentor:                       {value: true},
-	ReplayTLSProfile:                       {value: true},
-	ReplayFronting:                         {value: true},
-	ReplayHostname:                         {value: true},
-	ReplayQUICVersion:                      {value: true},
-	ReplayObfuscatedQUIC:                   {value: true},
-	ReplayObfuscatedQUICNonceTransformer:   {value: true},
-	ReplayConjureRegistration:              {value: true},
-	ReplayConjureTransport:                 {value: true},
-	ReplayLivenessTest:                     {value: true},
-	ReplayUserAgent:                        {value: true},
-	ReplayAPIRequestPadding:                {value: true},
-	ReplayHoldOffTunnel:                    {value: true},
-	ReplayResolveParameters:                {value: true},
-	ReplayHTTPTransformerParameters:        {value: true},
-	ReplayOSSHSeedTransformerParameters:    {value: true},
-	ReplayOSSHPrefix:                       {value: true},
-	ReplayShadowsocksPrefix:                {value: true},
-	ReplayTLSFragmentClientHello:           {value: true},
-	ReplayInproxyWebRTC:                    {value: true},
-	ReplayInproxySTUN:                      {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},
+	ReplayTargetTunnelDuration:                {value: 1 * time.Second, minimum: time.Duration(0)},
+	ReplayLaterRoundMoveToFrontProbability:    {value: 0.0, minimum: 0.0},
+	ReplayRetainFailedProbability:             {value: 0.5, minimum: 0.0},
+	ReplayIgnoreChangedConfigState:            {value: false},
+	ReplayIgnoreChangedConfigStateProbability: {value: 0.0, minimum: 0.0},
+	ReplayBPF:                            {value: true},
+	ReplaySSH:                            {value: true},
+	ReplayObfuscatorPadding:              {value: true},
+	ReplayFragmentor:                     {value: true},
+	ReplayTLSProfile:                     {value: true},
+	ReplayFronting:                       {value: true},
+	ReplayHostname:                       {value: true},
+	ReplayQUICVersion:                    {value: true},
+	ReplayObfuscatedQUIC:                 {value: true},
+	ReplayObfuscatedQUICNonceTransformer: {value: true},
+	ReplayConjureRegistration:            {value: true},
+	ReplayConjureTransport:               {value: true},
+	ReplayLivenessTest:                   {value: true},
+	ReplayUserAgent:                      {value: true},
+	ReplayAPIRequestPadding:              {value: true},
+	ReplayHoldOffTunnel:                  {value: true},
+	ReplayResolveParameters:              {value: true},
+	ReplayHTTPTransformerParameters:      {value: true},
+	ReplayOSSHSeedTransformerParameters:  {value: true},
+	ReplayOSSHPrefix:                     {value: true},
+	ReplayShadowsocksPrefix:              {value: true},
+	ReplayTLSFragmentClientHello:         {value: true},
+	ReplayInproxyWebRTC:                  {value: true},
+	ReplayInproxySTUN:                    {value: true},
 
 
 	APIRequestUpstreamPaddingMinBytes:   {value: 0, minimum: 0},
 	APIRequestUpstreamPaddingMinBytes:   {value: 0, minimum: 0},
 	APIRequestUpstreamPaddingMaxBytes:   {value: 1024, minimum: 0},
 	APIRequestUpstreamPaddingMaxBytes:   {value: 1024, minimum: 0},
@@ -1182,6 +1186,8 @@ var defaultParameters = map[string]struct {
 	DSLFetcherGetLastActiveOSLsTTL:                    {value: 24 * time.Hour, minimum: time.Duration(0)},
 	DSLFetcherGetLastActiveOSLsTTL:                    {value: 24 * time.Hour, minimum: time.Duration(0)},
 	DSLFetcherGetOSLFileSpecsMinCount:                 {value: 1, minimum: 0},
 	DSLFetcherGetOSLFileSpecsMinCount:                 {value: 1, minimum: 0},
 	DSLFetcherGetOSLFileSpecsMaxCount:                 {value: 1, minimum: 0},
 	DSLFetcherGetOSLFileSpecsMaxCount:                 {value: 1, minimum: 0},
+	DSLPrioritizeDialNewServerEntryProbability:        {value: 0.5, minimum: 0.0},
+	DSLPrioritizeDialExistingServerEntryProbability:   {value: 0.25, minimum: 0.0},
 }
 }
 
 
 // IsServerSideOnly indicates if the parameter specified by name is used
 // IsServerSideOnly indicates if the parameter specified by name is used

+ 3 - 1
psiphon/common/protocol/packed.go

@@ -841,8 +841,10 @@ func init() {
 		// Specs: server.baseDialParams
 		// Specs: server.baseDialParams
 
 
 		{169, "server_entry_count", intConverter},
 		{169, "server_entry_count", intConverter},
+		{170, "replay_ignored_change", intConverter},
+		{171, "dsl_prioritized", intConverter},
 
 
-		// Next key value = 170
+		// Next key value = 172
 	}
 	}
 
 
 	for _, spec := range packedAPIParameterSpecs {
 	for _, spec := range packedAPIParameterSpecs {

+ 10 - 10
psiphon/config.go

@@ -848,14 +848,14 @@ type Config struct {
 	LivenessTestMaxDownstreamBytes *int                         `json:",omitempty"`
 	LivenessTestMaxDownstreamBytes *int                         `json:",omitempty"`
 
 
 	// ReplayCandidateCount and other Replay fields are for testing purposes.
 	// ReplayCandidateCount and other Replay fields are for testing purposes.
-	ReplayCandidateCount                   *int     `json:",omitempty"`
-	ReplayDialParametersTTLSeconds         *int     `json:",omitempty"`
-	ReplayTargetUpstreamBytes              *int     `json:",omitempty"`
-	ReplayTargetDownstreamBytes            *int     `json:",omitempty"`
-	ReplayTargetTunnelDurationSeconds      *int     `json:",omitempty"`
-	ReplayLaterRoundMoveToFrontProbability *float64 `json:",omitempty"`
-	ReplayRetainFailedProbability          *float64 `json:",omitempty"`
-	ReplayIgnoreChangedConfigState         *bool    `json:",omitempty"`
+	ReplayCandidateCount                      *int     `json:",omitempty"`
+	ReplayDialParametersTTLSeconds            *int     `json:",omitempty"`
+	ReplayTargetUpstreamBytes                 *int     `json:",omitempty"`
+	ReplayTargetDownstreamBytes               *int     `json:",omitempty"`
+	ReplayTargetTunnelDurationSeconds         *int     `json:",omitempty"`
+	ReplayLaterRoundMoveToFrontProbability    *float64 `json:",omitempty"`
+	ReplayRetainFailedProbability             *float64 `json:",omitempty"`
+	ReplayIgnoreChangedConfigStateProbability *float64 `json:",omitempty"`
 
 
 	// NetworkLatencyMultiplierMin and other NetworkLatencyMultiplier fields are
 	// NetworkLatencyMultiplierMin and other NetworkLatencyMultiplier fields are
 	// for testing purposes.
 	// for testing purposes.
@@ -2193,8 +2193,8 @@ func (config *Config) makeConfigParameters() map[string]interface{} {
 		applyParameters[parameters.ReplayRetainFailedProbability] = *config.ReplayRetainFailedProbability
 		applyParameters[parameters.ReplayRetainFailedProbability] = *config.ReplayRetainFailedProbability
 	}
 	}
 
 
-	if config.ReplayIgnoreChangedConfigState != nil {
-		applyParameters[parameters.ReplayIgnoreChangedConfigState] = *config.ReplayIgnoreChangedConfigState
+	if config.ReplayIgnoreChangedConfigStateProbability != nil {
+		applyParameters[parameters.ReplayIgnoreChangedConfigStateProbability] = *config.ReplayIgnoreChangedConfigStateProbability
 	}
 	}
 
 
 	if config.UseOnlyCustomTLSProfiles != nil {
 	if config.UseOnlyCustomTLSProfiles != nil {

+ 157 - 19
psiphon/dataStore.go

@@ -272,7 +272,18 @@ func datastoreUpdate(fn func(tx *datastoreTx) error) error {
 //
 //
 // If the server entry data is malformed, an alert notice is issued and
 // If the server entry data is malformed, an alert notice is issued and
 // the entry is skipped; no error is returned.
 // the entry is skipped; no error is returned.
-func StoreServerEntry(serverEntryFields protocol.ServerEntryFields, replaceIfExists bool) error {
+func StoreServerEntry(
+	serverEntryFields protocol.ServerEntryFields,
+	replaceIfExists bool) error {
+
+	return errors.Trace(
+		storeServerEntry(serverEntryFields, replaceIfExists, nil))
+}
+
+func storeServerEntry(
+	serverEntryFields protocol.ServerEntryFields,
+	replaceIfExists bool,
+	additionalUpdates func(tx *datastoreTx, serverEntryID []byte) error) error {
 
 
 	// TODO: call serverEntryFields.VerifySignature. At this time, we do not do
 	// TODO: call serverEntryFields.VerifySignature. At this time, we do not do
 	// this as not all server entries have an individual signature field. All
 	// this as not all server entries have an individual signature field. All
@@ -374,6 +385,13 @@ func StoreServerEntry(serverEntryFields protocol.ServerEntryFields, replaceIfExi
 			return errors.Trace(err)
 			return errors.Trace(err)
 		}
 		}
 
 
+		if additionalUpdates != nil {
+			err = additionalUpdates(tx, serverEntryID)
+			if err != nil {
+				return errors.Trace(err)
+			}
+		}
+
 		NoticeInfo("updated server %s", serverEntryFields.GetDiagnosticID())
 		NoticeInfo("updated server %s", serverEntryFields.GetDiagnosticID())
 
 
 		return nil
 		return nil
@@ -893,13 +911,22 @@ func (iterator *ServerEntryIterator) reset(isInitialRound bool) error {
 		// In the first round, or with some probability, move _potential_ replay
 		// In the first round, or with some probability, move _potential_ replay
 		// candidates to the front of the list (excepting the server affinity slot,
 		// 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
 		// if any). This move is post-shuffle so the order is still randomized. To
-		// save the memory overhead of unmarshalling all dial parameters, this
+		// save the memory overhead of unmarshaling all dial parameters, this
 		// operation just moves any server with a dial parameter record to the
 		// operation just moves any server with a dial parameter record to the
 		// front. Whether the dial parameter remains valid for replay -- TTL,
 		// front. Whether the dial parameter remains valid for replay -- TTL,
 		// tactics/config unchanged, etc. --- is checked later.
 		// tactics/config unchanged, etc. --- is checked later.
 		//
 		//
 		// TODO: move only up to parameters.ReplayCandidateCount to front?
 		// TODO: move only up to parameters.ReplayCandidateCount to front?
 
 
+		// The DSLPendingPrioritizeDial case also implicitly assumes that mere
+		// existence of dial parameters will move server entries to the front
+		// of the list. See MakeDialParameters and doDSLFetch for more
+		// details about the DSLPendingPrioritizeDial scheme.
+		//
+		// Limitation: the move-to-front could be balanced beween
+		// DSLPendingPrioritizeDial and regular replay cases, however that
+		// would require unmarshaling all dial parameters, which we are avoiding.
+
 		p := iterator.config.GetParameters().Get()
 		p := iterator.config.GetParameters().Get()
 
 
 		if !iterator.isPruneServerEntryIterator &&
 		if !iterator.isPruneServerEntryIterator &&
@@ -2623,33 +2650,131 @@ func DSLSetLastTunneledFetchTime(time time.Time) error {
 	return errors.Trace(err)
 	return errors.Trace(err)
 }
 }
 
 
+// dslLookupServerEntry returns the server entry ID for the specified server
+// entry tag version when there's locally stored server entry for that tag
+// and with the specified version. Otherwise, it returns nil.
+func dslLookupServerEntry(
+	tx *datastoreTx,
+	tag dsl.ServerEntryTag,
+	version int) ([]byte, error) {
+
+	serverEntryTags := tx.bucket(datastoreServerEntryTagsBucket)
+
+	serverEntryTagRecord := serverEntryTags.get(tag)
+	if serverEntryTagRecord == nil {
+		return nil, nil
+	}
+
+	serverEntryID, configurationVersion, err := getServerEntryTagRecord(
+		serverEntryTagRecord)
+	if err != nil {
+		return nil, errors.Trace(err)
+	}
+
+	if configurationVersion != version {
+		return nil, nil
+	}
+
+	return serverEntryID, nil
+}
+
+// dslPrioritizeDialServerEntry will create a DSLPendingPrioritizeDial
+// placeholder dial parameters for the specified server entry, unless a dial
+// params already exists. Any existing dial param isn't unmarshaled and
+// inspected -- even if it's a replay past its TTL, the existence of the
+// record already suffices to move the server entry to the front in a server
+// entry iterator shuffle.
+//
+// See MakeDialParameters for more details about the DSLPendingPrioritizeDial
+// scheme.
+func dslPrioritizeDialServerEntry(
+	tx *datastoreTx,
+	networkID string,
+	serverEntryID []byte) error {
+
+	dialParamsBucket := tx.bucket(datastoreDialParametersBucket)
+
+	key := makeDialParametersKey(serverEntryID, []byte(networkID))
+
+	if dialParamsBucket.get(key) != nil {
+		return nil
+	}
+
+	dialParams := &DialParameters{
+		DSLPendingPrioritizeDial: true,
+	}
+
+	record, err := json.Marshal(dialParams)
+	if err != nil {
+		return errors.Trace(err)
+	}
+
+	err = dialParamsBucket.put(key, record)
+	if err != nil {
+		return errors.Trace(err)
+	}
+
+	return nil
+}
+
 // DSLHasServerEntry returns whether the datastore contains the server entry
 // DSLHasServerEntry returns whether the datastore contains the server entry
 // with the specified tag and version. DSLHasServerEntry uses a fast lookup
 // with the specified tag and version. DSLHasServerEntry uses a fast lookup
 // which avoids unmarshaling server entries.
 // which avoids unmarshaling server entries.
-func DSLHasServerEntry(tag dsl.ServerEntryTag, version int) bool {
+func DSLHasServerEntry(
+	tag dsl.ServerEntryTag,
+	version int,
+	prioritizeDial bool,
+	networkID string) bool {
 
 
 	hasServerEntry := false
 	hasServerEntry := false
+	var err error
 
 
-	err := datastoreView(func(tx *datastoreTx) error {
+	if !prioritizeDial {
 
 
-		serverEntryTags := tx.bucket(datastoreServerEntryTagsBucket)
+		// Use a more concurrency-friendly view transaction when
+		// prioritizeDial is false and there's no possibility of a datastore
+		// update.
 
 
-		serverEntryTagRecord := serverEntryTags.get(tag)
+		err = datastoreView(func(tx *datastoreTx) error {
+
+			serverEntryID, err := dslLookupServerEntry(tx, tag, version)
+			if err != nil {
+				return errors.Trace(err)
+			}
+
+			hasServerEntry = (serverEntryID != nil)
 
 
-		if serverEntryTagRecord == nil {
-			hasServerEntry = false
 			return nil
 			return nil
-		}
+		})
 
 
-		_, configurationVersion, err := getServerEntryTagRecord(
-			serverEntryTagRecord)
-		if err != nil {
-			return errors.Trace(err)
-		}
+	} else {
 
 
-		hasServerEntry = (configurationVersion == version)
-		return nil
-	})
+		err = datastoreUpdate(func(tx *datastoreTx) error {
+
+			serverEntryID, err := dslLookupServerEntry(tx, tag, version)
+			if err != nil {
+				return errors.Trace(err)
+			}
+
+			hasServerEntry = (serverEntryID != nil)
+
+			// If the local datastore contains a server entry for the
+			// specified tag, but the version doesn't match, the
+			// prioritization will be skipped. In this case, the updated
+			// server entry will most likely be downloaded and
+			// DSLStoreServerEntry will apply the prioritization.
+
+			if hasServerEntry {
+				err := dslPrioritizeDialServerEntry(tx, networkID, serverEntryID)
+				if err != nil {
+					return errors.Trace(err)
+				}
+			}
+
+			return nil
+		})
+
+	}
 
 
 	if err != nil {
 	if err != nil {
 		NoticeWarning("DSLHasServerEntry failed: %s", errors.Trace(err))
 		NoticeWarning("DSLHasServerEntry failed: %s", errors.Trace(err))
@@ -2664,7 +2789,9 @@ func DSLHasServerEntry(tag dsl.ServerEntryTag, version int) bool {
 func DSLStoreServerEntry(
 func DSLStoreServerEntry(
 	serverEntrySignaturePublicKey string,
 	serverEntrySignaturePublicKey string,
 	packedServerEntryFields protocol.PackedServerEntryFields,
 	packedServerEntryFields protocol.PackedServerEntryFields,
-	source string) error {
+	source string,
+	prioritizeDial bool,
+	networkID string) error {
 
 
 	serverEntryFields, err := protocol.DecodePackedServerEntryFields(packedServerEntryFields)
 	serverEntryFields, err := protocol.DecodePackedServerEntryFields(packedServerEntryFields)
 	if err != nil {
 	if err != nil {
@@ -2687,7 +2814,18 @@ func DSLStoreServerEntry(
 		return errors.Trace(err)
 		return errors.Trace(err)
 	}
 	}
 
 
-	err = StoreServerEntry(serverEntryFields, true)
+	var additionalUpdates func(tx *datastoreTx, serverEntryID []byte) error
+	if prioritizeDial {
+		additionalUpdates = func(tx *datastoreTx, serverEntryID []byte) error {
+			err := dslPrioritizeDialServerEntry(tx, networkID, serverEntryID)
+			if err != nil {
+				return errors.Trace(err)
+			}
+			return nil
+		}
+	}
+
+	err = storeServerEntry(serverEntryFields, true, additionalUpdates)
 	if err != nil {
 	if err != nil {
 		return errors.Trace(err)
 		return errors.Trace(err)
 	}
 	}

+ 137 - 82
psiphon/dialParameters.go

@@ -68,94 +68,95 @@ type DialParameters struct {
 	ServerEntry             *protocol.ServerEntry `json:"-"`
 	ServerEntry             *protocol.ServerEntry `json:"-"`
 	NetworkID               string                `json:"-"`
 	NetworkID               string                `json:"-"`
 	IsReplay                bool                  `json:"-"`
 	IsReplay                bool                  `json:"-"`
+	ReplayIgnoredChange     bool                  `json:"-"`
 	CandidateNumber         int                   `json:"-"`
 	CandidateNumber         int                   `json:"-"`
 	EstablishedTunnelsCount int                   `json:"-"`
 	EstablishedTunnelsCount int                   `json:"-"`
 
 
-	IsExchanged bool
+	IsExchanged bool `json:",omitempty"`
 
 
-	LastUsedTimestamp       time.Time
-	LastUsedConfigStateHash []byte
-	LastUsedServerEntryHash []byte
+	LastUsedTimestamp       time.Time `json:",omitempty"`
+	LastUsedConfigStateHash []byte    `json:",omitempty"`
+	LastUsedServerEntryHash []byte    `json:",omitempty"`
 
 
-	NetworkLatencyMultiplier float64
+	NetworkLatencyMultiplier float64 `json:",omitempty"`
 
 
-	TunnelProtocol string
+	TunnelProtocol string `json:",omitempty"`
 
 
-	DirectDialAddress              string
-	DialPortNumber                 string
+	DirectDialAddress              string   `json:",omitempty"`
+	DialPortNumber                 string   `json:",omitempty"`
 	UpstreamProxyType              string   `json:"-"`
 	UpstreamProxyType              string   `json:"-"`
 	UpstreamProxyCustomHeaderNames []string `json:"-"`
 	UpstreamProxyCustomHeaderNames []string `json:"-"`
 
 
-	BPFProgramName         string
-	BPFProgramInstructions []bpf.RawInstruction
+	BPFProgramName         string               `json:",omitempty"`
+	BPFProgramInstructions []bpf.RawInstruction `json:",omitempty"`
 
 
-	SelectedSSHClientVersion bool
-	SSHClientVersion         string
-	SSHKEXSeed               *prng.Seed
+	SelectedSSHClientVersion bool       `json:",omitempty"`
+	SSHClientVersion         string     `json:",omitempty"`
+	SSHKEXSeed               *prng.Seed `json:",omitempty"`
 
 
-	ObfuscatorPaddingSeed                   *prng.Seed
-	OSSHObfuscatorSeedTransformerParameters *transforms.ObfuscatorSeedTransformerParameters
+	ObfuscatorPaddingSeed                   *prng.Seed                                      `json:",omitempty"`
+	OSSHObfuscatorSeedTransformerParameters *transforms.ObfuscatorSeedTransformerParameters `json:",omitempty"`
 
 
-	OSSHPrefixSpec        *obfuscator.OSSHPrefixSpec
-	OSSHPrefixSplitConfig *obfuscator.OSSHPrefixSplitConfig
+	OSSHPrefixSpec        *obfuscator.OSSHPrefixSpec        `json:",omitempty"`
+	OSSHPrefixSplitConfig *obfuscator.OSSHPrefixSplitConfig `json:",omitempty"`
 
 
-	ShadowsocksPrefixSpec *ShadowsocksPrefixSpec
+	ShadowsocksPrefixSpec *ShadowsocksPrefixSpec `json:",omitempty"`
 
 
-	FragmentorSeed *prng.Seed
+	FragmentorSeed *prng.Seed `json:",omitempty"`
 
 
-	FrontingProviderID string
+	FrontingProviderID string `json:",omitempty"`
 
 
-	MeekFrontingDialAddress   string
-	MeekFrontingHost          string
-	MeekDialAddress           string
-	MeekTransformedHostName   bool
-	MeekSNIServerName         string
-	MeekVerifyServerName      string
-	MeekVerifyPins            []string
-	MeekHostHeader            string
-	MeekObfuscatorPaddingSeed *prng.Seed
+	MeekFrontingDialAddress   string       `json:",omitempty"`
+	MeekFrontingHost          string       `json:",omitempty"`
+	MeekDialAddress           string       `json:",omitempty"`
+	MeekTransformedHostName   bool         `json:",omitempty"`
+	MeekSNIServerName         string       `json:",omitempty"`
+	MeekVerifyServerName      string       `json:",omitempty"`
+	MeekVerifyPins            []string     `json:",omitempty"`
+	MeekHostHeader            string       `json:",omitempty"`
+	MeekObfuscatorPaddingSeed *prng.Seed   `json:",omitempty"`
 	MeekResolvedIPAddress     atomic.Value `json:"-"`
 	MeekResolvedIPAddress     atomic.Value `json:"-"`
 
 
-	TLSOSSHTransformedSNIServerName bool
-	TLSOSSHSNIServerName            string
-	TLSOSSHObfuscatorPaddingSeed    *prng.Seed
-
-	SelectedUserAgent bool
-	UserAgent         string
-
-	SelectedTLSProfile       bool
-	TLSProfile               string
-	NoDefaultTLSSessionID    bool
-	TLSVersion               string
-	RandomizedTLSProfileSeed *prng.Seed
-	TLSFragmentClientHello   bool
-
-	QUICVersion                              string
-	QUICDialSNIAddress                       string
-	QUICClientHelloSeed                      *prng.Seed
-	ObfuscatedQUICPaddingSeed                *prng.Seed
-	ObfuscatedQUICNonceTransformerParameters *transforms.ObfuscatorSeedTransformerParameters
-	QUICDialEarly                            bool
-	QUICUseObfuscatedPSK                     bool
-	QUICDisablePathMTUDiscovery              bool
-	QUICMaxPacketSizeAdjustment              int
-
-	ConjureCachedRegistrationTTL        time.Duration
-	ConjureAPIRegistration              bool
-	ConjureAPIRegistrarBidirectionalURL string
-	ConjureAPIRegistrarDelay            time.Duration
-	ConjureDecoyRegistration            bool
-	ConjureDecoyRegistrarDelay          time.Duration
-	ConjureDecoyRegistrarWidth          int
-	ConjureTransport                    string
-	ConjureSTUNServerAddress            string
-	ConjureDTLSEmptyInitialPacket       bool
-
-	LivenessTestSeed *prng.Seed
-
-	APIRequestPaddingSeed *prng.Seed
-
-	HoldOffTunnelDuration time.Duration
+	TLSOSSHTransformedSNIServerName bool       `json:",omitempty"`
+	TLSOSSHSNIServerName            string     `json:",omitempty"`
+	TLSOSSHObfuscatorPaddingSeed    *prng.Seed `json:",omitempty"`
+
+	SelectedUserAgent bool   `json:",omitempty"`
+	UserAgent         string `json:",omitempty"`
+
+	SelectedTLSProfile       bool       `json:",omitempty"`
+	TLSProfile               string     `json:",omitempty"`
+	NoDefaultTLSSessionID    bool       `json:",omitempty"`
+	TLSVersion               string     `json:",omitempty"`
+	RandomizedTLSProfileSeed *prng.Seed `json:",omitempty"`
+	TLSFragmentClientHello   bool       `json:",omitempty"`
+
+	QUICVersion                              string                                          `json:",omitempty"`
+	QUICDialSNIAddress                       string                                          `json:",omitempty"`
+	QUICClientHelloSeed                      *prng.Seed                                      `json:",omitempty"`
+	ObfuscatedQUICPaddingSeed                *prng.Seed                                      `json:",omitempty"`
+	ObfuscatedQUICNonceTransformerParameters *transforms.ObfuscatorSeedTransformerParameters `json:",omitempty"`
+	QUICDialEarly                            bool                                            `json:",omitempty"`
+	QUICUseObfuscatedPSK                     bool                                            `json:",omitempty"`
+	QUICDisablePathMTUDiscovery              bool                                            `json:",omitempty"`
+	QUICMaxPacketSizeAdjustment              int                                             `json:",omitempty"`
+
+	ConjureCachedRegistrationTTL        time.Duration `json:",omitempty"`
+	ConjureAPIRegistration              bool          `json:",omitempty"`
+	ConjureAPIRegistrarBidirectionalURL string        `json:",omitempty"`
+	ConjureAPIRegistrarDelay            time.Duration `json:",omitempty"`
+	ConjureDecoyRegistration            bool          `json:",omitempty"`
+	ConjureDecoyRegistrarDelay          time.Duration `json:",omitempty"`
+	ConjureDecoyRegistrarWidth          int           `json:",omitempty"`
+	ConjureTransport                    string        `json:",omitempty"`
+	ConjureSTUNServerAddress            string        `json:",omitempty"`
+	ConjureDTLSEmptyInitialPacket       bool          `json:",omitempty"`
+
+	LivenessTestSeed *prng.Seed `json:",omitempty"`
+
+	APIRequestPaddingSeed *prng.Seed `json:",omitempty"`
+
+	HoldOffTunnelDuration time.Duration `json:",omitempty"`
 
 
 	DialConnMetrics          common.MetricsSource       `json:"-"`
 	DialConnMetrics          common.MetricsSource       `json:"-"`
 	DialConnNoticeMetrics    common.NoticeMetricsSource `json:"-"`
 	DialConnNoticeMetrics    common.NoticeMetricsSource `json:"-"`
@@ -163,15 +164,18 @@ type DialParameters struct {
 
 
 	DialDuration time.Duration `json:"-"`
 	DialDuration time.Duration `json:"-"`
 
 
-	resolver          *resolver.Resolver `json:"-"`
-	ResolveParameters *resolver.ResolveParameters
+	resolver          *resolver.Resolver          `json:"-"`
+	ResolveParameters *resolver.ResolveParameters `json:",omitempty"`
 
 
-	HTTPTransformerParameters *transforms.HTTPTransformerParameters
+	HTTPTransformerParameters *transforms.HTTPTransformerParameters `json:",omitempty"`
 
 
-	SteeringIP         string
+	SteeringIP         string          `json:",omitempty"`
 	steeringIPCache    *lrucache.Cache `json:"-"`
 	steeringIPCache    *lrucache.Cache `json:"-"`
 	steeringIPCacheKey string          `json:"-"`
 	steeringIPCacheKey string          `json:"-"`
 
 
+	DSLPendingPrioritizeDial bool `json:",omitempty"`
+	DSLPrioritizedDial       bool `json:",omitempty"`
+
 	quicTLSClientSessionCache *common.TLSClientSessionCacheWrapper  `json:"-"`
 	quicTLSClientSessionCache *common.TLSClientSessionCacheWrapper  `json:"-"`
 	tlsClientSessionCache     *common.UtlsClientSessionCacheWrapper `json:"-"`
 	tlsClientSessionCache     *common.UtlsClientSessionCacheWrapper `json:"-"`
 
 
@@ -180,9 +184,9 @@ type DialParameters struct {
 	inproxyBrokerDialParameters    *InproxyBrokerDialParameters `json:"-"`
 	inproxyBrokerDialParameters    *InproxyBrokerDialParameters `json:"-"`
 	inproxyPackedSignedServerEntry []byte                       `json:"-"`
 	inproxyPackedSignedServerEntry []byte                       `json:"-"`
 	inproxyNATStateManager         *InproxyNATStateManager      `json:"-"`
 	inproxyNATStateManager         *InproxyNATStateManager      `json:"-"`
-	InproxySTUNDialParameters      *InproxySTUNDialParameters
-	InproxyWebRTCDialParameters    *InproxyWebRTCDialParameters
-	inproxyConn                    atomic.Value `json:"-"`
+	InproxySTUNDialParameters      *InproxySTUNDialParameters   `json:",omitempty"`
+	InproxyWebRTCDialParameters    *InproxyWebRTCDialParameters `json:",omitempty"`
+	inproxyConn                    atomic.Value                 `json:"-"`
 
 
 	dialConfig *DialConfig `json:"-"`
 	dialConfig *DialConfig `json:"-"`
 	meekConfig *MeekConfig `json:"-"`
 	meekConfig *MeekConfig `json:"-"`
@@ -230,7 +234,12 @@ func MakeDialParameters(
 	p := config.GetParameters().Get()
 	p := config.GetParameters().Get()
 
 
 	ttl := p.Duration(parameters.ReplayDialParametersTTL)
 	ttl := p.Duration(parameters.ReplayDialParametersTTL)
-	replayIgnoreChangedConfigState := p.Bool(parameters.ReplayIgnoreChangedConfigState)
+
+	// Replay ignoring tactics changes with a probability allows for a mix of
+	// sticking with replay and exploring use of new tactics.
+	replayIgnoreChangedConfigState := p.WeightedCoinFlip(
+		parameters.ReplayIgnoreChangedConfigStateProbability)
+
 	replayBPF := p.Bool(parameters.ReplayBPF)
 	replayBPF := p.Bool(parameters.ReplayBPF)
 	replaySSH := p.Bool(parameters.ReplaySSH)
 	replaySSH := p.Bool(parameters.ReplaySSH)
 	replayObfuscatorPadding := p.Bool(parameters.ReplayObfuscatorPadding)
 	replayObfuscatorPadding := p.Bool(parameters.ReplayObfuscatorPadding)
@@ -266,6 +275,39 @@ func MakeDialParameters(
 		// Proceed, without existing dial parameters.
 		// Proceed, without existing dial parameters.
 	}
 	}
 
 
+	// DSLPendingPrioritizeDial is a placeholder which indicates that the
+	// server entry was prioritized for selection due to a hint from the DSL
+	// backend. No other dial parameters are set in the placeholder.
+	// Prioritized selection is implemented by storing a
+	// DSLPendingPrioritizeDial dial parameters record, and relying on the
+	// move-to-front logic in the server entry iterator shuffle.
+	//
+	// Once selected, reset the DSLPendingPrioritizeDial placeholder and
+	// select new dial parameters. The DSLPrioritizedDial field is set and
+	// used to record dsl_prioritized metrics indicating that the dial was
+	// DSL prioritized. The DSLPrioritizedDial flag is retained, and
+	// dsl_prioritized reported, as long as the dial parameters are
+	// successfully replayed. Once the replay ends, the
+	// DSLPrioritizedDial/dsl_prioritized state is dropped.
+	//
+	// Currently there is no specific TTL for a DSLPendingPrioritizeDial
+	// placeholder, since the iterator shuffle move-to-front has taken place
+	// already, before the dial parameters is unmarshaled.
+	//
+	// The isTactics case is not excluded from this DSLPrioritizedDial logic,
+	// since a DSLPendingPrioritizeDial placeholder may be created for a
+	// TACTICS-capable server entry. Note that tactics doesn't invoke
+	// DialParameters.Succeed to replay, and will only replay if the same
+	// server entry happens to have been used for a tunnel protocol. See
+	// fetchTactics.
+
+	DSLPendingPrioritizeDial := false
+	DSLPrioritizedDial := false
+	if dialParams != nil {
+		DSLPendingPrioritizeDial = dialParams.DSLPendingPrioritizeDial
+		DSLPrioritizedDial = dialParams.DSLPrioritizedDial
+	}
+
 	// Check if replay is permitted:
 	// Check if replay is permitted:
 	// - TTL must be > 0 and existing dial parameters must not have expired
 	// - TTL must be > 0 and existing dial parameters must not have expired
 	//   as indicated by LastUsedTimestamp + TTL.
 	//   as indicated by LastUsedTimestamp + TTL.
@@ -282,6 +324,7 @@ func MakeDialParameters(
 	var currentTimestamp time.Time
 	var currentTimestamp time.Time
 	var configStateHash []byte
 	var configStateHash []byte
 	var serverEntryHash []byte
 	var serverEntryHash []byte
+	var configChanged bool
 
 
 	// When TTL is 0, replay is disabled; the timestamp remains 0 and the
 	// When TTL is 0, replay is disabled; the timestamp remains 0 and the
 	// output DialParameters will not be stored by Success.
 	// output DialParameters will not be stored by Success.
@@ -289,12 +332,18 @@ func MakeDialParameters(
 	if ttl > 0 {
 	if ttl > 0 {
 		currentTimestamp = time.Now()
 		currentTimestamp = time.Now()
 		configStateHash, serverEntryHash = getDialStateHashes(config, p, serverEntry)
 		configStateHash, serverEntryHash = getDialStateHashes(config, p, serverEntry)
+
+		configChanged = dialParams != nil && !bytes.Equal(
+			dialParams.LastUsedConfigStateHash, configStateHash)
 	}
 	}
 
 
 	if dialParams != nil &&
 	if dialParams != nil &&
 		(ttl <= 0 ||
 		(ttl <= 0 ||
 			dialParams.LastUsedTimestamp.Before(currentTimestamp.Add(-ttl)) ||
 			dialParams.LastUsedTimestamp.Before(currentTimestamp.Add(-ttl)) ||
 
 
+			// Replace DSL prioritize placeholder.
+			dialParams.DSLPendingPrioritizeDial ||
+
 			// Replay is disabled when the current config state hash -- config
 			// Replay is disabled when the current config state hash -- config
 			// dial parameters and the current tactics tag -- have changed
 			// dial parameters and the current tactics tag -- have changed
 			// since the last dial. This prioritizes applying any potential
 			// since the last dial. This prioritizes applying any potential
@@ -302,17 +351,16 @@ func MakeDialParameters(
 			// changed in tactics.
 			// changed in tactics.
 			//
 			//
 			// Because of this, frequent tactics changes may degrade replay
 			// Because of this, frequent tactics changes may degrade replay
-			// effectiveness. When ReplayIgnoreChangedConfigState is set,
+			// effectiveness. When replayIgnoreChangedConfigState is set,
 			// differences in the config state hash are ignored.
 			// differences in the config state hash are ignored.
 			//
 			//
 			// Limitation: some code which previously assumed that replay
 			// Limitation: some code which previously assumed that replay
 			// always implied unchanged tactics parameters may now use newer
 			// always implied unchanged tactics parameters may now use newer
 			// tactics parameters in replay cases when
 			// tactics parameters in replay cases when
-			// ReplayIgnoreChangedConfigState is set. One case is the call
+			// replayIgnoreChangedConfigState is set. One case is the call
 			// below to fragmentor.NewUpstreamConfig, made when initializing
 			// below to fragmentor.NewUpstreamConfig, made when initializing
 			// dialParams.dialConfig.
 			// dialParams.dialConfig.
-			(!replayIgnoreChangedConfigState &&
-				!bytes.Equal(dialParams.LastUsedConfigStateHash, configStateHash)) ||
+			(!replayIgnoreChangedConfigState && configChanged) ||
 
 
 			// Replay is disabled when the server entry has changed.
 			// Replay is disabled when the server entry has changed.
 			!bytes.Equal(dialParams.LastUsedServerEntryHash, serverEntryHash) ||
 			!bytes.Equal(dialParams.LastUsedServerEntryHash, serverEntryHash) ||
@@ -406,9 +454,16 @@ func MakeDialParameters(
 	dialParams.ServerEntry = serverEntry
 	dialParams.ServerEntry = serverEntry
 	dialParams.NetworkID = networkID
 	dialParams.NetworkID = networkID
 	dialParams.IsReplay = isReplay
 	dialParams.IsReplay = isReplay
+	dialParams.ReplayIgnoredChange = isReplay && configChanged
 	dialParams.CandidateNumber = candidateNumber
 	dialParams.CandidateNumber = candidateNumber
 	dialParams.EstablishedTunnelsCount = establishedTunnelsCount
 	dialParams.EstablishedTunnelsCount = establishedTunnelsCount
 
 
+	// Set the DSLPrioritizedDial flag for metrics. The flag is set after
+	// replacing the pending placholder and retained as long as the dial
+	// parameters are replayed.
+	dialParams.DSLPrioritizedDial =
+		DSLPendingPrioritizeDial || (isReplay && DSLPrioritizedDial)
+
 	// Even when replaying, LastUsedTimestamp is updated to extend the TTL of
 	// Even when replaying, LastUsedTimestamp is updated to extend the TTL of
 	// replayed dial parameters which will be updated in the datastore upon
 	// replayed dial parameters which will be updated in the datastore upon
 	// success.
 	// success.
@@ -1647,7 +1702,7 @@ func MakeDialParameters(
 	// Fragmentor configuration.
 	// Fragmentor configuration.
 	// Note: fragmentorConfig is nil if fragmentor is disabled for prefixed OSSH.
 	// Note: fragmentorConfig is nil if fragmentor is disabled for prefixed OSSH.
 	//
 	//
-	// Limitation: when replaying and with ReplayIgnoreChangedConfigState set,
+	// Limitation: when replaying and with replayIgnoreChangedConfigState set,
 	// fragmentor.NewUpstreamConfig may select a config using newer tactics
 	// fragmentor.NewUpstreamConfig may select a config using newer tactics
 	// parameters.
 	// parameters.
 	fragmentorConfig := fragmentor.NewUpstreamConfig(p, dialParams.TunnelProtocol, dialParams.FragmentorSeed)
 	fragmentorConfig := fragmentor.NewUpstreamConfig(p, dialParams.TunnelProtocol, dialParams.FragmentorSeed)

+ 97 - 4
psiphon/dialParameters_test.go

@@ -313,6 +313,10 @@ func runDialParametersAndReplay(t *testing.T, tunnelProtocol string) {
 		t.Fatalf("unexpected replay")
 		t.Fatalf("unexpected replay")
 	}
 	}
 
 
+	if dialParams.ReplayIgnoredChange {
+		t.Fatalf("unexpected replay ignored change")
+	}
+
 	// Test: no replay after network ID changes
 	// Test: no replay after network ID changes
 
 
 	dialParams.Succeeded()
 	dialParams.Succeeded()
@@ -334,6 +338,9 @@ func runDialParametersAndReplay(t *testing.T, tunnelProtocol string) {
 		t.Fatalf("unexpected replay")
 		t.Fatalf("unexpected replay")
 	}
 	}
 
 
+	if dialParams.ReplayIgnoredChange {
+		t.Fatalf("unexpected replay ignored change")
+	}
 	// Test: replay after dial reported to succeed, and replay fields match previous dial parameters
 	// Test: replay after dial reported to succeed, and replay fields match previous dial parameters
 
 
 	dialParams.Succeeded()
 	dialParams.Succeeded()
@@ -348,6 +355,10 @@ func runDialParametersAndReplay(t *testing.T, tunnelProtocol string) {
 		t.Fatalf("unexpected non-replay")
 		t.Fatalf("unexpected non-replay")
 	}
 	}
 
 
+	if replayDialParams.ReplayIgnoredChange {
+		t.Fatalf("unexpected replay ignored change")
+	}
+
 	if !replayDialParams.LastUsedTimestamp.After(dialParams.LastUsedTimestamp) {
 	if !replayDialParams.LastUsedTimestamp.After(dialParams.LastUsedTimestamp) {
 		t.Fatalf("unexpected non-updated timestamp")
 		t.Fatalf("unexpected non-updated timestamp")
 	}
 	}
@@ -447,10 +458,10 @@ func runDialParametersAndReplay(t *testing.T, tunnelProtocol string) {
 		t.Fatalf("mismatching ShadowsocksPrefixSpec fields")
 		t.Fatalf("mismatching ShadowsocksPrefixSpec fields")
 	}
 	}
 
 
-	// Test: replay after change tactics, with ReplayIgnoreChangedClientState = true
+	// Test: replay after change tactics, with ReplayIgnoreChangedConfigStateProbability = 1.0
 
 
 	applyParameters[parameters.ReplayDialParametersTTL] = "1s"
 	applyParameters[parameters.ReplayDialParametersTTL] = "1s"
-	applyParameters[parameters.ReplayIgnoreChangedConfigState] = true
+	applyParameters[parameters.ReplayIgnoreChangedConfigStateProbability] = 1.0
 	err = clientConfig.SetParameters("tag2a", false, applyParameters)
 	err = clientConfig.SetParameters("tag2a", false, applyParameters)
 	if err != nil {
 	if err != nil {
 		t.Fatalf("SetParameters failed: %s", err)
 		t.Fatalf("SetParameters failed: %s", err)
@@ -462,14 +473,18 @@ func runDialParametersAndReplay(t *testing.T, tunnelProtocol string) {
 		t.Fatalf("MakeDialParameters failed: %s", err)
 		t.Fatalf("MakeDialParameters failed: %s", err)
 	}
 	}
 
 
-	if !replayDialParams.IsReplay {
+	if !dialParams.IsReplay {
 		t.Fatalf("unexpected non-replay")
 		t.Fatalf("unexpected non-replay")
 	}
 	}
 
 
+	if !dialParams.ReplayIgnoredChange {
+		t.Fatalf("unexpected replay ignored change")
+	}
+
 	// Test: no replay after change tactics
 	// Test: no replay after change tactics
 
 
 	applyParameters[parameters.ReplayDialParametersTTL] = "1s"
 	applyParameters[parameters.ReplayDialParametersTTL] = "1s"
-	applyParameters[parameters.ReplayIgnoreChangedConfigState] = false
+	applyParameters[parameters.ReplayIgnoreChangedConfigStateProbability] = 0.0
 	err = clientConfig.SetParameters("tag2", false, applyParameters)
 	err = clientConfig.SetParameters("tag2", false, applyParameters)
 	if err != nil {
 	if err != nil {
 		t.Fatalf("SetParameters failed: %s", err)
 		t.Fatalf("SetParameters failed: %s", err)
@@ -485,6 +500,10 @@ func runDialParametersAndReplay(t *testing.T, tunnelProtocol string) {
 		t.Fatalf("unexpected replay")
 		t.Fatalf("unexpected replay")
 	}
 	}
 
 
+	if dialParams.ReplayIgnoredChange {
+		t.Fatalf("unexpected replay ignored change")
+	}
+
 	// Test: no replay after dial parameters expired
 	// Test: no replay after dial parameters expired
 
 
 	dialParams.Succeeded()
 	dialParams.Succeeded()
@@ -501,6 +520,10 @@ func runDialParametersAndReplay(t *testing.T, tunnelProtocol string) {
 		t.Fatalf("unexpected replay")
 		t.Fatalf("unexpected replay")
 	}
 	}
 
 
+	if dialParams.ReplayIgnoredChange {
+		t.Fatalf("unexpected replay ignored change")
+	}
+
 	// Test: no replay after server entry changes
 	// Test: no replay after server entry changes
 
 
 	dialParams.Succeeded()
 	dialParams.Succeeded()
@@ -517,6 +540,10 @@ func runDialParametersAndReplay(t *testing.T, tunnelProtocol string) {
 		t.Fatalf("unexpected replay")
 		t.Fatalf("unexpected replay")
 	}
 	}
 
 
+	if dialParams.ReplayIgnoredChange {
+		t.Fatalf("unexpected replay ignored change")
+	}
+
 	// Test: disable replay elements (partial coverage)
 	// Test: disable replay elements (partial coverage)
 
 
 	applyParameters[parameters.ReplayDialParametersTTL] = "24h"
 	applyParameters[parameters.ReplayDialParametersTTL] = "24h"
@@ -771,6 +798,72 @@ func runDialParametersAndReplay(t *testing.T, tunnelProtocol string) {
 		}
 		}
 	}
 	}
 
 
+	// Test: DSLPendingPrioritizeDial placeholder transformed to full dial parameters
+
+	networkID := clientConfig.GetNetworkID()
+
+	err = datastoreUpdate(func(tx *datastoreTx) error {
+		return dslPrioritizeDialServerEntry(
+			tx, networkID, []byte(serverEntries[1].IpAddress))
+	})
+	if err != nil {
+		t.Fatalf("dslPrioritizeDialServerEntry failed: %s", err)
+	}
+
+	dialParams, err = MakeDialParameters(
+		clientConfig, steeringIPCache, nil, nil, nil, canReplay, selectProtocol, serverEntries[1], nil, nil, false, 0, 0)
+	if err != nil {
+		t.Fatalf("MakeDialParameters failed: %s", err)
+	}
+
+	if dialParams.DSLPendingPrioritizeDial || !dialParams.DSLPrioritizedDial {
+		t.Fatalf("unexpected DSL prioritize state")
+	}
+
+	if dialParams.IsReplay {
+		t.Fatalf("unexpected replay")
+	}
+
+	dialParams.Succeeded()
+
+	dialParams, err = MakeDialParameters(
+		clientConfig, steeringIPCache, nil, nil, nil, canReplay, selectProtocol, serverEntries[1], nil, nil, false, 0, 0)
+	if err != nil {
+		t.Fatalf("MakeDialParameters failed: %s", err)
+	}
+
+	if dialParams.DSLPendingPrioritizeDial || !dialParams.DSLPrioritizedDial {
+		t.Fatalf("unexpected DSL prioritize state")
+	}
+
+	if !dialParams.IsReplay {
+		t.Fatalf("unexpected non-replay")
+	}
+
+	// Test: DSLPendingPrioritizeDial placeholder doesn't replace full dial parameters
+
+	err = datastoreUpdate(func(tx *datastoreTx) error {
+		return dslPrioritizeDialServerEntry(
+			tx, networkID, []byte(serverEntries[1].IpAddress))
+	})
+	if err != nil {
+		t.Fatalf("dslPrioritizeDialServerEntry failed: %s", err)
+	}
+
+	dialParams, err = MakeDialParameters(
+		clientConfig, steeringIPCache, nil, nil, nil, canReplay, selectProtocol, serverEntries[1], nil, nil, false, 0, 0)
+	if err != nil {
+		t.Fatalf("MakeDialParameters failed: %s", err)
+	}
+
+	if dialParams.DSLPendingPrioritizeDial || !dialParams.DSLPrioritizedDial {
+		t.Fatalf("unexpected DSL prioritize state")
+	}
+
+	if !dialParams.IsReplay {
+		t.Fatalf("unexpected non-replay")
+	}
+
 	// Test: iterator shuffles
 	// Test: iterator shuffles
 
 
 	for i, serverEntry := range serverEntries {
 	for i, serverEntry := range serverEntries {

+ 74 - 9
psiphon/dsl.go

@@ -56,9 +56,12 @@ fetcherLoop:
 
 
 		isTunneled := false
 		isTunneled := false
 
 
+		// Log the error notice for all errors in this block.
 		err := func() error {
 		err := func() error {
 
 
-			brokerClient, _, err := brokerClientManager.GetBrokerClient(config.GetNetworkID())
+			networkID := config.GetNetworkID()
+
+			brokerClient, _, err := brokerClientManager.GetBrokerClient(networkID)
 			if err != nil {
 			if err != nil {
 				return errors.Trace(err)
 				return errors.Trace(err)
 			}
 			}
@@ -87,7 +90,7 @@ fetcherLoop:
 			// TODO: add a failed_dsl_request log, similar to failed_tunnel,
 			// TODO: add a failed_dsl_request log, similar to failed_tunnel,
 			// to record and report failures?
 			// to record and report failures?
 
 
-			err = doDSLFetch(ctx, config, isTunneled, roundTripper)
+			err = doDSLFetch(ctx, config, networkID, isTunneled, roundTripper)
 			if err != nil {
 			if err != nil {
 				return errors.Trace(err)
 				return errors.Trace(err)
 			}
 			}
@@ -129,6 +132,8 @@ fetcherLoop:
 
 
 		isTunneled := true
 		isTunneled := true
 
 
+		networkID := config.GetNetworkID()
+
 		roundTripper := func(
 		roundTripper := func(
 			ctx context.Context,
 			ctx context.Context,
 			requestPayload []byte) ([]byte, error) {
 			requestPayload []byte) ([]byte, error) {
@@ -147,7 +152,7 @@ fetcherLoop:
 		// Detailed logging, retries, last request times, and
 		// Detailed logging, retries, last request times, and
 		// WaitForNetworkConnectivity are all handled inside dsl.Fetcher.
 		// WaitForNetworkConnectivity are all handled inside dsl.Fetcher.
 
 
-		err := doDSLFetch(ctx, config, isTunneled, roundTripper)
+		err := doDSLFetch(ctx, config, networkID, isTunneled, roundTripper)
 		if err != nil {
 		if err != nil {
 			NoticeError("tunneled DSL fetch failed: %v", errors.Trace(err))
 			NoticeError("tunneled DSL fetch failed: %v", errors.Trace(err))
 			// No cooldown pause, since runTunneledDSLFetcher is called only
 			// No cooldown pause, since runTunneledDSLFetcher is called only
@@ -161,6 +166,7 @@ fetcherLoop:
 func doDSLFetch(
 func doDSLFetch(
 	ctx context.Context,
 	ctx context.Context,
 	config *Config,
 	config *Config,
+	networkID string,
 	isTunneled bool,
 	isTunneled bool,
 	roundTripper dsl.FetcherRoundTripper) error {
 	roundTripper dsl.FetcherRoundTripper) error {
 
 
@@ -200,13 +206,72 @@ func doDSLFetch(
 		return key
 		return key
 	}
 	}
 
 
+	// hasServerEntry and storeServerEntry handle PrioritizeDial hints from
+	// the DSL backend for existing or new server entries respectively.
+	//
+	// In each case, a probability is applied to tune the rate of DSL
+	// prioritization since it impacts the rate of replay. DSL
+	// prioritizations don't _replace_ existing replay dial parameter
+	// records, but new DSLPendingPrioritizeDial dial parameters
+	// can _displace_ regular replays in the move-to-front server entry
+	// iterator shuffle. It's not clear a priori which out of replay or DSL
+	// prioritization is the optimal choice; the intention is to try a mix.
+	//
+	// When there's already an existing replay dial parameters for a server
+	// entry, no DSLPendingPrioritizeDial placeholder is created since any
+	// record suffices to move-to-front, and a non-expired replay dial
+	// parameters record can be more useful. As a result, there's no
+	// dsl_prioritized metric reported for cases where the client is already
+	// going to prioritize selecting a server entry.
+	//
+	// Limitation: For existing server entries, the client could already know
+	// that the server entry is not successful, but that knowledge is not
+	// applied here; instead, DSLPrioritizeDialExistingServerEntryProbability
+	// can merely be tuned lower. There could be failed_tunnel persistent
+	// stats with the server entry tag, but that data is only temporary, is
+	// truncated aggressively, and is expensive to unmarshal and process. A
+	// potential future enhancement would be to store a less ephemeral and
+	// simpler record of recent failures.
+	//
+	// Another potential future enhancement may be to count the number of
+	// existing replay records, including a TTL check, and use that count to
+	// adjust the rate of creating DSLPendingPrioritizeDial records.
+
+	prioritizeDialNewServerEntryProbability :=
+		p.Float(parameters.DSLPrioritizeDialNewServerEntryProbability)
+	prioritizeDialExistingServerEntryProbability :=
+		p.Float(parameters.DSLPrioritizeDialExistingServerEntryProbability)
+
+	hasServerEntry := func(
+		tag dsl.ServerEntryTag,
+		version int,
+		prioritizeDial bool) bool {
+
+		prioritizeDial = prioritizeDial &&
+			prng.FlipWeightedCoin(prioritizeDialExistingServerEntryProbability)
+
+		return DSLHasServerEntry(
+			tag,
+			version,
+			prioritizeDial,
+			networkID)
+	}
+
 	storeServerEntry := func(
 	storeServerEntry := func(
 		packedServerEntryFields protocol.PackedServerEntryFields,
 		packedServerEntryFields protocol.PackedServerEntryFields,
-		source string) error {
-		return errors.Trace(DSLStoreServerEntry(
-			config.ServerEntrySignaturePublicKey,
-			packedServerEntryFields,
-			source))
+		source string,
+		prioritizeDial bool) error {
+
+		prioritizeDial = prioritizeDial &&
+			prng.FlipWeightedCoin(prioritizeDialNewServerEntryProbability)
+
+		return errors.Trace(
+			DSLStoreServerEntry(
+				config.ServerEntrySignaturePublicKey,
+				packedServerEntryFields,
+				source,
+				prioritizeDial,
+				networkID))
 	}
 	}
 
 
 	c := &dsl.FetcherConfig{
 	c := &dsl.FetcherConfig{
@@ -216,7 +281,7 @@ func doDSLFetch(
 		Tunneled:          isTunneled,
 		Tunneled:          isTunneled,
 		RoundTripper:      roundTripper,
 		RoundTripper:      roundTripper,
 
 
-		DatastoreHasServerEntry:        DSLHasServerEntry,
+		DatastoreHasServerEntry:        hasServerEntry,
 		DatastoreStoreServerEntry:      storeServerEntry,
 		DatastoreStoreServerEntry:      storeServerEntry,
 		DatastoreGetLastActiveOSLsTime: DSLGetLastActiveOSLsTime,
 		DatastoreGetLastActiveOSLsTime: DSLGetLastActiveOSLsTime,
 		DatastoreSetLastActiveOSLsTime: DSLSetLastActiveOSLsTime,
 		DatastoreSetLastActiveOSLsTime: DSLSetLastActiveOSLsTime,

+ 1 - 1
psiphon/exchange.go

@@ -242,7 +242,7 @@ func importExchangePayload(config *Config, encodedPayload string) error {
 
 
 	// The following sequence of datastore calls -- StoreServerEntry,
 	// The following sequence of datastore calls -- StoreServerEntry,
 	// PromoteServerEntry, SetDialParameters -- is not an atomic transaction but
 	// PromoteServerEntry, SetDialParameters -- is not an atomic transaction but
-	// the  datastore will end up in a consistent state in case of failure to
+	// the datastore will end up in a consistent state in case of failure to
 	// complete the sequence. The existing calls are reused to avoid redundant
 	// complete the sequence. The existing calls are reused to avoid redundant
 	// code.
 	// code.
 	//
 	//

+ 31 - 12
psiphon/internal/testutils/dsl.go

@@ -67,8 +67,9 @@ type DSLBackendTestShim interface {
 
 
 	MarshalDiscoverServerEntriesResponse(
 	MarshalDiscoverServerEntriesResponse(
 		versionedServerEntryTags []*struct {
 		versionedServerEntryTags []*struct {
-			Tag     []byte
-			Version int32
+			Tag            []byte
+			Version        int32
+			PrioritizeDial bool
 		}) (
 		}) (
 		cborResponse []byte,
 		cborResponse []byte,
 		retErr error)
 		retErr error)
@@ -125,6 +126,7 @@ type TestDSLBackend struct {
 type dslSourcedServerEntry struct {
 type dslSourcedServerEntry struct {
 	ServerEntryFields protocol.PackedServerEntryFields
 	ServerEntryFields protocol.PackedServerEntryFields
 	Source            string
 	Source            string
+	PrioritizeDial    bool
 }
 }
 
 
 func NewTestDSLBackend(
 func NewTestDSLBackend(
@@ -207,6 +209,7 @@ func NewTestDSLBackend(
 			serverEntries[serverEntry.Tag] = &dslSourcedServerEntry{
 			serverEntries[serverEntry.Tag] = &dslSourcedServerEntry{
 				ServerEntryFields: packed,
 				ServerEntryFields: packed,
 				Source:            source,
 				Source:            source,
+				PrioritizeDial:    prng.FlipCoin(),
 			}
 			}
 
 
 			initMutex.Unlock()
 			initMutex.Unlock()
@@ -373,8 +376,24 @@ func (b *TestDSLBackend) GetServerEntryCount(isTunneled bool) int {
 	return len(b.untunneledServerEntries)
 	return len(b.untunneledServerEntries)
 }
 }
 
 
+func (b *TestDSLBackend) GetServerEntryProperties(
+	serverEntryTag string) (string, bool, error) {
+
+	entry, ok := b.untunneledServerEntries[serverEntryTag]
+	if !ok {
+		entry, ok = b.tunneledServerEntries[serverEntryTag]
+		if !ok {
+			return "", false, errors.TraceNew("unknown server entry tag")
+		}
+	}
+
+	return entry.Source, entry.PrioritizeDial, nil
+}
+
 func (b *TestDSLBackend) SetServerEntries(
 func (b *TestDSLBackend) SetServerEntries(
-	isTunneled bool, encodedServerEntries []string) error {
+	isTunneled bool,
+	prioritizeDial bool,
+	encodedServerEntries []string) error {
 
 
 	source := "DSL-untunneled"
 	source := "DSL-untunneled"
 	if isTunneled {
 	if isTunneled {
@@ -396,6 +415,7 @@ func (b *TestDSLBackend) SetServerEntries(
 		sourcedServerEntries[serverEntryFields.GetTag()] = &dslSourcedServerEntry{
 		sourcedServerEntries[serverEntryFields.GetTag()] = &dslSourcedServerEntry{
 			ServerEntryFields: packedServerEntryFields,
 			ServerEntryFields: packedServerEntryFields,
 			Source:            source,
 			Source:            source,
+			PrioritizeDial:    prioritizeDial,
 		}
 		}
 	}
 	}
 
 
@@ -453,14 +473,15 @@ func (b *TestDSLBackend) handleDiscoverServerEntries(
 	}
 	}
 
 
 	var versionedServerEntryTags []*struct {
 	var versionedServerEntryTags []*struct {
-		Tag     []byte
-		Version int32
+		Tag            []byte
+		Version        int32
+		PrioritizeDial bool
 	}
 	}
 
 
 	if !missingOSLs {
 	if !missingOSLs {
 
 
 		count := 0
 		count := 0
-		for tag := range serverEntries {
+		for tag, sourcedServerEntry := range serverEntries {
 			if count >= int(discoverCount) {
 			if count >= int(discoverCount) {
 				break
 				break
 			}
 			}
@@ -475,9 +496,10 @@ func (b *TestDSLBackend) handleDiscoverServerEntries(
 			versionedServerEntryTags = append(
 			versionedServerEntryTags = append(
 				versionedServerEntryTags,
 				versionedServerEntryTags,
 				&struct {
 				&struct {
-					Tag     []byte
-					Version int32
-				}{serverEntryTag, 0})
+					Tag            []byte
+					Version        int32
+					PrioritizeDial bool
+				}{serverEntryTag, 0, sourcedServerEntry.PrioritizeDial})
 		}
 		}
 	}
 	}
 
 
@@ -765,9 +787,6 @@ func NewTestDSLTLSConfig() (*TestDSLTLSConfig, error) {
 
 
 	CACertificatePEM := pem.EncodeToMemory(
 	CACertificatePEM := pem.EncodeToMemory(
 		&pem.Block{Type: "CERTIFICATE", Bytes: CACertificateDER})
 		&pem.Block{Type: "CERTIFICATE", Bytes: CACertificateDER})
-	if err != nil {
-		return nil, errors.Trace(err)
-	}
 
 
 	CACertificate, err := x509.ParseCertificate(CACertificateDER)
 	CACertificate, err := x509.ParseCertificate(CACertificateDER)
 	if err != nil {
 	if err != nil {

+ 2 - 0
psiphon/notice.go

@@ -489,6 +489,8 @@ func noticeWithDialParameters(noticeType string, dialParams *DialParameters, pos
 		"region", dialParams.ServerEntry.Region,
 		"region", dialParams.ServerEntry.Region,
 		"protocol", dialParams.TunnelProtocol,
 		"protocol", dialParams.TunnelProtocol,
 		"isReplay", dialParams.IsReplay,
 		"isReplay", dialParams.IsReplay,
+		"replayIgnoredChange", dialParams.ReplayIgnoredChange,
+		"DSLPrioritized", dialParams.DSLPrioritizedDial,
 		"candidateNumber", dialParams.CandidateNumber,
 		"candidateNumber", dialParams.CandidateNumber,
 		"establishedTunnelsCount", dialParams.EstablishedTunnelsCount,
 		"establishedTunnelsCount", dialParams.EstablishedTunnelsCount,
 		"networkType", dialParams.GetNetworkType(),
 		"networkType", dialParams.GetNetworkType(),

+ 8 - 4
psiphon/server/api.go

@@ -1183,6 +1183,7 @@ const (
 	requestParamLogFlagAsBool                                 = 1 << 7
 	requestParamLogFlagAsBool                                 = 1 << 7
 	requestParamLogOnlyForFrontedMeekOrConjure                = 1 << 8
 	requestParamLogOnlyForFrontedMeekOrConjure                = 1 << 8
 	requestParamNotLoggedForUnfrontedMeekNonTransformedHeader = 1 << 9
 	requestParamNotLoggedForUnfrontedMeekNonTransformedHeader = 1 << 9
+	requestParamLogOmittedFlagAsFalse                         = 1 << 10
 )
 )
 
 
 // baseParams are the basic request parameters that are expected for all API
 // baseParams are the basic request parameters that are expected for all API
@@ -1233,7 +1234,9 @@ var baseDialParams = []requestParamSpec{
 	{"upstream_max_delayed", isIntString, requestParamOptional | requestParamLogStringAsInt},
 	{"upstream_max_delayed", isIntString, requestParamOptional | requestParamLogStringAsInt},
 	{"padding", isAnyString, requestParamOptional | requestParamLogStringLengthAsInt},
 	{"padding", isAnyString, requestParamOptional | requestParamLogStringLengthAsInt},
 	{"pad_response", isIntString, requestParamOptional | requestParamLogStringAsInt},
 	{"pad_response", isIntString, requestParamOptional | requestParamLogStringAsInt},
-	{"is_replay", isBooleanFlag, requestParamOptional | requestParamLogFlagAsBool},
+	{"is_replay", isBooleanFlag, requestParamOptional | requestParamLogFlagAsBool | requestParamLogOmittedFlagAsFalse},
+	{"replay_ignored_change", isBooleanFlag, requestParamOptional | requestParamLogFlagAsBool | requestParamLogOmittedFlagAsFalse},
+	{"dsl_prioritized", isBooleanFlag, requestParamOptional | requestParamLogFlagAsBool | requestParamLogOmittedFlagAsFalse},
 	{"dial_duration", isIntString, requestParamOptional | requestParamLogStringAsInt},
 	{"dial_duration", isIntString, requestParamOptional | requestParamLogStringAsInt},
 	{"candidate_number", isIntString, requestParamOptional | requestParamLogStringAsInt},
 	{"candidate_number", isIntString, requestParamOptional | requestParamLogStringAsInt},
 	{"established_tunnels_count", isIntString, requestParamOptional | requestParamLogStringAsInt},
 	{"established_tunnels_count", isIntString, requestParamOptional | requestParamLogStringAsInt},
@@ -1531,9 +1534,10 @@ func getRequestLogFields(
 		value := params[expectedParam.name]
 		value := params[expectedParam.name]
 		if value == nil {
 		if value == nil {
 
 
-			// Special case: older clients don't send this value,
-			// so log a default.
-			if expectedParam.name == "tunnel_whole_device" {
+			// Provide a "false" default for specified, omitted boolean flag params.
+
+			if expectedParam.flags&requestParamLogFlagAsBool != 0 &&
+				expectedParam.flags&requestParamLogOmittedFlagAsFalse != 0 {
 				value = "0"
 				value = "0"
 			} else {
 			} else {
 				// Skip omitted, optional params
 				// Skip omitted, optional params

+ 23 - 3
psiphon/server/pb/psiphond/dial_params.pb.go

@@ -107,6 +107,8 @@ type DialParams struct {
 	NetworkLatencyMultiplier          *float64               `protobuf:"fixed64,81,opt,name=network_latency_multiplier,json=networkLatencyMultiplier,proto3,oneof" json:"network_latency_multiplier,omitempty"`
 	NetworkLatencyMultiplier          *float64               `protobuf:"fixed64,81,opt,name=network_latency_multiplier,json=networkLatencyMultiplier,proto3,oneof" json:"network_latency_multiplier,omitempty"`
 	SeedTransform                     *string                `protobuf:"bytes,82,opt,name=seed_transform,json=seedTransform,proto3,oneof" json:"seed_transform,omitempty"`
 	SeedTransform                     *string                `protobuf:"bytes,82,opt,name=seed_transform,json=seedTransform,proto3,oneof" json:"seed_transform,omitempty"`
 	ServerEntryCount                  *int64                 `protobuf:"varint,83,opt,name=server_entry_count,json=serverEntryCount,proto3,oneof" json:"server_entry_count,omitempty"`
 	ServerEntryCount                  *int64                 `protobuf:"varint,83,opt,name=server_entry_count,json=serverEntryCount,proto3,oneof" json:"server_entry_count,omitempty"`
+	ReplayIgnoredChange               *bool                  `protobuf:"varint,84,opt,name=replay_ignored_change,json=replayIgnoredChange,proto3,oneof" json:"replay_ignored_change,omitempty"`
+	DslPrioritized                    *bool                  `protobuf:"varint,85,opt,name=dsl_prioritized,json=dslPrioritized,proto3,oneof" json:"dsl_prioritized,omitempty"`
 	unknownFields                     protoimpl.UnknownFields
 	unknownFields                     protoimpl.UnknownFields
 	sizeCache                         protoimpl.SizeCache
 	sizeCache                         protoimpl.SizeCache
 }
 }
@@ -722,11 +724,25 @@ func (x *DialParams) GetServerEntryCount() int64 {
 	return 0
 	return 0
 }
 }
 
 
+func (x *DialParams) GetReplayIgnoredChange() bool {
+	if x != nil && x.ReplayIgnoredChange != nil {
+		return *x.ReplayIgnoredChange
+	}
+	return false
+}
+
+func (x *DialParams) GetDslPrioritized() bool {
+	if x != nil && x.DslPrioritized != nil {
+		return *x.DslPrioritized
+	}
+	return false
+}
+
 var File_ca_psiphon_psiphond_dial_params_proto protoreflect.FileDescriptor
 var File_ca_psiphon_psiphond_dial_params_proto protoreflect.FileDescriptor
 
 
 const file_ca_psiphon_psiphond_dial_params_proto_rawDesc = "" +
 const file_ca_psiphon_psiphond_dial_params_proto_rawDesc = "" +
 	"\n" +
 	"\n" +
-	"%ca.psiphon.psiphond/dial_params.proto\x12\x13ca.psiphon.psiphond\x1a\x1fgoogle/protobuf/timestamp.proto\"\xfc0\n" +
+	"%ca.psiphon.psiphond/dial_params.proto\x12\x13ca.psiphon.psiphond\x1a\x1fgoogle/protobuf/timestamp.proto\"\x912\n" +
 	"\n" +
 	"\n" +
 	"DialParams\x12*\n" +
 	"DialParams\x12*\n" +
 	"\x0econjure_cached\x18\x01 \x01(\bH\x00R\rconjureCached\x88\x01\x01\x12(\n" +
 	"\x0econjure_cached\x18\x01 \x01(\bH\x00R\rconjureCached\x88\x01\x01\x12(\n" +
@@ -821,7 +837,9 @@ const file_ca_psiphon_psiphond_dial_params_proto_rawDesc = "" +
 	"\x19established_tunnels_count\x18P \x01(\x03HNR\x17establishedTunnelsCount\x88\x01\x01\x12A\n" +
 	"\x19established_tunnels_count\x18P \x01(\x03HNR\x17establishedTunnelsCount\x88\x01\x01\x12A\n" +
 	"\x1anetwork_latency_multiplier\x18Q \x01(\x01HOR\x18networkLatencyMultiplier\x88\x01\x01\x12*\n" +
 	"\x1anetwork_latency_multiplier\x18Q \x01(\x01HOR\x18networkLatencyMultiplier\x88\x01\x01\x12*\n" +
 	"\x0eseed_transform\x18R \x01(\tHPR\rseedTransform\x88\x01\x01\x121\n" +
 	"\x0eseed_transform\x18R \x01(\tHPR\rseedTransform\x88\x01\x01\x121\n" +
-	"\x12server_entry_count\x18S \x01(\x03HQR\x10serverEntryCount\x88\x01\x01B\x11\n" +
+	"\x12server_entry_count\x18S \x01(\x03HQR\x10serverEntryCount\x88\x01\x01\x127\n" +
+	"\x15replay_ignored_change\x18T \x01(\bHRR\x13replayIgnoredChange\x88\x01\x01\x12,\n" +
+	"\x0fdsl_prioritized\x18U \x01(\bHSR\x0edslPrioritized\x88\x01\x01B\x11\n" +
 	"\x0f_conjure_cachedB\x10\n" +
 	"\x0f_conjure_cachedB\x10\n" +
 	"\x0e_conjure_delayB\x17\n" +
 	"\x0e_conjure_delayB\x17\n" +
 	"\x15_conjure_empty_packetB\x12\n" +
 	"\x15_conjure_empty_packetB\x12\n" +
@@ -905,7 +923,9 @@ const file_ca_psiphon_psiphond_dial_params_proto_rawDesc = "" +
 	"\x1a_established_tunnels_countB\x1d\n" +
 	"\x1a_established_tunnels_countB\x1d\n" +
 	"\x1b_network_latency_multiplierB\x11\n" +
 	"\x1b_network_latency_multiplierB\x11\n" +
 	"\x0f_seed_transformB\x15\n" +
 	"\x0f_seed_transformB\x15\n" +
-	"\x13_server_entry_countBHZFgithub.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/server/pb/psiphondb\x06proto3"
+	"\x13_server_entry_countB\x18\n" +
+	"\x16_replay_ignored_changeB\x12\n" +
+	"\x10_dsl_prioritizedBHZFgithub.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/server/pb/psiphondb\x06proto3"
 
 
 var (
 var (
 	file_ca_psiphon_psiphond_dial_params_proto_rawDescOnce sync.Once
 	file_ca_psiphon_psiphond_dial_params_proto_rawDescOnce sync.Once

+ 2 - 0
psiphon/server/proto/ca.psiphon.psiphond/dial_params.proto

@@ -90,4 +90,6 @@ message DialParams {
     optional double network_latency_multiplier = 81;
     optional double network_latency_multiplier = 81;
     optional string seed_transform = 82;
     optional string seed_transform = 82;
     optional int64 server_entry_count = 83;
     optional int64 server_entry_count = 83;
+    optional bool replay_ignored_change = 84;
+    optional bool dsl_prioritized = 85;
 }
 }

+ 100 - 42
psiphon/server/server_test.go

@@ -916,6 +916,9 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 	var dslTestConfig *dslTestConfig
 	var dslTestConfig *dslTestConfig
 	enableDSLFetcher := "false"
 	enableDSLFetcher := "false"
 	if doDSL {
 	if doDSL {
+
+		t.Log("testing DSL")
+
 		dslTestConfig, err = generateDSLTestConfig()
 		dslTestConfig, err = generateDSLTestConfig()
 		if err != nil {
 		if err != nil {
 			t.Fatalf("error generating DSL test config: %s", err)
 			t.Fatalf("error generating DSL test config: %s", err)
@@ -2048,6 +2051,10 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 					tunneled := payload["tunneled"].(bool)
 					tunneled := payload["tunneled"].(bool)
 					updated := int(payload["updated"].(float64))
 					updated := int(payload["updated"].(float64))
 					if tunneled && updated > 0 {
 					if tunneled && updated > 0 {
+						err := checkExpectedDSLPendingPrioritizeDial(clientConfig, networkID)
+						if err != nil {
+							t.Fatalf("checkExpectedDSLPendingPrioritizeDial failed: %v", err)
+						}
 						sendNotificationReceived(tunneledDSLFetched)
 						sendNotificationReceived(tunneledDSLFetched)
 					}
 					}
 				}
 				}
@@ -2350,6 +2357,7 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 	if doDSL || runConfig.doPruneServerEntries {
 	if doDSL || runConfig.doPruneServerEntries {
 		expectServerEntryCount = protocol.ServerEntryCountRoundingIncrement
 		expectServerEntryCount = protocol.ServerEntryCountRoundingIncrement
 	}
 	}
+	expectDSLPrioritized := doDSL
 
 
 	// The client still reports zero domain_bytes when no port forwards are
 	// The client still reports zero domain_bytes when no port forwards are
 	// allowed (expectTrafficFailure).
 	// allowed (expectTrafficFailure).
@@ -2386,6 +2394,7 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 			expectMeekHTTPVersion,
 			expectMeekHTTPVersion,
 			expectCheckServerEntryPruneCount,
 			expectCheckServerEntryPruneCount,
 			expectServerEntryCount,
 			expectServerEntryCount,
+			expectDSLPrioritized,
 			inproxyTestConfig,
 			inproxyTestConfig,
 			logFields)
 			logFields)
 		if err != nil {
 		if err != nil {
@@ -2610,47 +2619,9 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 		}
 		}
 	}
 	}
 
 
-	// Check that the client discovered one of the discovery servers.
-
-	discoveredServers := make(map[string]*protocol.ServerEntry)
-
-	// Otherwise NewServerEntryIterator only returns TargetServerEntry.
-	clientConfig.TargetServerEntry = ""
-
-	_, iterator, err := psiphon.NewServerEntryIterator(clientConfig)
+	err = checkExpectedDiscoveredServer(clientConfig, discoveryServers)
 	if err != nil {
 	if err != nil {
-		t.Fatalf("NewServerEntryIterator failed: %s", err)
-	}
-	defer iterator.Close()
-
-	for {
-		serverEntry, err := iterator.Next()
-		if err != nil {
-			t.Fatalf("ServerIterator.Next failed: %s", err)
-		}
-		if serverEntry == nil {
-			break
-		}
-		discoveredServers[serverEntry.IpAddress] = serverEntry
-	}
-
-	foundOne := false
-	for _, server := range discoveryServers {
-
-		serverEntry, err := protocol.DecodeServerEntry(server.EncodedServerEntry, "", "")
-		if err != nil {
-			t.Fatalf("protocol.DecodeServerEntry failed: %s", err)
-		}
-
-		if v, ok := discoveredServers[serverEntry.IpAddress]; ok {
-			if v.Tag == serverEntry.Tag {
-				foundOne = true
-				break
-			}
-		}
-	}
-	if !foundOne {
-		t.Fatalf("expected client to discover at least one server")
+		t.Fatalf("error checking client discovered server: %v", err)
 	}
 	}
 }
 }
 
 
@@ -2980,6 +2951,7 @@ func checkExpectedServerTunnelLogFields(
 	expectMeekHTTPVersion string,
 	expectMeekHTTPVersion string,
 	expectCheckServerEntryPruneCount int,
 	expectCheckServerEntryPruneCount int,
 	expectServerEntryCount int,
 	expectServerEntryCount int,
+	expectDSLPrioritized bool,
 	inproxyTestConfig *inproxyTestConfig,
 	inproxyTestConfig *inproxyTestConfig,
 	fields map[string]interface{}) error {
 	fields map[string]interface{}) error {
 
 
@@ -3013,6 +2985,8 @@ func checkExpectedServerTunnelLogFields(
 		"server_entry_timestamp",
 		"server_entry_timestamp",
 		"dial_port_number",
 		"dial_port_number",
 		"is_replay",
 		"is_replay",
+		"replay_ignored_change",
+		"dsl_prioritized",
 		"dial_duration",
 		"dial_duration",
 		"candidate_number",
 		"candidate_number",
 		"established_tunnels_count",
 		"established_tunnels_count",
@@ -3767,6 +3741,10 @@ func checkExpectedServerTunnelLogFields(
 		}
 		}
 	}
 	}
 
 
+	if fields["dsl_prioritized"] != expectDSLPrioritized {
+		return fmt.Errorf("unexpected dsl_prioritized %v", fields["dsl_prioritized"])
+	}
+
 	return nil
 	return nil
 }
 }
 
 
@@ -3819,6 +3797,77 @@ func checkExpectedDomainBytesLogFields(
 	return nil
 	return nil
 }
 }
 
 
+func checkExpectedDSLPendingPrioritizeDial(
+	clientConfig *psiphon.Config,
+	networkID string) error {
+
+	// The server entry discovered in the tunneled DSL request should have a
+	// DSLPendingPrioritizeDial placeholder.
+
+	dialParams, err := psiphon.GetDialParameters(
+		clientConfig, tunneledDSLServerEntryIPAddress, networkID)
+	if err != nil {
+		return errors.Trace(err)
+	}
+
+	if dialParams == nil ||
+		!dialParams.DSLPendingPrioritizeDial ||
+		dialParams.DSLPrioritizedDial {
+
+		return errors.TraceNew("unexpected server entry state")
+	}
+
+	return nil
+}
+
+func checkExpectedDiscoveredServer(
+	clientConfig *psiphon.Config,
+	discoveryServers []*psinet.DiscoveryServer) error {
+
+	discoveredServers := make(map[string]*protocol.ServerEntry)
+
+	// Otherwise NewServerEntryIterator only returns TargetServerEntry.
+	clientConfig.TargetServerEntry = ""
+
+	_, iterator, err := psiphon.NewServerEntryIterator(clientConfig)
+	if err != nil {
+		return errors.Trace(err)
+	}
+	defer iterator.Close()
+
+	for {
+		serverEntry, err := iterator.Next()
+		if err != nil {
+			return errors.Trace(err)
+		}
+		if serverEntry == nil {
+			break
+		}
+		discoveredServers[serverEntry.IpAddress] = serverEntry
+	}
+
+	foundOne := false
+	for _, server := range discoveryServers {
+
+		serverEntry, err := protocol.DecodeServerEntry(server.EncodedServerEntry, "", "")
+		if err != nil {
+			return errors.Trace(err)
+		}
+
+		if v, ok := discoveredServers[serverEntry.IpAddress]; ok {
+			if v.Tag == serverEntry.Tag {
+				foundOne = true
+				break
+			}
+		}
+	}
+	if !foundOne {
+		return errors.TraceNew("expected client to discover at least one server")
+	}
+
+	return nil
+}
+
 func makeTunneledWebRequest(
 func makeTunneledWebRequest(
 	t *testing.T,
 	t *testing.T,
 	localHTTPProxyPort int,
 	localHTTPProxyPort int,
@@ -4447,6 +4496,8 @@ func paveTacticsConfigFile(
           "ServerProtocolPacketManipulations": {"All" : ["test-packetman-spec"]},
           "ServerProtocolPacketManipulations": {"All" : ["test-packetman-spec"]},
           "ServerDiscoveryStrategy": "%s",
           "ServerDiscoveryStrategy": "%s",
           "EnableDSLFetcher": %s,
           "EnableDSLFetcher": %s,
+          "DSLPrioritizeDialNewServerEntryProbability" : 1.0,
+          "DSLPrioritizeDialExistingServerEntryProbability" : 1.0,
           "EstablishTunnelWorkTime" : "1s"
           "EstablishTunnelWorkTime" : "1s"
         }
         }
       },
       },
@@ -5462,6 +5513,9 @@ func newDiscoveryServers(ipAddresses []string) ([]*psinet.DiscoveryServer, error
 	return servers, nil
 	return servers, nil
 }
 }
 
 
+// Won't conflict with initializePruneServerEntriesTest
+var tunneledDSLServerEntryIPAddress = "192.0.3.1"
+
 func configureDSLTestServerEntries(
 func configureDSLTestServerEntries(
 	dslTestConfig *dslTestConfig,
 	dslTestConfig *dslTestConfig,
 	encodedServerEntry string,
 	encodedServerEntry string,
@@ -5489,9 +5543,13 @@ func configureDSLTestServerEntries(
 
 
 	// Store the full tunnel protocol server entry in the mock DSL backend.
 	// Store the full tunnel protocol server entry in the mock DSL backend.
 
 
+	// TODO: also excersize prioritizeDial = false?
+
 	isTunneled := false
 	isTunneled := false
+	prioritizeDial := true
 	dslTestConfig.backend.SetServerEntries(
 	dslTestConfig.backend.SetServerEntries(
 		isTunneled,
 		isTunneled,
+		prioritizeDial,
 		[]string{encodedServerEntry})
 		[]string{encodedServerEntry})
 
 
 	// Add an EMBEDDED tactics-only server entry to the client's datastore.
 	// Add an EMBEDDED tactics-only server entry to the client's datastore.
@@ -5526,18 +5584,18 @@ func configureDSLTestServerEntries(
 	// Prepare one additional server entry for the tunneled DSL request.
 	// Prepare one additional server entry for the tunneled DSL request.
 
 
 	dialPort := 4000
 	dialPort := 4000
-	ipAddress := "192.0.3.1" // Won't conflict with initializePruneServerEntriesTest
 	_, _, _, _, encodedServerEntryBytes, err := GenerateConfig(
 	_, _, _, _, encodedServerEntryBytes, err := GenerateConfig(
 		&GenerateConfigParams{
 		&GenerateConfigParams{
 			ServerEntrySignaturePublicKey:  serverEntrySignaturePublicKey,
 			ServerEntrySignaturePublicKey:  serverEntrySignaturePublicKey,
 			ServerEntrySignaturePrivateKey: serverEntrySignaturePrivateKey,
 			ServerEntrySignaturePrivateKey: serverEntrySignaturePrivateKey,
-			ServerIPAddress:                ipAddress,
+			ServerIPAddress:                tunneledDSLServerEntryIPAddress,
 			TunnelProtocolPorts:            map[string]int{protocol.TUNNEL_PROTOCOL_SSH: dialPort},
 			TunnelProtocolPorts:            map[string]int{protocol.TUNNEL_PROTOCOL_SSH: dialPort},
 		})
 		})
 
 
 	isTunneled = true
 	isTunneled = true
 	dslTestConfig.backend.SetServerEntries(
 	dslTestConfig.backend.SetServerEntries(
 		isTunneled,
 		isTunneled,
+		prioritizeDial,
 		[]string{string(encodedServerEntryBytes)})
 		[]string{string(encodedServerEntryBytes)})
 
 
 	return nil
 	return nil

+ 13 - 3
psiphon/serverApi.go

@@ -1282,11 +1282,21 @@ func getBaseAPIParameters(
 			params["quic_disable_client_path_mtu_discovery"] = "1"
 			params["quic_disable_client_path_mtu_discovery"] = "1"
 		}
 		}
 
 
-		isReplay := "0"
+		// The server will log a default false value for is_replay,
+		// replay_ignored_change, and dsl_prioritized when omitted. Omitting
+		// reduces the handshake parameter size in common cases.
+
 		if dialParams.IsReplay {
 		if dialParams.IsReplay {
-			isReplay = "1"
+			params["is_replay"] = "1"
+		}
+
+		if dialParams.ReplayIgnoredChange {
+			params["replay_ignored_change"] = "1"
+		}
+
+		if dialParams.DSLPrioritizedDial {
+			params["dsl_prioritized"] = "1"
 		}
 		}
-		params["is_replay"] = isReplay
 
 
 		// dialParams.DialDuration is nanoseconds; report milliseconds
 		// dialParams.DialDuration is nanoseconds; report milliseconds
 		params["dial_duration"] = fmt.Sprintf("%d", dialParams.DialDuration/time.Millisecond)
 		params["dial_duration"] = fmt.Sprintf("%d", dialParams.DialDuration/time.Millisecond)