tactics_test.go 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. /*
  2. * Copyright (c) 2020, 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 server
  20. import (
  21. "fmt"
  22. "io/ioutil"
  23. "path/filepath"
  24. "testing"
  25. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
  26. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/tactics"
  27. )
  28. func TestServerTacticsParametersCache(t *testing.T) {
  29. tacticsConfigJSONFormat := `
  30. {
  31. "RequestPublicKey" : "%s",
  32. "RequestPrivateKey" : "%s",
  33. "RequestObfuscatedKey" : "%s",
  34. "DefaultTactics" : {
  35. "TTL" : "60s",
  36. "Probability" : 1.0,
  37. "Parameters" : {
  38. "ConnectionWorkerPoolSize" : 1
  39. }
  40. },
  41. "FilteredTactics" : [
  42. {
  43. "Filter" : {
  44. "Regions": ["R1"]
  45. },
  46. "Tactics" : {
  47. "Parameters" : {
  48. "ConnectionWorkerPoolSize" : 2
  49. }
  50. }
  51. },
  52. {
  53. "Filter" : {
  54. "Regions": ["R2"],
  55. "ISPs": ["I2a"]
  56. },
  57. "Tactics" : {
  58. "Parameters" : {
  59. "ConnectionWorkerPoolSize" : 3
  60. }
  61. }
  62. },
  63. {
  64. "Filter" : {
  65. "Regions": ["R2"],
  66. "ISPs": ["I2b"]
  67. },
  68. "Tactics" : {
  69. "Parameters" : {
  70. "ConnectionWorkerPoolSize" : 4
  71. }
  72. }
  73. },
  74. {
  75. "Filter" : {
  76. "Regions": ["R2"],
  77. "ISPs": ["I2c"]
  78. },
  79. "Tactics" : {
  80. "Parameters" : {
  81. "ConnectionWorkerPoolSize" : 4
  82. }
  83. }
  84. },
  85. {
  86. "Filter" : {
  87. "Regions": ["R3"],
  88. "ASNs": ["31"]
  89. },
  90. "Tactics" : {
  91. "Parameters" : {
  92. "ConnectionWorkerPoolSize" : 5
  93. }
  94. }
  95. },
  96. {
  97. "Filter" : {
  98. "Regions": ["R3"],
  99. "ASNs": ["32"]
  100. },
  101. "Tactics" : {
  102. "Parameters" : {
  103. "ConnectionWorkerPoolSize" : 6
  104. }
  105. }
  106. },
  107. {
  108. "Filter" : {
  109. "Regions": ["R3"],
  110. "ASNs": ["33"]
  111. },
  112. "Tactics" : {
  113. "Parameters" : {
  114. "ConnectionWorkerPoolSize" : 6
  115. }
  116. }
  117. }
  118. ]
  119. }
  120. `
  121. tacticsRequestPublicKey, tacticsRequestPrivateKey, tacticsRequestObfuscatedKey, err :=
  122. tactics.GenerateKeys()
  123. if err != nil {
  124. t.Fatalf("error generating tactics keys: %s", err)
  125. }
  126. tacticsConfigJSON := fmt.Sprintf(
  127. tacticsConfigJSONFormat,
  128. tacticsRequestPublicKey, tacticsRequestPrivateKey, tacticsRequestObfuscatedKey)
  129. tacticsConfigFilename := filepath.Join(testDataDirName, "tactics_config.json")
  130. err = ioutil.WriteFile(tacticsConfigFilename, []byte(tacticsConfigJSON), 0600)
  131. if err != nil {
  132. t.Fatalf("error paving tactics config file: %s", err)
  133. }
  134. tacticsServer, err := tactics.NewServer(
  135. nil,
  136. nil,
  137. nil,
  138. tacticsConfigFilename)
  139. if err != nil {
  140. t.Fatalf("NewServer failed: %s", err)
  141. }
  142. support := &SupportServices{
  143. TacticsServer: tacticsServer,
  144. }
  145. support.ReplayCache = NewReplayCache(support)
  146. support.ServerTacticsParametersCache =
  147. NewServerTacticsParametersCache(support)
  148. keySplitTestCases := []struct {
  149. description string
  150. geoIPData GeoIPData
  151. expectedConnectionWorkerPoolSize int
  152. expectedCacheSizeBefore int
  153. expectedCacheSizeAfter int
  154. expectedParameterReferencesSizeAfter int
  155. }{
  156. {
  157. "add new cache entry, default parameter",
  158. GeoIPData{Country: "R0", ISP: "I0", City: "C0"},
  159. 1,
  160. 0, 1, 1,
  161. },
  162. {
  163. "region already cached, region-only key",
  164. GeoIPData{Country: "R0", ISP: "I1", City: "C1"},
  165. 1,
  166. 1, 1, 1,
  167. },
  168. {
  169. "add new cache entry, filtered parameter",
  170. GeoIPData{Country: "R1", ISP: "I1a", City: "C1a"},
  171. 2,
  172. 1, 2, 2,
  173. },
  174. {
  175. "region already cached, region-only key",
  176. GeoIPData{Country: "R1", ISP: "I1a", City: "C1a"},
  177. 2,
  178. 2, 2, 2,
  179. },
  180. {
  181. "region already cached, region-only key",
  182. GeoIPData{Country: "R1", ISP: "I1b", City: "C1b"},
  183. 2,
  184. 2, 2, 2,
  185. },
  186. {
  187. "region already cached, region-only key",
  188. GeoIPData{Country: "R1", ISP: "I1b", City: "C1c"},
  189. 2,
  190. 2, 2, 2,
  191. },
  192. {
  193. "add new cache entry, filtered parameter, region/ISP key",
  194. GeoIPData{Country: "R2", ISP: "I2a", City: "C2a"},
  195. 3,
  196. 2, 3, 3,
  197. },
  198. {
  199. "region/ISP already cached",
  200. GeoIPData{Country: "R2", ISP: "I2a", City: "C2a"},
  201. 3,
  202. 3, 3, 3,
  203. },
  204. {
  205. "region/ISP already cached, city is ignored",
  206. GeoIPData{Country: "R2", ISP: "I2a", City: "C2b"},
  207. 3,
  208. 3, 3, 3,
  209. },
  210. {
  211. "add new cache entry, filtered parameter, region/ISP key",
  212. GeoIPData{Country: "R2", ISP: "I2b", City: "C2a"},
  213. 4,
  214. 3, 4, 4,
  215. },
  216. {
  217. "region/ISP already cached, city is ignored",
  218. GeoIPData{Country: "R2", ISP: "I2b", City: "C2b"},
  219. 4,
  220. 4, 4, 4,
  221. },
  222. {
  223. "add new cache entry, filtered parameter, region/ISP key, duplicate parameters",
  224. GeoIPData{Country: "R2", ISP: "I2c", City: "C2a"},
  225. 4,
  226. 4, 5, 4,
  227. },
  228. {
  229. "region already cached, region-only key",
  230. GeoIPData{Country: "R0", ASN: "0", City: "C1"},
  231. 1,
  232. 5, 5, 4,
  233. },
  234. {
  235. "region already cached, region-only key",
  236. GeoIPData{Country: "R1", ASN: "1", City: "C1a"},
  237. 2,
  238. 5, 5, 4,
  239. },
  240. {
  241. "add new cache entry, filtered parameter, region/ASN key",
  242. GeoIPData{Country: "R3", ASN: "31", City: "C2a"},
  243. 5,
  244. 5, 6, 5,
  245. },
  246. {
  247. "region/ASN already cached",
  248. GeoIPData{Country: "R3", ASN: "31", City: "C2a"},
  249. 5,
  250. 6, 6, 5,
  251. },
  252. {
  253. "region/ASN already cached, city is ignored",
  254. GeoIPData{Country: "R3", ASN: "31", City: "C2b"},
  255. 5,
  256. 6, 6, 5,
  257. },
  258. {
  259. "add new cache entry, filtered parameter, region/ASN key",
  260. GeoIPData{Country: "R3", ASN: "32", City: "C2a"},
  261. 6,
  262. 6, 7, 6,
  263. },
  264. {
  265. "region/ASN already cached, city is ignored",
  266. GeoIPData{Country: "R3", ASN: "32", City: "C2b"},
  267. 6,
  268. 7, 7, 6,
  269. },
  270. {
  271. "add new cache entry, filtered parameter, region/ASN key, duplicate parameters",
  272. GeoIPData{Country: "R3", ASN: "33", City: "C2a"},
  273. 6,
  274. 7, 8, 6,
  275. },
  276. }
  277. for _, testCase := range keySplitTestCases {
  278. t.Run(testCase.description, func(t *testing.T) {
  279. support.ServerTacticsParametersCache.mutex.Lock()
  280. cacheSize := support.ServerTacticsParametersCache.tacticsCache.Len()
  281. support.ServerTacticsParametersCache.mutex.Unlock()
  282. if cacheSize != testCase.expectedCacheSizeBefore {
  283. t.Fatalf("unexpected tacticsCache size before lookup: %d", cacheSize)
  284. }
  285. p, err := support.ServerTacticsParametersCache.Get(testCase.geoIPData)
  286. if err != nil {
  287. t.Fatalf("ServerTacticsParametersCache.Get failed: %d", err)
  288. }
  289. connectionWorkerPoolSize := p.Int(parameters.ConnectionWorkerPoolSize)
  290. if connectionWorkerPoolSize != testCase.expectedConnectionWorkerPoolSize {
  291. t.Fatalf("unexpected ConnectionWorkerPoolSize value: %d", connectionWorkerPoolSize)
  292. }
  293. support.ServerTacticsParametersCache.mutex.Lock()
  294. cacheSize = support.ServerTacticsParametersCache.tacticsCache.Len()
  295. support.ServerTacticsParametersCache.mutex.Unlock()
  296. if cacheSize != testCase.expectedCacheSizeAfter {
  297. t.Fatalf("unexpected cache size after lookup: %d", cacheSize)
  298. }
  299. support.ServerTacticsParametersCache.mutex.Lock()
  300. paramRefsSize := len(support.ServerTacticsParametersCache.parameterReferences)
  301. support.ServerTacticsParametersCache.mutex.Unlock()
  302. if paramRefsSize != testCase.expectedParameterReferencesSizeAfter {
  303. t.Fatalf("unexpected parameterReferences size after lookup: %d", paramRefsSize)
  304. }
  305. })
  306. }
  307. metrics := support.ServerTacticsParametersCache.GetMetrics()
  308. if metrics["server_tactics_max_cache_entries"].(int64) != 8 ||
  309. metrics["server_tactics_max_parameter_references"].(int64) != 6 ||
  310. metrics["server_tactics_cache_hit_count"].(int64) != 12 ||
  311. metrics["server_tactics_cache_miss_count"].(int64) != 8 {
  312. t.Fatalf("unexpected metrics: %v", metrics)
  313. }
  314. // Test: force eviction and check parameterReferences cleanup.
  315. for i := 0; i < TACTICS_CACHE_MAX_ENTRIES*2; i++ {
  316. _, err := support.ServerTacticsParametersCache.Get(
  317. GeoIPData{Country: "R2", ISP: fmt.Sprintf("I-%d", i), City: "C2a"})
  318. if err != nil {
  319. t.Fatalf("ServerTacticsParametersCache.Get failed: %d", err)
  320. }
  321. }
  322. support.ServerTacticsParametersCache.mutex.Lock()
  323. cacheSize := support.ServerTacticsParametersCache.tacticsCache.Len()
  324. paramRefsSize := len(support.ServerTacticsParametersCache.parameterReferences)
  325. support.ServerTacticsParametersCache.mutex.Unlock()
  326. if cacheSize != TACTICS_CACHE_MAX_ENTRIES {
  327. t.Fatalf("unexpected tacticsCache size before lookup: %d", cacheSize)
  328. }
  329. if paramRefsSize != 1 {
  330. t.Fatalf("unexpected parameterReferences size after lookup: %d", paramRefsSize)
  331. }
  332. }