tactics_test.go 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200
  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. package tactics
  20. import (
  21. "bytes"
  22. "context"
  23. "fmt"
  24. "io/ioutil"
  25. "net"
  26. "net/http"
  27. "os"
  28. "reflect"
  29. "strings"
  30. "testing"
  31. "time"
  32. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
  33. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
  34. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
  35. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/internal/testutils"
  36. )
  37. func TestTactics(t *testing.T) {
  38. // Server tactics configuration
  39. // Long and short region lists test both map and slice lookups.
  40. //
  41. // Repeated median aggregation tests aggregation memoization.
  42. //
  43. // The test-packetman-spec tests a reference between a filter tactics
  44. // and default tactics.
  45. tacticsConfigTemplate := `
  46. {
  47. "RequestPublicKey" : "%s",
  48. "RequestPrivateKey" : "%s",
  49. "RequestObfuscatedKey" : "%s",
  50. "DefaultTactics" : {
  51. "TTL" : "1s",
  52. "Parameters" : {
  53. "NetworkLatencyMultiplier" : %0.1f,
  54. "ServerPacketManipulationSpecs" : [{"Name": "test-packetman-spec", "PacketSpecs": [["TCP-flags S"]]}]
  55. }
  56. },
  57. "FilteredTactics" : [
  58. {
  59. "Filter" : {
  60. "Regions": ["R1", "R2", "R3", "R4", "R5", "R6"],
  61. "APIParameters" : {"client_platform" : ["P1"]},
  62. "SpeedTestRTTMilliseconds" : {
  63. "Aggregation" : "Median",
  64. "AtLeast" : 1
  65. }
  66. },
  67. "Tactics" : {
  68. "Parameters" : {
  69. "ConnectionWorkerPoolSize" : %d
  70. }
  71. }
  72. },
  73. {
  74. "Filter" : {
  75. "Regions": ["R1"],
  76. "ASNs": ["1"],
  77. "APIParameters" : {"client_platform" : ["P1"]},
  78. "MinClientVersion" : 4,
  79. "MaxClientVersion" : 10,
  80. "SpeedTestRTTMilliseconds" : {
  81. "Aggregation" : "Median",
  82. "AtLeast" : 1
  83. }
  84. },
  85. "Tactics" : {
  86. "Parameters" : {
  87. %s
  88. }
  89. }
  90. },
  91. {
  92. "Filter" : {
  93. "APIParameters" : {"client_platform" : ["P1"]},
  94. "MinClientVersion" : 6,
  95. "MaxClientVersion" : 10
  96. },
  97. "Tactics" : {
  98. "Parameters" : {
  99. "ConnectionWorkerPoolSize" : 1
  100. }
  101. }
  102. },
  103. {
  104. "Filter" : {
  105. "APIParameters" : {"client_platform" : ["P2"], "client_version": ["2"]}
  106. },
  107. "Tactics" : {
  108. "Parameters" : {
  109. "ConnectionWorkerPoolSize" : 1
  110. }
  111. }
  112. },
  113. {
  114. "Filter" : {
  115. "Regions": ["R2"]
  116. },
  117. "Tactics" : {
  118. "Parameters" : {
  119. "ConnectionWorkerPoolSize" : %d
  120. }
  121. }
  122. },
  123. {
  124. "Filter" : {
  125. "Regions": ["R7"]
  126. },
  127. "Tactics" : {
  128. "Parameters" : {
  129. "ServerProtocolPacketManipulations": {"All" : ["test-packetman-spec"]}
  130. }
  131. }
  132. }
  133. ]
  134. }
  135. `
  136. if stringLookupThreshold != 5 {
  137. t.Fatalf("unexpected stringLookupThreshold")
  138. }
  139. encodedRequestPublicKey, encodedRequestPrivateKey, encodedObfuscatedKey, err := GenerateKeys()
  140. if err != nil {
  141. t.Fatalf("GenerateKeys failed: %s", err)
  142. }
  143. tacticsNetworkLatencyMultiplier := 2.0
  144. tacticsConnectionWorkerPoolSize := 5
  145. tacticsLimitTunnelProtocols := protocol.TunnelProtocols{"OSSH", "SSH"}
  146. jsonTacticsLimitTunnelProtocols := `"LimitTunnelProtocols" : ["OSSH", "SSH"]`
  147. expectedApplyCount := 3
  148. tacticsConfig := fmt.Sprintf(
  149. tacticsConfigTemplate,
  150. encodedRequestPublicKey,
  151. encodedRequestPrivateKey,
  152. encodedObfuscatedKey,
  153. tacticsNetworkLatencyMultiplier,
  154. tacticsConnectionWorkerPoolSize,
  155. jsonTacticsLimitTunnelProtocols,
  156. tacticsConnectionWorkerPoolSize+1)
  157. file, err := ioutil.TempFile("", "tactics.config")
  158. if err != nil {
  159. t.Fatalf("TempFile create failed: %s", err)
  160. }
  161. _, err = file.Write([]byte(tacticsConfig))
  162. if err != nil {
  163. t.Fatalf("TempFile write failed: %s", err)
  164. }
  165. file.Close()
  166. configFileName := file.Name()
  167. defer os.Remove(configFileName)
  168. // Configure and run server
  169. // Mock server uses an insecure HTTP transport that exposes endpoint names
  170. clientGeoIPData := common.GeoIPData{Country: "R1", ASN: "1"}
  171. logger := testutils.NewTestLogger()
  172. validator := func(
  173. apiParams common.APIParameters) error {
  174. expectedParams := []string{"client_platform", "client_version"}
  175. for _, name := range expectedParams {
  176. value, ok := apiParams[name]
  177. if !ok {
  178. return fmt.Errorf("missing param: %s", name)
  179. }
  180. _, ok = value.(string)
  181. if !ok {
  182. return fmt.Errorf("invalid param type: %s", name)
  183. }
  184. }
  185. return nil
  186. }
  187. formatter := func(
  188. _ string,
  189. _ common.GeoIPData,
  190. apiParams common.APIParameters) common.LogFields {
  191. return common.LogFields(apiParams)
  192. }
  193. server, err := NewServer(
  194. logger,
  195. formatter,
  196. validator,
  197. configFileName,
  198. "",
  199. "",
  200. "")
  201. if err != nil {
  202. t.Fatalf("NewServer failed: %s", err)
  203. }
  204. listener, err := net.Listen("tcp", "127.0.0.1:0")
  205. if err != nil {
  206. t.Fatalf("Listen failed: %s", err)
  207. }
  208. serverAddress := listener.Addr().String()
  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. // Configure client
  228. params, err := parameters.NewParameters(
  229. func(err error) {
  230. t.Fatalf("Parameters getValue failed: %s", err)
  231. })
  232. if err != nil {
  233. t.Fatalf("NewParameters failed: %s", err)
  234. }
  235. networkID := "NETWORK1"
  236. getNetworkID := func() string { return networkID }
  237. apiParams := common.APIParameters{
  238. "client_platform": "P1",
  239. "client_version": "5"}
  240. storer := newTestStorer()
  241. endPointRegion := "R0"
  242. endPointProtocol := "OSSH"
  243. differentEndPointProtocol := "SSH"
  244. obfuscatedRoundTripper := func(
  245. ctx context.Context,
  246. endPoint string,
  247. requestBody []byte) ([]byte, error) {
  248. // This mock ObfuscatedRoundTripper does not actually obfuscate the endpoint
  249. // value.
  250. request, err := http.NewRequest(
  251. "POST",
  252. fmt.Sprintf("http://%s/%s", serverAddress, endPoint),
  253. bytes.NewReader(requestBody))
  254. if err != nil {
  255. return nil, err
  256. }
  257. request = request.WithContext(ctx)
  258. response, err := http.DefaultClient.Do(request)
  259. if err != nil {
  260. return nil, err
  261. }
  262. defer response.Body.Close()
  263. if response.StatusCode != http.StatusOK {
  264. return nil, fmt.Errorf("HTTP request failed: %d", response.StatusCode)
  265. }
  266. body, err := ioutil.ReadAll(response.Body)
  267. if err != nil {
  268. return nil, err
  269. }
  270. return body, nil
  271. }
  272. // There should be no local tactics
  273. tacticsRecord, err := UseStoredTactics(storer, networkID)
  274. if err != nil {
  275. t.Fatalf("UseStoredTactics failed: %s", err)
  276. }
  277. if tacticsRecord != nil {
  278. t.Fatalf("unexpected tactics record")
  279. }
  280. // Helper to check that expected tactics parameters are returned
  281. checkParameters := func(r *Record) {
  282. p, err := parameters.NewParameters(nil)
  283. if err != nil {
  284. t.Fatalf("NewParameters failed: %s", err)
  285. }
  286. // ValidationSkipOnError is set for Psiphon clients
  287. counts, err := p.Set(r.Tag, parameters.ValidationSkipOnError, r.Tactics.Parameters)
  288. if err != nil {
  289. t.Fatalf("Apply failed: %s", err)
  290. }
  291. if counts[0] != expectedApplyCount {
  292. t.Fatalf("Unexpected apply count: %d", counts[0])
  293. }
  294. multipler := p.Get().Float(parameters.NetworkLatencyMultiplier)
  295. if multipler != tacticsNetworkLatencyMultiplier {
  296. t.Fatalf("Unexpected NetworkLatencyMultiplier: %v", multipler)
  297. }
  298. connectionWorkerPoolSize := p.Get().Int(parameters.ConnectionWorkerPoolSize)
  299. if connectionWorkerPoolSize != tacticsConnectionWorkerPoolSize {
  300. t.Fatalf("Unexpected ConnectionWorkerPoolSize: %v", connectionWorkerPoolSize)
  301. }
  302. limitTunnelProtocols := p.Get().TunnelProtocols(parameters.LimitTunnelProtocols)
  303. if !reflect.DeepEqual(limitTunnelProtocols, tacticsLimitTunnelProtocols) {
  304. t.Fatalf("Unexpected LimitTunnelProtocols: %v", limitTunnelProtocols)
  305. }
  306. }
  307. // Helper to check server-side cachedTacticsData state
  308. checkServerCache := func(cacheEntryFilterMatches ...[]bool) {
  309. cacheItems := server.cachedTacticsData.Items()
  310. if len(cacheItems) != len(cacheEntryFilterMatches) {
  311. t.Fatalf("Unexpected cachedTacticsData size: %v", len(cacheItems))
  312. }
  313. for _, filterMatches := range cacheEntryFilterMatches {
  314. includeServerSizeOnly := false
  315. hasFilterMatches := true
  316. cacheKey := getCacheKey(includeServerSizeOnly, hasFilterMatches, filterMatches)
  317. _, ok := server.cachedTacticsData.Get(cacheKey)
  318. if !ok {
  319. t.Fatalf("Unexpected missing cachedTacticsData entry: %v", filterMatches)
  320. }
  321. }
  322. }
  323. // Initial tactics request; will also run a speed test
  324. // Request should complete in < 1 second
  325. ctx, cancelFunc := context.WithTimeout(context.Background(), 1*time.Second)
  326. initialFetchTacticsRecord, err := FetchTactics(
  327. ctx,
  328. params,
  329. storer,
  330. getNetworkID,
  331. apiParams,
  332. endPointProtocol,
  333. endPointRegion,
  334. encodedRequestPublicKey,
  335. encodedObfuscatedKey,
  336. obfuscatedRoundTripper)
  337. cancelFunc()
  338. if err != nil {
  339. t.Fatalf("FetchTactics failed: %s", err)
  340. }
  341. if initialFetchTacticsRecord == nil {
  342. t.Fatalf("expected tactics record")
  343. }
  344. checkParameters(initialFetchTacticsRecord)
  345. // Server should be caching tactics data for tactics matching first two
  346. // filters.
  347. checkServerCache([]bool{true, true, false, false, false, false})
  348. // There should now be cached local tactics
  349. storedTacticsRecord, err := UseStoredTactics(storer, networkID)
  350. if err != nil {
  351. t.Fatalf("UseStoredTactics failed: %s", err)
  352. }
  353. if storedTacticsRecord == nil {
  354. t.Fatalf("expected stored tactics record")
  355. }
  356. // Strip monotonic component so comparisons will work
  357. initialFetchTacticsRecord.Expiry = initialFetchTacticsRecord.Expiry.Round(0)
  358. if !reflect.DeepEqual(initialFetchTacticsRecord, storedTacticsRecord) {
  359. t.Fatalf("tactics records are not identical:\n\n%#v\n\n%#v\n\n",
  360. initialFetchTacticsRecord, storedTacticsRecord)
  361. }
  362. checkParameters(storedTacticsRecord)
  363. // There should now be a speed test sample
  364. speedTestSamples, err := getSpeedTestSamples(storer, networkID)
  365. if err != nil {
  366. t.Fatalf("getSpeedTestSamples failed: %s", err)
  367. }
  368. if len(speedTestSamples) != 1 {
  369. t.Fatalf("unexpected speed test samples count")
  370. }
  371. // Wait for tactics to expire
  372. time.Sleep(1 * time.Second)
  373. storedTacticsRecord, err = UseStoredTactics(storer, networkID)
  374. if err != nil {
  375. t.Fatalf("UseStoredTactics failed: %s", err)
  376. }
  377. if storedTacticsRecord != nil {
  378. t.Fatalf("unexpected stored tactics record")
  379. }
  380. // Next fetch should merge empty payload as tag matches
  381. // TODO: inspect tactics response payload
  382. fetchTacticsRecord, err := FetchTactics(
  383. context.Background(),
  384. params,
  385. storer,
  386. getNetworkID,
  387. apiParams,
  388. endPointProtocol,
  389. endPointRegion,
  390. encodedRequestPublicKey,
  391. encodedObfuscatedKey,
  392. obfuscatedRoundTripper)
  393. if err != nil {
  394. t.Fatalf("FetchTactics failed: %s", err)
  395. }
  396. if fetchTacticsRecord == nil {
  397. t.Fatalf("expected tactics record")
  398. }
  399. if initialFetchTacticsRecord.Tag != fetchTacticsRecord.Tag {
  400. t.Fatalf("tags are not identical")
  401. }
  402. if initialFetchTacticsRecord.Expiry.Equal(fetchTacticsRecord.Expiry) {
  403. t.Fatalf("expiries unexpectedly identical")
  404. }
  405. if !reflect.DeepEqual(initialFetchTacticsRecord.Tactics, fetchTacticsRecord.Tactics) {
  406. t.Fatalf("tactics are not identical:\n\n%#v\n\n%#v\n\n",
  407. initialFetchTacticsRecord.Tactics, fetchTacticsRecord.Tactics)
  408. }
  409. checkParameters(fetchTacticsRecord)
  410. // Server cache should be the same
  411. checkServerCache([]bool{true, true, false, false, false, false})
  412. // Modify tactics configuration to change payload
  413. tacticsConnectionWorkerPoolSize = 6
  414. tacticsLimitTunnelProtocols = protocol.TunnelProtocols{}
  415. jsonTacticsLimitTunnelProtocols = ``
  416. expectedApplyCount = 2
  417. // Omitting LimitTunnelProtocols entirely tests this bug fix: When a new
  418. // tactics payload is obtained, all previous parameters should be cleared.
  419. //
  420. // In the bug, any previous parameters not in the new tactics were
  421. // incorrectly retained. In this test case, LimitTunnelProtocols is
  422. // omitted in the new tactics; if FetchTactics fails to clear the old
  423. // LimitTunnelProtocols then the test will fail.
  424. tacticsConfig = fmt.Sprintf(
  425. tacticsConfigTemplate,
  426. encodedRequestPublicKey,
  427. encodedRequestPrivateKey,
  428. encodedObfuscatedKey,
  429. tacticsNetworkLatencyMultiplier,
  430. tacticsConnectionWorkerPoolSize,
  431. jsonTacticsLimitTunnelProtocols,
  432. tacticsConnectionWorkerPoolSize+1)
  433. err = ioutil.WriteFile(configFileName, []byte(tacticsConfig), 0600)
  434. if err != nil {
  435. t.Fatalf("WriteFile failed: %s", err)
  436. }
  437. reloaded, err := server.Reload()
  438. if err != nil {
  439. t.Fatalf("Reload failed: %s", err)
  440. }
  441. if !reloaded {
  442. t.Fatalf("Server config failed to reload")
  443. }
  444. // Server cache should be flushed
  445. checkServerCache()
  446. // Next fetch should return a different payload
  447. fetchTacticsRecord, err = FetchTactics(
  448. context.Background(),
  449. params,
  450. storer,
  451. getNetworkID,
  452. apiParams,
  453. endPointProtocol,
  454. endPointRegion,
  455. encodedRequestPublicKey,
  456. encodedObfuscatedKey,
  457. obfuscatedRoundTripper)
  458. if err != nil {
  459. t.Fatalf("FetchTactics failed: %s", err)
  460. }
  461. if fetchTacticsRecord == nil {
  462. t.Fatalf("expected tactics record")
  463. }
  464. if initialFetchTacticsRecord.Tag == fetchTacticsRecord.Tag {
  465. t.Fatalf("tags unexpectedly identical")
  466. }
  467. if initialFetchTacticsRecord.Expiry.Equal(fetchTacticsRecord.Expiry) {
  468. t.Fatalf("expires unexpectedly identical")
  469. }
  470. if reflect.DeepEqual(initialFetchTacticsRecord.Tactics, fetchTacticsRecord.Tactics) {
  471. t.Fatalf("tactics unexpectedly identical")
  472. }
  473. checkParameters(fetchTacticsRecord)
  474. checkServerCache([]bool{true, true, false, false, false, false})
  475. // Exercise handshake transport of tactics
  476. // Wait for tactics to expire; handshake should renew
  477. time.Sleep(1 * time.Second)
  478. handshakeParams := common.APIParameters{
  479. "client_platform": "P1",
  480. "client_version": "5"}
  481. err = SetTacticsAPIParameters(storer, networkID, handshakeParams)
  482. if err != nil {
  483. t.Fatalf("SetTacticsAPIParameters failed: %s", err)
  484. }
  485. // FetchTactics will exercise the compression case.
  486. compressPayload := false
  487. tacticsPayload, err := server.GetTacticsPayload(clientGeoIPData, handshakeParams, compressPayload)
  488. if err != nil {
  489. t.Fatalf("GetTacticsPayload failed: %s", err)
  490. }
  491. handshakeTacticsRecord, err := HandleTacticsPayload(storer, networkID, tacticsPayload)
  492. if err != nil {
  493. t.Fatalf("HandleTacticsPayload failed: %s", err)
  494. }
  495. // When tactic parameters are unchanged, HandleTacticsPayload returns nil,
  496. // so that callers do not apply tactics unnecessarily.
  497. //
  498. // Check that nil is returned, but then directly load the record stored by
  499. // HandleTacticsPayload in order to check metadata including the updated
  500. // TTL.
  501. if handshakeTacticsRecord != nil {
  502. t.Fatalf("unexpected tactics record")
  503. }
  504. handshakeTacticsRecord, err = getStoredTacticsRecord(storer, networkID)
  505. if err != nil {
  506. t.Fatalf("getStoredTacticsRecord failed: %s", err)
  507. }
  508. if fetchTacticsRecord.Tag != handshakeTacticsRecord.Tag {
  509. t.Fatalf("tags are not identical")
  510. }
  511. if fetchTacticsRecord.Expiry.Equal(handshakeTacticsRecord.Expiry) {
  512. t.Fatalf("expiries unexpectedly identical")
  513. }
  514. if !reflect.DeepEqual(fetchTacticsRecord.Tactics, handshakeTacticsRecord.Tactics) {
  515. t.Fatalf("tactics are not identical:\n\n%#v\n\n%#v\n\n",
  516. fetchTacticsRecord.Tactics, handshakeTacticsRecord.Tactics)
  517. }
  518. checkParameters(handshakeTacticsRecord)
  519. checkServerCache([]bool{true, true, false, false, false, false})
  520. // Now there should be stored tactics
  521. storedTacticsRecord, err = UseStoredTactics(storer, networkID)
  522. if err != nil {
  523. t.Fatalf("UseStoredTactics failed: %s", err)
  524. }
  525. if storedTacticsRecord == nil {
  526. t.Fatalf("expected stored tactics record")
  527. }
  528. handshakeTacticsRecord.Expiry = handshakeTacticsRecord.Expiry.Round(0)
  529. if !reflect.DeepEqual(handshakeTacticsRecord, storedTacticsRecord) {
  530. t.Fatalf("tactics records are not identical:\n\n%#v\n\n%#v\n\n",
  531. handshakeTacticsRecord, storedTacticsRecord)
  532. }
  533. checkParameters(storedTacticsRecord)
  534. // Change network ID, should be no stored tactics
  535. networkID = "NETWORK2"
  536. storedTacticsRecord, err = UseStoredTactics(storer, networkID)
  537. if err != nil {
  538. t.Fatalf("UseStoredTactics failed: %s", err)
  539. }
  540. if storedTacticsRecord != nil {
  541. t.Fatalf("unexpected stored tactics record")
  542. }
  543. // Server should cache a new entry for different filter matches
  544. apiParams2 := common.APIParameters{
  545. "client_platform": "P2",
  546. "client_version": "2"}
  547. fetchTacticsRecord, err = FetchTactics(
  548. context.Background(),
  549. params,
  550. storer,
  551. getNetworkID,
  552. apiParams2,
  553. endPointProtocol,
  554. endPointRegion,
  555. encodedRequestPublicKey,
  556. encodedObfuscatedKey,
  557. obfuscatedRoundTripper)
  558. if err != nil {
  559. t.Fatalf("FetchTactics failed: %s", err)
  560. }
  561. if fetchTacticsRecord == nil {
  562. t.Fatalf("expected tactics record")
  563. }
  564. checkServerCache(
  565. []bool{true, true, false, false, false, false},
  566. []bool{false, false, false, true, false, false})
  567. // Exercise speed test sample truncation
  568. maxSamples := params.Get().Int(parameters.SpeedTestMaxSampleCount)
  569. for i := 0; i < maxSamples*2; i++ {
  570. response, err := MakeSpeedTestResponse(0, 0)
  571. if err != nil {
  572. t.Fatalf("MakeSpeedTestResponse failed: %s", err)
  573. }
  574. err = AddSpeedTestSample(
  575. params,
  576. storer,
  577. networkID,
  578. "",
  579. differentEndPointProtocol,
  580. 100*time.Millisecond,
  581. nil,
  582. response)
  583. if err != nil {
  584. t.Fatalf("AddSpeedTestSample failed: %s", err)
  585. }
  586. }
  587. speedTestSamples, err = getSpeedTestSamples(storer, networkID)
  588. if err != nil {
  589. t.Fatalf("getSpeedTestSamples failed: %s", err)
  590. }
  591. if len(speedTestSamples) != maxSamples {
  592. t.Fatalf("unexpected speed test samples count")
  593. }
  594. for _, sample := range speedTestSamples {
  595. if sample.EndPointProtocol == endPointProtocol {
  596. t.Fatalf("unexpected old speed test sample")
  597. }
  598. }
  599. // Fetch should fail when using incorrect keys
  600. encodedIncorrectRequestPublicKey, _, encodedIncorrectObfuscatedKey, err := GenerateKeys()
  601. if err != nil {
  602. t.Fatalf("GenerateKeys failed: %s", err)
  603. }
  604. _, err = FetchTactics(
  605. context.Background(),
  606. params,
  607. storer,
  608. getNetworkID,
  609. apiParams,
  610. endPointProtocol,
  611. endPointRegion,
  612. encodedIncorrectRequestPublicKey,
  613. encodedObfuscatedKey,
  614. obfuscatedRoundTripper)
  615. if err == nil {
  616. t.Fatalf("FetchTactics succeeded unexpectedly with incorrect request key")
  617. }
  618. _, err = FetchTactics(
  619. context.Background(),
  620. params,
  621. storer,
  622. getNetworkID,
  623. apiParams,
  624. endPointProtocol,
  625. endPointRegion,
  626. encodedRequestPublicKey,
  627. encodedIncorrectObfuscatedKey,
  628. obfuscatedRoundTripper)
  629. if err == nil {
  630. t.Fatalf("FetchTactics succeeded unexpectedly with incorrect obfuscated key")
  631. }
  632. // When no keys are supplied, untunneled tactics requests are not supported, but
  633. // handshake tactics (GetTacticsPayload) should still work.
  634. tacticsConfig = fmt.Sprintf(
  635. tacticsConfigTemplate,
  636. "",
  637. "",
  638. "",
  639. tacticsNetworkLatencyMultiplier,
  640. tacticsConnectionWorkerPoolSize,
  641. jsonTacticsLimitTunnelProtocols,
  642. tacticsConnectionWorkerPoolSize+1)
  643. err = ioutil.WriteFile(configFileName, []byte(tacticsConfig), 0600)
  644. if err != nil {
  645. t.Fatalf("WriteFile failed: %s", err)
  646. }
  647. reloaded, err = server.Reload()
  648. if err != nil {
  649. t.Fatalf("Reload failed: %s", err)
  650. }
  651. if !reloaded {
  652. t.Fatalf("Server config failed to reload")
  653. }
  654. _, err = server.GetTacticsPayload(clientGeoIPData, handshakeParams, compressPayload)
  655. if err != nil {
  656. t.Fatalf("GetTacticsPayload failed: %s", err)
  657. }
  658. handled := server.HandleEndPoint(TACTICS_END_POINT, clientGeoIPData, nil, nil)
  659. if handled {
  660. t.Fatalf("HandleEndPoint unexpectedly handled request")
  661. }
  662. handled = server.HandleEndPoint(SPEED_TEST_END_POINT, clientGeoIPData, nil, nil)
  663. if handled {
  664. t.Fatalf("HandleEndPoint unexpectedly handled request")
  665. }
  666. // TODO: test replay attack defence
  667. // TODO: test Server.Validate with invalid tactics configurations
  668. }
  669. func TestTacticsFilterGeoIPScope(t *testing.T) {
  670. encodedRequestPublicKey, encodedRequestPrivateKey, encodedObfuscatedKey, err := GenerateKeys()
  671. if err != nil {
  672. t.Fatalf("GenerateKeys failed: %s", err)
  673. }
  674. // Exercise specifying keys in NewServer instead of config file.
  675. tacticsConfigTemplate := `
  676. {
  677. "DefaultTactics" : {
  678. "TTL" : "60s"
  679. },
  680. %s
  681. }
  682. `
  683. // Test: region-only scope
  684. filteredTactics := `
  685. "FilteredTactics" : [
  686. {
  687. "Filter" : {
  688. "Regions": ["R1", "R2", "R3"]
  689. }
  690. },
  691. {
  692. "Filter" : {
  693. "Regions": ["R4", "R5", "R6"]
  694. }
  695. }
  696. ]
  697. `
  698. tacticsConfig := fmt.Sprintf(tacticsConfigTemplate, filteredTactics)
  699. file, err := ioutil.TempFile("", "tactics.config")
  700. if err != nil {
  701. t.Fatalf("TempFile create failed: %s", err)
  702. }
  703. _, err = file.Write([]byte(tacticsConfig))
  704. if err != nil {
  705. t.Fatalf("TempFile write failed: %s", err)
  706. }
  707. file.Close()
  708. configFileName := file.Name()
  709. defer os.Remove(configFileName)
  710. server, err := NewServer(
  711. nil,
  712. nil,
  713. nil,
  714. configFileName,
  715. encodedRequestPublicKey,
  716. encodedRequestPrivateKey,
  717. encodedObfuscatedKey)
  718. if err != nil {
  719. t.Fatalf("NewServer failed: %s", err)
  720. }
  721. reload := func() {
  722. tacticsConfig = fmt.Sprintf(tacticsConfigTemplate, filteredTactics)
  723. err = ioutil.WriteFile(configFileName, []byte(tacticsConfig), 0600)
  724. if err != nil {
  725. t.Fatalf("WriteFile failed: %s", err)
  726. }
  727. reloaded, err := server.Reload()
  728. if err != nil {
  729. t.Fatalf("Reload failed: %s", err)
  730. }
  731. if !reloaded {
  732. t.Fatalf("Server config failed to reload")
  733. }
  734. }
  735. geoIPData := common.GeoIPData{
  736. Country: "R0",
  737. ISP: "I0",
  738. ASN: "0",
  739. City: "C0",
  740. }
  741. scope := server.GetFilterGeoIPScope(geoIPData)
  742. if scope != GeoIPScopeRegion {
  743. t.Fatalf("unexpected scope: %b", scope)
  744. }
  745. // Test: ISP-only scope
  746. filteredTactics = `
  747. "FilteredTactics" : [
  748. {
  749. "Filter" : {
  750. "ISPs": ["I1", "I2", "I3"]
  751. }
  752. },
  753. {
  754. "Filter" : {
  755. "ISPs": ["I4", "I5", "I6"]
  756. }
  757. }
  758. ]
  759. `
  760. reload()
  761. scope = server.GetFilterGeoIPScope(geoIPData)
  762. if scope != GeoIPScopeISP {
  763. t.Fatalf("unexpected scope: %b", scope)
  764. }
  765. // Test: ASN-only scope
  766. filteredTactics = `
  767. "FilteredTactics" : [
  768. {
  769. "Filter" : {
  770. "ASNs": ["1", "2", "3"]
  771. }
  772. },
  773. {
  774. "Filter" : {
  775. "ASNs": ["4", "5", "6"]
  776. }
  777. }
  778. ]
  779. `
  780. reload()
  781. scope = server.GetFilterGeoIPScope(geoIPData)
  782. if scope != GeoIPScopeASN {
  783. t.Fatalf("unexpected scope: %b", scope)
  784. }
  785. // Test: City-only scope
  786. filteredTactics = `
  787. "FilteredTactics" : [
  788. {
  789. "Filter" : {
  790. "Cities": ["C1", "C2", "C3"]
  791. }
  792. },
  793. {
  794. "Filter" : {
  795. "Cities": ["C4", "C5", "C6"]
  796. }
  797. }
  798. ]
  799. `
  800. reload()
  801. scope = server.GetFilterGeoIPScope(geoIPData)
  802. if scope != GeoIPScopeCity {
  803. t.Fatalf("unexpected scope: %b", scope)
  804. }
  805. // Test: full scope
  806. filteredTactics = `
  807. "FilteredTactics" : [
  808. {
  809. "Filter" : {
  810. "Regions": ["R1", "R2", "R3"]
  811. }
  812. },
  813. {
  814. "Filter" : {
  815. "ISPs": ["I1", "I2", "I3"]
  816. }
  817. },
  818. {
  819. "Filter" : {
  820. "ASNs": ["1", "2", "3"]
  821. }
  822. },
  823. {
  824. "Filter" : {
  825. "Cities": ["C4", "C5", "C6"]
  826. }
  827. }
  828. ]
  829. `
  830. reload()
  831. scope = server.GetFilterGeoIPScope(geoIPData)
  832. if scope != GeoIPScopeRegion|GeoIPScopeISP|GeoIPScopeASN|GeoIPScopeCity {
  833. t.Fatalf("unexpected scope: %b", scope)
  834. }
  835. // Test: conditional scopes
  836. filteredTactics = `
  837. "FilteredTactics" : [
  838. {
  839. "Filter" : {
  840. "Regions": ["R1"]
  841. }
  842. },
  843. {
  844. "Filter" : {
  845. "Regions": ["R2"],
  846. "ISPs": ["I2a"]
  847. }
  848. },
  849. {
  850. "Filter" : {
  851. "Regions": ["R2"],
  852. "ISPs": ["I2b"]
  853. }
  854. },
  855. {
  856. "Filter" : {
  857. "Regions": ["R3"],
  858. "ISPs": ["I3a"],
  859. "Cities": ["C3a"]
  860. }
  861. },
  862. {
  863. "Filter" : {
  864. "Regions": ["R3"],
  865. "ISPs": ["I3b"],
  866. "Cities": ["C3b"]
  867. }
  868. },
  869. {
  870. "Filter" : {
  871. "Regions": ["R4"],
  872. "ASNs": ["4"]
  873. }
  874. },
  875. {
  876. "Filter" : {
  877. "Regions": ["R4"],
  878. "ASNs": ["4"]
  879. }
  880. },
  881. {
  882. "Filter" : {
  883. "Regions": ["R5"],
  884. "ASNs": ["5"],
  885. "Cities": ["C3a"]
  886. }
  887. },
  888. {
  889. "Filter" : {
  890. "Regions": ["R5"],
  891. "ASNs": ["5"],
  892. "Cities": ["C3b"]
  893. }
  894. }
  895. ]
  896. `
  897. reload()
  898. scope = server.GetFilterGeoIPScope(common.GeoIPData{Country: "R0"})
  899. if scope != GeoIPScopeRegion {
  900. t.Fatalf("unexpected scope: %b", scope)
  901. }
  902. scope = server.GetFilterGeoIPScope(common.GeoIPData{Country: "R1"})
  903. if scope != GeoIPScopeRegion {
  904. t.Fatalf("unexpected scope: %b", scope)
  905. }
  906. scope = server.GetFilterGeoIPScope(common.GeoIPData{Country: "R2"})
  907. if scope != GeoIPScopeRegion|GeoIPScopeISP {
  908. t.Fatalf("unexpected scope: %b", scope)
  909. }
  910. scope = server.GetFilterGeoIPScope(common.GeoIPData{Country: "R3"})
  911. if scope != GeoIPScopeRegion|GeoIPScopeISP|GeoIPScopeCity {
  912. t.Fatalf("unexpected scope: %b", scope)
  913. }
  914. scope = server.GetFilterGeoIPScope(common.GeoIPData{Country: "R4"})
  915. if scope != GeoIPScopeRegion|GeoIPScopeASN {
  916. t.Fatalf("unexpected scope: %b", scope)
  917. }
  918. scope = server.GetFilterGeoIPScope(common.GeoIPData{Country: "R5"})
  919. if scope != GeoIPScopeRegion|GeoIPScopeASN|GeoIPScopeCity {
  920. t.Fatalf("unexpected scope: %b", scope)
  921. }
  922. // Test: reset regional map optimization
  923. filteredTactics = `
  924. "FilteredTactics" : [
  925. {
  926. "Filter" : {
  927. "Regions": ["R1"],
  928. "ISPs": ["I1"]
  929. }
  930. },
  931. {
  932. "Filter" : {
  933. "Cities": ["C1"]
  934. }
  935. }
  936. ]
  937. `
  938. reload()
  939. scope = server.GetFilterGeoIPScope(common.GeoIPData{Country: "R0"})
  940. if scope != GeoIPScopeRegion|GeoIPScopeISP|GeoIPScopeCity {
  941. t.Fatalf("unexpected scope: %b", scope)
  942. }
  943. filteredTactics = `
  944. "FilteredTactics" : [
  945. {
  946. "Filter" : {
  947. "Regions": ["R1"],
  948. "Cities": ["C1"]
  949. }
  950. },
  951. {
  952. "Filter" : {
  953. "ISPs": ["I1"]
  954. }
  955. }
  956. ]
  957. `
  958. reload()
  959. scope = server.GetFilterGeoIPScope(common.GeoIPData{Country: "R0"})
  960. if scope != GeoIPScopeRegion|GeoIPScopeISP|GeoIPScopeCity {
  961. t.Fatalf("unexpected scope: %b", scope)
  962. }
  963. }
  964. type testStorer struct {
  965. tacticsRecords map[string][]byte
  966. speedTestSampleRecords map[string][]byte
  967. }
  968. func newTestStorer() *testStorer {
  969. return &testStorer{
  970. tacticsRecords: make(map[string][]byte),
  971. speedTestSampleRecords: make(map[string][]byte),
  972. }
  973. }
  974. func (s *testStorer) SetTacticsRecord(networkID string, record []byte) error {
  975. s.tacticsRecords[networkID] = record
  976. return nil
  977. }
  978. func (s *testStorer) GetTacticsRecord(networkID string) ([]byte, error) {
  979. return s.tacticsRecords[networkID], nil
  980. }
  981. func (s *testStorer) SetSpeedTestSamplesRecord(networkID string, record []byte) error {
  982. s.speedTestSampleRecords[networkID] = record
  983. return nil
  984. }
  985. func (s *testStorer) GetSpeedTestSamplesRecord(networkID string) ([]byte, error) {
  986. return s.speedTestSampleRecords[networkID], nil
  987. }