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 }