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