github.com/psiphon-Labs/psiphon-tunnel-core@v2.0.28+incompatible/psiphon/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 psiphon
    21  
    22  import (
    23  	"context"
    24  	"time"
    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/prng"
    30  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
    31  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/tactics"
    32  )
    33  
    34  // GetTactics attempts to apply tactics, for the current network, to the given
    35  // config. GetTactics first checks for unexpired stored tactics, which it will
    36  // immediately return. If no unexpired stored tactics are found, tactics
    37  // requests are attempted until the input context is cancelled.
    38  //
    39  // Callers may pass in a context that is already done. In this case, stored
    40  // tactics, when available, are applied but no request will be attempted.
    41  //
    42  // Callers are responsible for ensuring that the input context eventually
    43  // cancels, and should synchronize GetTactics calls to ensure no unintended
    44  // concurrent fetch attempts occur.
    45  //
    46  // GetTactics implements a limited workaround for multiprocess datastore
    47  // synchronization, enabling, for example, SendFeedback in one process to
    48  // access tactics as long as a Controller is not running in another process;
    49  // and without blocking the Controller from starting. Accessing tactics is
    50  // most critical for untunneled network operations; when a Controller is
    51  // running, a tunnel may be used. See TacticsStorer for more details.
    52  func GetTactics(ctx context.Context, config *Config) {
    53  
    54  	// Limitation: GetNetworkID may not account for device VPN status, so
    55  	// Psiphon-over-Psiphon or Psiphon-over-other-VPN scenarios can encounter
    56  	// this issue:
    57  	//
    58  	// 1. Tactics are established when tunneling through a VPN and egressing
    59  	//    through a remote region/ISP.
    60  	// 2. Psiphon is next run when _not_ tunneling through the VPN. Yet the
    61  	//    network ID remains the same. Initial applied tactics will be for the
    62  	//    remote egress region/ISP, not the local region/ISP.
    63  
    64  	tacticsRecord, err := tactics.UseStoredTactics(
    65  		GetTacticsStorer(config),
    66  		config.GetNetworkID())
    67  	if err != nil {
    68  		NoticeWarning("get stored tactics failed: %s", err)
    69  
    70  		// The error will be due to a local datastore problem.
    71  		// While we could proceed with the tactics request, this
    72  		// could result in constant tactics requests. So, abort.
    73  		return
    74  	}
    75  
    76  	// If the context is already Done, don't even start the request.
    77  	if ctx.Err() != nil {
    78  		return
    79  	}
    80  
    81  	if tacticsRecord == nil {
    82  
    83  		iterator, err := NewTacticsServerEntryIterator(config)
    84  		if err != nil {
    85  			NoticeWarning("tactics iterator failed: %s", err)
    86  			return
    87  		}
    88  		defer iterator.Close()
    89  
    90  		noCapableServers := true
    91  
    92  		for iteration := 0; ; iteration++ {
    93  
    94  			if !WaitForNetworkConnectivity(
    95  				ctx, config.NetworkConnectivityChecker) {
    96  				return
    97  			}
    98  
    99  			serverEntry, err := iterator.Next()
   100  			if err != nil {
   101  				NoticeWarning("tactics iterator failed: %s", err)
   102  				return
   103  			}
   104  
   105  			if serverEntry == nil {
   106  				if noCapableServers {
   107  					// Abort when no capable servers have been found after
   108  					// a full iteration. Server entries that are skipped are
   109  					// classified as not capable.
   110  					NoticeWarning("tactics request aborted: no capable servers")
   111  					return
   112  				}
   113  
   114  				iterator.Reset()
   115  				continue
   116  			}
   117  
   118  			tacticsRecord, err = fetchTactics(
   119  				ctx, config, serverEntry)
   120  
   121  			if tacticsRecord != nil || err != nil {
   122  				// The fetch succeeded or failed but was not skipped.
   123  				noCapableServers = false
   124  			}
   125  
   126  			if err == nil {
   127  				if tacticsRecord != nil {
   128  					// The fetch succeeded, so exit the fetch loop and apply
   129  					// the result.
   130  					break
   131  				} else {
   132  					// MakeDialParameters, via fetchTactics, returns nil/nil
   133  					// when the server entry is to be skipped. See
   134  					// MakeDialParameters for skip cases and skip logging.
   135  					// Silently select a new candidate in this case.
   136  					continue
   137  				}
   138  			}
   139  
   140  			NoticeWarning("tactics request failed: %s", err)
   141  
   142  			// On error, proceed with a retry, as the error is likely
   143  			// due to a network failure.
   144  			//
   145  			// TODO: distinguish network and local errors and abort
   146  			// on local errors.
   147  
   148  			p := config.GetParameters().Get()
   149  			timeout := prng.JitterDuration(
   150  				p.Duration(parameters.TacticsRetryPeriod),
   151  				p.Float(parameters.TacticsRetryPeriodJitter))
   152  			p.Close()
   153  
   154  			tacticsRetryDelay := time.NewTimer(timeout)
   155  
   156  			select {
   157  			case <-ctx.Done():
   158  				return
   159  			case <-tacticsRetryDelay.C:
   160  			}
   161  
   162  			tacticsRetryDelay.Stop()
   163  		}
   164  	}
   165  
   166  	if tacticsRecord != nil &&
   167  		prng.FlipWeightedCoin(tacticsRecord.Tactics.Probability) {
   168  
   169  		err := config.SetParameters(
   170  			tacticsRecord.Tag, true, tacticsRecord.Tactics.Parameters)
   171  		if err != nil {
   172  			NoticeWarning("apply tactics failed: %s", err)
   173  
   174  			// The error will be due to invalid tactics values from
   175  			// the server. When SetParameters fails, all
   176  			// previous tactics values are left in place. Abort
   177  			// without retry since the server is highly unlikely
   178  			// to return different values immediately.
   179  			return
   180  		}
   181  	}
   182  
   183  	// Reclaim memory from the completed tactics request as we're likely
   184  	// to be proceeding to the memory-intensive tunnel establishment phase.
   185  	DoGarbageCollection()
   186  	emitMemoryMetrics()
   187  }
   188  
   189  // fetchTactics performs a tactics request using the specified server entry.
   190  // fetchTactics will return nil/nil when the candidate server entry is
   191  // skipped.
   192  func fetchTactics(
   193  	ctx context.Context,
   194  	config *Config,
   195  	serverEntry *protocol.ServerEntry) (*tactics.Record, error) {
   196  
   197  	canReplay := func(serverEntry *protocol.ServerEntry, replayProtocol string) bool {
   198  		return common.Contains(
   199  			serverEntry.GetSupportedTacticsProtocols(), replayProtocol)
   200  	}
   201  
   202  	selectProtocol := func(serverEntry *protocol.ServerEntry) (string, bool) {
   203  		tacticsProtocols := serverEntry.GetSupportedTacticsProtocols()
   204  		if len(tacticsProtocols) == 0 {
   205  			return "", false
   206  		}
   207  		index := prng.Intn(len(tacticsProtocols))
   208  		return tacticsProtocols[index], true
   209  	}
   210  
   211  	// No upstreamProxyErrorCallback is set: for tunnel establishment, the
   212  	// tactics head start is short, and tunnel connections will eventually post
   213  	// NoticeUpstreamProxyError for any persistent upstream proxy error
   214  	// conditions. Non-tunnel establishment cases, such as SendFeedback, which
   215  	// use tactics are not currently expected to post NoticeUpstreamProxyError.
   216  
   217  	dialParams, err := MakeDialParameters(
   218  		config,
   219  		nil,
   220  		canReplay,
   221  		selectProtocol,
   222  		serverEntry,
   223  		true,
   224  		0,
   225  		0)
   226  	if dialParams == nil {
   227  		return nil, nil
   228  	}
   229  	if err != nil {
   230  		return nil, errors.Tracef(
   231  			"failed to make dial parameters for %s: %v",
   232  			serverEntry.GetDiagnosticID(),
   233  			err)
   234  	}
   235  
   236  	NoticeRequestingTactics(dialParams)
   237  
   238  	// TacticsTimeout should be a very long timeout, since it's not
   239  	// adjusted by tactics in a new network context, and so clients
   240  	// with very slow connections must be accomodated. This long
   241  	// timeout will not entirely block the beginning of tunnel
   242  	// establishment, which beings after the shorter TacticsWaitPeriod.
   243  	//
   244  	// Using controller.establishCtx will cancel FetchTactics
   245  	// if tunnel establishment completes first.
   246  
   247  	timeout := config.GetParameters().Get().Duration(
   248  		parameters.TacticsTimeout)
   249  
   250  	ctx, cancelFunc := context.WithTimeout(ctx, timeout)
   251  	defer cancelFunc()
   252  
   253  	// DialMeek completes the TCP/TLS handshakes for HTTPS
   254  	// meek protocols but _not_ for HTTP meek protocols.
   255  	//
   256  	// TODO: pre-dial HTTP protocols to conform with speed
   257  	// test RTT spec.
   258  	//
   259  	// TODO: ensure that meek in round trip mode will fail
   260  	// the request when the pre-dial connection is broken,
   261  	// to minimize the possibility of network ID mismatches.
   262  
   263  	meekConn, err := DialMeek(
   264  		ctx, dialParams.GetMeekConfig(), dialParams.GetDialConfig())
   265  	if err != nil {
   266  		return nil, errors.Trace(err)
   267  	}
   268  	defer meekConn.Close()
   269  
   270  	apiParams := getBaseAPIParameters(
   271  		baseParametersAll, config, dialParams)
   272  
   273  	tacticsRecord, err := tactics.FetchTactics(
   274  		ctx,
   275  		config.GetParameters(),
   276  		GetTacticsStorer(config),
   277  		config.GetNetworkID,
   278  		apiParams,
   279  		serverEntry.Region,
   280  		dialParams.TunnelProtocol,
   281  		serverEntry.TacticsRequestPublicKey,
   282  		serverEntry.TacticsRequestObfuscatedKey,
   283  		meekConn.ObfuscatedRoundTrip)
   284  	if err != nil {
   285  		return nil, errors.Trace(err)
   286  	}
   287  
   288  	NoticeRequestedTactics(dialParams)
   289  
   290  	return tacticsRecord, nil
   291  }