github.com/psiphon-labs/psiphon-tunnel-core@v2.0.28+incompatible/psiphon/server/replay.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  	"time"
    26  
    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/prng"
    30  	lrucache "github.com/cognusion/go-cache-lru"
    31  )
    32  
    33  const (
    34  	REPLAY_CACHE_MAX_ENTRIES      = 100000
    35  	REPLAY_CACHE_CLEANUP_INTERVAL = 1 * time.Minute
    36  )
    37  
    38  // ReplayCache is a cache of recently used and successful network obfuscation
    39  // parameters that may be replayed -- reused -- for subsequent tunnel
    40  // connections.
    41  //
    42  // Server-side replay is analogous to client-side replay, with one key
    43  // difference: server-side replay can be applied across multiple clients in
    44  // the same GeoIP scope.
    45  //
    46  // Replay is enabled with tactics, and tactics determine the tunnel quality
    47  // targets for establishing and clearing replay parameters.
    48  //
    49  // ReplayCache has a maximum capacity with an LRU strategy to cap memory
    50  // overhead.
    51  type ReplayCache struct {
    52  	support    *SupportServices
    53  	cacheMutex sync.Mutex
    54  	cache      *lrucache.Cache
    55  	metrics    *replayCacheMetrics
    56  }
    57  
    58  type replayCacheMetrics struct {
    59  	MaxCacheEntries    int64
    60  	SetReplayCount     int64
    61  	GetReplayHitCount  int64
    62  	GetReplayMissCount int64
    63  	FailedReplayCount  int64
    64  	DeleteReplayCount  int64
    65  }
    66  
    67  type replayParameters struct {
    68  	replayPacketManipulation   bool
    69  	packetManipulationSpecName string
    70  	replayFragmentor           bool
    71  	fragmentorSeed             *prng.Seed
    72  	failedCount                int
    73  }
    74  
    75  // NewReplayCache creates a new ReplayCache.
    76  func NewReplayCache(support *SupportServices) *ReplayCache {
    77  	// Cache TTL may vary based on tactics filtering, so each cache.Add must set
    78  	// the entry TTL.
    79  	return &ReplayCache{
    80  		support: support,
    81  		cache: lrucache.NewWithLRU(
    82  			lrucache.NoExpiration,
    83  			REPLAY_CACHE_CLEANUP_INTERVAL,
    84  			REPLAY_CACHE_MAX_ENTRIES),
    85  		metrics: &replayCacheMetrics{},
    86  	}
    87  }
    88  
    89  // Flush clears all entries in the ReplayCache. Flush should be called when
    90  // tactics hot reload and change to clear any cached replay parameters that
    91  // may be based on stale tactics.
    92  func (r *ReplayCache) Flush() {
    93  
    94  	r.cacheMutex.Lock()
    95  	defer r.cacheMutex.Unlock()
    96  
    97  	r.cache.Flush()
    98  }
    99  
   100  // GetMetrics returns a snapshop of current ReplayCache event counters and
   101  // resets all counters to zero.
   102  func (r *ReplayCache) GetMetrics() LogFields {
   103  
   104  	r.cacheMutex.Lock()
   105  	defer r.cacheMutex.Unlock()
   106  
   107  	logFields := LogFields{
   108  		"replay_max_cache_entries":     r.metrics.MaxCacheEntries,
   109  		"replay_set_replay_count":      r.metrics.SetReplayCount,
   110  		"replay_get_replay_hit_count":  r.metrics.GetReplayHitCount,
   111  		"replay_get_replay_miss_count": r.metrics.GetReplayMissCount,
   112  		"replay_failed_replay_count":   r.metrics.FailedReplayCount,
   113  		"replay_delete_replay_count":   r.metrics.DeleteReplayCount,
   114  	}
   115  
   116  	r.metrics = &replayCacheMetrics{}
   117  
   118  	return logFields
   119  }
   120  
   121  // GetReplayTargetDuration returns the tactics replay target tunnel duration
   122  // for the specified GeoIP data. Tunnels which are active for the specified
   123  // duration are candidates for setting or extending replay parameters. Wait
   124  // for the returned wait duration before evaluating the tunnel duration. Once
   125  // this target is met, call SetReplayParameters, which will check additional
   126  // targets and conditionally set replay parameters.
   127  func (r *ReplayCache) GetReplayTargetDuration(
   128  	geoIPData GeoIPData) (bool, time.Duration, time.Duration) {
   129  
   130  	p, err := r.support.ServerTacticsParametersCache.Get(geoIPData)
   131  	if err != nil {
   132  		log.WithTraceFields(LogFields{"error": errors.Trace(err)}).Warning(
   133  			"ServerTacticsParametersCache.Get failed")
   134  		return false, 0, 0
   135  	}
   136  
   137  	if p.IsNil() {
   138  		// No tactics are configured; replay is disabled.
   139  		return false, 0, 0
   140  	}
   141  
   142  	if !p.Bool(parameters.ServerReplayUnknownGeoIP) &&
   143  		geoIPData.Country == GEOIP_UNKNOWN_VALUE &&
   144  		geoIPData.ASN == GEOIP_UNKNOWN_VALUE {
   145  		// Unless configured otherwise, skip replay for unknown GeoIP, since clients
   146  		// may not have equivilent network conditions.
   147  		return false, 0, 0
   148  	}
   149  
   150  	TTL := p.Duration(parameters.ServerReplayTTL)
   151  
   152  	if TTL == 0 {
   153  		// Server replay is disabled when TTL is 0.
   154  		return false, 0, 0
   155  	}
   156  
   157  	return true,
   158  		p.Duration(parameters.ServerReplayTargetWaitDuration),
   159  		p.Duration(parameters.ServerReplayTargetTunnelDuration)
   160  }
   161  
   162  // SetReplayParameters sets replay parameters, packetManipulationSpecName and
   163  // fragmentorSeed, for the specified tunnel protocol and GeoIP scope.
   164  // Once set, replay parameters are active for a tactics-configurable TTL.
   165  //
   166  // The specified tunneledBytesUp/Down must meet tactics replay bytes
   167  // transferred targets. SetReplayParameters should be called only after first
   168  // calling ReplayTargetDuration and ensuring the tunnel meets the active
   169  // tunnel duration target. When cached replay parameters exist, their TTL is
   170  // extended and any failure counts are reset to zero.
   171  //
   172  // SetReplayParameters must be called only once per tunnel. Extending replay
   173  // parameters TTL should only be done only immediately after a successful
   174  // tunnel dial and target achievement, as this is the part of a tunnel
   175  // lifecycle at highest risk of blocking.
   176  //
   177  // The value pointed to by fragmentorSeed must not be mutated after calling
   178  // SetReplayParameters.
   179  func (r *ReplayCache) SetReplayParameters(
   180  	tunnelProtocol string,
   181  	geoIPData GeoIPData,
   182  	packetManipulationSpecName string,
   183  	fragmentorSeed *prng.Seed,
   184  	tunneledBytesUp int64,
   185  	tunneledBytesDown int64) {
   186  
   187  	p, err := r.support.ServerTacticsParametersCache.Get(geoIPData)
   188  	if err != nil {
   189  		log.WithTraceFields(LogFields{"error": errors.Trace(err)}).Warning(
   190  			"ServerTacticsParametersCache.Get failed")
   191  		return
   192  	}
   193  
   194  	if p.IsNil() {
   195  		// No tactics are configured; replay is disabled.
   196  		return
   197  	}
   198  
   199  	TTL := p.Duration(parameters.ServerReplayTTL)
   200  
   201  	if TTL == 0 {
   202  		return
   203  	}
   204  
   205  	targetUpstreamBytes := p.Int(parameters.ServerReplayTargetUpstreamBytes)
   206  	targetDownstreamBytes := p.Int(parameters.ServerReplayTargetDownstreamBytes)
   207  
   208  	if tunneledBytesUp < int64(targetUpstreamBytes) {
   209  		return
   210  	}
   211  	if tunneledBytesDown < int64(targetDownstreamBytes) {
   212  		return
   213  	}
   214  
   215  	key := r.makeKey(tunnelProtocol, geoIPData)
   216  
   217  	value := &replayParameters{}
   218  
   219  	if p.Bool(parameters.ServerReplayPacketManipulation) {
   220  		value.replayPacketManipulation = true
   221  		value.packetManipulationSpecName = packetManipulationSpecName
   222  	}
   223  
   224  	if p.Bool(parameters.ServerReplayFragmentor) {
   225  		value.replayFragmentor = (fragmentorSeed != nil)
   226  		value.fragmentorSeed = fragmentorSeed
   227  	}
   228  
   229  	r.cacheMutex.Lock()
   230  	defer r.cacheMutex.Unlock()
   231  
   232  	r.cache.Add(key, value, TTL)
   233  
   234  	// go-cache-lru is typically safe for concurrent access but explicit
   235  	// synchronization is required when accessing Items. Items may include
   236  	// entries that are expired but not yet purged.
   237  	cacheSize := int64(len(r.cache.Items()))
   238  
   239  	if cacheSize > r.metrics.MaxCacheEntries {
   240  		r.metrics.MaxCacheEntries = cacheSize
   241  	}
   242  	r.metrics.SetReplayCount += 1
   243  }
   244  
   245  // GetReplayPacketManipulation returns an active replay packet manipulation
   246  // spec for the specified tunnel protocol and GeoIP scope.
   247  //
   248  // While Flush should be called to clear parameters based on stale tactics,
   249  // it's still possible for GetReplayPacketManipulation to return a spec name
   250  // that's no longer in the current list of known specs.
   251  func (r *ReplayCache) GetReplayPacketManipulation(
   252  	tunnelProtocol string,
   253  	geoIPData GeoIPData) (string, bool) {
   254  
   255  	r.cacheMutex.Lock()
   256  	defer r.cacheMutex.Unlock()
   257  
   258  	parameters, ok := r.getReplayParameters(
   259  		tunnelProtocol, geoIPData)
   260  	if !ok {
   261  		return "", false
   262  	}
   263  
   264  	if !parameters.replayPacketManipulation {
   265  		return "", false
   266  	}
   267  
   268  	return parameters.packetManipulationSpecName, true
   269  }
   270  
   271  // GetReplayFragmentor returns an active replay fragmentor seed for the
   272  // specified tunnel protocol and GeoIP scope.
   273  func (r *ReplayCache) GetReplayFragmentor(
   274  	tunnelProtocol string,
   275  	geoIPData GeoIPData) (*prng.Seed, bool) {
   276  
   277  	r.cacheMutex.Lock()
   278  	defer r.cacheMutex.Unlock()
   279  
   280  	parameters, ok := r.getReplayParameters(
   281  		tunnelProtocol, geoIPData)
   282  	if !ok {
   283  		return nil, false
   284  	}
   285  
   286  	if !parameters.replayFragmentor {
   287  		return nil, false
   288  	}
   289  
   290  	return parameters.fragmentorSeed, true
   291  }
   292  
   293  func (r *ReplayCache) getReplayParameters(
   294  	tunnelProtocol string,
   295  	geoIPData GeoIPData) (*replayParameters, bool) {
   296  
   297  	key := r.makeKey(tunnelProtocol, geoIPData)
   298  
   299  	value, ok := r.cache.Get(key)
   300  
   301  	if !ok {
   302  		r.metrics.GetReplayMissCount += 1
   303  		return nil, false
   304  	}
   305  
   306  	r.metrics.GetReplayHitCount += 1
   307  
   308  	parameters, ok := value.(*replayParameters)
   309  
   310  	return parameters, ok
   311  }
   312  
   313  // FailedReplayParameters increments the count of tunnels which failed to
   314  // complete any liveness test and API handshake after using replay parameters.
   315  // Once a failure threshold is reached, cached replay parameters are cleared.
   316  // Call this function for tunnels which meet the failure criteria.
   317  func (r *ReplayCache) FailedReplayParameters(
   318  	tunnelProtocol string,
   319  	geoIPData GeoIPData,
   320  	packetManipulationSpecName string,
   321  	fragmentorSeed *prng.Seed) {
   322  
   323  	p, err := r.support.ServerTacticsParametersCache.Get(geoIPData)
   324  	if err != nil {
   325  		log.WithTraceFields(LogFields{"error": errors.Trace(err)}).Warning(
   326  			"ServerTacticsParametersCache.Get failed")
   327  		return
   328  	}
   329  
   330  	thresholdFailedCount := p.Int(parameters.ServerReplayFailedCountThreshold)
   331  
   332  	key := r.makeKey(tunnelProtocol, geoIPData)
   333  
   334  	r.cacheMutex.Lock()
   335  	defer r.cacheMutex.Unlock()
   336  
   337  	parameters, ok := r.getReplayParameters(tunnelProtocol, geoIPData)
   338  	if !ok {
   339  		return
   340  	}
   341  
   342  	// Do not count the failure if the replay values for the tunnel protocol and
   343  	// GeoIP scope are now different; these parameters now reflect a newer,
   344  	// successful tunnel.
   345  
   346  	if (parameters.replayPacketManipulation &&
   347  		parameters.packetManipulationSpecName != packetManipulationSpecName) ||
   348  		(parameters.replayFragmentor &&
   349  			(fragmentorSeed == nil ||
   350  				*parameters.fragmentorSeed != *fragmentorSeed)) {
   351  		return
   352  	}
   353  
   354  	parameters.failedCount += 1
   355  	r.metrics.FailedReplayCount += 1
   356  
   357  	if thresholdFailedCount == 0 {
   358  		// No failure limit; the entry will not be deleted.
   359  		return
   360  	}
   361  
   362  	if parameters.failedCount >= thresholdFailedCount {
   363  		r.cache.Delete(key)
   364  		r.metrics.DeleteReplayCount += 1
   365  	}
   366  }
   367  
   368  func (r *ReplayCache) makeKey(
   369  	tunnelProtocol string, geoIPData GeoIPData) string {
   370  	return fmt.Sprintf(
   371  		"%s-%s-%s",
   372  		tunnelProtocol, geoIPData.Country, geoIPData.ASN)
   373  }