tactics.go 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  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(
  125. "", parameters.ValidationServerSide, tactics.Parameters)
  126. if err != nil {
  127. return nilAccessor, errors.Trace(err)
  128. }
  129. // Update the cache.
  130. //
  131. // Two optimizations are used to limit the memory size of the cache:
  132. //
  133. // 1. The scope of the GeoIP data cache key is limited to the fields --
  134. // Country/ISP/ASN/City -- that are present in tactics filters. E.g., if only
  135. // Country appears in filters, then the key will omit ISP, ASN, and City.
  136. //
  137. // 2. Two maps are maintained: GeoIP-key -> tactics-tag; and tactics-tag ->
  138. // parameters. For N keys with the same filtered parameters, the mapped value
  139. // overhead is N tags and 1 larger parameters data structure.
  140. //
  141. // If the cache is full, the LRU entry will be ejected.
  142. // Update the parameterRefence _before_ calling Add: if Add happens to evict
  143. // the last other entry referencing the same parameters, this order avoids an
  144. // unnecessary delete/re-add.
  145. paramRef, ok := c.parameterReferences[tag]
  146. if !ok {
  147. c.parameterReferences[tag] = &parameterReference{
  148. params: params,
  149. referenceCount: 1,
  150. }
  151. } else {
  152. paramRef.referenceCount += 1
  153. }
  154. c.tacticsCache.Add(key, tag)
  155. cacheSize := int64(c.tacticsCache.Len())
  156. if cacheSize > c.metrics.MaxCacheEntries {
  157. c.metrics.MaxCacheEntries = cacheSize
  158. }
  159. paramRefsSize := int64(len(c.parameterReferences))
  160. if paramRefsSize > c.metrics.MaxParameterReferences {
  161. c.metrics.MaxParameterReferences = paramRefsSize
  162. }
  163. return params.Get(), nil
  164. }
  165. func (c *ServerTacticsParametersCache) Flush() {
  166. c.mutex.Lock()
  167. defer c.mutex.Unlock()
  168. // onEvicted will clear c.parameterReferences.
  169. c.tacticsCache.Clear()
  170. }
  171. func (c *ServerTacticsParametersCache) onEvicted(
  172. key lru.Key, value interface{}) {
  173. // Cleanup unreferenced parameterReferences. Assumes mutex is held by Get,
  174. // which calls Add, which may call onEvicted.
  175. tag := value.(string)
  176. paramRef, ok := c.parameterReferences[tag]
  177. if !ok {
  178. return
  179. }
  180. paramRef.referenceCount -= 1
  181. if paramRef.referenceCount == 0 {
  182. delete(c.parameterReferences, tag)
  183. }
  184. }
  185. func (c *ServerTacticsParametersCache) makeKey(geoIPData GeoIPData) string {
  186. scope := c.support.TacticsServer.GetFilterGeoIPScope(
  187. common.GeoIPData(geoIPData))
  188. var region, ISP, ASN, city string
  189. if scope&tactics.GeoIPScopeRegion != 0 {
  190. region = geoIPData.Country
  191. }
  192. if scope&tactics.GeoIPScopeISP != 0 {
  193. ISP = geoIPData.ISP
  194. }
  195. if scope&tactics.GeoIPScopeASN != 0 {
  196. ASN = geoIPData.ASN
  197. }
  198. if scope&tactics.GeoIPScopeCity != 0 {
  199. city = geoIPData.City
  200. }
  201. return fmt.Sprintf("%s-%s-%s-%s", region, ISP, ASN, city)
  202. }