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 }