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