tactics.go 7.1 KB

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