/* * 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 . * */ package psiphon import ( "context" "time" "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/prng" "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol" "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/tactics" ) // GetTactics attempts to apply tactics, for the current network, to the given // config. GetTactics first checks for unexpired stored tactics, which it will // immediately return. If no unexpired stored tactics are found, tactics // requests are attempted until the input context is cancelled. // // Callers may pass in a context that is already done. In this case, stored // tactics, when available, are applied but no request will be attempted. // // Callers are responsible for ensuring that the input context eventually // cancels, and should synchronize GetTactics calls to ensure no unintended // concurrent fetch attempts occur. // // GetTactics implements a limited workaround for multiprocess datastore // synchronization, enabling, for example, SendFeedback in one process to // access tactics as long as a Controller is not running in another process; // and without blocking the Controller from starting. Accessing tactics is // most critical for untunneled network operations; when a Controller is // running, a tunnel may be used. See TacticsStorer for more details. func GetTactics(ctx context.Context, config *Config) { // Limitation: GetNetworkID may not account for device VPN status, so // Psiphon-over-Psiphon or Psiphon-over-other-VPN scenarios can encounter // this issue: // // 1. Tactics are established when tunneling through a VPN and egressing // through a remote region/ISP. // 2. Psiphon is next run when _not_ tunneling through the VPN. Yet the // network ID remains the same. Initial applied tactics will be for the // remote egress region/ISP, not the local region/ISP. tacticsRecord, err := tactics.UseStoredTactics( GetTacticsStorer(config), config.GetNetworkID()) if err != nil { NoticeWarning("get stored tactics failed: %s", err) // The error will be due to a local datastore problem. // While we could proceed with the tactics request, this // could result in constant tactics requests. So, abort. return } if tacticsRecord == nil { // If the context is already Done, don't even start the request. if ctx.Err() != nil { return } iterator, err := NewTacticsServerEntryIterator(config) if err != nil { NoticeWarning("tactics iterator failed: %s", err) return } defer iterator.Close() noCapableServers := true for iteration := 0; ; iteration++ { if !WaitForNetworkConnectivity( ctx, config.NetworkConnectivityChecker) { return } serverEntry, err := iterator.Next() if err != nil { NoticeWarning("tactics iterator failed: %s", err) return } if serverEntry == nil { if noCapableServers { // Abort when no capable servers have been found after // a full iteration. Server entries that are skipped are // classified as not capable. NoticeWarning("tactics request aborted: no capable servers") return } iterator.Reset() continue } tacticsRecord, err = fetchTactics( ctx, config, serverEntry) if tacticsRecord != nil || err != nil { // The fetch succeeded or failed but was not skipped. noCapableServers = false } if err == nil { if tacticsRecord != nil { // The fetch succeeded, so exit the fetch loop and apply // the result. break } else { // MakeDialParameters, via fetchTactics, returns nil/nil // when the server entry is to be skipped. See // MakeDialParameters for skip cases and skip logging. // Silently select a new candidate in this case. continue } } NoticeWarning("tactics request failed: %s", err) // On error, proceed with a retry, as the error is likely // due to a network failure. // // TODO: distinguish network and local errors and abort // on local errors. p := config.GetParameters().Get() timeout := prng.JitterDuration( p.Duration(parameters.TacticsRetryPeriod), p.Float(parameters.TacticsRetryPeriodJitter)) p.Close() tacticsRetryDelay := time.NewTimer(timeout) select { case <-ctx.Done(): return case <-tacticsRetryDelay.C: } tacticsRetryDelay.Stop() } } if tacticsRecord != nil && prng.FlipWeightedCoin(tacticsRecord.Tactics.Probability) { err := config.SetParameters( tacticsRecord.Tag, true, tacticsRecord.Tactics.Parameters) if err != nil { NoticeWarning("apply tactics failed: %s", err) // The error will be due to invalid tactics values from // the server. When SetParameters fails, all // previous tactics values are left in place. Abort // without retry since the server is highly unlikely // to return different values immediately. return } } // Reclaim memory from the completed tactics request as we're likely // to be proceeding to the memory-intensive tunnel establishment phase. DoGarbageCollection() emitMemoryMetrics() } // fetchTactics performs a tactics request using the specified server entry. // fetchTactics will return nil/nil when the candidate server entry is // skipped. func fetchTactics( ctx context.Context, config *Config, serverEntry *protocol.ServerEntry) (*tactics.Record, error) { canReplay := func(serverEntry *protocol.ServerEntry, replayProtocol string) bool { return common.Contains( serverEntry.GetSupportedTacticsProtocols(), replayProtocol) } selectProtocol := func(serverEntry *protocol.ServerEntry) (string, bool) { tacticsProtocols := serverEntry.GetSupportedTacticsProtocols() if len(tacticsProtocols) == 0 { return "", false } index := prng.Intn(len(tacticsProtocols)) return tacticsProtocols[index], true } // No upstreamProxyErrorCallback is set: for tunnel establishment, the // tactics head start is short, and tunnel connections will eventually post // NoticeUpstreamProxyError for any persistent upstream proxy error // conditions. Non-tunnel establishment cases, such as SendFeedback, which // use tactics are not currently expected to post NoticeUpstreamProxyError. dialParams, err := MakeDialParameters( config, nil, nil, canReplay, selectProtocol, serverEntry, true, 0, 0) if dialParams == nil { return nil, nil } if err != nil { return nil, errors.Tracef( "failed to make dial parameters for %s: %v", serverEntry.GetDiagnosticID(), err) } NoticeRequestingTactics(dialParams) // TacticsTimeout should be a very long timeout, since it's not // adjusted by tactics in a new network context, and so clients // with very slow connections must be accomodated. This long // timeout will not entirely block the beginning of tunnel // establishment, which beings after the shorter TacticsWaitPeriod. // // Using controller.establishCtx will cancel FetchTactics // if tunnel establishment completes first. timeout := config.GetParameters().Get().Duration( parameters.TacticsTimeout) ctx, cancelFunc := context.WithTimeout(ctx, timeout) defer cancelFunc() // DialMeek completes the TCP/TLS handshakes for HTTPS // meek protocols but _not_ for HTTP meek protocols. // // TODO: pre-dial HTTP protocols to conform with speed // test RTT spec. // // TODO: ensure that meek in round trip mode will fail // the request when the pre-dial connection is broken, // to minimize the possibility of network ID mismatches. meekConn, err := DialMeek( ctx, dialParams.GetMeekConfig(), dialParams.GetDialConfig()) if err != nil { return nil, errors.Trace(err) } defer meekConn.Close() apiParams := getBaseAPIParameters( baseParametersAll, config, dialParams) tacticsRecord, err := tactics.FetchTactics( ctx, config.GetParameters(), GetTacticsStorer(config), config.GetNetworkID, apiParams, serverEntry.Region, dialParams.TunnelProtocol, serverEntry.TacticsRequestPublicKey, serverEntry.TacticsRequestObfuscatedKey, meekConn.ObfuscatedRoundTrip) if err != nil { return nil, errors.Trace(err) } NoticeRequestedTactics(dialParams) return tacticsRecord, nil }