Răsfoiți Sursa

Exercise tactics in end-to-end tests

- Associated bug fixes
Rod Hynes 8 ani în urmă
părinte
comite
8f415c2bc5

+ 36 - 14
psiphon/common/tactics/tactics.go

@@ -378,6 +378,25 @@ type SpeedTestSample struct {
 	BytesDown int `json:"dn"`
 	BytesDown int `json:"dn"`
 }
 }
 
 
+// GenerateKeys generates a tactics request key pair and obfuscation key.
+func GenerateKeys() (encodedRequestPublicKey, encodedRequestPrivateKey, encodedObfuscatedKey string, err error) {
+
+	requestPublicKey, requestPrivateKey, err := box.GenerateKey(rand.Reader)
+	if err != nil {
+		return "", "", "", common.ContextError(err)
+	}
+
+	obfuscatedKey, err := common.MakeSecureRandomBytes(common.OBFUSCATE_KEY_LENGTH)
+	if err != nil {
+		return "", "", "", common.ContextError(err)
+	}
+
+	return base64.StdEncoding.EncodeToString(requestPublicKey[:]),
+		base64.StdEncoding.EncodeToString(requestPrivateKey[:]),
+		base64.StdEncoding.EncodeToString(obfuscatedKey[:]),
+		nil
+}
+
 // NewServer creates Server using the specified tactics configuration file.
 // NewServer creates Server using the specified tactics configuration file.
 //
 //
 // The logger and logFieldFormatter callbacks are used to log errors and
 // The logger and logFieldFormatter callbacks are used to log errors and
@@ -931,13 +950,16 @@ func (server *Server) handleTacticsRequest(
 		return
 		return
 	}
 	}
 
 
-	err = server.apiParameterValidator(apiParams)
-	if err != nil {
-		server.logger.WithContextFields(
-			common.LogFields{"error": err}).Warning("invalid request parameters")
-		w.WriteHeader(http.StatusNotFound)
-		return
-	}
+	// *TODO* fix validator and formatter
+	/*
+		err = server.apiParameterValidator(apiParams)
+		if err != nil {
+			server.logger.WithContextFields(
+				common.LogFields{"error": err}).Warning("invalid request parameters")
+			w.WriteHeader(http.StatusNotFound)
+			return
+		}
+	*/
 
 
 	tacticsPayload, err := server.GetTacticsPayload(geoIPData, apiParams)
 	tacticsPayload, err := server.GetTacticsPayload(geoIPData, apiParams)
 	if err != nil {
 	if err != nil {
@@ -968,7 +990,11 @@ func (server *Server) handleTacticsRequest(
 
 
 	// Log a metric.
 	// Log a metric.
 
 
-	logFields := server.logFieldFormatter(geoIPData, apiParams)
+	// *TODO* fix validator and formatter
+	/*
+		logFields := server.logFieldFormatter(geoIPData, apiParams)
+	*/
+	logFields := make(common.LogFields)
 
 
 	logFields[NEW_TACTICS_TAG_LOG_FIELD_NAME] = tacticsPayload.Tag
 	logFields[NEW_TACTICS_TAG_LOG_FIELD_NAME] = tacticsPayload.Tag
 	logFields[IS_TACTICS_REQUEST_LOG_FIELD_NAME] = true
 	logFields[IS_TACTICS_REQUEST_LOG_FIELD_NAME] = true
@@ -1081,7 +1107,7 @@ func UseStoredTactics(
 		return nil, common.ContextError(err)
 		return nil, common.ContextError(err)
 	}
 	}
 
 
-	if record != nil && record.Tag != "" && record.Expiry.After(time.Now()) {
+	if record.Tag != "" && record.Expiry.After(time.Now()) {
 		return record, nil
 		return record, nil
 	}
 	}
 
 
@@ -1119,10 +1145,6 @@ func FetchTactics(
 		return nil, common.ContextError(err)
 		return nil, common.ContextError(err)
 	}
 	}
 
 
-	if record == nil {
-		record = &Record{}
-	}
-
 	speedTestSamples, err := getSpeedTestSamples(storer, networkID)
 	speedTestSamples, err := getSpeedTestSamples(storer, networkID)
 	if err != nil {
 	if err != nil {
 		return nil, common.ContextError(err)
 		return nil, common.ContextError(err)
@@ -1307,7 +1329,7 @@ func getStoredTacticsRecord(
 	}
 	}
 
 
 	if marshaledRecord == nil {
 	if marshaledRecord == nil {
-		return nil, nil
+		return &Record{}, nil
 	}
 	}
 
 
 	var record *Record
 	var record *Record

+ 4 - 26
psiphon/common/tactics/tactics_test.go

@@ -22,8 +22,6 @@ package tactics
 import (
 import (
 	"bytes"
 	"bytes"
 	"context"
 	"context"
-	"crypto/rand"
-	"encoding/base64"
 	"encoding/json"
 	"encoding/json"
 	"fmt"
 	"fmt"
 	"io/ioutil"
 	"io/ioutil"
@@ -36,7 +34,6 @@ import (
 	"time"
 	"time"
 
 
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
-	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/crypto/nacl/box"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
 )
 )
@@ -108,21 +105,11 @@ func TestTactics(t *testing.T) {
 		t.Fatalf("unexpected lookupThreshold")
 		t.Fatalf("unexpected lookupThreshold")
 	}
 	}
 
 
-	requestPublicKey, requestPrivateKey, err := box.GenerateKey(rand.Reader)
+	encodedRequestPublicKey, encodedRequestPrivateKey, encodedObfuscatedKey, err := GenerateKeys()
 	if err != nil {
 	if err != nil {
-		t.Fatalf("GenerateKey failed: %s", err)
+		t.Fatalf("GenerateKeys failed: %s", err)
 	}
 	}
 
 
-	encodedRequestPublicKey := base64.StdEncoding.EncodeToString(requestPublicKey[:])
-	encodedRequestPrivateKey := base64.StdEncoding.EncodeToString(requestPrivateKey[:])
-
-	obfuscatedKey, err := common.MakeSecureRandomBytes(common.OBFUSCATE_KEY_LENGTH)
-	if err != nil {
-		t.Fatalf("MakeSecureRandomBytes failed: %s", err)
-	}
-
-	encodedObfuscatedKey := base64.StdEncoding.EncodeToString(obfuscatedKey[:])
-
 	tacticsProbability := 0.5
 	tacticsProbability := 0.5
 	tacticsNetworkLatencyMultiplier := 2.0
 	tacticsNetworkLatencyMultiplier := 2.0
 	tacticsConnectionWorkerPoolSize := 5
 	tacticsConnectionWorkerPoolSize := 5
@@ -600,20 +587,11 @@ func TestTactics(t *testing.T) {
 
 
 	// Fetch should fail when using incorrect keys
 	// Fetch should fail when using incorrect keys
 
 
-	incorrectRequestPublicKey, _, err := box.GenerateKey(rand.Reader)
+	encodedIncorrectRequestPublicKey, _, encodedIncorrectObfuscatedKey, err := GenerateKeys()
 	if err != nil {
 	if err != nil {
-		t.Fatalf("GenerateKey failed: %s", err)
+		t.Fatalf("GenerateKeys failed: %s", err)
 	}
 	}
 
 
-	encodedIncorrectRequestPublicKey := base64.StdEncoding.EncodeToString(incorrectRequestPublicKey[:])
-
-	incorrectObfuscatedKey, err := common.MakeSecureRandomBytes(common.OBFUSCATE_KEY_LENGTH)
-	if err != nil {
-		t.Fatalf("MakeSecureRandomBytes failed: %s", err)
-	}
-
-	encodedIncorrectObfuscatedKey := base64.StdEncoding.EncodeToString(incorrectObfuscatedKey[:])
-
 	_, err = FetchTactics(
 	_, err = FetchTactics(
 		context.Background(),
 		context.Background(),
 		clientParams,
 		clientParams,

+ 10 - 6
psiphon/controller.go

@@ -1274,16 +1274,15 @@ func (controller *Controller) getTactics(done chan struct{}) {
 
 
 	if tacticsRecord == nil {
 	if tacticsRecord == nil {
 
 
-		iterator, err := NewTacticsServerEntryIterator()
+		iterator, err := NewTacticsServerEntryIterator(
+			controller.config)
 		if err != nil {
 		if err != nil {
 			NoticeAlert("tactics iterator failed: %s", err)
 			NoticeAlert("tactics iterator failed: %s", err)
 			return
 			return
 		}
 		}
 		defer iterator.Close()
 		defer iterator.Close()
 
 
-		firstIteration := true
-
-		for {
+		for iteration := 0; ; iteration++ {
 
 
 			serverEntry, err := iterator.Next()
 			serverEntry, err := iterator.Next()
 			if err != nil {
 			if err != nil {
@@ -1292,8 +1291,9 @@ func (controller *Controller) getTactics(done chan struct{}) {
 			}
 			}
 
 
 			if serverEntry == nil {
 			if serverEntry == nil {
-				if firstIteration {
+				if iteration == 0 {
 					NoticeAlert("tactics request skipped: no capable servers")
 					NoticeAlert("tactics request skipped: no capable servers")
+					return
 				}
 				}
 
 
 				iterator.Reset()
 				iterator.Reset()
@@ -1376,6 +1376,8 @@ func (controller *Controller) doFetchTactics(
 		return nil, common.ContextError(err)
 		return nil, common.ContextError(err)
 	}
 	}
 
 
+	meekConfig.RoundTripperOnly = true
+
 	dialConfig, _, _ := initDialConfig(
 	dialConfig, _, _ := initDialConfig(
 		controller.config, meekConfig)
 		controller.config, meekConfig)
 
 
@@ -1419,7 +1421,9 @@ func (controller *Controller) doFetchTactics(
 	}
 	}
 	defer meekConn.Close()
 	defer meekConn.Close()
 
 
-	var apiParams common.APIParameters // *TODO* populate
+	var apiParams common.APIParameters
+	// *TODO* populate
+	apiParams = make(common.APIParameters)
 
 
 	tacticsRecord, err := tactics.FetchTactics(
 	tacticsRecord, err := tactics.FetchTactics(
 		ctx,
 		ctx,

+ 12 - 7
psiphon/controller_test.go

@@ -487,6 +487,11 @@ func controllerRun(t *testing.T, runConfig *controllerRunConfig) {
 		config.CustomHeaders = upstreamProxyCustomHeaders
 		config.CustomHeaders = upstreamProxyCustomHeaders
 	}
 	}
 
 
+	// Enable tactics requests. This will passively exercise the code
+	// paths. server_test runs a more comprehensive test that checks
+	// that the tactics request succeeds.
+	config.NetworkIDGetter = &testNetworkGetter{}
+
 	// The following config values must be applied through client parameters
 	// The following config values must be applied through client parameters
 	// (setting the fields in Config directly will have no effect since the
 	// (setting the fields in Config directly will have no effect since the
 	// client parameters have been populated by LoadConfig).
 	// client parameters have been populated by LoadConfig).
@@ -819,13 +824,6 @@ func controllerRun(t *testing.T, runConfig *controllerRunConfig) {
 	}
 	}
 }
 }
 
 
-type TestHostNameTransformer struct {
-}
-
-func (TestHostNameTransformer) TransformHostName(string) (string, bool) {
-	return "example.com", true
-}
-
 func fetchAndVerifyWebsite(t *testing.T, httpProxyPort int) error {
 func fetchAndVerifyWebsite(t *testing.T, httpProxyPort int) error {
 
 
 	testUrl := "https://psiphon.ca"
 	testUrl := "https://psiphon.ca"
@@ -1147,3 +1145,10 @@ func initUpstreamProxy() {
 
 
 	// TODO: wait until listener is active?
 	// TODO: wait until listener is active?
 }
 }
+
+type testNetworkGetter struct {
+}
+
+func (testNetworkGetter) GetNetworkID() string {
+	return "NETWORK1"
+}

+ 67 - 35
psiphon/dataStore.go

@@ -487,14 +487,14 @@ func insertRankedServerEntry(tx *bolt.Tx, serverEntryId string, position int) er
 // ServerEntryIterator is used to iterate over
 // ServerEntryIterator is used to iterate over
 // stored server entries in rank order.
 // stored server entries in rank order.
 type ServerEntryIterator struct {
 type ServerEntryIterator struct {
-	config                      *Config
-	supportsTactics             bool
-	shuffleHeadLength           int
-	serverEntryIds              []string
-	serverEntryIndex            int
-	isTargetServerEntryIterator bool
-	hasNextTargetServerEntry    bool
-	targetServerEntry           *protocol.ServerEntry
+	config                       *Config
+	shuffleHeadLength            int
+	serverEntryIds               []string
+	serverEntryIndex             int
+	isTacticsServerEntryIterator bool
+	isTargetServerEntryIterator  bool
+	hasNextTargetServerEntry     bool
+	targetServerEntry            *protocol.ServerEntry
 }
 }
 
 
 // NewServerEntryIterator creates a new ServerEntryIterator.
 // NewServerEntryIterator creates a new ServerEntryIterator.
@@ -513,7 +513,7 @@ func NewServerEntryIterator(config *Config) (bool, *ServerEntryIterator, error)
 
 
 	// When configured, this target server entry is the only candidate
 	// When configured, this target server entry is the only candidate
 	if config.TargetServerEntry != "" {
 	if config.TargetServerEntry != "" {
-		return newTargetServerEntryIterator(config)
+		return newTargetServerEntryIterator(config, false)
 	}
 	}
 
 
 	checkInitDataStore()
 	checkInitDataStore()
@@ -526,9 +526,8 @@ func NewServerEntryIterator(config *Config) (bool, *ServerEntryIterator, error)
 	applyServerAffinity := !filterChanged
 	applyServerAffinity := !filterChanged
 
 
 	iterator := &ServerEntryIterator{
 	iterator := &ServerEntryIterator{
-		config:                      config,
-		shuffleHeadLength:           config.TunnelPoolSize,
-		isTargetServerEntryIterator: false,
+		config:            config,
+		shuffleHeadLength: config.TunnelPoolSize,
 	}
 	}
 
 
 	err = iterator.Reset()
 	err = iterator.Reset()
@@ -539,13 +538,19 @@ func NewServerEntryIterator(config *Config) (bool, *ServerEntryIterator, error)
 	return applyServerAffinity, iterator, nil
 	return applyServerAffinity, iterator, nil
 }
 }
 
 
-func NewTacticsServerEntryIterator() (*ServerEntryIterator, error) {
+func NewTacticsServerEntryIterator(config *Config) (*ServerEntryIterator, error) {
+
+	// When configured, this target server entry is the only candidate
+	if config.TargetServerEntry != "" {
+		_, iterator, err := newTargetServerEntryIterator(config, true)
+		return iterator, err
+	}
 
 
 	checkInitDataStore()
 	checkInitDataStore()
 
 
 	iterator := &ServerEntryIterator{
 	iterator := &ServerEntryIterator{
-		supportsTactics:   true,
-		shuffleHeadLength: 0,
+		shuffleHeadLength:            0,
+		isTacticsServerEntryIterator: true,
 	}
 	}
 
 
 	err := iterator.Reset()
 	err := iterator.Reset()
@@ -557,7 +562,7 @@ func NewTacticsServerEntryIterator() (*ServerEntryIterator, error) {
 }
 }
 
 
 // newTargetServerEntryIterator is a helper for initializing the TargetServerEntry case
 // newTargetServerEntryIterator is a helper for initializing the TargetServerEntry case
-func newTargetServerEntryIterator(config *Config) (bool, *ServerEntryIterator, error) {
+func newTargetServerEntryIterator(config *Config, isTactics bool) (bool, *ServerEntryIterator, error) {
 
 
 	serverEntry, err := protocol.DecodeServerEntry(
 	serverEntry, err := protocol.DecodeServerEntry(
 		config.TargetServerEntry, common.GetCurrentTimestamp(), protocol.SERVER_ENTRY_SOURCE_TARGET)
 		config.TargetServerEntry, common.GetCurrentTimestamp(), protocol.SERVER_ENTRY_SOURCE_TARGET)
@@ -565,24 +570,37 @@ func newTargetServerEntryIterator(config *Config) (bool, *ServerEntryIterator, e
 		return false, nil, common.ContextError(err)
 		return false, nil, common.ContextError(err)
 	}
 	}
 
 
-	if config.EgressRegion != "" && serverEntry.Region != config.EgressRegion {
-		return false, nil, common.ContextError(errors.New("TargetServerEntry does not support EgressRegion"))
-	}
+	if isTactics {
 
 
-	limitTunnelProtocols := config.clientParameters.Get().TunnelProtocols(parameters.LimitTunnelProtocols)
-	if len(limitTunnelProtocols) > 0 {
-		// At the ServerEntryIterator level, only limitTunnelProtocols is applied;
-		// impairedTunnelProtocols and excludeMeek are handled higher up.
-		if len(serverEntry.GetSupportedProtocols(limitTunnelProtocols, nil, false)) == 0 {
-			return false, nil, common.ContextError(errors.New("TargetServerEntry does not support LimitTunnelProtocols"))
+		if len(serverEntry.GetSupportedTacticsProtocols()) == 0 {
+			return false, nil, common.ContextError(errors.New("TargetServerEntry does not support tactics protocols"))
+		}
+
+	} else {
+
+		if config.EgressRegion != "" && serverEntry.Region != config.EgressRegion {
+			return false, nil, common.ContextError(errors.New("TargetServerEntry does not support EgressRegion"))
+		}
+
+		limitTunnelProtocols := config.clientParameters.Get().TunnelProtocols(parameters.LimitTunnelProtocols)
+		if len(limitTunnelProtocols) > 0 {
+			// At the ServerEntryIterator level, only limitTunnelProtocols is applied;
+			// impairedTunnelProtocols and excludeMeek are handled higher up.
+			if len(serverEntry.GetSupportedProtocols(limitTunnelProtocols, nil, false)) == 0 {
+				return false, nil, common.ContextError(errors.New("TargetServerEntry does not support LimitTunnelProtocols"))
+			}
 		}
 		}
 	}
 	}
+
 	iterator := &ServerEntryIterator{
 	iterator := &ServerEntryIterator{
-		isTargetServerEntryIterator: true,
-		hasNextTargetServerEntry:    true,
-		targetServerEntry:           serverEntry,
+		isTacticsServerEntryIterator: isTactics,
+		isTargetServerEntryIterator:  true,
+		hasNextTargetServerEntry:     true,
+		targetServerEntry:            serverEntry,
 	}
 	}
+
 	NoticeInfo("using TargetServerEntry: %s", serverEntry.IpAddress)
 	NoticeInfo("using TargetServerEntry: %s", serverEntry.IpAddress)
+
 	return false, iterator, nil
 	return false, iterator, nil
 }
 }
 
 
@@ -602,11 +620,15 @@ func (iterator *ServerEntryIterator) Reset() error {
 	// as protocol filtering, including impaire protocol and exclude-meek
 	// as protocol filtering, including impaire protocol and exclude-meek
 	// logic, is all handled higher up.
 	// logic, is all handled higher up.
 
 
-	limitTunnelProtocols := iterator.config.clientParameters.Get().TunnelProtocols(
-		parameters.LimitTunnelProtocols)
+	// TODO: for isTacticsServerEntryIterator, emit tactics candidate count.
 
 
-	count := CountServerEntries(iterator.config.EgressRegion, limitTunnelProtocols)
-	NoticeCandidateServers(iterator.config.EgressRegion, limitTunnelProtocols, count)
+	if !iterator.isTacticsServerEntryIterator {
+		limitTunnelProtocols := iterator.config.clientParameters.Get().TunnelProtocols(
+			parameters.LimitTunnelProtocols)
+
+		count := CountServerEntries(iterator.config.EgressRegion, limitTunnelProtocols)
+		NoticeCandidateServers(iterator.config.EgressRegion, limitTunnelProtocols, count)
+	}
 
 
 	// This query implements the Psiphon server candidate selection
 	// This query implements the Psiphon server candidate selection
 	// algorithm: the first TunnelPoolSize server candidates are in rank
 	// algorithm: the first TunnelPoolSize server candidates are in rank
@@ -737,10 +759,20 @@ func (iterator *ServerEntryIterator) Next() (*protocol.ServerEntry, error) {
 		}
 		}
 
 
 		// Check filter requirements
 		// Check filter requirements
-		if (iterator.config.EgressRegion == "" || serverEntry.Region == iterator.config.EgressRegion) &&
-			(!iterator.supportsTactics || len(serverEntry.GetSupportedTacticsProtocols()) > 0) {
 
 
-			break
+		if iterator.isTacticsServerEntryIterator {
+
+			// Tactics doesn't filter by egress region.
+			if len(serverEntry.GetSupportedTacticsProtocols()) > 0 {
+				break
+			}
+
+		} else {
+
+			if iterator.config.EgressRegion == "" ||
+				serverEntry.Region == iterator.config.EgressRegion {
+				break
+			}
 		}
 		}
 	}
 	}
 
 

+ 27 - 13
psiphon/server/config.go

@@ -428,23 +428,29 @@ func validateNetworkAddress(address string, requireIPaddress bool) error {
 // GenerateConfigParams specifies customizations to be applied to
 // GenerateConfigParams specifies customizations to be applied to
 // a generated server config.
 // a generated server config.
 type GenerateConfigParams struct {
 type GenerateConfigParams struct {
-	LogFilename            string
-	SkipPanickingLogWriter bool
-	LogLevel               string
-	ServerIPAddress        string
-	WebServerPort          int
-	EnableSSHAPIRequests   bool
-	TunnelProtocolPorts    map[string]int
-	TrafficRulesFilename   string
+	LogFilename                 string
+	SkipPanickingLogWriter      bool
+	LogLevel                    string
+	ServerIPAddress             string
+	WebServerPort               int
+	EnableSSHAPIRequests        bool
+	TunnelProtocolPorts         map[string]int
+	TrafficRulesFilename        string
+	TacticsRequestPublicKey     string
+	TacticsRequestObfuscatedKey string
 }
 }
 
 
-// GenerateConfig creates a new Psiphon server config. It returns JSON
-// encoded configs and a client-compatible "server entry" for the server. It
-// generates all necessary secrets and key material, which are emitted in
-// the config file and server entry as necessary.
+// GenerateConfig creates a new Psiphon server config. It returns JSON encoded
+// configs and a client-compatible "server entry" for the server. It generates
+// all necessary secrets and key material, which are emitted in the config
+// file and server entry as necessary.
+//
 // GenerateConfig uses sample values for many fields. The intention is for
 // GenerateConfig uses sample values for many fields. The intention is for
-// generated configs to be used for testing or as a template for production
+// generated configs to be used for testing or as examples for production
 // setup, not to generate production-ready configurations.
 // setup, not to generate production-ready configurations.
+//
+// When tactics key material is provided in GenerateConfigParams, tactics
+// capabilities are added for all meek protocols in TunnelProtocolPorts.
 func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, []byte, error) {
 func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, []byte, error) {
 
 
 	// Input validation
 	// Input validation
@@ -653,6 +659,12 @@ func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, []byte, error
 
 
 	for tunnelProtocol := range params.TunnelProtocolPorts {
 	for tunnelProtocol := range params.TunnelProtocolPorts {
 		capabilities = append(capabilities, protocol.GetCapability(tunnelProtocol))
 		capabilities = append(capabilities, protocol.GetCapability(tunnelProtocol))
+
+		if params.TacticsRequestPublicKey != "" && params.TacticsRequestObfuscatedKey != "" &&
+			protocol.TunnelProtocolUsesMeek(tunnelProtocol) {
+
+			capabilities = append(capabilities, protocol.GetTacticsCapability(tunnelProtocol))
+		}
 	}
 	}
 
 
 	sshPort := params.TunnelProtocolPorts["SSH"]
 	sshPort := params.TunnelProtocolPorts["SSH"]
@@ -703,6 +715,8 @@ func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, []byte, error
 		MeekFrontingHosts:             []string{params.ServerIPAddress},
 		MeekFrontingHosts:             []string{params.ServerIPAddress},
 		MeekFrontingAddresses:         []string{params.ServerIPAddress},
 		MeekFrontingAddresses:         []string{params.ServerIPAddress},
 		MeekFrontingDisableSNI:        false,
 		MeekFrontingDisableSNI:        false,
+		TacticsRequestPublicKey:       params.TacticsRequestPublicKey,
+		TacticsRequestObfuscatedKey:   params.TacticsRequestObfuscatedKey,
 	}
 	}
 
 
 	encodedServerEntry, err := protocol.EncodeServerEntry(serverEntry)
 	encodedServerEntry, err := protocol.EncodeServerEntry(serverEntry)

+ 163 - 57
psiphon/server/server_test.go

@@ -40,6 +40,9 @@ import (
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/accesscontrol"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/accesscontrol"
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/tactics"
 	"golang.org/x/net/proxy"
 	"golang.org/x/net/proxy"
 )
 )
 
 
@@ -123,7 +126,7 @@ func TestSSH(t *testing.T) {
 			tunnelProtocol:       "SSH",
 			tunnelProtocol:       "SSH",
 			enableSSHAPIRequests: true,
 			enableSSHAPIRequests: true,
 			doHotReload:          false,
 			doHotReload:          false,
-			doDefaultSessionID:   false,
+			doDefaultSponsorID:   false,
 			denyTrafficRules:     false,
 			denyTrafficRules:     false,
 			requireAuthorization: true,
 			requireAuthorization: true,
 			omitAuthorization:    false,
 			omitAuthorization:    false,
@@ -139,7 +142,7 @@ func TestOSSH(t *testing.T) {
 			tunnelProtocol:       "OSSH",
 			tunnelProtocol:       "OSSH",
 			enableSSHAPIRequests: true,
 			enableSSHAPIRequests: true,
 			doHotReload:          false,
 			doHotReload:          false,
-			doDefaultSessionID:   false,
+			doDefaultSponsorID:   false,
 			denyTrafficRules:     false,
 			denyTrafficRules:     false,
 			requireAuthorization: true,
 			requireAuthorization: true,
 			omitAuthorization:    false,
 			omitAuthorization:    false,
@@ -155,7 +158,7 @@ func TestUnfrontedMeek(t *testing.T) {
 			tunnelProtocol:       "UNFRONTED-MEEK-OSSH",
 			tunnelProtocol:       "UNFRONTED-MEEK-OSSH",
 			enableSSHAPIRequests: true,
 			enableSSHAPIRequests: true,
 			doHotReload:          false,
 			doHotReload:          false,
-			doDefaultSessionID:   false,
+			doDefaultSponsorID:   false,
 			denyTrafficRules:     false,
 			denyTrafficRules:     false,
 			requireAuthorization: true,
 			requireAuthorization: true,
 			omitAuthorization:    false,
 			omitAuthorization:    false,
@@ -171,7 +174,7 @@ func TestUnfrontedMeekHTTPS(t *testing.T) {
 			tunnelProtocol:       "UNFRONTED-MEEK-HTTPS-OSSH",
 			tunnelProtocol:       "UNFRONTED-MEEK-HTTPS-OSSH",
 			enableSSHAPIRequests: true,
 			enableSSHAPIRequests: true,
 			doHotReload:          false,
 			doHotReload:          false,
-			doDefaultSessionID:   false,
+			doDefaultSponsorID:   false,
 			denyTrafficRules:     false,
 			denyTrafficRules:     false,
 			requireAuthorization: true,
 			requireAuthorization: true,
 			omitAuthorization:    false,
 			omitAuthorization:    false,
@@ -187,7 +190,7 @@ func TestUnfrontedMeekSessionTicket(t *testing.T) {
 			tunnelProtocol:       "UNFRONTED-MEEK-SESSION-TICKET-OSSH",
 			tunnelProtocol:       "UNFRONTED-MEEK-SESSION-TICKET-OSSH",
 			enableSSHAPIRequests: true,
 			enableSSHAPIRequests: true,
 			doHotReload:          false,
 			doHotReload:          false,
-			doDefaultSessionID:   false,
+			doDefaultSponsorID:   false,
 			denyTrafficRules:     false,
 			denyTrafficRules:     false,
 			requireAuthorization: true,
 			requireAuthorization: true,
 			omitAuthorization:    false,
 			omitAuthorization:    false,
@@ -203,7 +206,7 @@ func TestWebTransportAPIRequests(t *testing.T) {
 			tunnelProtocol:       "OSSH",
 			tunnelProtocol:       "OSSH",
 			enableSSHAPIRequests: false,
 			enableSSHAPIRequests: false,
 			doHotReload:          false,
 			doHotReload:          false,
-			doDefaultSessionID:   false,
+			doDefaultSponsorID:   false,
 			denyTrafficRules:     false,
 			denyTrafficRules:     false,
 			requireAuthorization: false,
 			requireAuthorization: false,
 			omitAuthorization:    true,
 			omitAuthorization:    true,
@@ -219,7 +222,7 @@ func TestHotReload(t *testing.T) {
 			tunnelProtocol:       "OSSH",
 			tunnelProtocol:       "OSSH",
 			enableSSHAPIRequests: true,
 			enableSSHAPIRequests: true,
 			doHotReload:          true,
 			doHotReload:          true,
-			doDefaultSessionID:   false,
+			doDefaultSponsorID:   false,
 			denyTrafficRules:     false,
 			denyTrafficRules:     false,
 			requireAuthorization: true,
 			requireAuthorization: true,
 			omitAuthorization:    false,
 			omitAuthorization:    false,
@@ -235,7 +238,7 @@ func TestDefaultSessionID(t *testing.T) {
 			tunnelProtocol:       "OSSH",
 			tunnelProtocol:       "OSSH",
 			enableSSHAPIRequests: true,
 			enableSSHAPIRequests: true,
 			doHotReload:          true,
 			doHotReload:          true,
-			doDefaultSessionID:   true,
+			doDefaultSponsorID:   true,
 			denyTrafficRules:     false,
 			denyTrafficRules:     false,
 			requireAuthorization: true,
 			requireAuthorization: true,
 			omitAuthorization:    false,
 			omitAuthorization:    false,
@@ -251,7 +254,7 @@ func TestDenyTrafficRules(t *testing.T) {
 			tunnelProtocol:       "OSSH",
 			tunnelProtocol:       "OSSH",
 			enableSSHAPIRequests: true,
 			enableSSHAPIRequests: true,
 			doHotReload:          true,
 			doHotReload:          true,
-			doDefaultSessionID:   false,
+			doDefaultSponsorID:   false,
 			denyTrafficRules:     true,
 			denyTrafficRules:     true,
 			requireAuthorization: true,
 			requireAuthorization: true,
 			omitAuthorization:    false,
 			omitAuthorization:    false,
@@ -267,7 +270,7 @@ func TestOmitAuthorization(t *testing.T) {
 			tunnelProtocol:       "OSSH",
 			tunnelProtocol:       "OSSH",
 			enableSSHAPIRequests: true,
 			enableSSHAPIRequests: true,
 			doHotReload:          true,
 			doHotReload:          true,
-			doDefaultSessionID:   false,
+			doDefaultSponsorID:   false,
 			denyTrafficRules:     false,
 			denyTrafficRules:     false,
 			requireAuthorization: true,
 			requireAuthorization: true,
 			omitAuthorization:    true,
 			omitAuthorization:    true,
@@ -283,7 +286,7 @@ func TestNoAuthorization(t *testing.T) {
 			tunnelProtocol:       "OSSH",
 			tunnelProtocol:       "OSSH",
 			enableSSHAPIRequests: true,
 			enableSSHAPIRequests: true,
 			doHotReload:          true,
 			doHotReload:          true,
-			doDefaultSessionID:   false,
+			doDefaultSponsorID:   false,
 			denyTrafficRules:     false,
 			denyTrafficRules:     false,
 			requireAuthorization: false,
 			requireAuthorization: false,
 			omitAuthorization:    true,
 			omitAuthorization:    true,
@@ -299,7 +302,7 @@ func TestUnusedAuthorization(t *testing.T) {
 			tunnelProtocol:       "OSSH",
 			tunnelProtocol:       "OSSH",
 			enableSSHAPIRequests: true,
 			enableSSHAPIRequests: true,
 			doHotReload:          true,
 			doHotReload:          true,
-			doDefaultSessionID:   false,
+			doDefaultSponsorID:   false,
 			denyTrafficRules:     false,
 			denyTrafficRules:     false,
 			requireAuthorization: false,
 			requireAuthorization: false,
 			omitAuthorization:    false,
 			omitAuthorization:    false,
@@ -315,7 +318,7 @@ func TestTCPOnlySLOK(t *testing.T) {
 			tunnelProtocol:       "OSSH",
 			tunnelProtocol:       "OSSH",
 			enableSSHAPIRequests: true,
 			enableSSHAPIRequests: true,
 			doHotReload:          false,
 			doHotReload:          false,
-			doDefaultSessionID:   false,
+			doDefaultSponsorID:   false,
 			denyTrafficRules:     false,
 			denyTrafficRules:     false,
 			requireAuthorization: true,
 			requireAuthorization: true,
 			omitAuthorization:    false,
 			omitAuthorization:    false,
@@ -331,7 +334,7 @@ func TestUDPOnlySLOK(t *testing.T) {
 			tunnelProtocol:       "OSSH",
 			tunnelProtocol:       "OSSH",
 			enableSSHAPIRequests: true,
 			enableSSHAPIRequests: true,
 			doHotReload:          false,
 			doHotReload:          false,
-			doDefaultSessionID:   false,
+			doDefaultSponsorID:   false,
 			denyTrafficRules:     false,
 			denyTrafficRules:     false,
 			requireAuthorization: true,
 			requireAuthorization: true,
 			omitAuthorization:    false,
 			omitAuthorization:    false,
@@ -345,7 +348,7 @@ type runServerConfig struct {
 	tunnelProtocol       string
 	tunnelProtocol       string
 	enableSSHAPIRequests bool
 	enableSSHAPIRequests bool
 	doHotReload          bool
 	doHotReload          bool
-	doDefaultSessionID   bool
+	doDefaultSponsorID   bool
 	denyTrafficRules     bool
 	denyTrafficRules     bool
 	requireAuthorization bool
 	requireAuthorization bool
 	omitAuthorization    bool
 	omitAuthorization    bool
@@ -354,27 +357,6 @@ type runServerConfig struct {
 	doTunneledNTPRequest bool
 	doTunneledNTPRequest bool
 }
 }
 
 
-func sendNotificationReceived(c chan<- struct{}) {
-	select {
-	case c <- *new(struct{}):
-	default:
-	}
-}
-
-func waitOnNotification(t *testing.T, c, timeoutSignal <-chan struct{}, timeoutMessage string) {
-	select {
-	case <-c:
-	case <-timeoutSignal:
-		t.Fatalf(timeoutMessage)
-	}
-}
-
-const dummyClientVerificationPayload = `
-{
-	"status": 0,
-	"payload": ""
-}`
-
 func runServer(t *testing.T, runConfig *runServerConfig) {
 func runServer(t *testing.T, runConfig *runServerConfig) {
 
 
 	// configure authorized access
 	// configure authorized access
@@ -400,15 +382,36 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 		t.Fatalf("error issuing authorization: %s", err)
 		t.Fatalf("error issuing authorization: %s", err)
 	}
 	}
 
 
+	// Enable tactics when the test protocol is meek. Both the client and the
+	// server will be configured to support tactics. The client config will be
+	// set with a nonfunctional config so thatthe tactics request must
+	// succeed, overriding the nonfunctional values, for the tunnel to
+	// establish.
+
+	doTactics := protocol.TunnelProtocolUsesMeek(runConfig.tunnelProtocol)
+
+	// All servers require a tactics config with valid keys.
+	tacticsRequestPublicKey, tacticsRequestPrivateKey, tacticsRequestObfuscatedKey, err :=
+		tactics.GenerateKeys()
+	if err != nil {
+		t.Fatalf("error generating tactics keys: %s", err)
+	}
+
 	// create a server
 	// create a server
 
 
-	serverConfigJSON, _, encodedServerEntry, err := GenerateConfig(
-		&GenerateConfigParams{
-			ServerIPAddress:      serverIPAddress,
-			EnableSSHAPIRequests: runConfig.enableSSHAPIRequests,
-			WebServerPort:        8000,
-			TunnelProtocolPorts:  map[string]int{runConfig.tunnelProtocol: 4000},
-		})
+	generateConfigParams := &GenerateConfigParams{
+		ServerIPAddress:      serverIPAddress,
+		EnableSSHAPIRequests: runConfig.enableSSHAPIRequests,
+		WebServerPort:        8000,
+		TunnelProtocolPorts:  map[string]int{runConfig.tunnelProtocol: 4000},
+	}
+
+	if doTactics {
+		generateConfigParams.TacticsRequestPublicKey = tacticsRequestPublicKey
+		generateConfigParams.TacticsRequestObfuscatedKey = tacticsRequestObfuscatedKey
+	}
+
+	serverConfigJSON, _, encodedServerEntry, err := GenerateConfig(generateConfigParams)
 	if err != nil {
 	if err != nil {
 		t.Fatalf("error generating server config: %s", err)
 		t.Fatalf("error generating server config: %s", err)
 	}
 	}
@@ -418,7 +421,7 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 	// Pave psinet with random values to test handshake homepages.
 	// Pave psinet with random values to test handshake homepages.
 	psinetFilename := filepath.Join(testDataDirName, "psinet.json")
 	psinetFilename := filepath.Join(testDataDirName, "psinet.json")
 	sponsorID, expectedHomepageURL := pavePsinetDatabaseFile(
 	sponsorID, expectedHomepageURL := pavePsinetDatabaseFile(
-		t, runConfig.doDefaultSessionID, psinetFilename)
+		t, runConfig.doDefaultSponsorID, psinetFilename)
 
 
 	// Pave OSL config for SLOK testing
 	// Pave OSL config for SLOK testing
 	oslConfigFilename := filepath.Join(testDataDirName, "osl_config.json")
 	oslConfigFilename := filepath.Join(testDataDirName, "osl_config.json")
@@ -432,12 +435,19 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 		t, trafficRulesFilename, propagationChannelID, accessType,
 		t, trafficRulesFilename, propagationChannelID, accessType,
 		runConfig.requireAuthorization, runConfig.denyTrafficRules)
 		runConfig.requireAuthorization, runConfig.denyTrafficRules)
 
 
+	tacticsConfigFilename := filepath.Join(testDataDirName, "tactics_config.json")
+	paveTacticsConfigFile(
+		t, tacticsConfigFilename,
+		tacticsRequestPublicKey, tacticsRequestPrivateKey, tacticsRequestObfuscatedKey,
+		propagationChannelID)
+
 	var serverConfig map[string]interface{}
 	var serverConfig map[string]interface{}
 	json.Unmarshal(serverConfigJSON, &serverConfig)
 	json.Unmarshal(serverConfigJSON, &serverConfig)
 	serverConfig["GeoIPDatabaseFilename"] = ""
 	serverConfig["GeoIPDatabaseFilename"] = ""
 	serverConfig["PsinetDatabaseFilename"] = psinetFilename
 	serverConfig["PsinetDatabaseFilename"] = psinetFilename
 	serverConfig["TrafficRulesFilename"] = trafficRulesFilename
 	serverConfig["TrafficRulesFilename"] = trafficRulesFilename
 	serverConfig["OSLConfigFilename"] = oslConfigFilename
 	serverConfig["OSLConfigFilename"] = oslConfigFilename
+	serverConfig["TacticsConfigFilename"] = tacticsConfigFilename
 	serverConfig["LogFilename"] = filepath.Join(testDataDirName, "psiphond.log")
 	serverConfig["LogFilename"] = filepath.Join(testDataDirName, "psiphond.log")
 	serverConfig["LogLevel"] = "debug"
 	serverConfig["LogLevel"] = "debug"
 
 
@@ -495,7 +505,7 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 
 
 		// Pave new config files with different random values.
 		// Pave new config files with different random values.
 		sponsorID, expectedHomepageURL = pavePsinetDatabaseFile(
 		sponsorID, expectedHomepageURL = pavePsinetDatabaseFile(
-			t, runConfig.doDefaultSessionID, psinetFilename)
+			t, runConfig.doDefaultSponsorID, psinetFilename)
 
 
 		propagationChannelID = paveOSLConfigFile(t, oslConfigFilename)
 		propagationChannelID = paveOSLConfigFile(t, oslConfigFilename)
 
 
@@ -524,9 +534,8 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 	numTunnels := 1
 	numTunnels := 1
 	localSOCKSProxyPort := 1081
 	localSOCKSProxyPort := 1081
 	localHTTPProxyPort := 8081
 	localHTTPProxyPort := 8081
-	establishTunnelPausePeriodSeconds := 1
 
 
-	// Note: calling LoadConfig ensures all *int config fields are initialized
+	// Note: calling LoadConfig ensures the Config is fully initialized
 	clientConfigJSON := `
 	clientConfigJSON := `
     {
     {
         "ClientPlatform" : "Windows",
         "ClientPlatform" : "Windows",
@@ -538,33 +547,57 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
     }`
     }`
 	clientConfig, _ := psiphon.LoadConfig([]byte(clientConfigJSON))
 	clientConfig, _ := psiphon.LoadConfig([]byte(clientConfigJSON))
 
 
-	if !runConfig.doDefaultSessionID {
+	clientConfig.DataStoreDirectory = testDataDirName
+	err = psiphon.InitDataStore(clientConfig)
+	if err != nil {
+		t.Fatalf("error initializing client datastore: %s", err)
+	}
+	psiphon.DeleteSLOKs()
+
+	if !runConfig.doDefaultSponsorID {
 		clientConfig.SponsorId = sponsorID
 		clientConfig.SponsorId = sponsorID
 	}
 	}
 	clientConfig.PropagationChannelId = propagationChannelID
 	clientConfig.PropagationChannelId = propagationChannelID
-	clientConfig.ConnectionWorkerPoolSize = numTunnels
 	clientConfig.TunnelPoolSize = numTunnels
 	clientConfig.TunnelPoolSize = numTunnels
-	clientConfig.EstablishTunnelPausePeriodSeconds = &establishTunnelPausePeriodSeconds
 	clientConfig.TargetServerEntry = string(encodedServerEntry)
 	clientConfig.TargetServerEntry = string(encodedServerEntry)
-	clientConfig.TunnelProtocol = runConfig.tunnelProtocol
 	clientConfig.LocalSocksProxyPort = localSOCKSProxyPort
 	clientConfig.LocalSocksProxyPort = localSOCKSProxyPort
 	clientConfig.LocalHttpProxyPort = localHTTPProxyPort
 	clientConfig.LocalHttpProxyPort = localHTTPProxyPort
 	clientConfig.EmitSLOKs = true
 	clientConfig.EmitSLOKs = true
 
 
+	if runConfig.doClientVerification {
+		clientConfig.ClientPlatform = "Android"
+	}
+
 	if !runConfig.omitAuthorization {
 	if !runConfig.omitAuthorization {
 		clientConfig.Authorizations = []string{clientAuthorization}
 		clientConfig.Authorizations = []string{clientAuthorization}
 	}
 	}
 
 
-	if runConfig.doClientVerification {
-		clientConfig.ClientPlatform = "Android"
+	if doTactics {
+		clientConfig.NetworkIDGetter = &testNetworkGetter{}
 	}
 	}
 
 
-	clientConfig.DataStoreDirectory = testDataDirName
-	err = psiphon.InitDataStore(clientConfig)
+	// The following config values must be applied through client parameters
+	// (setting the fields in Config directly will have no effect since the
+	// client parameters have been populated by LoadConfig).
+
+	applyParameters := make(map[string]interface{})
+
+	applyParameters[parameters.ConnectionWorkerPoolSize] = numTunnels
+
+	applyParameters[parameters.EstablishTunnelPausePeriod] = "250ms"
+
+	applyParameters[parameters.LimitTunnelProtocols] = protocol.TunnelProtocols{runConfig.tunnelProtocol}
+
+	if doTactics {
+		// Configure nonfunctional values that must be overridden by tactics.
+		applyParameters[parameters.TunnelConnectTimeout] = "1s"
+		applyParameters[parameters.TunnelRateLimits] = common.RateLimits{WriteBytesPerSecond: 1}
+	}
+
+	err = clientConfig.SetClientParameters("", true, applyParameters)
 	if err != nil {
 	if err != nil {
-		t.Fatalf("error initializing client datastore: %s", err)
+		t.Fatalf("SetClientParameters failed: %s", err)
 	}
 	}
-	psiphon.DeleteSLOKs()
 
 
 	controller, err := psiphon.NewController(clientConfig)
 	controller, err := psiphon.NewController(clientConfig)
 	if err != nil {
 	if err != nil {
@@ -1144,3 +1177,76 @@ func paveOSLConfigFile(t *testing.T, oslConfigFilename string) string {
 
 
 	return propagationChannelID
 	return propagationChannelID
 }
 }
+
+func paveTacticsConfigFile(
+	t *testing.T, tacticsConfigFilename string,
+	tacticsRequestPublicKey, tacticsRequestPrivateKey, tacticsRequestObfuscatedKey string,
+	propagationChannelID string) {
+
+	tacticsConfigJSONFormat := `
+    {
+      "RequestPublicKey" : "%s",
+      "RequestPrivateKey" : "%s",
+      "RequestObfuscatedKey" : "%s",
+      "DefaultTactics" : {
+        "TTL" : "60s",
+        "Probability" : 1.0
+      },
+      "FilteredTactics" : [
+        {
+          "Filter" : {
+            "APIParameters" : {"propagation_channel_id" : ["%s"]},
+            "SpeedTestRTTMilliseconds" : {
+              "Aggregation" : "Median",
+              "AtLeast" : 1
+            }
+          },
+          "Tactics" : {
+            "Parameters" : {
+              "TunnelConnectTimeout" : "20s",
+              "TunnelRateLimits" : {"WriteBytesPerSecond": 1000000}
+            }
+          }
+        }
+      ]
+    }
+    `
+
+	tacticsConfigJSON := fmt.Sprintf(
+		tacticsConfigJSONFormat,
+		tacticsRequestPublicKey, tacticsRequestPrivateKey, tacticsRequestObfuscatedKey,
+		propagationChannelID)
+
+	err := ioutil.WriteFile(tacticsConfigFilename, []byte(tacticsConfigJSON), 0600)
+	if err != nil {
+		t.Fatalf("error paving tactics config file: %s", err)
+	}
+}
+
+func sendNotificationReceived(c chan<- struct{}) {
+	select {
+	case c <- *new(struct{}):
+	default:
+	}
+}
+
+func waitOnNotification(t *testing.T, c, timeoutSignal <-chan struct{}, timeoutMessage string) {
+	select {
+	case <-c:
+	case <-timeoutSignal:
+		t.Fatalf(timeoutMessage)
+	}
+}
+
+const dummyClientVerificationPayload = `
+{
+	"status": 0,
+	"payload": ""
+}`
+
+type testNetworkGetter struct {
+}
+
+func (testNetworkGetter) GetNetworkID() string {
+	return "NETWORK1"
+}