github.com/psiphon-labs/psiphon-tunnel-core@v2.0.28+incompatible/psiphon/server/tactics.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 "sync" 25 26 "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common" 27 "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors" 28 "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters" 29 "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/tactics" 30 "github.com/golang/groupcache/lru" 31 ) 32 33 const ( 34 TACTICS_CACHE_MAX_ENTRIES = 10000 35 ) 36 37 // ServerTacticsParametersCache is a cache of filtered server-side tactics, 38 // intended to speed-up frequent tactics lookups. 39 // 40 // Presently, the cache is targeted at pre-handshake lookups which are both 41 // the most time critical and have a low tactic cardinality, as only GeoIP 42 // filter inputs are available. 43 // 44 // There is no TTL for cache entries as the cached filtered tactics remain 45 // valid until the tactics config changes; Flush must be called on tactics 46 // config hot reloads. 47 type ServerTacticsParametersCache struct { 48 support *SupportServices 49 mutex sync.Mutex 50 tacticsCache *lru.Cache 51 parameterReferences map[string]*parameterReference 52 metrics *serverTacticsParametersCacheMetrics 53 } 54 55 type parameterReference struct { 56 params *parameters.Parameters 57 referenceCount int 58 } 59 60 type serverTacticsParametersCacheMetrics struct { 61 MaxCacheEntries int64 62 MaxParameterReferences int64 63 CacheHitCount int64 64 CacheMissCount int64 65 } 66 67 // NewServerTacticsParametersCache creates a new ServerTacticsParametersCache. 68 func NewServerTacticsParametersCache( 69 support *SupportServices) *ServerTacticsParametersCache { 70 71 cache := &ServerTacticsParametersCache{ 72 support: support, 73 tacticsCache: lru.New(TACTICS_CACHE_MAX_ENTRIES), 74 parameterReferences: make(map[string]*parameterReference), 75 metrics: &serverTacticsParametersCacheMetrics{}, 76 } 77 78 cache.tacticsCache.OnEvicted = cache.onEvicted 79 80 return cache 81 } 82 83 // GetMetrics returns a snapshop of current ServerTacticsParametersCache event 84 // counters and resets all counters to zero. 85 func (c *ServerTacticsParametersCache) GetMetrics() LogFields { 86 87 c.mutex.Lock() 88 defer c.mutex.Unlock() 89 90 logFields := LogFields{ 91 "server_tactics_max_cache_entries": c.metrics.MaxCacheEntries, 92 "server_tactics_max_parameter_references": c.metrics.MaxParameterReferences, 93 "server_tactics_cache_hit_count": c.metrics.CacheHitCount, 94 "server_tactics_cache_miss_count": c.metrics.CacheMissCount, 95 } 96 97 c.metrics = &serverTacticsParametersCacheMetrics{} 98 99 return logFields 100 } 101 102 // Get returns server-side tactics parameters for the specified GeoIP scope. 103 // Get is designed to be called before the API handshake and does not filter 104 // by API parameters. IsNil guards must be used when accessing the returned 105 // ParametersAccessor. 106 func (c *ServerTacticsParametersCache) Get( 107 geoIPData GeoIPData) (parameters.ParametersAccessor, error) { 108 109 c.mutex.Lock() 110 defer c.mutex.Unlock() 111 112 nilAccessor := parameters.MakeNilParametersAccessor() 113 114 key := c.makeKey(geoIPData) 115 116 // Check for cached result. 117 118 if tag, ok := c.tacticsCache.Get(key); ok { 119 paramRef, ok := c.parameterReferences[tag.(string)] 120 if !ok { 121 return nilAccessor, errors.TraceNew("missing parameters") 122 } 123 124 c.metrics.CacheHitCount += 1 125 126 // The returned accessor is read-only, and paramRef.params is never 127 // modified, so the return value is safe of concurrent use and may be 128 // references both while the entry remains in the cache or after it is 129 // evicted. 130 131 return paramRef.params.Get(), nil 132 } 133 134 c.metrics.CacheMissCount += 1 135 136 // Construct parameters from tactics. 137 138 tactics, tag, err := c.support.TacticsServer.GetTacticsWithTag( 139 true, common.GeoIPData(geoIPData), make(common.APIParameters)) 140 if err != nil { 141 return nilAccessor, errors.Trace(err) 142 } 143 144 if tactics == nil { 145 // This server isn't configured with tactics. 146 return nilAccessor, nil 147 } 148 149 // Tactics.Probability is ignored for server-side tactics. 150 151 params, err := parameters.NewParameters(nil) 152 if err != nil { 153 return nilAccessor, errors.Trace(err) 154 } 155 _, err = params.Set("", false, tactics.Parameters) 156 if err != nil { 157 return nilAccessor, errors.Trace(err) 158 } 159 160 // Update the cache. 161 // 162 // Two optimizations are used to limit the memory size of the cache: 163 // 164 // 1. The scope of the GeoIP data cache key is limited to the fields -- 165 // Country/ISP/ASN/City -- that are present in tactics filters. E.g., if only 166 // Country appears in filters, then the key will omit ISP, ASN, and City. 167 // 168 // 2. Two maps are maintained: GeoIP-key -> tactics-tag; and tactics-tag -> 169 // parameters. For N keys with the same filtered parameters, the mapped value 170 // overhead is N tags and 1 larger parameters data structure. 171 // 172 // If the cache is full, the LRU entry will be ejected. 173 174 // Update the parameterRefence _before_ calling Add: if Add happens to evict 175 // the last other entry referencing the same parameters, this order avoids an 176 // unnecessary delete/re-add. 177 178 paramRef, ok := c.parameterReferences[tag] 179 if !ok { 180 c.parameterReferences[tag] = ¶meterReference{ 181 params: params, 182 referenceCount: 1, 183 } 184 } else { 185 paramRef.referenceCount += 1 186 } 187 c.tacticsCache.Add(key, tag) 188 189 cacheSize := int64(c.tacticsCache.Len()) 190 if cacheSize > c.metrics.MaxCacheEntries { 191 c.metrics.MaxCacheEntries = cacheSize 192 } 193 194 paramRefsSize := int64(len(c.parameterReferences)) 195 if paramRefsSize > c.metrics.MaxParameterReferences { 196 c.metrics.MaxParameterReferences = paramRefsSize 197 } 198 199 return params.Get(), nil 200 } 201 202 func (c *ServerTacticsParametersCache) Flush() { 203 c.mutex.Lock() 204 defer c.mutex.Unlock() 205 206 // onEvicted will clear c.parameterReferences. 207 208 c.tacticsCache.Clear() 209 } 210 211 func (c *ServerTacticsParametersCache) onEvicted( 212 key lru.Key, value interface{}) { 213 214 // Cleanup unreferenced parameterReferences. Assumes mutex is held by Get, 215 // which calls Add, which may call onEvicted. 216 217 tag := value.(string) 218 219 paramRef, ok := c.parameterReferences[tag] 220 if !ok { 221 return 222 } 223 224 paramRef.referenceCount -= 1 225 if paramRef.referenceCount == 0 { 226 delete(c.parameterReferences, tag) 227 } 228 } 229 230 func (c *ServerTacticsParametersCache) makeKey(geoIPData GeoIPData) string { 231 232 scope := c.support.TacticsServer.GetFilterGeoIPScope( 233 common.GeoIPData(geoIPData)) 234 235 var region, ISP, ASN, city string 236 237 if scope&tactics.GeoIPScopeRegion != 0 { 238 region = geoIPData.Country 239 } 240 if scope&tactics.GeoIPScopeISP != 0 { 241 ISP = geoIPData.ISP 242 } 243 if scope&tactics.GeoIPScopeASN != 0 { 244 ASN = geoIPData.ASN 245 } 246 if scope&tactics.GeoIPScopeCity != 0 { 247 city = geoIPData.City 248 } 249 250 return fmt.Sprintf("%s-%s-%s-%s", region, ISP, ASN, city) 251 }