github.com/Psiphon-Labs/psiphon-tunnel-core@v2.0.28+incompatible/psiphon/common/tactics/tactics.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  /*
    21  Package tactics provides dynamic Psiphon client configuration based on GeoIP
    22  attributes, API parameters, and speed test data. The tactics implementation
    23  works in concert with the "parameters" package, allowing contextual
    24  optimization of Psiphon client parameters; for example, customizing
    25  NetworkLatencyMultiplier to adjust timeouts for clients on slow networks; or
    26  customizing LimitTunnelProtocols and ConnectionWorkerPoolSize to circumvent
    27  specific blocking conditions.
    28  
    29  Clients obtain tactics from a Psiphon server. Tactics are configured with a hot-
    30  reloadable, JSON format server config file. The config file specifies default
    31  tactics for all clients as well as a list of filtered tactics. For each filter,
    32  if the client's attributes satisfy the filter then additional tactics are merged
    33  into the tactics set provided to the client.
    34  
    35  Tactics configuration is optimized for a modest number of filters -- dozens --
    36  and very many GeoIP matches in each filter.
    37  
    38  A Psiphon client "tactics request" is an an untunneled, pre-establishment
    39  request to obtain tactics, which will in turn be applied and used in the normal
    40  tunnel establishment sequence; the tactics request may result in custom
    41  timeouts, protocol selection, and other tunnel establishment behavior.
    42  
    43  The client will delay its normal establishment sequence and launch a tactics
    44  request only when it has no stored, valid tactics for its current network
    45  context. The normal establishment sequence will begin, regardless of tactics
    46  request outcome, after TacticsWaitPeriod; this ensures that the client will not
    47  stall its establishment process when the tactics request cannot complete.
    48  
    49  Tactics are configured with a TTL, which is converted to an expiry time on the
    50  client when tactics are received and stored. When the client starts its
    51  establishment sequence and finds stored, unexpired tactics, no tactics request
    52  is made. The expiry time serves to prevent execess tactics requests and avoid a
    53  fingerprintable network sequence that would result from always performing the
    54  tactics request.
    55  
    56  The client calls UseStoredTactics to check for stored tactics; and if none is
    57  found (there is no record or it is expired) the client proceeds to call
    58  FetchTactics to make the tactics request.
    59  
    60  In the Psiphon client and server, the tactics request is transported using the
    61  meek protocol. In this case, meek is configured as a simple HTTP round trip
    62  transport and does not relay arbitrary streams of data and does not allocate
    63  resources required for relay mode. On the Psiphon server, the same meek
    64  component handles both tactics requests and tunnel relays. Anti-probing for
    65  tactics endpoints are thus provided as usual by meek. A meek request is routed
    66  based on an routing field in the obfuscated meek cookie.
    67  
    68  As meek may be plaintext and as TLS certificate verification is sometimes
    69  skipped, the tactics request payload is wrapped with NaCl box and further
    70  wrapped in a padded obfuscator. Distinct request and response nonces are used to
    71  mitigate replay attacks. Clients generate ephemeral NaCl key pairs and the
    72  server public key is obtained from the server entry. The server entry also
    73  contains capabilities indicating that a Psiphon server supports tactics requests
    74  and which meek protocol is to be used.
    75  
    76  The Psiphon client requests, stores, and applies distinct tactics based on its
    77  current network context. The client uses platform-specific APIs to obtain a fine
    78  grain network ID based on, for example BSSID for WiFi or MCC/MNC for mobile.
    79  These values provides accurate detection of network context changes and can be
    80  obtained from the client device without any network activity. As the network ID
    81  is personally identifying, this ID is only used by the client and is never sent
    82  to the Psiphon server. The client obtains the current network ID from a callback
    83  made from tunnel-core to native client code.
    84  
    85  Tactics returned to the Psiphon client are accompanied by a "tag" which is a
    86  hash digest of the merged tactics data. This tag uniquely identifies the
    87  tactics. The client reports the tactics it is employing through the
    88  "applied_tactics" common metrics API parameter. When fetching new tactics, the
    89  client reports the stored (and possibly expired) tactics it has through the
    90  "stored_tactics" API parameter. The stored tactics tag is used to avoid
    91  redownloading redundant tactics data; when the tactics response indicates the
    92  tag is unchanged, no tactics data is returned and the client simply extends the
    93  expiry of the data is already has.
    94  
    95  The Psiphon handshake API returns tactics in its response. This enabled regular
    96  tactics expiry extension without requiring any distinct tactics request or
    97  tactics data transfer when the tag is unchanged. Psiphon clients that connect
    98  regularly and successfully with make almost no untunnled tactics requests except
    99  for new network IDs. Returning tactics in the handshake reponse also provides
   100  tactics in the case where a client is unable to complete an untunneled tactics
   101  request but can otherwise establish a tunnel. Clients will abort any outstanding
   102  untunneled tactics requests or scheduled retries once a handshake has completed.
   103  
   104  The client handshake request component calls SetTacticsAPIParameters to populate
   105  the handshake request parameters with tactics inputs, and calls
   106  HandleTacticsPayload to process the tactics payload in the handshake response.
   107  
   108  The core tactics data is custom values for a subset of the parameters in
   109  parameters.Parameters. A client takes the default Parameters, applies any
   110  custom values set in its config file, and then applies any stored or received
   111  tactics. Each time the tactics changes, this process is repeated so that
   112  obsolete tactics parameters are not retained in the client's Parameters
   113  instance.
   114  
   115  Tactics has a probability parameter that is used in a weighted coin flip to
   116  determine if the tactics is to be applied or skipped for the current client
   117  session. This allows for experimenting with provisional tactics; and obtaining
   118  non-tactic sample metrics in situations which would otherwise always use a
   119  tactic.
   120  
   121  Speed test data is used in filtered tactics for selection of parameters such as
   122  timeouts.
   123  
   124  A speed test sample records the RTT of an application-level round trip to a
   125  Psiphon server -- either a meek HTTP round trip or an SSH request round trip.
   126  The round trip should be preformed after an TCP, TLS, SSH, etc. handshake so
   127  that the RTT includes only the application-level round trip. Each sample also
   128  records the tunnel/meek protocol used, the Psiphon server region, and a
   129  timestamp; these values may be used to filter out outliers or stale samples. The
   130  samples record bytes up/down, although at this time the speed test is focused on
   131  latency and the payload is simply anti-fingerprint padding and should not be
   132  larger than an IP packet.
   133  
   134  The Psiphon client records the latest SpeedTestMaxSampleCount speed test samples
   135  for each network context. SpeedTestMaxSampleCount should be  a modest size, as
   136  each speed test sample is ~100 bytes when serialzied and all samples (for one
   137  network ID) are loaded into memory and  sent as API inputs to tactics and
   138  handshake requests.
   139  
   140  When a tactics request is initiated and there are no speed test samples for
   141  current network ID, the tactics request is proceeded by a speed test round trip,
   142  using the same meek round tripper, and that sample is stored and used for the
   143  tactics request. with a speed test The client records additional samples taken
   144  from regular SSH keep alive round trips and calls AddSpeedTestSample to store
   145  these.
   146  
   147  The client sends all its speed test samples, for the current network context, to
   148  the server in tactics and handshake requests; this allows the server logic to
   149  handle outliers and aggregation. Currently, filtered tactics support filerting
   150  on speed test RTT maximum, minimum, and median.
   151  */
   152  package tactics
   153  
   154  import (
   155  	"bytes"
   156  	"context"
   157  	"crypto/md5"
   158  	"crypto/rand"
   159  	"encoding/base64"
   160  	"encoding/hex"
   161  	"encoding/json"
   162  	"fmt"
   163  	"io/ioutil"
   164  	"net/http"
   165  	"sort"
   166  	"time"
   167  
   168  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
   169  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
   170  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/obfuscator"
   171  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
   172  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
   173  	"golang.org/x/crypto/nacl/box"
   174  )
   175  
   176  // TACTICS_PADDING_MAX_SIZE is used by the client as well as the server. This
   177  // value is not a dynamic client parameter since a tactics request is made
   178  // only when the client has no valid tactics, so no override of
   179  // TACTICS_PADDING_MAX_SIZE can be applied.
   180  
   181  const (
   182  	SPEED_TEST_END_POINT               = "speedtest"
   183  	TACTICS_END_POINT                  = "tactics"
   184  	MAX_REQUEST_BODY_SIZE              = 65536
   185  	SPEED_TEST_PADDING_MIN_SIZE        = 0
   186  	SPEED_TEST_PADDING_MAX_SIZE        = 256
   187  	TACTICS_PADDING_MAX_SIZE           = 256
   188  	TACTICS_OBFUSCATED_KEY_SIZE        = 32
   189  	SPEED_TEST_SAMPLES_PARAMETER_NAME  = "speed_test_samples"
   190  	APPLIED_TACTICS_TAG_PARAMETER_NAME = "applied_tactics_tag"
   191  	STORED_TACTICS_TAG_PARAMETER_NAME  = "stored_tactics_tag"
   192  	TACTICS_METRIC_EVENT_NAME          = "tactics"
   193  	NEW_TACTICS_TAG_LOG_FIELD_NAME     = "new_tactics_tag"
   194  	IS_TACTICS_REQUEST_LOG_FIELD_NAME  = "is_tactics_request"
   195  	AGGREGATION_MINIMUM                = "Minimum"
   196  	AGGREGATION_MAXIMUM                = "Maximum"
   197  	AGGREGATION_MEDIAN                 = "Median"
   198  )
   199  
   200  var (
   201  	TACTICS_REQUEST_NONCE  = []byte{1}
   202  	TACTICS_RESPONSE_NONCE = []byte{2}
   203  )
   204  
   205  // Server is a tactics server to be integrated with the Psiphon server meek and handshake
   206  // components.
   207  //
   208  // The meek server calls HandleEndPoint to handle untunneled tactics and speed test requests.
   209  // The handshake handler calls GetTacticsPayload to obtain a tactics payload to include with
   210  // the handsake response.
   211  //
   212  // The Server is a reloadable file; its exported fields are read from the tactics configuration
   213  // file.
   214  //
   215  // Each client will receive at least the DefaultTactics. Client GeoIP, API parameter, and speed
   216  // test sample attributes are matched against all filters and the tactics corresponding to any
   217  // matching filter are merged into the client tactics.
   218  //
   219  // The merge operation replaces any existing item in Parameter with a Parameter specified in
   220  // the newest matching tactics. The TTL and Probability of the newest matching tactics is taken,
   221  // although all but the DefaultTactics can omit the TTL and Probability fields.
   222  type Server struct {
   223  	common.ReloadableFile
   224  
   225  	// RequestPublicKey is the Server's tactics request NaCl box public key.
   226  	RequestPublicKey []byte
   227  
   228  	// RequestPublicKey is the Server's tactics request NaCl box private key.
   229  	RequestPrivateKey []byte
   230  
   231  	// RequestObfuscatedKey is the tactics request obfuscation key.
   232  	RequestObfuscatedKey []byte
   233  
   234  	// DefaultTactics is the baseline tactics for all clients. It must include a
   235  	// TTL and Probability.
   236  	DefaultTactics Tactics
   237  
   238  	// FilteredTactics is an ordered list of filter/tactics pairs. For a client,
   239  	// each fltered tactics is checked in order and merged into the clients
   240  	// tactics if the client's attributes satisfy the filter.
   241  	FilteredTactics []struct {
   242  		Filter  Filter
   243  		Tactics Tactics
   244  	}
   245  
   246  	// When no tactics configuration file is provided, there will be no
   247  	// request key material or default tactics, and the server will not
   248  	// support tactics. The loaded flag, set to true only when a configuration
   249  	// file has been successfully loaded, provides an explict check for this
   250  	// condition (vs., say, checking for a zero-value Server).
   251  	loaded bool
   252  
   253  	filterGeoIPScope   int
   254  	filterRegionScopes map[string]int
   255  
   256  	logger                common.Logger
   257  	logFieldFormatter     common.APIParameterLogFieldFormatter
   258  	apiParameterValidator common.APIParameterValidator
   259  }
   260  
   261  const (
   262  	GeoIPScopeRegion = 1
   263  	GeoIPScopeISP    = 2
   264  	GeoIPScopeASN    = 4
   265  	GeoIPScopeCity   = 8
   266  )
   267  
   268  // Filter defines a filter to match against client attributes.
   269  // Each field within the filter is optional and may be omitted.
   270  type Filter struct {
   271  
   272  	// Regions specifies a list of GeoIP regions/countries the client
   273  	// must match.
   274  	Regions []string
   275  
   276  	// ISPs specifies a list of GeoIP ISPs the client must match.
   277  	ISPs []string
   278  
   279  	// ASNs specifies a list of GeoIP ASNs the client must match.
   280  	ASNs []string
   281  
   282  	// Cities specifies a list of GeoIP Cities the client must match.
   283  	Cities []string
   284  
   285  	// APIParameters specifies API, e.g. handshake, parameter names and
   286  	// a list of values, one of which must be specified to match this
   287  	// filter. Only scalar string API parameters may be filtered.
   288  	// Values may be patterns containing the '*' wildcard.
   289  	APIParameters map[string][]string
   290  
   291  	// SpeedTestRTTMilliseconds specifies a Range filter field that the
   292  	// client speed test samples must satisfy.
   293  	SpeedTestRTTMilliseconds *Range
   294  
   295  	regionLookup map[string]bool
   296  	ispLookup    map[string]bool
   297  	asnLookup    map[string]bool
   298  	cityLookup   map[string]bool
   299  }
   300  
   301  // Range is a filter field which specifies that the aggregation of
   302  // the a client attribute is within specified upper and lower bounds.
   303  // At least one bound must be specified.
   304  //
   305  // For example, Range is to aggregate and filter client speed test
   306  // sample RTTs.
   307  type Range struct {
   308  
   309  	// Aggregation may be "Maximum", "Minimum", or "Median"
   310  	Aggregation string
   311  
   312  	// AtLeast specifies a lower bound for the aggregarted
   313  	// client value.
   314  	AtLeast *int
   315  
   316  	// AtMost specifies an upper bound for the aggregarted
   317  	// client value.
   318  	AtMost *int
   319  }
   320  
   321  // Payload is the data to be returned to the client in response to a
   322  // tactics request or in the handshake response.
   323  type Payload struct {
   324  
   325  	// Tag is the hash  tag of the accompanying Tactics. When the Tag
   326  	// is the same as the stored tag the client specified in its
   327  	// request, the Tactics will be empty as the client already has the
   328  	// correct data.
   329  	Tag string
   330  
   331  	// Tactics is a JSON-encoded Tactics struct and may be nil.
   332  	Tactics json.RawMessage
   333  }
   334  
   335  // Record is the tactics data persisted by the client. There is one
   336  // record for each network ID.
   337  type Record struct {
   338  
   339  	// The Tag is the hash of the tactics data and is used as the
   340  	// stored tag when making requests.
   341  	Tag string
   342  
   343  	// Expiry is the time when this perisisted tactics expires as
   344  	// determined by the client applying the TTL against its local
   345  	// clock when the tactics was stored.
   346  	Expiry time.Time
   347  
   348  	// Tactics is the core tactics data.
   349  	Tactics Tactics
   350  }
   351  
   352  // Tactics is the core tactics data. This is both what is set in
   353  // in the server configuration file and what is stored and used
   354  // by the cient.
   355  type Tactics struct {
   356  
   357  	// TTL is a string duration (e.g., "24h", the syntax supported
   358  	// by time.ParseDuration). This specifies how long the client
   359  	// should use the accompanying tactics until it expires.
   360  	//
   361  	// The client stores the TTL to use for extending the tactics
   362  	// expiry when a tactics request or handshake response returns
   363  	// no tactics data when the tag is unchanged.
   364  	TTL string
   365  
   366  	// Probability specifies the probability [0.0 - 1.0] with which
   367  	// the client should apply the tactics in a new session.
   368  	Probability float64
   369  
   370  	// Parameters specify client parameters to override. These must
   371  	// be a subset of parameter.ClientParameter values and follow
   372  	// the corresponding data type and minimum value constraints.
   373  	Parameters map[string]interface{}
   374  }
   375  
   376  // Note: the SpeedTestSample json tags are selected to minimize marshaled
   377  // size. In psiphond, for logging metrics, the field names are translated to
   378  // more verbose values. psiphon/server.makeSpeedTestSamplesLogField currently
   379  // hard-codes these same SpeedTestSample json tag values for that translation.
   380  
   381  // SpeedTestSample is speed test data for a single RTT event.
   382  type SpeedTestSample struct {
   383  
   384  	// Timestamp is the speed test event time, and may be used to discard
   385  	// stale samples. The server supplies the speed test timestamp. This
   386  	// value is truncated to the nearest hour as a privacy measure.
   387  	Timestamp time.Time `json:"s"`
   388  
   389  	// EndPointRegion is the region of the endpoint, the Psiphon server,
   390  	// used for the speed test. This may be used to exclude outlier samples
   391  	// using remote data centers.
   392  	EndPointRegion string `json:"r"`
   393  
   394  	// EndPointProtocol is the tactics or tunnel protocol use for the
   395  	// speed test round trip. The protocol may impact RTT.
   396  	EndPointProtocol string `json:"p"`
   397  
   398  	// All speed test samples should measure RTT as the time to complete
   399  	// an application-level round trip on top of a previously established
   400  	// tactics or tunnel prococol connection. The RTT should not include
   401  	// TCP, TLS, or SSH handshakes.
   402  	// This value is truncated to the nearest millisecond as a privacy
   403  	// measure.
   404  	RTTMilliseconds int `json:"t"`
   405  
   406  	// BytesUp is the size of the upstream payload in the round trip.
   407  	// Currently, the payload is limited to anti-fingerprint padding.
   408  	BytesUp int `json:"u"`
   409  
   410  	// BytesDown is the size of the downstream payload in the round trip.
   411  	// Currently, the payload is limited to anti-fingerprint padding.
   412  	BytesDown int `json:"d"`
   413  }
   414  
   415  // GenerateKeys generates a tactics request key pair and obfuscation key.
   416  func GenerateKeys() (encodedRequestPublicKey, encodedRequestPrivateKey, encodedObfuscatedKey string, err error) {
   417  
   418  	requestPublicKey, requestPrivateKey, err := box.GenerateKey(rand.Reader)
   419  	if err != nil {
   420  		return "", "", "", errors.Trace(err)
   421  	}
   422  
   423  	obfuscatedKey, err := common.MakeSecureRandomBytes(TACTICS_OBFUSCATED_KEY_SIZE)
   424  	if err != nil {
   425  		return "", "", "", errors.Trace(err)
   426  	}
   427  
   428  	return base64.StdEncoding.EncodeToString(requestPublicKey[:]),
   429  		base64.StdEncoding.EncodeToString(requestPrivateKey[:]),
   430  		base64.StdEncoding.EncodeToString(obfuscatedKey[:]),
   431  		nil
   432  }
   433  
   434  // NewServer creates Server using the specified tactics configuration file.
   435  //
   436  // The logger and logFieldFormatter callbacks are used to log errors and
   437  // metrics. The apiParameterValidator callback is used to validate client
   438  // API parameters submitted to the tactics request.
   439  func NewServer(
   440  	logger common.Logger,
   441  	logFieldFormatter common.APIParameterLogFieldFormatter,
   442  	apiParameterValidator common.APIParameterValidator,
   443  	configFilename string) (*Server, error) {
   444  
   445  	server := &Server{
   446  		logger:                logger,
   447  		logFieldFormatter:     logFieldFormatter,
   448  		apiParameterValidator: apiParameterValidator,
   449  	}
   450  
   451  	server.ReloadableFile = common.NewReloadableFile(
   452  		configFilename,
   453  		true,
   454  		func(fileContent []byte, _ time.Time) error {
   455  
   456  			var newServer Server
   457  			err := json.Unmarshal(fileContent, &newServer)
   458  			if err != nil {
   459  				return errors.Trace(err)
   460  			}
   461  
   462  			err = newServer.Validate()
   463  			if err != nil {
   464  				return errors.Trace(err)
   465  			}
   466  
   467  			// Modify actual traffic rules only after validation
   468  			server.RequestPublicKey = newServer.RequestPublicKey
   469  			server.RequestPrivateKey = newServer.RequestPrivateKey
   470  			server.RequestObfuscatedKey = newServer.RequestObfuscatedKey
   471  			server.DefaultTactics = newServer.DefaultTactics
   472  			server.FilteredTactics = newServer.FilteredTactics
   473  
   474  			server.initLookups()
   475  
   476  			server.loaded = true
   477  
   478  			return nil
   479  		})
   480  
   481  	_, err := server.Reload()
   482  	if err != nil {
   483  		return nil, errors.Trace(err)
   484  	}
   485  
   486  	return server, nil
   487  }
   488  
   489  // Validate checks for correct tactics configuration values.
   490  func (server *Server) Validate() error {
   491  
   492  	// Key material must either be entirely omitted, or fully populated.
   493  	if len(server.RequestPublicKey) == 0 {
   494  		if len(server.RequestPrivateKey) != 0 ||
   495  			len(server.RequestObfuscatedKey) != 0 {
   496  			return errors.TraceNew("unexpected request key")
   497  		}
   498  	} else {
   499  		if len(server.RequestPublicKey) != 32 ||
   500  			len(server.RequestPrivateKey) != 32 ||
   501  			len(server.RequestObfuscatedKey) != TACTICS_OBFUSCATED_KEY_SIZE {
   502  			return errors.TraceNew("invalid request key")
   503  		}
   504  	}
   505  
   506  	// validateTactics validates either the defaultTactics, when filteredTactics
   507  	// is nil, or the filteredTactics otherwise. In the second case,
   508  	// defaultTactics must be passed in to validate filtered tactics references
   509  	// to default tactics parameters, such as CustomTLSProfiles or
   510  	// PacketManipulationSpecs.
   511  	//
   512  	// Limitation: references must point to the default tactics or the filtered
   513  	// tactics itself; referring to parameters in a previous filtered tactics is
   514  	// not suported.
   515  
   516  	validateTactics := func(defaultTactics, filteredTactics *Tactics) error {
   517  
   518  		tactics := defaultTactics
   519  		validatingDefault := true
   520  		if filteredTactics != nil {
   521  			tactics = filteredTactics
   522  			validatingDefault = false
   523  		}
   524  
   525  		// Allow "" for 0, even though ParseDuration does not.
   526  		var d time.Duration
   527  		if tactics.TTL != "" {
   528  			var err error
   529  			d, err = time.ParseDuration(tactics.TTL)
   530  			if err != nil {
   531  				return errors.Trace(err)
   532  			}
   533  		}
   534  
   535  		if d <= 0 {
   536  			if validatingDefault {
   537  				return errors.TraceNew("invalid duration")
   538  			}
   539  			// For merging logic, Normalize any 0 duration to "".
   540  			tactics.TTL = ""
   541  		}
   542  
   543  		if (validatingDefault && tactics.Probability == 0.0) ||
   544  			tactics.Probability < 0.0 ||
   545  			tactics.Probability > 1.0 {
   546  
   547  			return errors.TraceNew("invalid probability")
   548  		}
   549  
   550  		params, err := parameters.NewParameters(nil)
   551  		if err != nil {
   552  			return errors.Trace(err)
   553  		}
   554  
   555  		applyParameters := []map[string]interface{}{
   556  			defaultTactics.Parameters,
   557  		}
   558  		if filteredTactics != nil {
   559  			applyParameters = append(
   560  				applyParameters, filteredTactics.Parameters)
   561  		}
   562  
   563  		_, err = params.Set("", false, applyParameters...)
   564  		if err != nil {
   565  			return errors.Trace(err)
   566  		}
   567  
   568  		return nil
   569  	}
   570  
   571  	validateRange := func(r *Range) error {
   572  		if r == nil {
   573  			return nil
   574  		}
   575  
   576  		if (r.AtLeast == nil && r.AtMost == nil) ||
   577  			((r.AtLeast != nil && r.AtMost != nil) && *r.AtLeast > *r.AtMost) {
   578  			return errors.TraceNew("invalid range")
   579  		}
   580  
   581  		switch r.Aggregation {
   582  		case AGGREGATION_MINIMUM, AGGREGATION_MAXIMUM, AGGREGATION_MEDIAN:
   583  		default:
   584  			return errors.TraceNew("invalid aggregation")
   585  		}
   586  
   587  		return nil
   588  	}
   589  
   590  	err := validateTactics(&server.DefaultTactics, nil)
   591  	if err != nil {
   592  		return errors.Tracef("invalid default tactics: %s", err)
   593  	}
   594  
   595  	for i, filteredTactics := range server.FilteredTactics {
   596  
   597  		err := validateTactics(&server.DefaultTactics, &filteredTactics.Tactics)
   598  
   599  		if err == nil {
   600  			err = validateRange(filteredTactics.Filter.SpeedTestRTTMilliseconds)
   601  		}
   602  
   603  		// TODO: validate Filter.APIParameters names are valid?
   604  
   605  		if err != nil {
   606  			return errors.Tracef("invalid filtered tactics %d: %s", i, err)
   607  		}
   608  	}
   609  
   610  	return nil
   611  }
   612  
   613  const stringLookupThreshold = 5
   614  
   615  // initLookups creates map lookups for filters where the number
   616  // of string values to compare against exceeds a threshold where
   617  // benchmarks show maps are faster than looping through a string
   618  // slice.
   619  func (server *Server) initLookups() {
   620  
   621  	server.filterGeoIPScope = 0
   622  	server.filterRegionScopes = make(map[string]int)
   623  
   624  	for _, filteredTactics := range server.FilteredTactics {
   625  
   626  		if len(filteredTactics.Filter.Regions) >= stringLookupThreshold {
   627  			filteredTactics.Filter.regionLookup = make(map[string]bool)
   628  			for _, region := range filteredTactics.Filter.Regions {
   629  				filteredTactics.Filter.regionLookup[region] = true
   630  			}
   631  		}
   632  
   633  		if len(filteredTactics.Filter.ISPs) >= stringLookupThreshold {
   634  			filteredTactics.Filter.ispLookup = make(map[string]bool)
   635  			for _, ISP := range filteredTactics.Filter.ISPs {
   636  				filteredTactics.Filter.ispLookup[ISP] = true
   637  			}
   638  		}
   639  
   640  		if len(filteredTactics.Filter.ASNs) >= stringLookupThreshold {
   641  			filteredTactics.Filter.asnLookup = make(map[string]bool)
   642  			for _, ASN := range filteredTactics.Filter.ASNs {
   643  				filteredTactics.Filter.asnLookup[ASN] = true
   644  			}
   645  		}
   646  
   647  		if len(filteredTactics.Filter.Cities) >= stringLookupThreshold {
   648  			filteredTactics.Filter.cityLookup = make(map[string]bool)
   649  			for _, city := range filteredTactics.Filter.Cities {
   650  				filteredTactics.Filter.cityLookup[city] = true
   651  			}
   652  		}
   653  
   654  		// Initialize the filter GeoIP scope fields used by GetFilterGeoIPScope.
   655  		//
   656  		// The basic case is, for example, when only Regions appear in filters, then
   657  		// only GeoIPScopeRegion is set.
   658  		//
   659  		// As an optimization, a regional map is populated so that, for example,
   660  		// GeoIPScopeRegion&GeoIPScopeISP will be set only for regions for which
   661  		// there is a filter with region and ISP, while other regions will set only
   662  		// GeoIPScopeRegion.
   663  		//
   664  		// When any ISP, ASN, or City appears in a filter without a Region,
   665  		// the regional map optimization is disabled.
   666  
   667  		if len(filteredTactics.Filter.Regions) == 0 {
   668  			disableRegionScope := false
   669  			if len(filteredTactics.Filter.ISPs) > 0 {
   670  				server.filterGeoIPScope |= GeoIPScopeISP
   671  				disableRegionScope = true
   672  			}
   673  			if len(filteredTactics.Filter.ASNs) > 0 {
   674  				server.filterGeoIPScope |= GeoIPScopeASN
   675  				disableRegionScope = true
   676  			}
   677  			if len(filteredTactics.Filter.Cities) > 0 {
   678  				server.filterGeoIPScope |= GeoIPScopeCity
   679  				disableRegionScope = true
   680  			}
   681  			if disableRegionScope && server.filterRegionScopes != nil {
   682  				for _, regionScope := range server.filterRegionScopes {
   683  					server.filterGeoIPScope |= regionScope
   684  				}
   685  				server.filterRegionScopes = nil
   686  			}
   687  		} else {
   688  			server.filterGeoIPScope |= GeoIPScopeRegion
   689  			if server.filterRegionScopes != nil {
   690  				regionScope := 0
   691  				if len(filteredTactics.Filter.ISPs) > 0 {
   692  					regionScope |= GeoIPScopeISP
   693  				}
   694  				if len(filteredTactics.Filter.ASNs) > 0 {
   695  					regionScope |= GeoIPScopeASN
   696  				}
   697  				if len(filteredTactics.Filter.Cities) > 0 {
   698  					regionScope |= GeoIPScopeCity
   699  				}
   700  				for _, region := range filteredTactics.Filter.Regions {
   701  					server.filterRegionScopes[region] |= regionScope
   702  				}
   703  			}
   704  		}
   705  
   706  		// TODO: add lookups for APIParameters?
   707  		// Not expected to be long lists of values.
   708  	}
   709  }
   710  
   711  // GetFilterGeoIPScope returns which GeoIP fields are relevent to tactics
   712  // filters. The return value is a bit array containing some combination of
   713  // the GeoIPScopeRegion, GeoIPScopeISP, GeoIPScopeASN, and GeoIPScopeCity
   714  // flags. For the given geoIPData, all tactics filters reference only the
   715  // flagged fields.
   716  func (server *Server) GetFilterGeoIPScope(geoIPData common.GeoIPData) int {
   717  
   718  	scope := server.filterGeoIPScope
   719  
   720  	if server.filterRegionScopes != nil {
   721  
   722  		regionScope, ok := server.filterRegionScopes[geoIPData.Country]
   723  		if ok {
   724  			scope |= regionScope
   725  		}
   726  	}
   727  
   728  	return scope
   729  }
   730  
   731  // GetTacticsPayload assembles and returns a tactics payload for a client with
   732  // the specified GeoIP, API parameter, and speed test attributes.
   733  //
   734  // The speed test samples are expected to be in apiParams, as is the stored
   735  // tactics tag.
   736  //
   737  // Unless no tactics configuration was loaded, GetTacticsPayload will always
   738  // return a payload for any client. When the client's stored tactics tag is
   739  // identical to the assembled tactics, the Payload.Tactics is nil.
   740  //
   741  // Elements of the returned Payload, e.g., tactics parameters, will point to
   742  // data in DefaultTactics and FilteredTactics and must not be modifed.
   743  func (server *Server) GetTacticsPayload(
   744  	geoIPData common.GeoIPData,
   745  	apiParams common.APIParameters) (*Payload, error) {
   746  
   747  	// includeServerSideOnly is false: server-side only parameters are not
   748  	// used by the client, so including them wastes space and unnecessarily
   749  	// exposes the values.
   750  	tactics, err := server.GetTactics(false, geoIPData, apiParams)
   751  	if err != nil {
   752  		return nil, errors.Trace(err)
   753  	}
   754  
   755  	if tactics == nil {
   756  		return nil, nil
   757  	}
   758  
   759  	marshaledTactics, tag, err := marshalTactics(tactics)
   760  	if err != nil {
   761  		return nil, errors.Trace(err)
   762  	}
   763  
   764  	payload := &Payload{
   765  		Tag: tag,
   766  	}
   767  
   768  	// New clients should always send STORED_TACTICS_TAG_PARAMETER_NAME. When they have no
   769  	// stored tactics, the stored tag will be "" and not match payload.Tag and payload.Tactics
   770  	// will be sent.
   771  	//
   772  	// When new clients send a stored tag that matches payload.Tag, the client already has
   773  	// the correct data and payload.Tactics is not sent.
   774  	//
   775  	// Old clients will not send STORED_TACTICS_TAG_PARAMETER_NAME. In this case, do not
   776  	// send payload.Tactics as the client will not use it, will not store it, will not send
   777  	// back the new tag and so the handshake response will always contain wasteful tactics
   778  	// data.
   779  
   780  	sendPayloadTactics := true
   781  
   782  	clientStoredTag, err := getStringRequestParam(apiParams, STORED_TACTICS_TAG_PARAMETER_NAME)
   783  
   784  	// Old client or new client with same tag.
   785  	if err != nil || payload.Tag == clientStoredTag {
   786  		sendPayloadTactics = false
   787  	}
   788  
   789  	if sendPayloadTactics {
   790  		payload.Tactics = marshaledTactics
   791  	}
   792  
   793  	return payload, nil
   794  }
   795  
   796  func marshalTactics(tactics *Tactics) ([]byte, string, error) {
   797  	marshaledTactics, err := json.Marshal(tactics)
   798  	if err != nil {
   799  		return nil, "", errors.Trace(err)
   800  	}
   801  
   802  	// MD5 hash is used solely as a data checksum and not for any security purpose.
   803  	digest := md5.Sum(marshaledTactics)
   804  	tag := hex.EncodeToString(digest[:])
   805  
   806  	return marshaledTactics, tag, nil
   807  }
   808  
   809  // GetTacticsWithTag returns a GetTactics value along with the associated tag value.
   810  func (server *Server) GetTacticsWithTag(
   811  	includeServerSideOnly bool,
   812  	geoIPData common.GeoIPData,
   813  	apiParams common.APIParameters) (*Tactics, string, error) {
   814  
   815  	tactics, err := server.GetTactics(
   816  		includeServerSideOnly, geoIPData, apiParams)
   817  	if err != nil {
   818  		return nil, "", errors.Trace(err)
   819  	}
   820  
   821  	if tactics == nil {
   822  		return nil, "", nil
   823  	}
   824  
   825  	_, tag, err := marshalTactics(tactics)
   826  	if err != nil {
   827  		return nil, "", errors.Trace(err)
   828  	}
   829  
   830  	return tactics, tag, nil
   831  }
   832  
   833  // GetTactics assembles and returns tactics data for a client with the
   834  // specified GeoIP, API parameter, and speed test attributes.
   835  //
   836  // The tactics return value may be nil.
   837  func (server *Server) GetTactics(
   838  	includeServerSideOnly bool,
   839  	geoIPData common.GeoIPData,
   840  	apiParams common.APIParameters) (*Tactics, error) {
   841  
   842  	server.ReloadableFile.RLock()
   843  	defer server.ReloadableFile.RUnlock()
   844  
   845  	if !server.loaded {
   846  		// No tactics configuration was loaded.
   847  		return nil, nil
   848  	}
   849  
   850  	tactics := server.DefaultTactics.clone(includeServerSideOnly)
   851  
   852  	var aggregatedValues map[string]int
   853  
   854  	for _, filteredTactics := range server.FilteredTactics {
   855  
   856  		if len(filteredTactics.Filter.Regions) > 0 {
   857  			if filteredTactics.Filter.regionLookup != nil {
   858  				if !filteredTactics.Filter.regionLookup[geoIPData.Country] {
   859  					continue
   860  				}
   861  			} else {
   862  				if !common.Contains(filteredTactics.Filter.Regions, geoIPData.Country) {
   863  					continue
   864  				}
   865  			}
   866  		}
   867  
   868  		if len(filteredTactics.Filter.ISPs) > 0 {
   869  			if filteredTactics.Filter.ispLookup != nil {
   870  				if !filteredTactics.Filter.ispLookup[geoIPData.ISP] {
   871  					continue
   872  				}
   873  			} else {
   874  				if !common.Contains(filteredTactics.Filter.ISPs, geoIPData.ISP) {
   875  					continue
   876  				}
   877  			}
   878  		}
   879  
   880  		if len(filteredTactics.Filter.ASNs) > 0 {
   881  			if filteredTactics.Filter.asnLookup != nil {
   882  				if !filteredTactics.Filter.asnLookup[geoIPData.ASN] {
   883  					continue
   884  				}
   885  			} else {
   886  				if !common.Contains(filteredTactics.Filter.ASNs, geoIPData.ASN) {
   887  					continue
   888  				}
   889  			}
   890  		}
   891  
   892  		if len(filteredTactics.Filter.Cities) > 0 {
   893  			if filteredTactics.Filter.cityLookup != nil {
   894  				if !filteredTactics.Filter.cityLookup[geoIPData.City] {
   895  					continue
   896  				}
   897  			} else {
   898  				if !common.Contains(filteredTactics.Filter.Cities, geoIPData.City) {
   899  					continue
   900  				}
   901  			}
   902  		}
   903  
   904  		if filteredTactics.Filter.APIParameters != nil {
   905  			mismatch := false
   906  			for name, values := range filteredTactics.Filter.APIParameters {
   907  				clientValue, err := getStringRequestParam(apiParams, name)
   908  				if err != nil || !common.ContainsWildcard(values, clientValue) {
   909  					mismatch = true
   910  					break
   911  				}
   912  			}
   913  			if mismatch {
   914  				continue
   915  			}
   916  		}
   917  
   918  		if filteredTactics.Filter.SpeedTestRTTMilliseconds != nil {
   919  
   920  			var speedTestSamples []SpeedTestSample
   921  			err := getJSONRequestParam(apiParams, SPEED_TEST_SAMPLES_PARAMETER_NAME, &speedTestSamples)
   922  			if err != nil {
   923  				// TODO: log speed test parameter errors?
   924  				// This API param is not explicitly validated elsewhere.
   925  				continue
   926  			}
   927  
   928  			// As there must be at least one Range bound, there must be data to aggregate.
   929  			if len(speedTestSamples) == 0 {
   930  				continue
   931  			}
   932  
   933  			if aggregatedValues == nil {
   934  				aggregatedValues = make(map[string]int)
   935  			}
   936  
   937  			// Note: here we could filter out outliers such as samples that are unusually old
   938  			// or client/endPoint region pair too distant.
   939  
   940  			// aggregate may mutate (sort) the speedTestSamples slice.
   941  			value := aggregate(
   942  				filteredTactics.Filter.SpeedTestRTTMilliseconds.Aggregation,
   943  				speedTestSamples,
   944  				aggregatedValues)
   945  
   946  			if filteredTactics.Filter.SpeedTestRTTMilliseconds.AtLeast != nil &&
   947  				value < *filteredTactics.Filter.SpeedTestRTTMilliseconds.AtLeast {
   948  				continue
   949  			}
   950  			if filteredTactics.Filter.SpeedTestRTTMilliseconds.AtMost != nil &&
   951  				value > *filteredTactics.Filter.SpeedTestRTTMilliseconds.AtMost {
   952  				continue
   953  			}
   954  		}
   955  
   956  		tactics.merge(includeServerSideOnly, &filteredTactics.Tactics)
   957  
   958  		// Continue to apply more matches. Last matching tactics has priority for any field.
   959  	}
   960  
   961  	return tactics, nil
   962  }
   963  
   964  // TODO: refactor this copy of psiphon/server.getStringRequestParam into common?
   965  func getStringRequestParam(apiParams common.APIParameters, name string) (string, error) {
   966  	if apiParams[name] == nil {
   967  		return "", errors.Tracef("missing param: %s", name)
   968  	}
   969  	value, ok := apiParams[name].(string)
   970  	if !ok {
   971  		return "", errors.Tracef("invalid param: %s", name)
   972  	}
   973  	return value, nil
   974  }
   975  
   976  func getJSONRequestParam(apiParams common.APIParameters, name string, value interface{}) error {
   977  	if apiParams[name] == nil {
   978  		return errors.Tracef("missing param: %s", name)
   979  	}
   980  
   981  	// Remarshal the parameter from common.APIParameters, as the initial API parameter
   982  	// unmarshal will not have known the correct target type. I.e., instead of doing
   983  	// unmarhsal-into-struct, common.APIParameters will have an unmarshal-into-interface
   984  	// value as described here: https://golang.org/pkg/encoding/json/#Unmarshal.
   985  
   986  	jsonValue, err := json.Marshal(apiParams[name])
   987  	if err != nil {
   988  		return errors.Trace(err)
   989  	}
   990  	err = json.Unmarshal(jsonValue, value)
   991  	if err != nil {
   992  		return errors.Trace(err)
   993  	}
   994  	return nil
   995  }
   996  
   997  // aggregate may mutate (sort) the speedTestSamples slice.
   998  func aggregate(
   999  	aggregation string,
  1000  	speedTestSamples []SpeedTestSample,
  1001  	aggregatedValues map[string]int) int {
  1002  
  1003  	// Aggregated values are memoized to save recalculating for each filter.
  1004  	if value, ok := aggregatedValues[aggregation]; ok {
  1005  		return value
  1006  	}
  1007  
  1008  	var value int
  1009  
  1010  	switch aggregation {
  1011  	case AGGREGATION_MINIMUM:
  1012  		value = minimumSampleRTTMilliseconds(speedTestSamples)
  1013  	case AGGREGATION_MAXIMUM:
  1014  		value = maximumSampleRTTMilliseconds(speedTestSamples)
  1015  	case AGGREGATION_MEDIAN:
  1016  		value = medianSampleRTTMilliseconds(speedTestSamples)
  1017  	default:
  1018  		return 0
  1019  	}
  1020  
  1021  	aggregatedValues[aggregation] = value
  1022  	return value
  1023  }
  1024  
  1025  func minimumSampleRTTMilliseconds(samples []SpeedTestSample) int {
  1026  
  1027  	if len(samples) == 0 {
  1028  		return 0
  1029  	}
  1030  	min := 0
  1031  	for i := 1; i < len(samples); i++ {
  1032  		if samples[i].RTTMilliseconds < samples[min].RTTMilliseconds {
  1033  			min = i
  1034  		}
  1035  	}
  1036  	return samples[min].RTTMilliseconds
  1037  }
  1038  
  1039  func maximumSampleRTTMilliseconds(samples []SpeedTestSample) int {
  1040  
  1041  	if len(samples) == 0 {
  1042  		return 0
  1043  	}
  1044  	max := 0
  1045  	for i := 1; i < len(samples); i++ {
  1046  		if samples[i].RTTMilliseconds > samples[max].RTTMilliseconds {
  1047  			max = i
  1048  		}
  1049  	}
  1050  	return samples[max].RTTMilliseconds
  1051  }
  1052  
  1053  func medianSampleRTTMilliseconds(samples []SpeedTestSample) int {
  1054  
  1055  	if len(samples) == 0 {
  1056  		return 0
  1057  	}
  1058  
  1059  	// This in-place sort mutates the input slice.
  1060  	sort.Slice(
  1061  		samples,
  1062  		func(i, j int) bool {
  1063  			return samples[i].RTTMilliseconds < samples[j].RTTMilliseconds
  1064  		})
  1065  
  1066  	// See: https://en.wikipedia.org/wiki/Median#Easy_explanation_of_the_sample_median
  1067  
  1068  	mid := len(samples) / 2
  1069  
  1070  	if len(samples)%2 == 1 {
  1071  		return samples[mid].RTTMilliseconds
  1072  	}
  1073  
  1074  	return (samples[mid-1].RTTMilliseconds + samples[mid].RTTMilliseconds) / 2
  1075  }
  1076  
  1077  func (t *Tactics) clone(includeServerSideOnly bool) *Tactics {
  1078  
  1079  	u := &Tactics{
  1080  		TTL:         t.TTL,
  1081  		Probability: t.Probability,
  1082  	}
  1083  
  1084  	// Note: there is no deep copy of parameter values; the the returned
  1085  	// Tactics shares memory with the original and it individual parameters
  1086  	// should not be modified.
  1087  	if t.Parameters != nil {
  1088  		u.Parameters = make(map[string]interface{})
  1089  		for k, v := range t.Parameters {
  1090  			if includeServerSideOnly || !parameters.IsServerSideOnly(k) {
  1091  				u.Parameters[k] = v
  1092  			}
  1093  		}
  1094  	}
  1095  
  1096  	return u
  1097  }
  1098  
  1099  func (t *Tactics) merge(includeServerSideOnly bool, u *Tactics) {
  1100  
  1101  	if u.TTL != "" {
  1102  		t.TTL = u.TTL
  1103  	}
  1104  
  1105  	if u.Probability != 0.0 {
  1106  		t.Probability = u.Probability
  1107  	}
  1108  
  1109  	// Note: there is no deep copy of parameter values; the the returned
  1110  	// Tactics shares memory with the original and it individual parameters
  1111  	// should not be modified.
  1112  	if u.Parameters != nil {
  1113  		if t.Parameters == nil {
  1114  			t.Parameters = make(map[string]interface{})
  1115  		}
  1116  		for k, v := range u.Parameters {
  1117  			if includeServerSideOnly || !parameters.IsServerSideOnly(k) {
  1118  				t.Parameters[k] = v
  1119  			}
  1120  		}
  1121  	}
  1122  }
  1123  
  1124  // HandleEndPoint routes the request to either handleSpeedTestRequest
  1125  // or handleTacticsRequest; or returns false if not handled.
  1126  func (server *Server) HandleEndPoint(
  1127  	endPoint string,
  1128  	geoIPData common.GeoIPData,
  1129  	w http.ResponseWriter,
  1130  	r *http.Request) bool {
  1131  
  1132  	server.ReloadableFile.RLock()
  1133  	loaded := server.loaded
  1134  	hasRequestKeys := len(server.RequestPublicKey) > 0
  1135  	server.ReloadableFile.RUnlock()
  1136  
  1137  	if !loaded || !hasRequestKeys {
  1138  		// No tactics configuration was loaded, or the configuration contained
  1139  		// no key material for tactics requests.
  1140  		return false
  1141  	}
  1142  
  1143  	switch endPoint {
  1144  	case SPEED_TEST_END_POINT:
  1145  		server.handleSpeedTestRequest(geoIPData, w, r)
  1146  		return true
  1147  	case TACTICS_END_POINT:
  1148  		server.handleTacticsRequest(geoIPData, w, r)
  1149  		return true
  1150  	default:
  1151  		return false
  1152  	}
  1153  }
  1154  
  1155  func (server *Server) handleSpeedTestRequest(
  1156  	_ common.GeoIPData, w http.ResponseWriter, r *http.Request) {
  1157  
  1158  	_, err := ioutil.ReadAll(http.MaxBytesReader(w, r.Body, MAX_REQUEST_BODY_SIZE))
  1159  	if err != nil {
  1160  		server.logger.WithTraceFields(
  1161  			common.LogFields{"error": err}).Warning("failed to read request body")
  1162  		common.TerminateHTTPConnection(w, r)
  1163  		return
  1164  	}
  1165  
  1166  	response, err := MakeSpeedTestResponse(
  1167  		SPEED_TEST_PADDING_MIN_SIZE, SPEED_TEST_PADDING_MAX_SIZE)
  1168  	if err != nil {
  1169  		server.logger.WithTraceFields(
  1170  			common.LogFields{"error": err}).Warning("failed to make response")
  1171  		common.TerminateHTTPConnection(w, r)
  1172  		return
  1173  	}
  1174  
  1175  	w.WriteHeader(http.StatusOK)
  1176  	w.Write(response)
  1177  }
  1178  
  1179  func (server *Server) handleTacticsRequest(
  1180  	geoIPData common.GeoIPData, w http.ResponseWriter, r *http.Request) {
  1181  
  1182  	server.ReloadableFile.RLock()
  1183  	requestPrivateKey := server.RequestPrivateKey
  1184  	requestObfuscatedKey := server.RequestObfuscatedKey
  1185  	server.ReloadableFile.RUnlock()
  1186  
  1187  	// Read, decode, and unbox request payload.
  1188  
  1189  	boxedRequest, err := ioutil.ReadAll(http.MaxBytesReader(w, r.Body, MAX_REQUEST_BODY_SIZE))
  1190  	if err != nil {
  1191  		server.logger.WithTraceFields(
  1192  			common.LogFields{"error": err}).Warning("failed to read request body")
  1193  		common.TerminateHTTPConnection(w, r)
  1194  		return
  1195  	}
  1196  
  1197  	var apiParams common.APIParameters
  1198  	bundledPeerPublicKey, err := unboxPayload(
  1199  		TACTICS_REQUEST_NONCE,
  1200  		nil,
  1201  		requestPrivateKey,
  1202  		requestObfuscatedKey,
  1203  		boxedRequest,
  1204  		&apiParams)
  1205  	if err != nil {
  1206  		server.logger.WithTraceFields(
  1207  			common.LogFields{"error": err}).Warning("failed to unbox request")
  1208  		common.TerminateHTTPConnection(w, r)
  1209  		return
  1210  	}
  1211  
  1212  	err = server.apiParameterValidator(apiParams)
  1213  	if err != nil {
  1214  		server.logger.WithTraceFields(
  1215  			common.LogFields{"error": err}).Warning("invalid request parameters")
  1216  		common.TerminateHTTPConnection(w, r)
  1217  		return
  1218  	}
  1219  
  1220  	tacticsPayload, err := server.GetTacticsPayload(geoIPData, apiParams)
  1221  	if err == nil && tacticsPayload == nil {
  1222  		err = errors.TraceNew("unexpected missing tactics payload")
  1223  	}
  1224  	if err != nil {
  1225  		server.logger.WithTraceFields(
  1226  			common.LogFields{"error": err}).Warning("failed to get tactics")
  1227  		common.TerminateHTTPConnection(w, r)
  1228  		return
  1229  	}
  1230  
  1231  	// Marshal, box, and write response payload.
  1232  
  1233  	boxedResponse, err := boxPayload(
  1234  		TACTICS_RESPONSE_NONCE,
  1235  		bundledPeerPublicKey,
  1236  		requestPrivateKey,
  1237  		requestObfuscatedKey,
  1238  		nil,
  1239  		tacticsPayload)
  1240  	if err != nil {
  1241  		server.logger.WithTraceFields(
  1242  			common.LogFields{"error": err}).Warning("failed to box response")
  1243  		common.TerminateHTTPConnection(w, r)
  1244  		return
  1245  	}
  1246  
  1247  	w.WriteHeader(http.StatusOK)
  1248  	w.Write(boxedResponse)
  1249  
  1250  	// Log a metric.
  1251  
  1252  	logFields := server.logFieldFormatter(geoIPData, apiParams)
  1253  
  1254  	logFields[NEW_TACTICS_TAG_LOG_FIELD_NAME] = tacticsPayload.Tag
  1255  	logFields[IS_TACTICS_REQUEST_LOG_FIELD_NAME] = true
  1256  
  1257  	server.logger.LogMetric(TACTICS_METRIC_EVENT_NAME, logFields)
  1258  }
  1259  
  1260  // ObfuscatedRoundTripper performs a round trip to the specified endpoint,
  1261  // sending the request body and returning the response body, with an
  1262  // obfuscation layer applied to the endpoint value. The context may be used
  1263  // to set a timeout or cancel the round trip.
  1264  //
  1265  // The Psiphon client provides a ObfuscatedRoundTripper using MeekConn. The
  1266  // client will handle connection details including server selection, dialing
  1267  // details including device binding and upstream proxy, etc.
  1268  type ObfuscatedRoundTripper func(
  1269  	ctx context.Context,
  1270  	endPoint string,
  1271  	requestBody []byte) ([]byte, error)
  1272  
  1273  // Storer provides a facility to persist tactics and speed test data.
  1274  type Storer interface {
  1275  	SetTacticsRecord(networkID string, record []byte) error
  1276  	GetTacticsRecord(networkID string) ([]byte, error)
  1277  	SetSpeedTestSamplesRecord(networkID string, record []byte) error
  1278  	GetSpeedTestSamplesRecord(networkID string) ([]byte, error)
  1279  }
  1280  
  1281  // SetTacticsAPIParameters populates apiParams with the additional
  1282  // parameters for tactics. This is used by the Psiphon client when
  1283  // preparing its handshake request.
  1284  func SetTacticsAPIParameters(
  1285  	storer Storer,
  1286  	networkID string,
  1287  	apiParams common.APIParameters) error {
  1288  
  1289  	// TODO: store the tag in its own record to avoid loading the whole tactics record?
  1290  
  1291  	record, err := getStoredTacticsRecord(storer, networkID)
  1292  	if err != nil {
  1293  		return errors.Trace(err)
  1294  	}
  1295  
  1296  	speedTestSamples, err := getSpeedTestSamples(storer, networkID)
  1297  	if err != nil {
  1298  		return errors.Trace(err)
  1299  	}
  1300  
  1301  	apiParams[STORED_TACTICS_TAG_PARAMETER_NAME] = record.Tag
  1302  	apiParams[SPEED_TEST_SAMPLES_PARAMETER_NAME] = speedTestSamples
  1303  
  1304  	return nil
  1305  }
  1306  
  1307  // HandleTacticsPayload updates the stored tactics with the given payload.
  1308  // If the payload has a new tag/tactics, this is stored and a new expiry
  1309  // time is set. If the payload has the same tag, the existing tactics are
  1310  // retained and the exipry is extended using the previous TTL.
  1311  // HandleTacticsPayload is called by the Psiphon client to handle the
  1312  // tactics payload in the handshake response.
  1313  func HandleTacticsPayload(
  1314  	storer Storer,
  1315  	networkID string,
  1316  	payload *Payload) (*Record, error) {
  1317  
  1318  	// Note: since, in the client, a tactics request and a handshake
  1319  	// request could be in flight concurrently, there exists a possibility
  1320  	// that one clobbers the others result, and the clobbered result may
  1321  	// be newer.
  1322  	//
  1323  	// However:
  1324  	// - in the Storer, the tactics record is a single key/value, so its
  1325  	//   elements are updated atomically;
  1326  	// - the client Controller typically stops/aborts any outstanding
  1327  	//   tactics request before the handshake
  1328  	// - this would have to be concurrent with a tactics configuration hot
  1329  	//   reload on the server
  1330  	// - old and new tactics should both be valid
  1331  
  1332  	if payload == nil {
  1333  		return nil, errors.TraceNew("unexpected nil payload")
  1334  	}
  1335  
  1336  	record, err := getStoredTacticsRecord(storer, networkID)
  1337  	if err != nil {
  1338  		return nil, errors.Trace(err)
  1339  	}
  1340  
  1341  	err = applyTacticsPayload(storer, networkID, record, payload)
  1342  	if err != nil {
  1343  		return nil, errors.Trace(err)
  1344  	}
  1345  
  1346  	// TODO: if tags match, just set an expiry record, not the whole tactics record?
  1347  
  1348  	err = setStoredTacticsRecord(storer, networkID, record)
  1349  	if err != nil {
  1350  		return nil, errors.Trace(err)
  1351  	}
  1352  
  1353  	return record, nil
  1354  }
  1355  
  1356  // UseStoredTactics checks for an unexpired stored tactics record for the
  1357  // given network ID that may be used immediately. When there is no error
  1358  // and the record is nil, the caller should proceed with FetchTactics.
  1359  //
  1360  // When used, Record.Tag should be reported as the applied tactics tag.
  1361  func UseStoredTactics(
  1362  	storer Storer, networkID string) (*Record, error) {
  1363  
  1364  	record, err := getStoredTacticsRecord(storer, networkID)
  1365  	if err != nil {
  1366  		return nil, errors.Trace(err)
  1367  	}
  1368  
  1369  	if record.Tag != "" && record.Expiry.After(time.Now().UTC()) {
  1370  		return record, nil
  1371  	}
  1372  
  1373  	return nil, nil
  1374  }
  1375  
  1376  // FetchTactics performs a tactics request. When there are no stored
  1377  // speed test samples for the network ID, a speed test request is
  1378  // performed immediately before the tactics request, using the same
  1379  // ObfuscatedRoundTripper.
  1380  //
  1381  // The ObfuscatedRoundTripper transport should be established in advance, so
  1382  // that calls to ObfuscatedRoundTripper don't take additional time in TCP,
  1383  // TLS, etc. handshakes.
  1384  //
  1385  // The caller should first call UseStoredTactics and skip FetchTactics
  1386  // when there is an unexpired stored tactics record available. The
  1387  // caller is expected to set any overall timeout in the context input.
  1388  //
  1389  // Limitation: it is assumed that the network ID obtained from getNetworkID
  1390  // is the one that is active when the tactics request is received by the
  1391  // server. However, it is remotely possible to switch networks
  1392  // immediately after invoking the GetNetworkID callback and initiating
  1393  // the request. This is partially mitigated by rechecking the network ID
  1394  // after the request and failing if it differs from the initial network ID.
  1395  //
  1396  // FetchTactics modifies the apiParams input.
  1397  func FetchTactics(
  1398  	ctx context.Context,
  1399  	params *parameters.Parameters,
  1400  	storer Storer,
  1401  	getNetworkID func() string,
  1402  	apiParams common.APIParameters,
  1403  	endPointRegion string,
  1404  	endPointProtocol string,
  1405  	encodedRequestPublicKey string,
  1406  	encodedRequestObfuscatedKey string,
  1407  	obfuscatedRoundTripper ObfuscatedRoundTripper) (*Record, error) {
  1408  
  1409  	networkID := getNetworkID()
  1410  
  1411  	record, err := getStoredTacticsRecord(storer, networkID)
  1412  	if err != nil {
  1413  		return nil, errors.Trace(err)
  1414  	}
  1415  
  1416  	speedTestSamples, err := getSpeedTestSamples(storer, networkID)
  1417  	if err != nil {
  1418  		return nil, errors.Trace(err)
  1419  	}
  1420  
  1421  	// Perform a speed test when there are no samples.
  1422  
  1423  	if len(speedTestSamples) == 0 {
  1424  
  1425  		p := params.Get()
  1426  		request := prng.Padding(
  1427  			p.Int(parameters.SpeedTestPaddingMinBytes),
  1428  			p.Int(parameters.SpeedTestPaddingMaxBytes))
  1429  
  1430  		startTime := time.Now()
  1431  
  1432  		response, err := obfuscatedRoundTripper(ctx, SPEED_TEST_END_POINT, request)
  1433  
  1434  		elapsedTime := time.Since(startTime)
  1435  
  1436  		if err != nil {
  1437  			return nil, errors.Trace(err)
  1438  		}
  1439  
  1440  		if networkID != getNetworkID() {
  1441  			return nil, errors.TraceNew("network ID changed")
  1442  		}
  1443  
  1444  		err = AddSpeedTestSample(
  1445  			params,
  1446  			storer,
  1447  			networkID,
  1448  			endPointRegion,
  1449  			endPointProtocol,
  1450  			elapsedTime,
  1451  			request,
  1452  			response)
  1453  		if err != nil {
  1454  			return nil, errors.Trace(err)
  1455  		}
  1456  
  1457  		speedTestSamples, err = getSpeedTestSamples(storer, networkID)
  1458  		if err != nil {
  1459  			return nil, errors.Trace(err)
  1460  		}
  1461  	}
  1462  
  1463  	// Perform the tactics request.
  1464  
  1465  	apiParams[STORED_TACTICS_TAG_PARAMETER_NAME] = record.Tag
  1466  	apiParams[SPEED_TEST_SAMPLES_PARAMETER_NAME] = speedTestSamples
  1467  
  1468  	requestPublicKey, err := base64.StdEncoding.DecodeString(encodedRequestPublicKey)
  1469  	if err != nil {
  1470  		return nil, errors.Trace(err)
  1471  	}
  1472  
  1473  	requestObfuscatedKey, err := base64.StdEncoding.DecodeString(encodedRequestObfuscatedKey)
  1474  	if err != nil {
  1475  		return nil, errors.Trace(err)
  1476  	}
  1477  
  1478  	ephemeralPublicKey, ephemeralPrivateKey, err := box.GenerateKey(rand.Reader)
  1479  	if err != nil {
  1480  		return nil, errors.Trace(err)
  1481  	}
  1482  
  1483  	boxedRequest, err := boxPayload(
  1484  		TACTICS_REQUEST_NONCE,
  1485  		requestPublicKey,
  1486  		ephemeralPrivateKey[:],
  1487  		requestObfuscatedKey,
  1488  		ephemeralPublicKey[:],
  1489  		&apiParams)
  1490  	if err != nil {
  1491  		return nil, errors.Trace(err)
  1492  	}
  1493  
  1494  	boxedResponse, err := obfuscatedRoundTripper(ctx, TACTICS_END_POINT, boxedRequest)
  1495  	if err != nil {
  1496  		return nil, errors.Trace(err)
  1497  	}
  1498  
  1499  	if networkID != getNetworkID() {
  1500  		return nil, errors.TraceNew("network ID changed")
  1501  	}
  1502  
  1503  	// Process and store the response payload.
  1504  
  1505  	var payload *Payload
  1506  
  1507  	_, err = unboxPayload(
  1508  		TACTICS_RESPONSE_NONCE,
  1509  		requestPublicKey,
  1510  		ephemeralPrivateKey[:],
  1511  		requestObfuscatedKey,
  1512  		boxedResponse,
  1513  		&payload)
  1514  	if err != nil {
  1515  		return nil, errors.Trace(err)
  1516  	}
  1517  
  1518  	err = applyTacticsPayload(storer, networkID, record, payload)
  1519  	if err != nil {
  1520  		return nil, errors.Trace(err)
  1521  	}
  1522  
  1523  	err = setStoredTacticsRecord(storer, networkID, record)
  1524  	if err != nil {
  1525  		return nil, errors.Trace(err)
  1526  	}
  1527  
  1528  	return record, nil
  1529  }
  1530  
  1531  // MakeSpeedTestResponse creates a speed test response prefixed
  1532  // with a timestamp and followed by random padding. The timestamp
  1533  // enables the client performing the speed test to record the
  1534  // sample time with an accurate server clock; the random padding
  1535  // is to frustrate fingerprinting.
  1536  // The speed test timestamp is truncated as a privacy measure.
  1537  func MakeSpeedTestResponse(minPadding, maxPadding int) ([]byte, error) {
  1538  
  1539  	// MarshalBinary encoding (version 1) is 15 bytes:
  1540  	// https://github.com/golang/go/blob/release-branch.go1.9/src/time/time.go#L1112
  1541  
  1542  	timestamp, err := time.Now().UTC().Truncate(1 * time.Hour).MarshalBinary()
  1543  	if err == nil && len(timestamp) > 255 {
  1544  		err = fmt.Errorf("unexpected marshaled time size: %d", len(timestamp))
  1545  	}
  1546  	if err != nil {
  1547  		return nil, errors.Trace(err)
  1548  	}
  1549  
  1550  	randomPadding := prng.Padding(minPadding, maxPadding)
  1551  	// On error, proceed without random padding.
  1552  	// TODO: log error, even if proceeding?
  1553  
  1554  	response := make([]byte, 0, 1+len(timestamp)+len(randomPadding))
  1555  
  1556  	response = append(response, byte(len(timestamp)))
  1557  	response = append(response, timestamp...)
  1558  	response = append(response, randomPadding...)
  1559  
  1560  	return response, nil
  1561  }
  1562  
  1563  // AddSpeedTestSample stores a new speed test sample. A maximum of
  1564  // SpeedTestMaxSampleCount samples per network ID are stored, so once
  1565  // that limit is reached, the oldest samples are removed to make room
  1566  // for the new sample.
  1567  func AddSpeedTestSample(
  1568  	params *parameters.Parameters,
  1569  	storer Storer,
  1570  	networkID string,
  1571  	endPointRegion string,
  1572  	endPointProtocol string,
  1573  	elaspedTime time.Duration,
  1574  	request []byte,
  1575  	response []byte) error {
  1576  
  1577  	if len(response) < 1 {
  1578  		return errors.TraceNew("unexpected empty response")
  1579  	}
  1580  	timestampLength := int(response[0])
  1581  	if len(response) < 1+timestampLength {
  1582  		return errors.Tracef(
  1583  			"unexpected response shorter than timestamp size %d", timestampLength)
  1584  	}
  1585  	var timestamp time.Time
  1586  	err := timestamp.UnmarshalBinary(response[1 : 1+timestampLength])
  1587  	if err != nil {
  1588  		return errors.Trace(err)
  1589  	}
  1590  
  1591  	sample := SpeedTestSample{
  1592  		Timestamp:        timestamp,
  1593  		EndPointRegion:   endPointRegion,
  1594  		EndPointProtocol: endPointProtocol,
  1595  		RTTMilliseconds:  int(elaspedTime / time.Millisecond),
  1596  		BytesUp:          len(request),
  1597  		BytesDown:        len(response),
  1598  	}
  1599  
  1600  	maxCount := params.Get().Int(parameters.SpeedTestMaxSampleCount)
  1601  	if maxCount == 0 {
  1602  		return errors.TraceNew("speed test max sample count is 0")
  1603  	}
  1604  
  1605  	speedTestSamples, err := getSpeedTestSamples(storer, networkID)
  1606  	if err != nil {
  1607  		return errors.Trace(err)
  1608  	}
  1609  
  1610  	if speedTestSamples == nil {
  1611  		speedTestSamples = make([]SpeedTestSample, 0)
  1612  	}
  1613  
  1614  	if len(speedTestSamples)+1 > maxCount {
  1615  		speedTestSamples = speedTestSamples[len(speedTestSamples)+1-maxCount:]
  1616  	}
  1617  	speedTestSamples = append(speedTestSamples, sample)
  1618  
  1619  	record, err := json.Marshal(speedTestSamples)
  1620  	if err != nil {
  1621  		return errors.Trace(err)
  1622  	}
  1623  
  1624  	err = storer.SetSpeedTestSamplesRecord(networkID, record)
  1625  	if err != nil {
  1626  		return errors.Trace(err)
  1627  	}
  1628  
  1629  	return nil
  1630  }
  1631  
  1632  func getSpeedTestSamples(
  1633  	storer Storer, networkID string) ([]SpeedTestSample, error) {
  1634  
  1635  	record, err := storer.GetSpeedTestSamplesRecord(networkID)
  1636  	if err != nil {
  1637  		return nil, errors.Trace(err)
  1638  	}
  1639  
  1640  	if record == nil {
  1641  		return nil, nil
  1642  	}
  1643  
  1644  	var speedTestSamples []SpeedTestSample
  1645  	err = json.Unmarshal(record, &speedTestSamples)
  1646  	if err != nil {
  1647  		return nil, errors.Trace(err)
  1648  	}
  1649  
  1650  	return speedTestSamples, nil
  1651  }
  1652  
  1653  func getStoredTacticsRecord(
  1654  	storer Storer, networkID string) (*Record, error) {
  1655  
  1656  	marshaledRecord, err := storer.GetTacticsRecord(networkID)
  1657  	if err != nil {
  1658  		return nil, errors.Trace(err)
  1659  	}
  1660  
  1661  	if marshaledRecord == nil {
  1662  		return &Record{}, nil
  1663  	}
  1664  
  1665  	var record *Record
  1666  	err = json.Unmarshal(marshaledRecord, &record)
  1667  	if err != nil {
  1668  		return nil, errors.Trace(err)
  1669  	}
  1670  
  1671  	if record == nil {
  1672  		record = &Record{}
  1673  	}
  1674  
  1675  	return record, nil
  1676  }
  1677  
  1678  func applyTacticsPayload(
  1679  	storer Storer,
  1680  	networkID string,
  1681  	record *Record,
  1682  	payload *Payload) error {
  1683  
  1684  	if payload.Tag == "" {
  1685  		return errors.TraceNew("invalid tag")
  1686  	}
  1687  
  1688  	// Replace the tactics data when the tags differ.
  1689  
  1690  	if payload.Tag != record.Tag {
  1691  		record.Tag = payload.Tag
  1692  		record.Tactics = Tactics{}
  1693  		err := json.Unmarshal(payload.Tactics, &record.Tactics)
  1694  		if err != nil {
  1695  			return errors.Trace(err)
  1696  		}
  1697  	}
  1698  
  1699  	// Note: record.Tactics.TTL is validated by server
  1700  	ttl, err := time.ParseDuration(record.Tactics.TTL)
  1701  	if err != nil {
  1702  		return errors.Trace(err)
  1703  	}
  1704  
  1705  	if ttl <= 0 {
  1706  		return errors.TraceNew("invalid TTL")
  1707  	}
  1708  	if record.Tactics.Probability <= 0.0 {
  1709  		return errors.TraceNew("invalid probability")
  1710  	}
  1711  
  1712  	// Set or extend the expiry.
  1713  
  1714  	record.Expiry = time.Now().UTC().Add(ttl)
  1715  
  1716  	return nil
  1717  }
  1718  
  1719  func setStoredTacticsRecord(
  1720  	storer Storer,
  1721  	networkID string,
  1722  	record *Record) error {
  1723  
  1724  	marshaledRecord, err := json.Marshal(record)
  1725  	if err != nil {
  1726  		return errors.Trace(err)
  1727  	}
  1728  
  1729  	err = storer.SetTacticsRecord(networkID, marshaledRecord)
  1730  	if err != nil {
  1731  		return errors.Trace(err)
  1732  	}
  1733  
  1734  	return nil
  1735  }
  1736  
  1737  func boxPayload(
  1738  	nonce, peerPublicKey, privateKey, obfuscatedKey, bundlePublicKey []byte,
  1739  	payload interface{}) ([]byte, error) {
  1740  
  1741  	if len(nonce) > 24 ||
  1742  		len(peerPublicKey) != 32 ||
  1743  		len(privateKey) != 32 {
  1744  		return nil, errors.TraceNew("unexpected box key length")
  1745  	}
  1746  
  1747  	marshaledPayload, err := json.Marshal(payload)
  1748  	if err != nil {
  1749  		return nil, errors.Trace(err)
  1750  	}
  1751  
  1752  	var nonceArray [24]byte
  1753  	copy(nonceArray[:], nonce)
  1754  
  1755  	var peerPublicKeyArray, privateKeyArray [32]byte
  1756  	copy(peerPublicKeyArray[:], peerPublicKey)
  1757  	copy(privateKeyArray[:], privateKey)
  1758  
  1759  	box := box.Seal(nil, marshaledPayload, &nonceArray, &peerPublicKeyArray, &privateKeyArray)
  1760  
  1761  	if bundlePublicKey != nil {
  1762  		bundledBox := make([]byte, 32+len(box))
  1763  		copy(bundledBox[0:32], bundlePublicKey[0:32])
  1764  		copy(bundledBox[32:], box)
  1765  		box = bundledBox
  1766  	}
  1767  
  1768  	// TODO: replay tactics request padding?
  1769  	paddingPRNGSeed, err := prng.NewSeed()
  1770  	if err != nil {
  1771  		return nil, errors.Trace(err)
  1772  	}
  1773  
  1774  	maxPadding := TACTICS_PADDING_MAX_SIZE
  1775  
  1776  	obfuscator, err := obfuscator.NewClientObfuscator(
  1777  		&obfuscator.ObfuscatorConfig{
  1778  			Keyword:         string(obfuscatedKey),
  1779  			PaddingPRNGSeed: paddingPRNGSeed,
  1780  			MaxPadding:      &maxPadding})
  1781  	if err != nil {
  1782  		return nil, errors.Trace(err)
  1783  	}
  1784  
  1785  	obfuscatedBox := obfuscator.SendSeedMessage()
  1786  	seedLen := len(obfuscatedBox)
  1787  
  1788  	obfuscatedBox = append(obfuscatedBox, box...)
  1789  	obfuscator.ObfuscateClientToServer(obfuscatedBox[seedLen:])
  1790  
  1791  	return obfuscatedBox, nil
  1792  }
  1793  
  1794  // unboxPayload mutates obfuscatedBoxedPayload by deobfuscating in-place.
  1795  func unboxPayload(
  1796  	nonce, peerPublicKey, privateKey, obfuscatedKey, obfuscatedBoxedPayload []byte,
  1797  	payload interface{}) ([]byte, error) {
  1798  
  1799  	if len(nonce) > 24 ||
  1800  		(peerPublicKey != nil && len(peerPublicKey) != 32) ||
  1801  		len(privateKey) != 32 {
  1802  		return nil, errors.TraceNew("unexpected box key length")
  1803  	}
  1804  
  1805  	obfuscatedReader := bytes.NewReader(obfuscatedBoxedPayload[:])
  1806  
  1807  	obfuscator, err := obfuscator.NewServerObfuscator(
  1808  		&obfuscator.ObfuscatorConfig{Keyword: string(obfuscatedKey)},
  1809  		"",
  1810  		obfuscatedReader)
  1811  	if err != nil {
  1812  		return nil, errors.Trace(err)
  1813  	}
  1814  
  1815  	seedLen, err := obfuscatedReader.Seek(0, 1)
  1816  	if err != nil {
  1817  		return nil, errors.Trace(err)
  1818  	}
  1819  
  1820  	boxedPayload := obfuscatedBoxedPayload[seedLen:]
  1821  	obfuscator.ObfuscateClientToServer(boxedPayload)
  1822  
  1823  	var nonceArray [24]byte
  1824  	copy(nonceArray[:], nonce)
  1825  
  1826  	var peerPublicKeyArray, privateKeyArray [32]byte
  1827  	copy(privateKeyArray[:], privateKey)
  1828  
  1829  	var bundledPeerPublicKey []byte
  1830  
  1831  	if peerPublicKey != nil {
  1832  		copy(peerPublicKeyArray[:], peerPublicKey)
  1833  	} else {
  1834  		if len(boxedPayload) < 32 {
  1835  			return nil, errors.TraceNew("unexpected box size")
  1836  		}
  1837  		bundledPeerPublicKey = boxedPayload[0:32]
  1838  		copy(peerPublicKeyArray[:], bundledPeerPublicKey)
  1839  		boxedPayload = boxedPayload[32:]
  1840  	}
  1841  
  1842  	marshaledPayload, ok := box.Open(nil, boxedPayload, &nonceArray, &peerPublicKeyArray, &privateKeyArray)
  1843  	if !ok {
  1844  		return nil, errors.TraceNew("invalid box")
  1845  	}
  1846  
  1847  	err = json.Unmarshal(marshaledPayload, payload)
  1848  	if err != nil {
  1849  		return nil, errors.Trace(err)
  1850  	}
  1851  
  1852  	return bundledPeerPublicKey, nil
  1853  }