tactics.go 8.9 KB

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