tactics.go 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  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. // Note: since ServerTacticsParametersCache was implemented,
  111. // tactics.Server.cachedTacticsData was added. That new cache is
  112. // primarily intended to reduce server allocations and computations
  113. // when _clients_ request tactics. cachedTacticsData also impacts
  114. // GetTacticsWithTag.
  115. //
  116. // ServerTacticsParametersCache still optimizes performance for
  117. // server-side tactics, since cachedTacticsData doesn't avoid filter
  118. // checks, and ServerTacticsParametersCache includes a prepared
  119. // parameters.ParametersAccessor.
  120. tactics, tag, err := c.support.TacticsServer.GetTacticsWithTag(
  121. true, common.GeoIPData(geoIPData), make(common.APIParameters))
  122. if err != nil {
  123. return nilAccessor, errors.Trace(err)
  124. }
  125. if tactics == nil {
  126. // This server isn't configured with tactics.
  127. return nilAccessor, nil
  128. }
  129. params, err := parameters.NewParameters(nil)
  130. if err != nil {
  131. return nilAccessor, errors.Trace(err)
  132. }
  133. _, err = params.Set(
  134. "", parameters.ValidationServerSide, tactics.Parameters)
  135. if err != nil {
  136. return nilAccessor, errors.Trace(err)
  137. }
  138. // Update the cache.
  139. //
  140. // Two optimizations are used to limit the memory size of the cache:
  141. //
  142. // 1. The scope of the GeoIP data cache key is limited to the fields --
  143. // Country/ISP/ASN/City -- that are present in tactics filters. E.g., if only
  144. // Country appears in filters, then the key will omit ISP, ASN, and City.
  145. //
  146. // 2. Two maps are maintained: GeoIP-key -> tactics-tag; and tactics-tag ->
  147. // parameters. For N keys with the same filtered parameters, the mapped value
  148. // overhead is N tags and 1 larger parameters data structure.
  149. //
  150. // If the cache is full, the LRU entry will be ejected.
  151. // Update the parameterRefence _before_ calling Add: if Add happens to evict
  152. // the last other entry referencing the same parameters, this order avoids an
  153. // unnecessary delete/re-add.
  154. paramRef, ok := c.parameterReferences[tag]
  155. if !ok {
  156. c.parameterReferences[tag] = &parameterReference{
  157. params: params,
  158. referenceCount: 1,
  159. }
  160. } else {
  161. paramRef.referenceCount += 1
  162. }
  163. c.tacticsCache.Add(key, tag)
  164. cacheSize := int64(c.tacticsCache.Len())
  165. if cacheSize > c.metrics.MaxCacheEntries {
  166. c.metrics.MaxCacheEntries = cacheSize
  167. }
  168. paramRefsSize := int64(len(c.parameterReferences))
  169. if paramRefsSize > c.metrics.MaxParameterReferences {
  170. c.metrics.MaxParameterReferences = paramRefsSize
  171. }
  172. return params.Get(), nil
  173. }
  174. func (c *ServerTacticsParametersCache) Flush() {
  175. c.mutex.Lock()
  176. defer c.mutex.Unlock()
  177. // onEvicted will clear c.parameterReferences.
  178. c.tacticsCache.Clear()
  179. }
  180. func (c *ServerTacticsParametersCache) onEvicted(
  181. key lru.Key, value interface{}) {
  182. // Cleanup unreferenced parameterReferences. Assumes mutex is held by Get,
  183. // which calls Add, which may call onEvicted.
  184. tag := value.(string)
  185. paramRef, ok := c.parameterReferences[tag]
  186. if !ok {
  187. return
  188. }
  189. paramRef.referenceCount -= 1
  190. if paramRef.referenceCount == 0 {
  191. delete(c.parameterReferences, tag)
  192. }
  193. }
  194. func (c *ServerTacticsParametersCache) makeKey(geoIPData GeoIPData) string {
  195. scope := c.support.TacticsServer.GetFilterGeoIPScope(
  196. common.GeoIPData(geoIPData))
  197. var region, ISP, ASN, city string
  198. if scope&tactics.GeoIPScopeRegion != 0 {
  199. region = geoIPData.Country
  200. }
  201. if scope&tactics.GeoIPScopeISP != 0 {
  202. ISP = geoIPData.ISP
  203. }
  204. if scope&tactics.GeoIPScopeASN != 0 {
  205. ASN = geoIPData.ASN
  206. }
  207. if scope&tactics.GeoIPScopeCity != 0 {
  208. city = geoIPData.City
  209. }
  210. return fmt.Sprintf("%s-%s-%s-%s", region, ISP, ASN, city)
  211. }