tactics_test.go 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  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, nil, nil, tacticsConfigFilename, "", "", "")
  136. if err != nil {
  137. t.Fatalf("NewServer failed: %s", err)
  138. }
  139. support := &SupportServices{
  140. TacticsServer: tacticsServer,
  141. }
  142. support.ReplayCache = NewReplayCache(support)
  143. support.ServerTacticsParametersCache =
  144. NewServerTacticsParametersCache(support)
  145. keySplitTestCases := []struct {
  146. description string
  147. geoIPData GeoIPData
  148. expectedConnectionWorkerPoolSize int
  149. expectedCacheSizeBefore int
  150. expectedCacheSizeAfter int
  151. expectedParameterReferencesSizeAfter int
  152. }{
  153. {
  154. "add new cache entry, default parameter",
  155. GeoIPData{Country: "R0", ISP: "I0", City: "C0"},
  156. 1,
  157. 0, 1, 1,
  158. },
  159. {
  160. "region already cached, region-only key",
  161. GeoIPData{Country: "R0", ISP: "I1", City: "C1"},
  162. 1,
  163. 1, 1, 1,
  164. },
  165. {
  166. "add new cache entry, filtered parameter",
  167. GeoIPData{Country: "R1", ISP: "I1a", City: "C1a"},
  168. 2,
  169. 1, 2, 2,
  170. },
  171. {
  172. "region already cached, region-only key",
  173. GeoIPData{Country: "R1", ISP: "I1a", City: "C1a"},
  174. 2,
  175. 2, 2, 2,
  176. },
  177. {
  178. "region already cached, region-only key",
  179. GeoIPData{Country: "R1", ISP: "I1b", City: "C1b"},
  180. 2,
  181. 2, 2, 2,
  182. },
  183. {
  184. "region already cached, region-only key",
  185. GeoIPData{Country: "R1", ISP: "I1b", City: "C1c"},
  186. 2,
  187. 2, 2, 2,
  188. },
  189. {
  190. "add new cache entry, filtered parameter, region/ISP key",
  191. GeoIPData{Country: "R2", ISP: "I2a", City: "C2a"},
  192. 3,
  193. 2, 3, 3,
  194. },
  195. {
  196. "region/ISP already cached",
  197. GeoIPData{Country: "R2", ISP: "I2a", City: "C2a"},
  198. 3,
  199. 3, 3, 3,
  200. },
  201. {
  202. "region/ISP already cached, city is ignored",
  203. GeoIPData{Country: "R2", ISP: "I2a", City: "C2b"},
  204. 3,
  205. 3, 3, 3,
  206. },
  207. {
  208. "add new cache entry, filtered parameter, region/ISP key",
  209. GeoIPData{Country: "R2", ISP: "I2b", City: "C2a"},
  210. 4,
  211. 3, 4, 4,
  212. },
  213. {
  214. "region/ISP already cached, city is ignored",
  215. GeoIPData{Country: "R2", ISP: "I2b", City: "C2b"},
  216. 4,
  217. 4, 4, 4,
  218. },
  219. {
  220. "add new cache entry, filtered parameter, region/ISP key, duplicate parameters",
  221. GeoIPData{Country: "R2", ISP: "I2c", City: "C2a"},
  222. 4,
  223. 4, 5, 4,
  224. },
  225. {
  226. "region already cached, region-only key",
  227. GeoIPData{Country: "R0", ASN: "0", City: "C1"},
  228. 1,
  229. 5, 5, 4,
  230. },
  231. {
  232. "region already cached, region-only key",
  233. GeoIPData{Country: "R1", ASN: "1", City: "C1a"},
  234. 2,
  235. 5, 5, 4,
  236. },
  237. {
  238. "add new cache entry, filtered parameter, region/ASN key",
  239. GeoIPData{Country: "R3", ASN: "31", City: "C2a"},
  240. 5,
  241. 5, 6, 5,
  242. },
  243. {
  244. "region/ASN already cached",
  245. GeoIPData{Country: "R3", ASN: "31", City: "C2a"},
  246. 5,
  247. 6, 6, 5,
  248. },
  249. {
  250. "region/ASN already cached, city is ignored",
  251. GeoIPData{Country: "R3", ASN: "31", City: "C2b"},
  252. 5,
  253. 6, 6, 5,
  254. },
  255. {
  256. "add new cache entry, filtered parameter, region/ASN key",
  257. GeoIPData{Country: "R3", ASN: "32", City: "C2a"},
  258. 6,
  259. 6, 7, 6,
  260. },
  261. {
  262. "region/ASN already cached, city is ignored",
  263. GeoIPData{Country: "R3", ASN: "32", City: "C2b"},
  264. 6,
  265. 7, 7, 6,
  266. },
  267. {
  268. "add new cache entry, filtered parameter, region/ASN key, duplicate parameters",
  269. GeoIPData{Country: "R3", ASN: "33", City: "C2a"},
  270. 6,
  271. 7, 8, 6,
  272. },
  273. }
  274. for _, testCase := range keySplitTestCases {
  275. t.Run(testCase.description, func(t *testing.T) {
  276. support.ServerTacticsParametersCache.mutex.Lock()
  277. cacheSize := support.ServerTacticsParametersCache.tacticsCache.Len()
  278. support.ServerTacticsParametersCache.mutex.Unlock()
  279. if cacheSize != testCase.expectedCacheSizeBefore {
  280. t.Fatalf("unexpected tacticsCache size before lookup: %d", cacheSize)
  281. }
  282. p, err := support.ServerTacticsParametersCache.Get(testCase.geoIPData)
  283. if err != nil {
  284. t.Fatalf("ServerTacticsParametersCache.Get failed: %d", err)
  285. }
  286. connectionWorkerPoolSize := p.Int(parameters.ConnectionWorkerPoolSize)
  287. if connectionWorkerPoolSize != testCase.expectedConnectionWorkerPoolSize {
  288. t.Fatalf("unexpected ConnectionWorkerPoolSize value: %d", connectionWorkerPoolSize)
  289. }
  290. support.ServerTacticsParametersCache.mutex.Lock()
  291. cacheSize = support.ServerTacticsParametersCache.tacticsCache.Len()
  292. support.ServerTacticsParametersCache.mutex.Unlock()
  293. if cacheSize != testCase.expectedCacheSizeAfter {
  294. t.Fatalf("unexpected cache size after lookup: %d", cacheSize)
  295. }
  296. support.ServerTacticsParametersCache.mutex.Lock()
  297. paramRefsSize := len(support.ServerTacticsParametersCache.parameterReferences)
  298. support.ServerTacticsParametersCache.mutex.Unlock()
  299. if paramRefsSize != testCase.expectedParameterReferencesSizeAfter {
  300. t.Fatalf("unexpected parameterReferences size after lookup: %d", paramRefsSize)
  301. }
  302. })
  303. }
  304. metrics := support.ServerTacticsParametersCache.GetMetrics()
  305. if metrics["server_tactics_max_cache_entries"].(int64) != 8 ||
  306. metrics["server_tactics_max_parameter_references"].(int64) != 6 ||
  307. metrics["server_tactics_cache_hit_count"].(int64) != 12 ||
  308. metrics["server_tactics_cache_miss_count"].(int64) != 8 {
  309. t.Fatalf("unexpected metrics: %v", metrics)
  310. }
  311. // Test: force eviction and check parameterReferences cleanup.
  312. for i := 0; i < TACTICS_CACHE_MAX_ENTRIES*2; i++ {
  313. _, err := support.ServerTacticsParametersCache.Get(
  314. GeoIPData{Country: "R2", ISP: fmt.Sprintf("I-%d", i), City: "C2a"})
  315. if err != nil {
  316. t.Fatalf("ServerTacticsParametersCache.Get failed: %d", err)
  317. }
  318. }
  319. support.ServerTacticsParametersCache.mutex.Lock()
  320. cacheSize := support.ServerTacticsParametersCache.tacticsCache.Len()
  321. paramRefsSize := len(support.ServerTacticsParametersCache.parameterReferences)
  322. support.ServerTacticsParametersCache.mutex.Unlock()
  323. if cacheSize != TACTICS_CACHE_MAX_ENTRIES {
  324. t.Fatalf("unexpected tacticsCache size before lookup: %d", cacheSize)
  325. }
  326. if paramRefsSize != 1 {
  327. t.Fatalf("unexpected parameterReferences size after lookup: %d", paramRefsSize)
  328. }
  329. }