github.com/psiphon-labs/psiphon-tunnel-core@v2.0.28+incompatible/psiphon/common/tactics/tactics_test.go (about)

     1  /*
     2   * Copyright (c) 2018, Psiphon Inc.
     3   * All rights reserved.
     4   *
     5   * This program is free software: you can redistribute it and/or modify
     6   * it under the terms of the GNU General Public License as published by
     7   * the Free Software Foundation, either version 3 of the License, or
     8   * (at your option) any later version.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package tactics
    21  
    22  import (
    23  	"bytes"
    24  	"context"
    25  	"fmt"
    26  	"io/ioutil"
    27  	"net"
    28  	"net/http"
    29  	"os"
    30  	"reflect"
    31  	"strings"
    32  	"testing"
    33  	"time"
    34  
    35  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
    36  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
    37  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
    38  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/stacktrace"
    39  )
    40  
    41  func TestTactics(t *testing.T) {
    42  
    43  	// Server tactics configuration
    44  
    45  	// Long and short region lists test both map and slice lookups.
    46  	//
    47  	// Repeated median aggregation tests aggregation memoization.
    48  	//
    49  	// The test-packetman-spec tests a reference between a filter tactics
    50  	// and default tactics.
    51  
    52  	tacticsConfigTemplate := `
    53      {
    54        "RequestPublicKey" : "%s",
    55        "RequestPrivateKey" : "%s",
    56        "RequestObfuscatedKey" : "%s",
    57        "DefaultTactics" : {
    58          "TTL" : "1s",
    59          "Probability" : %0.1f,
    60          "Parameters" : {
    61            "NetworkLatencyMultiplier" : %0.1f,
    62            "ServerPacketManipulationSpecs" : [{"Name": "test-packetman-spec", "PacketSpecs": [["TCP-flags S"]]}]
    63          }
    64        },
    65        "FilteredTactics" : [
    66          {
    67            "Filter" : {
    68              "Regions": ["R1", "R2", "R3", "R4", "R5", "R6"],
    69              "APIParameters" : {"client_platform" : ["P1"]},
    70              "SpeedTestRTTMilliseconds" : {
    71                "Aggregation" : "Median",
    72                "AtLeast" : 1
    73              }
    74            },
    75            "Tactics" : {
    76              "Parameters" : {
    77                "ConnectionWorkerPoolSize" : %d
    78              }
    79            }
    80          },
    81          {
    82            "Filter" : {
    83              "Regions": ["R1"],
    84              "ASNs": ["1"],
    85              "APIParameters" : {"client_platform" : ["P1"], "client_version": ["V1"]},
    86              "SpeedTestRTTMilliseconds" : {
    87                "Aggregation" : "Median",
    88                "AtLeast" : 1
    89              }
    90            },
    91            "Tactics" : {
    92              "Parameters" : {
    93                %s
    94              }
    95            }
    96          },
    97          {
    98            "Filter" : {
    99              "Regions": ["R2"]
   100            },
   101            "Tactics" : {
   102              "Parameters" : {
   103                "ConnectionWorkerPoolSize" : %d
   104              }
   105            }
   106          },
   107          {
   108            "Filter" : {
   109              "Regions": ["R7"]
   110            },
   111            "Tactics" : {
   112              "Parameters" : {
   113                "ServerProtocolPacketManipulations": {"All" : ["test-packetman-spec"]}
   114              }
   115            }
   116          }
   117        ]
   118      }
   119      `
   120  	if stringLookupThreshold != 5 {
   121  		t.Fatalf("unexpected stringLookupThreshold")
   122  	}
   123  
   124  	encodedRequestPublicKey, encodedRequestPrivateKey, encodedObfuscatedKey, err := GenerateKeys()
   125  	if err != nil {
   126  		t.Fatalf("GenerateKeys failed: %s", err)
   127  	}
   128  
   129  	tacticsProbability := 0.5
   130  	tacticsNetworkLatencyMultiplier := 2.0
   131  	tacticsConnectionWorkerPoolSize := 5
   132  	tacticsLimitTunnelProtocols := protocol.TunnelProtocols{"OSSH", "SSH"}
   133  	jsonTacticsLimitTunnelProtocols := `"LimitTunnelProtocols" : ["OSSH", "SSH"]`
   134  
   135  	expectedApplyCount := 3
   136  
   137  	tacticsConfig := fmt.Sprintf(
   138  		tacticsConfigTemplate,
   139  		encodedRequestPublicKey,
   140  		encodedRequestPrivateKey,
   141  		encodedObfuscatedKey,
   142  		tacticsProbability,
   143  		tacticsNetworkLatencyMultiplier,
   144  		tacticsConnectionWorkerPoolSize,
   145  		jsonTacticsLimitTunnelProtocols,
   146  		tacticsConnectionWorkerPoolSize+1)
   147  
   148  	file, err := ioutil.TempFile("", "tactics.config")
   149  	if err != nil {
   150  		t.Fatalf("TempFile create failed: %s", err)
   151  	}
   152  	_, err = file.Write([]byte(tacticsConfig))
   153  	if err != nil {
   154  		t.Fatalf("TempFile write failed: %s", err)
   155  	}
   156  	file.Close()
   157  
   158  	configFileName := file.Name()
   159  	defer os.Remove(configFileName)
   160  
   161  	// Configure and run server
   162  
   163  	// Mock server uses an insecure HTTP transport that exposes endpoint names
   164  
   165  	clientGeoIPData := common.GeoIPData{Country: "R1", ASN: "1"}
   166  
   167  	logger := newTestLogger()
   168  
   169  	validator := func(
   170  		apiParams common.APIParameters) error {
   171  
   172  		expectedParams := []string{"client_platform", "client_version"}
   173  		for _, name := range expectedParams {
   174  			value, ok := apiParams[name]
   175  			if !ok {
   176  				return fmt.Errorf("missing param: %s", name)
   177  			}
   178  			_, ok = value.(string)
   179  			if !ok {
   180  				return fmt.Errorf("invalid param type: %s", name)
   181  			}
   182  		}
   183  		return nil
   184  	}
   185  
   186  	formatter := func(
   187  		geoIPData common.GeoIPData,
   188  		apiParams common.APIParameters) common.LogFields {
   189  
   190  		return common.LogFields(apiParams)
   191  	}
   192  
   193  	server, err := NewServer(
   194  		logger,
   195  		formatter,
   196  		validator,
   197  		configFileName)
   198  	if err != nil {
   199  		t.Fatalf("NewServer failed: %s", err)
   200  	}
   201  
   202  	listener, err := net.Listen("tcp", "127.0.0.1:0")
   203  	if err != nil {
   204  		t.Fatalf("Listen failed: %s", err)
   205  	}
   206  
   207  	serverAddress := listener.Addr().String()
   208  
   209  	go func() {
   210  		serveMux := http.NewServeMux()
   211  		serveMux.HandleFunc(
   212  			"/",
   213  			func(w http.ResponseWriter, r *http.Request) {
   214  				// Ensure RTT takes at least 1 millisecond for speed test
   215  				time.Sleep(1 * time.Millisecond)
   216  				endPoint := strings.Trim(r.URL.Path, "/")
   217  				if !server.HandleEndPoint(endPoint, clientGeoIPData, w, r) {
   218  					http.NotFound(w, r)
   219  				}
   220  			})
   221  		httpServer := &http.Server{
   222  			Addr:    serverAddress,
   223  			Handler: serveMux,
   224  		}
   225  		httpServer.Serve(listener)
   226  	}()
   227  
   228  	// Configure client
   229  
   230  	params, err := parameters.NewParameters(
   231  		func(err error) {
   232  			t.Fatalf("Parameters getValue failed: %s", err)
   233  		})
   234  	if err != nil {
   235  		t.Fatalf("NewParameters failed: %s", err)
   236  	}
   237  
   238  	networkID := "NETWORK1"
   239  
   240  	getNetworkID := func() string { return networkID }
   241  
   242  	apiParams := common.APIParameters{
   243  		"client_platform": "P1",
   244  		"client_version":  "V1"}
   245  
   246  	storer := newTestStorer()
   247  
   248  	endPointRegion := "R0"
   249  	endPointProtocol := "OSSH"
   250  	differentEndPointProtocol := "SSH"
   251  
   252  	obfuscatedRoundTripper := func(
   253  		ctx context.Context,
   254  		endPoint string,
   255  		requestBody []byte) ([]byte, error) {
   256  
   257  		// This mock ObfuscatedRoundTripper does not actually obfuscate the endpoint
   258  		// value.
   259  
   260  		request, err := http.NewRequest(
   261  			"POST",
   262  			fmt.Sprintf("http://%s/%s", serverAddress, endPoint),
   263  			bytes.NewReader(requestBody))
   264  		if err != nil {
   265  			return nil, err
   266  		}
   267  		request = request.WithContext(ctx)
   268  		response, err := http.DefaultClient.Do(request)
   269  		if err != nil {
   270  			return nil, err
   271  		}
   272  		defer response.Body.Close()
   273  		if response.StatusCode != http.StatusOK {
   274  			return nil, fmt.Errorf("HTTP request failed: %d", response.StatusCode)
   275  		}
   276  		body, err := ioutil.ReadAll(response.Body)
   277  		if err != nil {
   278  			return nil, err
   279  		}
   280  		return body, nil
   281  	}
   282  
   283  	// There should be no local tactics
   284  
   285  	tacticsRecord, err := UseStoredTactics(storer, networkID)
   286  	if err != nil {
   287  		t.Fatalf("UseStoredTactics failed: %s", err)
   288  	}
   289  
   290  	if tacticsRecord != nil {
   291  		t.Fatalf("unexpected tactics record")
   292  	}
   293  
   294  	// Helper to check that expected tactics parameters are returned
   295  
   296  	checkParameters := func(r *Record) {
   297  
   298  		p, err := parameters.NewParameters(nil)
   299  		if err != nil {
   300  			t.Fatalf("NewParameters failed: %s", err)
   301  		}
   302  
   303  		if r.Tactics.Probability != tacticsProbability {
   304  			t.Fatalf("Unexpected probability: %f", r.Tactics.Probability)
   305  		}
   306  
   307  		// skipOnError is true for Psiphon clients
   308  		counts, err := p.Set(r.Tag, true, r.Tactics.Parameters)
   309  		if err != nil {
   310  			t.Fatalf("Apply failed: %s", err)
   311  		}
   312  
   313  		if counts[0] != expectedApplyCount {
   314  			t.Fatalf("Unexpected apply count: %d", counts[0])
   315  		}
   316  
   317  		multipler := p.Get().Float(parameters.NetworkLatencyMultiplier)
   318  		if multipler != tacticsNetworkLatencyMultiplier {
   319  			t.Fatalf("Unexpected NetworkLatencyMultiplier: %v", multipler)
   320  		}
   321  
   322  		connectionWorkerPoolSize := p.Get().Int(parameters.ConnectionWorkerPoolSize)
   323  		if connectionWorkerPoolSize != tacticsConnectionWorkerPoolSize {
   324  			t.Fatalf("Unexpected ConnectionWorkerPoolSize: %v", connectionWorkerPoolSize)
   325  		}
   326  
   327  		limitTunnelProtocols := p.Get().TunnelProtocols(parameters.LimitTunnelProtocols)
   328  		if !reflect.DeepEqual(limitTunnelProtocols, tacticsLimitTunnelProtocols) {
   329  			t.Fatalf("Unexpected LimitTunnelProtocols: %v", limitTunnelProtocols)
   330  		}
   331  	}
   332  
   333  	// Initial tactics request; will also run a speed test
   334  
   335  	// Request should complete in < 1 second
   336  	ctx, cancelFunc := context.WithTimeout(context.Background(), 1*time.Second)
   337  
   338  	initialFetchTacticsRecord, err := FetchTactics(
   339  		ctx,
   340  		params,
   341  		storer,
   342  		getNetworkID,
   343  		apiParams,
   344  		endPointProtocol,
   345  		endPointRegion,
   346  		encodedRequestPublicKey,
   347  		encodedObfuscatedKey,
   348  		obfuscatedRoundTripper)
   349  
   350  	cancelFunc()
   351  
   352  	if err != nil {
   353  		t.Fatalf("FetchTactics failed: %s", err)
   354  	}
   355  
   356  	if initialFetchTacticsRecord == nil {
   357  		t.Fatalf("expected tactics record")
   358  	}
   359  
   360  	checkParameters(initialFetchTacticsRecord)
   361  
   362  	// There should now be cached local tactics
   363  
   364  	storedTacticsRecord, err := UseStoredTactics(storer, networkID)
   365  	if err != nil {
   366  		t.Fatalf("UseStoredTactics failed: %s", err)
   367  	}
   368  
   369  	if storedTacticsRecord == nil {
   370  		t.Fatalf("expected stored tactics record")
   371  	}
   372  
   373  	// Strip monotonic component so comparisons will work
   374  	initialFetchTacticsRecord.Expiry = initialFetchTacticsRecord.Expiry.Round(0)
   375  
   376  	if !reflect.DeepEqual(initialFetchTacticsRecord, storedTacticsRecord) {
   377  		t.Fatalf("tactics records are not identical:\n\n%#v\n\n%#v\n\n",
   378  			initialFetchTacticsRecord, storedTacticsRecord)
   379  	}
   380  
   381  	checkParameters(storedTacticsRecord)
   382  
   383  	// There should now be a speed test sample
   384  
   385  	speedTestSamples, err := getSpeedTestSamples(storer, networkID)
   386  	if err != nil {
   387  		t.Fatalf("getSpeedTestSamples failed: %s", err)
   388  	}
   389  
   390  	if len(speedTestSamples) != 1 {
   391  		t.Fatalf("unexpected speed test samples count")
   392  	}
   393  
   394  	// Wait for tactics to expire
   395  
   396  	time.Sleep(1 * time.Second)
   397  
   398  	storedTacticsRecord, err = UseStoredTactics(storer, networkID)
   399  	if err != nil {
   400  		t.Fatalf("UseStoredTactics failed: %s", err)
   401  	}
   402  
   403  	if storedTacticsRecord != nil {
   404  		t.Fatalf("unexpected stored tactics record")
   405  	}
   406  
   407  	// Next fetch should merge empty payload as tag matches
   408  	// TODO: inspect tactics response payload
   409  
   410  	fetchTacticsRecord, err := FetchTactics(
   411  		context.Background(),
   412  		params,
   413  		storer,
   414  		getNetworkID,
   415  		apiParams,
   416  		endPointProtocol,
   417  		endPointRegion,
   418  		encodedRequestPublicKey,
   419  		encodedObfuscatedKey,
   420  		obfuscatedRoundTripper)
   421  	if err != nil {
   422  		t.Fatalf("FetchTactics failed: %s", err)
   423  	}
   424  
   425  	if fetchTacticsRecord == nil {
   426  		t.Fatalf("expected tactics record")
   427  	}
   428  
   429  	if initialFetchTacticsRecord.Tag != fetchTacticsRecord.Tag {
   430  		t.Fatalf("tags are not identical")
   431  	}
   432  
   433  	if initialFetchTacticsRecord.Expiry.Equal(fetchTacticsRecord.Expiry) {
   434  		t.Fatalf("expiries unexpectedly identical")
   435  	}
   436  
   437  	if !reflect.DeepEqual(initialFetchTacticsRecord.Tactics, fetchTacticsRecord.Tactics) {
   438  		t.Fatalf("tactics are not identical:\n\n%#v\n\n%#v\n\n",
   439  			initialFetchTacticsRecord.Tactics, fetchTacticsRecord.Tactics)
   440  	}
   441  
   442  	checkParameters(fetchTacticsRecord)
   443  
   444  	// Modify tactics configuration to change payload
   445  
   446  	tacticsConnectionWorkerPoolSize = 6
   447  
   448  	tacticsLimitTunnelProtocols = protocol.TunnelProtocols{}
   449  	jsonTacticsLimitTunnelProtocols = ``
   450  	expectedApplyCount = 2
   451  
   452  	// Omitting LimitTunnelProtocols entirely tests this bug fix: When a new
   453  	// tactics payload is obtained, all previous parameters should be cleared.
   454  	//
   455  	// In the bug, any previous parameters not in the new tactics were
   456  	// incorrectly retained. In this test case, LimitTunnelProtocols is
   457  	// omitted in the new tactics; if FetchTactics fails to clear the old
   458  	// LimitTunnelProtocols then the test will fail.
   459  
   460  	tacticsConfig = fmt.Sprintf(
   461  		tacticsConfigTemplate,
   462  		encodedRequestPublicKey,
   463  		encodedRequestPrivateKey,
   464  		encodedObfuscatedKey,
   465  		tacticsProbability,
   466  		tacticsNetworkLatencyMultiplier,
   467  		tacticsConnectionWorkerPoolSize,
   468  		jsonTacticsLimitTunnelProtocols,
   469  		tacticsConnectionWorkerPoolSize+1)
   470  
   471  	err = ioutil.WriteFile(configFileName, []byte(tacticsConfig), 0600)
   472  	if err != nil {
   473  		t.Fatalf("WriteFile failed: %s", err)
   474  	}
   475  
   476  	reloaded, err := server.Reload()
   477  	if err != nil {
   478  		t.Fatalf("Reload failed: %s", err)
   479  	}
   480  
   481  	if !reloaded {
   482  		t.Fatalf("Server config failed to reload")
   483  	}
   484  
   485  	// Next fetch should return a different payload
   486  
   487  	fetchTacticsRecord, err = FetchTactics(
   488  		context.Background(),
   489  		params,
   490  		storer,
   491  		getNetworkID,
   492  		apiParams,
   493  		endPointProtocol,
   494  		endPointRegion,
   495  		encodedRequestPublicKey,
   496  		encodedObfuscatedKey,
   497  		obfuscatedRoundTripper)
   498  	if err != nil {
   499  		t.Fatalf("FetchTactics failed: %s", err)
   500  	}
   501  
   502  	if fetchTacticsRecord == nil {
   503  		t.Fatalf("expected tactics record")
   504  	}
   505  
   506  	if initialFetchTacticsRecord.Tag == fetchTacticsRecord.Tag {
   507  		t.Fatalf("tags unexpectedly identical")
   508  	}
   509  
   510  	if initialFetchTacticsRecord.Expiry.Equal(fetchTacticsRecord.Expiry) {
   511  		t.Fatalf("expires unexpectedly identical")
   512  	}
   513  
   514  	if reflect.DeepEqual(initialFetchTacticsRecord.Tactics, fetchTacticsRecord.Tactics) {
   515  		t.Fatalf("tactics unexpectedly identical")
   516  	}
   517  
   518  	checkParameters(fetchTacticsRecord)
   519  
   520  	// Exercise handshake transport of tactics
   521  
   522  	// Wait for tactics to expire; handshake should renew
   523  	time.Sleep(1 * time.Second)
   524  
   525  	handshakeParams := common.APIParameters{
   526  		"client_platform": "P1",
   527  		"client_version":  "V1"}
   528  
   529  	err = SetTacticsAPIParameters(storer, networkID, handshakeParams)
   530  	if err != nil {
   531  		t.Fatalf("SetTacticsAPIParameters failed: %s", err)
   532  	}
   533  
   534  	tacticsPayload, err := server.GetTacticsPayload(clientGeoIPData, handshakeParams)
   535  	if err != nil {
   536  		t.Fatalf("GetTacticsPayload failed: %s", err)
   537  	}
   538  
   539  	handshakeTacticsRecord, err := HandleTacticsPayload(storer, networkID, tacticsPayload)
   540  	if err != nil {
   541  		t.Fatalf("HandleTacticsPayload failed: %s", err)
   542  	}
   543  
   544  	if handshakeTacticsRecord == nil {
   545  		t.Fatalf("expected tactics record")
   546  	}
   547  
   548  	if fetchTacticsRecord.Tag != handshakeTacticsRecord.Tag {
   549  		t.Fatalf("tags are not identical")
   550  	}
   551  
   552  	if fetchTacticsRecord.Expiry.Equal(handshakeTacticsRecord.Expiry) {
   553  		t.Fatalf("expiries unexpectedly identical")
   554  	}
   555  
   556  	if !reflect.DeepEqual(fetchTacticsRecord.Tactics, handshakeTacticsRecord.Tactics) {
   557  		t.Fatalf("tactics are not identical:\n\n%#v\n\n%#v\n\n",
   558  			fetchTacticsRecord.Tactics, handshakeTacticsRecord.Tactics)
   559  	}
   560  
   561  	checkParameters(handshakeTacticsRecord)
   562  
   563  	// Now there should be stored tactics
   564  
   565  	storedTacticsRecord, err = UseStoredTactics(storer, networkID)
   566  	if err != nil {
   567  		t.Fatalf("UseStoredTactics failed: %s", err)
   568  	}
   569  
   570  	if storedTacticsRecord == nil {
   571  		t.Fatalf("expected stored tactics record")
   572  	}
   573  
   574  	handshakeTacticsRecord.Expiry = handshakeTacticsRecord.Expiry.Round(0)
   575  
   576  	if !reflect.DeepEqual(handshakeTacticsRecord, storedTacticsRecord) {
   577  		t.Fatalf("tactics records are not identical:\n\n%#v\n\n%#v\n\n",
   578  			handshakeTacticsRecord, storedTacticsRecord)
   579  	}
   580  
   581  	checkParameters(storedTacticsRecord)
   582  
   583  	// Change network ID, should be no stored tactics
   584  
   585  	networkID = "NETWORK2"
   586  
   587  	storedTacticsRecord, err = UseStoredTactics(storer, networkID)
   588  	if err != nil {
   589  		t.Fatalf("UseStoredTactics failed: %s", err)
   590  	}
   591  
   592  	if storedTacticsRecord != nil {
   593  		t.Fatalf("unexpected stored tactics record")
   594  	}
   595  
   596  	// Exercise speed test sample truncation
   597  
   598  	maxSamples := params.Get().Int(parameters.SpeedTestMaxSampleCount)
   599  
   600  	for i := 0; i < maxSamples*2; i++ {
   601  
   602  		response, err := MakeSpeedTestResponse(0, 0)
   603  		if err != nil {
   604  			t.Fatalf("MakeSpeedTestResponse failed: %s", err)
   605  		}
   606  
   607  		err = AddSpeedTestSample(
   608  			params,
   609  			storer,
   610  			networkID,
   611  			"",
   612  			differentEndPointProtocol,
   613  			100*time.Millisecond,
   614  			nil,
   615  			response)
   616  		if err != nil {
   617  			t.Fatalf("AddSpeedTestSample failed: %s", err)
   618  		}
   619  	}
   620  
   621  	speedTestSamples, err = getSpeedTestSamples(storer, networkID)
   622  	if err != nil {
   623  		t.Fatalf("getSpeedTestSamples failed: %s", err)
   624  	}
   625  
   626  	if len(speedTestSamples) != maxSamples {
   627  		t.Fatalf("unexpected speed test samples count")
   628  	}
   629  
   630  	for _, sample := range speedTestSamples {
   631  		if sample.EndPointProtocol == endPointProtocol {
   632  			t.Fatalf("unexpected old speed test sample")
   633  		}
   634  	}
   635  
   636  	// Fetch should fail when using incorrect keys
   637  
   638  	encodedIncorrectRequestPublicKey, _, encodedIncorrectObfuscatedKey, err := GenerateKeys()
   639  	if err != nil {
   640  		t.Fatalf("GenerateKeys failed: %s", err)
   641  	}
   642  
   643  	_, err = FetchTactics(
   644  		context.Background(),
   645  		params,
   646  		storer,
   647  		getNetworkID,
   648  		apiParams,
   649  		endPointProtocol,
   650  		endPointRegion,
   651  		encodedIncorrectRequestPublicKey,
   652  		encodedObfuscatedKey,
   653  		obfuscatedRoundTripper)
   654  	if err == nil {
   655  		t.Fatalf("FetchTactics succeeded unexpectedly with incorrect request key")
   656  	}
   657  
   658  	_, err = FetchTactics(
   659  		context.Background(),
   660  		params,
   661  		storer,
   662  		getNetworkID,
   663  		apiParams,
   664  		endPointProtocol,
   665  		endPointRegion,
   666  		encodedRequestPublicKey,
   667  		encodedIncorrectObfuscatedKey,
   668  		obfuscatedRoundTripper)
   669  	if err == nil {
   670  		t.Fatalf("FetchTactics succeeded unexpectedly with incorrect obfuscated key")
   671  	}
   672  
   673  	// When no keys are supplied, untunneled tactics requests are not supported, but
   674  	// handshake tactics (GetTacticsPayload) should still work.
   675  
   676  	tacticsConfig = fmt.Sprintf(
   677  		tacticsConfigTemplate,
   678  		"",
   679  		"",
   680  		"",
   681  		tacticsProbability,
   682  		tacticsNetworkLatencyMultiplier,
   683  		tacticsConnectionWorkerPoolSize,
   684  		jsonTacticsLimitTunnelProtocols,
   685  		tacticsConnectionWorkerPoolSize+1)
   686  
   687  	err = ioutil.WriteFile(configFileName, []byte(tacticsConfig), 0600)
   688  	if err != nil {
   689  		t.Fatalf("WriteFile failed: %s", err)
   690  	}
   691  
   692  	reloaded, err = server.Reload()
   693  	if err != nil {
   694  		t.Fatalf("Reload failed: %s", err)
   695  	}
   696  
   697  	if !reloaded {
   698  		t.Fatalf("Server config failed to reload")
   699  	}
   700  
   701  	_, err = server.GetTacticsPayload(clientGeoIPData, handshakeParams)
   702  	if err != nil {
   703  		t.Fatalf("GetTacticsPayload failed: %s", err)
   704  	}
   705  
   706  	handled := server.HandleEndPoint(TACTICS_END_POINT, clientGeoIPData, nil, nil)
   707  	if handled {
   708  		t.Fatalf("HandleEndPoint unexpectedly handled request")
   709  	}
   710  
   711  	handled = server.HandleEndPoint(SPEED_TEST_END_POINT, clientGeoIPData, nil, nil)
   712  	if handled {
   713  		t.Fatalf("HandleEndPoint unexpectedly handled request")
   714  	}
   715  
   716  	// TODO: test replay attack defence
   717  	// TODO: test Server.Validate with invalid tactics configurations
   718  }
   719  
   720  func TestTacticsFilterGeoIPScope(t *testing.T) {
   721  
   722  	encodedRequestPublicKey, encodedRequestPrivateKey, encodedObfuscatedKey, err := GenerateKeys()
   723  	if err != nil {
   724  		t.Fatalf("GenerateKeys failed: %s", err)
   725  	}
   726  
   727  	tacticsConfigTemplate := fmt.Sprintf(`
   728      {
   729        "RequestPublicKey" : "%s",
   730        "RequestPrivateKey" : "%s",
   731        "RequestObfuscatedKey" : "%s",
   732        "DefaultTactics" : {
   733          "TTL" : "60s",
   734          "Probability" : 1.0
   735        },
   736        %%s
   737      }
   738      `, encodedRequestPublicKey, encodedRequestPrivateKey, encodedObfuscatedKey)
   739  
   740  	// Test: region-only scope
   741  
   742  	filteredTactics := `
   743        "FilteredTactics" : [
   744          {
   745            "Filter" : {
   746              "Regions": ["R1", "R2", "R3"]
   747            }
   748          },
   749          {
   750            "Filter" : {
   751              "Regions": ["R4", "R5", "R6"]
   752            }
   753          }
   754        ]
   755  	`
   756  
   757  	tacticsConfig := fmt.Sprintf(tacticsConfigTemplate, filteredTactics)
   758  
   759  	file, err := ioutil.TempFile("", "tactics.config")
   760  	if err != nil {
   761  		t.Fatalf("TempFile create failed: %s", err)
   762  	}
   763  	_, err = file.Write([]byte(tacticsConfig))
   764  	if err != nil {
   765  		t.Fatalf("TempFile write failed: %s", err)
   766  	}
   767  	file.Close()
   768  
   769  	configFileName := file.Name()
   770  	defer os.Remove(configFileName)
   771  
   772  	server, err := NewServer(
   773  		nil,
   774  		nil,
   775  		nil,
   776  		configFileName)
   777  	if err != nil {
   778  		t.Fatalf("NewServer failed: %s", err)
   779  	}
   780  
   781  	reload := func() {
   782  		tacticsConfig = fmt.Sprintf(tacticsConfigTemplate, filteredTactics)
   783  
   784  		err = ioutil.WriteFile(configFileName, []byte(tacticsConfig), 0600)
   785  		if err != nil {
   786  			t.Fatalf("WriteFile failed: %s", err)
   787  		}
   788  
   789  		reloaded, err := server.Reload()
   790  		if err != nil {
   791  			t.Fatalf("Reload failed: %s", err)
   792  		}
   793  
   794  		if !reloaded {
   795  			t.Fatalf("Server config failed to reload")
   796  		}
   797  	}
   798  
   799  	geoIPData := common.GeoIPData{
   800  		Country: "R0",
   801  		ISP:     "I0",
   802  		ASN:     "0",
   803  		City:    "C0",
   804  	}
   805  
   806  	scope := server.GetFilterGeoIPScope(geoIPData)
   807  
   808  	if scope != GeoIPScopeRegion {
   809  		t.Fatalf("unexpected scope: %b", scope)
   810  	}
   811  
   812  	// Test: ISP-only scope
   813  
   814  	filteredTactics = `
   815        "FilteredTactics" : [
   816          {
   817            "Filter" : {
   818              "ISPs": ["I1", "I2", "I3"]
   819            }
   820          },
   821          {
   822            "Filter" : {
   823              "ISPs": ["I4", "I5", "I6"]
   824            }
   825          }
   826        ]
   827  	`
   828  
   829  	reload()
   830  
   831  	scope = server.GetFilterGeoIPScope(geoIPData)
   832  
   833  	if scope != GeoIPScopeISP {
   834  		t.Fatalf("unexpected scope: %b", scope)
   835  	}
   836  
   837  	// Test: ASN-only scope
   838  
   839  	filteredTactics = `
   840        "FilteredTactics" : [
   841          {
   842            "Filter" : {
   843              "ASNs": ["1", "2", "3"]
   844            }
   845          },
   846          {
   847            "Filter" : {
   848              "ASNs": ["4", "5", "6"]
   849            }
   850          }
   851        ]
   852  	`
   853  
   854  	reload()
   855  
   856  	scope = server.GetFilterGeoIPScope(geoIPData)
   857  
   858  	if scope != GeoIPScopeASN {
   859  		t.Fatalf("unexpected scope: %b", scope)
   860  	}
   861  
   862  	// Test: City-only scope
   863  
   864  	filteredTactics = `
   865        "FilteredTactics" : [
   866          {
   867            "Filter" : {
   868              "Cities": ["C1", "C2", "C3"]
   869            }
   870          },
   871          {
   872            "Filter" : {
   873              "Cities": ["C4", "C5", "C6"]
   874            }
   875          }
   876        ]
   877  	`
   878  
   879  	reload()
   880  
   881  	scope = server.GetFilterGeoIPScope(geoIPData)
   882  
   883  	if scope != GeoIPScopeCity {
   884  		t.Fatalf("unexpected scope: %b", scope)
   885  	}
   886  
   887  	// Test: full scope
   888  
   889  	filteredTactics = `
   890        "FilteredTactics" : [
   891          {
   892            "Filter" : {
   893              "Regions": ["R1", "R2", "R3"]
   894            }
   895          },
   896          {
   897            "Filter" : {
   898              "ISPs": ["I1", "I2", "I3"]
   899            }
   900          },
   901          {
   902            "Filter" : {
   903              "ASNs": ["1", "2", "3"]
   904            }
   905          },
   906          {
   907            "Filter" : {
   908              "Cities": ["C4", "C5", "C6"]
   909            }
   910          }
   911        ]
   912  	`
   913  
   914  	reload()
   915  
   916  	scope = server.GetFilterGeoIPScope(geoIPData)
   917  
   918  	if scope != GeoIPScopeRegion|GeoIPScopeISP|GeoIPScopeASN|GeoIPScopeCity {
   919  		t.Fatalf("unexpected scope: %b", scope)
   920  	}
   921  
   922  	// Test: conditional scopes
   923  
   924  	filteredTactics = `
   925        "FilteredTactics" : [
   926          {
   927            "Filter" : {
   928              "Regions": ["R1"]
   929            }
   930          },
   931          {
   932            "Filter" : {
   933              "Regions": ["R2"],
   934              "ISPs": ["I2a"]
   935            }
   936          },
   937          {
   938            "Filter" : {
   939              "Regions": ["R2"],
   940              "ISPs": ["I2b"]
   941            }
   942          },
   943          {
   944            "Filter" : {
   945              "Regions": ["R3"],
   946              "ISPs": ["I3a"],
   947              "Cities": ["C3a"]
   948            }
   949          },
   950          {
   951            "Filter" : {
   952              "Regions": ["R3"],
   953              "ISPs": ["I3b"],
   954              "Cities": ["C3b"]
   955            }
   956          },
   957          {
   958            "Filter" : {
   959              "Regions": ["R4"],
   960              "ASNs": ["4"]
   961            }
   962          },
   963          {
   964            "Filter" : {
   965              "Regions": ["R4"],
   966              "ASNs": ["4"]
   967            }
   968          },
   969          {
   970            "Filter" : {
   971              "Regions": ["R5"],
   972              "ASNs": ["5"],
   973              "Cities": ["C3a"]
   974            }
   975          },
   976          {
   977            "Filter" : {
   978              "Regions": ["R5"],
   979              "ASNs": ["5"],
   980              "Cities": ["C3b"]
   981            }
   982          }
   983        ]
   984  	`
   985  
   986  	reload()
   987  
   988  	scope = server.GetFilterGeoIPScope(common.GeoIPData{Country: "R0"})
   989  
   990  	if scope != GeoIPScopeRegion {
   991  		t.Fatalf("unexpected scope: %b", scope)
   992  	}
   993  
   994  	scope = server.GetFilterGeoIPScope(common.GeoIPData{Country: "R1"})
   995  
   996  	if scope != GeoIPScopeRegion {
   997  		t.Fatalf("unexpected scope: %b", scope)
   998  	}
   999  
  1000  	scope = server.GetFilterGeoIPScope(common.GeoIPData{Country: "R2"})
  1001  
  1002  	if scope != GeoIPScopeRegion|GeoIPScopeISP {
  1003  		t.Fatalf("unexpected scope: %b", scope)
  1004  	}
  1005  
  1006  	scope = server.GetFilterGeoIPScope(common.GeoIPData{Country: "R3"})
  1007  
  1008  	if scope != GeoIPScopeRegion|GeoIPScopeISP|GeoIPScopeCity {
  1009  		t.Fatalf("unexpected scope: %b", scope)
  1010  	}
  1011  
  1012  	scope = server.GetFilterGeoIPScope(common.GeoIPData{Country: "R4"})
  1013  
  1014  	if scope != GeoIPScopeRegion|GeoIPScopeASN {
  1015  		t.Fatalf("unexpected scope: %b", scope)
  1016  	}
  1017  
  1018  	scope = server.GetFilterGeoIPScope(common.GeoIPData{Country: "R5"})
  1019  
  1020  	if scope != GeoIPScopeRegion|GeoIPScopeASN|GeoIPScopeCity {
  1021  		t.Fatalf("unexpected scope: %b", scope)
  1022  	}
  1023  
  1024  	// Test: reset regional map optimization
  1025  
  1026  	filteredTactics = `
  1027        "FilteredTactics" : [
  1028          {
  1029            "Filter" : {
  1030              "Regions": ["R1"],
  1031              "ISPs": ["I1"]
  1032            }
  1033          },
  1034          {
  1035            "Filter" : {
  1036              "Cities": ["C1"]
  1037            }
  1038          }
  1039        ]
  1040  	`
  1041  
  1042  	reload()
  1043  
  1044  	scope = server.GetFilterGeoIPScope(common.GeoIPData{Country: "R0"})
  1045  
  1046  	if scope != GeoIPScopeRegion|GeoIPScopeISP|GeoIPScopeCity {
  1047  		t.Fatalf("unexpected scope: %b", scope)
  1048  	}
  1049  
  1050  	filteredTactics = `
  1051        "FilteredTactics" : [
  1052          {
  1053            "Filter" : {
  1054              "Regions": ["R1"],
  1055              "Cities": ["C1"]
  1056            }
  1057          },
  1058          {
  1059            "Filter" : {
  1060              "ISPs": ["I1"]
  1061            }
  1062          }
  1063        ]
  1064  	`
  1065  
  1066  	reload()
  1067  
  1068  	scope = server.GetFilterGeoIPScope(common.GeoIPData{Country: "R0"})
  1069  
  1070  	if scope != GeoIPScopeRegion|GeoIPScopeISP|GeoIPScopeCity {
  1071  		t.Fatalf("unexpected scope: %b", scope)
  1072  	}
  1073  }
  1074  
  1075  type testStorer struct {
  1076  	tacticsRecords         map[string][]byte
  1077  	speedTestSampleRecords map[string][]byte
  1078  }
  1079  
  1080  func newTestStorer() *testStorer {
  1081  	return &testStorer{
  1082  		tacticsRecords:         make(map[string][]byte),
  1083  		speedTestSampleRecords: make(map[string][]byte),
  1084  	}
  1085  }
  1086  
  1087  func (s *testStorer) SetTacticsRecord(networkID string, record []byte) error {
  1088  	s.tacticsRecords[networkID] = record
  1089  	return nil
  1090  }
  1091  
  1092  func (s *testStorer) GetTacticsRecord(networkID string) ([]byte, error) {
  1093  	return s.tacticsRecords[networkID], nil
  1094  }
  1095  
  1096  func (s *testStorer) SetSpeedTestSamplesRecord(networkID string, record []byte) error {
  1097  	s.speedTestSampleRecords[networkID] = record
  1098  	return nil
  1099  }
  1100  
  1101  func (s *testStorer) GetSpeedTestSamplesRecord(networkID string) ([]byte, error) {
  1102  	return s.speedTestSampleRecords[networkID], nil
  1103  }
  1104  
  1105  type testLogger struct {
  1106  }
  1107  
  1108  func newTestLogger() *testLogger {
  1109  	return &testLogger{}
  1110  }
  1111  
  1112  func (l *testLogger) WithTrace() common.LogTrace {
  1113  	return &testLoggerTrace{trace: stacktrace.GetParentFunctionName()}
  1114  }
  1115  
  1116  func (l *testLogger) WithTraceFields(fields common.LogFields) common.LogTrace {
  1117  	return &testLoggerTrace{
  1118  		trace:  stacktrace.GetParentFunctionName(),
  1119  		fields: fields,
  1120  	}
  1121  }
  1122  
  1123  func (l *testLogger) LogMetric(metric string, fields common.LogFields) {
  1124  	fmt.Printf("METRIC: %s: fields=%+v\n", metric, fields)
  1125  }
  1126  
  1127  type testLoggerTrace struct {
  1128  	trace  string
  1129  	fields common.LogFields
  1130  }
  1131  
  1132  func (l *testLoggerTrace) log(priority, message string) {
  1133  	fmt.Printf("%s: %s: %s fields=%+v\n", priority, l.trace, message, l.fields)
  1134  }
  1135  
  1136  func (l *testLoggerTrace) Debug(args ...interface{}) {
  1137  	l.log("DEBUG", fmt.Sprint(args...))
  1138  }
  1139  
  1140  func (l *testLoggerTrace) Info(args ...interface{}) {
  1141  	l.log("INFO", fmt.Sprint(args...))
  1142  }
  1143  
  1144  func (l *testLoggerTrace) Warning(args ...interface{}) {
  1145  	l.log("WARNING", fmt.Sprint(args...))
  1146  }
  1147  
  1148  func (l *testLoggerTrace) Error(args ...interface{}) {
  1149  	l.log("ERROR", fmt.Sprint(args...))
  1150  }