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] = &parameterReference{
   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  }