| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251 |
- /*
- * Copyright (c) 2020, Psiphon Inc.
- * All rights reserved.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
- */
- package server
- import (
- "fmt"
- "sync"
- "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
- "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
- "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
- "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/tactics"
- "github.com/golang/groupcache/lru"
- )
- const (
- TACTICS_CACHE_MAX_ENTRIES = 10000
- )
- // ServerTacticsParametersCache is a cache of filtered server-side tactics,
- // intended to speed-up frequent tactics lookups.
- //
- // Presently, the cache is targeted at pre-handshake lookups which are both
- // the most time critical and have a low tactic cardinality, as only GeoIP
- // filter inputs are available.
- //
- // There is no TTL for cache entries as the cached filtered tactics remain
- // valid until the tactics config changes; Flush must be called on tactics
- // config hot reloads.
- type ServerTacticsParametersCache struct {
- support *SupportServices
- mutex sync.Mutex
- tacticsCache *lru.Cache
- parameterReferences map[string]*parameterReference
- metrics *serverTacticsParametersCacheMetrics
- }
- type parameterReference struct {
- params *parameters.Parameters
- referenceCount int
- }
- type serverTacticsParametersCacheMetrics struct {
- MaxCacheEntries int64
- MaxParameterReferences int64
- CacheHitCount int64
- CacheMissCount int64
- }
- // NewServerTacticsParametersCache creates a new ServerTacticsParametersCache.
- func NewServerTacticsParametersCache(
- support *SupportServices) *ServerTacticsParametersCache {
- cache := &ServerTacticsParametersCache{
- support: support,
- tacticsCache: lru.New(TACTICS_CACHE_MAX_ENTRIES),
- parameterReferences: make(map[string]*parameterReference),
- metrics: &serverTacticsParametersCacheMetrics{},
- }
- cache.tacticsCache.OnEvicted = cache.onEvicted
- return cache
- }
- // GetMetrics returns a snapshop of current ServerTacticsParametersCache event
- // counters and resets all counters to zero.
- func (c *ServerTacticsParametersCache) GetMetrics() LogFields {
- c.mutex.Lock()
- defer c.mutex.Unlock()
- logFields := LogFields{
- "server_tactics_max_cache_entries": c.metrics.MaxCacheEntries,
- "server_tactics_max_parameter_references": c.metrics.MaxParameterReferences,
- "server_tactics_cache_hit_count": c.metrics.CacheHitCount,
- "server_tactics_cache_miss_count": c.metrics.CacheMissCount,
- }
- c.metrics = &serverTacticsParametersCacheMetrics{}
- return logFields
- }
- // Get returns server-side tactics parameters for the specified GeoIP scope.
- // Get is designed to be called before the API handshake and does not filter
- // by API parameters. IsNil guards must be used when accessing the returned
- // ParametersAccessor.
- func (c *ServerTacticsParametersCache) Get(
- geoIPData GeoIPData) (parameters.ParametersAccessor, error) {
- c.mutex.Lock()
- defer c.mutex.Unlock()
- nilAccessor := parameters.MakeNilParametersAccessor()
- key := c.makeKey(geoIPData)
- // Check for cached result.
- if tag, ok := c.tacticsCache.Get(key); ok {
- paramRef, ok := c.parameterReferences[tag.(string)]
- if !ok {
- return nilAccessor, errors.TraceNew("missing parameters")
- }
- c.metrics.CacheHitCount += 1
- // The returned accessor is read-only, and paramRef.params is never
- // modified, so the return value is safe of concurrent use and may be
- // references both while the entry remains in the cache or after it is
- // evicted.
- return paramRef.params.Get(), nil
- }
- c.metrics.CacheMissCount += 1
- // Construct parameters from tactics.
- tactics, tag, err := c.support.TacticsServer.GetTacticsWithTag(
- true, common.GeoIPData(geoIPData), make(common.APIParameters))
- if err != nil {
- return nilAccessor, errors.Trace(err)
- }
- if tactics == nil {
- // This server isn't configured with tactics.
- return nilAccessor, nil
- }
- // Tactics.Probability is ignored for server-side tactics.
- params, err := parameters.NewParameters(nil)
- if err != nil {
- return nilAccessor, errors.Trace(err)
- }
- _, err = params.Set("", false, tactics.Parameters)
- if err != nil {
- return nilAccessor, errors.Trace(err)
- }
- // Update the cache.
- //
- // Two optimizations are used to limit the memory size of the cache:
- //
- // 1. The scope of the GeoIP data cache key is limited to the fields --
- // Country/ISP/ASN/City -- that are present in tactics filters. E.g., if only
- // Country appears in filters, then the key will omit ISP, ASN, and City.
- //
- // 2. Two maps are maintained: GeoIP-key -> tactics-tag; and tactics-tag ->
- // parameters. For N keys with the same filtered parameters, the mapped value
- // overhead is N tags and 1 larger parameters data structure.
- //
- // If the cache is full, the LRU entry will be ejected.
- // Update the parameterRefence _before_ calling Add: if Add happens to evict
- // the last other entry referencing the same parameters, this order avoids an
- // unnecessary delete/re-add.
- paramRef, ok := c.parameterReferences[tag]
- if !ok {
- c.parameterReferences[tag] = ¶meterReference{
- params: params,
- referenceCount: 1,
- }
- } else {
- paramRef.referenceCount += 1
- }
- c.tacticsCache.Add(key, tag)
- cacheSize := int64(c.tacticsCache.Len())
- if cacheSize > c.metrics.MaxCacheEntries {
- c.metrics.MaxCacheEntries = cacheSize
- }
- paramRefsSize := int64(len(c.parameterReferences))
- if paramRefsSize > c.metrics.MaxParameterReferences {
- c.metrics.MaxParameterReferences = paramRefsSize
- }
- return params.Get(), nil
- }
- func (c *ServerTacticsParametersCache) Flush() {
- c.mutex.Lock()
- defer c.mutex.Unlock()
- // onEvicted will clear c.parameterReferences.
- c.tacticsCache.Clear()
- }
- func (c *ServerTacticsParametersCache) onEvicted(
- key lru.Key, value interface{}) {
- // Cleanup unreferenced parameterReferences. Assumes mutex is held by Get,
- // which calls Add, which may call onEvicted.
- tag := value.(string)
- paramRef, ok := c.parameterReferences[tag]
- if !ok {
- return
- }
- paramRef.referenceCount -= 1
- if paramRef.referenceCount == 0 {
- delete(c.parameterReferences, tag)
- }
- }
- func (c *ServerTacticsParametersCache) makeKey(geoIPData GeoIPData) string {
- scope := c.support.TacticsServer.GetFilterGeoIPScope(
- common.GeoIPData(geoIPData))
- var region, ISP, ASN, city string
- if scope&tactics.GeoIPScopeRegion != 0 {
- region = geoIPData.Country
- }
- if scope&tactics.GeoIPScopeISP != 0 {
- ISP = geoIPData.ISP
- }
- if scope&tactics.GeoIPScopeASN != 0 {
- ASN = geoIPData.ASN
- }
- if scope&tactics.GeoIPScopeCity != 0 {
- city = geoIPData.City
- }
- return fmt.Sprintf("%s-%s-%s-%s", region, ISP, ASN, city)
- }
|