tactics.go 8.1 KB

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