tactics.go 10 KB

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