discovery.go 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. /*
  2. * Copyright (c) 2023, 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 inproxy
  20. import (
  21. "context"
  22. "sync"
  23. "time"
  24. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
  25. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
  26. )
  27. const (
  28. discoverNATTimeout = 10 * time.Second
  29. discoverNATRoundTripTimeout = 2 * time.Second
  30. )
  31. // NATDiscoverConfig specifies the configuration for a NATDiscover run.
  32. type NATDiscoverConfig struct {
  33. // Logger is used to log events.
  34. Logger common.Logger
  35. // WebRTCDialCoordinator specifies specific STUN and discovery and
  36. // settings, and receives discovery results.
  37. WebRTCDialCoordinator WebRTCDialCoordinator
  38. // SkipPortMapping indicates whether to skip port mapping type discovery,
  39. // as clients do since they will gather the same stats during the WebRTC
  40. // offer preparation.
  41. SkipPortMapping bool
  42. }
  43. // NATDiscover runs NAT type and port mapping type discovery operations.
  44. //
  45. // Successfuly results are delivered to NATDiscoverConfig.WebRTCDialCoordinator
  46. // callbacks, SetNATType and SetPortMappingTypes, which should cache results
  47. // associated with the current network, by network ID.
  48. //
  49. // NAT discovery will invoke WebRTCDialCoordinator callbacks
  50. // STUNServerAddressSucceeded and STUNServerAddressFailed, which may be used
  51. // to mark or unmark STUN servers for replay.
  52. func NATDiscover(
  53. ctx context.Context,
  54. config *NATDiscoverConfig) {
  55. // Run discovery until the specified timeout, or ctx is done. NAT and port
  56. // mapping discovery are run concurrently.
  57. discoverCtx, cancelFunc := context.WithTimeout(
  58. ctx, common.ValueOrDefault(config.WebRTCDialCoordinator.DiscoverNATTimeout(), discoverNATTimeout))
  59. defer cancelFunc()
  60. discoveryWaitGroup := new(sync.WaitGroup)
  61. if config.WebRTCDialCoordinator.NATType().NeedsDiscovery() &&
  62. !config.WebRTCDialCoordinator.DisableSTUN() {
  63. discoveryWaitGroup.Add(1)
  64. go func() {
  65. defer discoveryWaitGroup.Done()
  66. natType, err := discoverNATType(discoverCtx, config)
  67. if err == nil {
  68. // Deliver the result. The WebRTCDialCoordinator provider may cache
  69. // this result, associated wih the current networkID.
  70. config.WebRTCDialCoordinator.SetNATType(natType)
  71. }
  72. config.Logger.WithTraceFields(common.LogFields{
  73. "nat_type": natType,
  74. "error": err,
  75. }).Info("NAT type discovery")
  76. }()
  77. }
  78. if !config.SkipPortMapping &&
  79. config.WebRTCDialCoordinator.PortMappingTypes().NeedsDiscovery() &&
  80. !config.WebRTCDialCoordinator.DisablePortMapping() {
  81. discoveryWaitGroup.Add(1)
  82. go func() {
  83. defer discoveryWaitGroup.Done()
  84. portMappingTypes, err := discoverPortMappingTypes(
  85. discoverCtx, config.Logger)
  86. if err == nil {
  87. // Deliver the result. The WebRTCDialCoordinator provider may cache
  88. // this result, associated wih the current networkID.
  89. config.WebRTCDialCoordinator.SetPortMappingTypes(portMappingTypes)
  90. }
  91. config.Logger.WithTraceFields(common.LogFields{
  92. "port_mapping_types": portMappingTypes,
  93. "error": err,
  94. }).Info("Port mapping type discovery")
  95. }()
  96. }
  97. discoveryWaitGroup.Wait()
  98. }
  99. func discoverNATType(
  100. ctx context.Context,
  101. config *NATDiscoverConfig) (NATType, error) {
  102. RFC5780 := true
  103. stunServerAddress := config.WebRTCDialCoordinator.STUNServerAddress(RFC5780)
  104. if stunServerAddress == "" {
  105. return NATTypeUnknown, errors.TraceNew("no RFC5780 STUN server")
  106. }
  107. serverAddress, err := config.WebRTCDialCoordinator.ResolveAddress(
  108. ctx, "ip", stunServerAddress)
  109. if err != nil {
  110. return NATTypeUnknown, errors.Trace(err)
  111. }
  112. // The STUN server will observe proxy IP addresses. Enumeration is
  113. // mitigated by using various public STUN servers, including Psiphon STUN
  114. // servers for proxies in non-censored regions. Proxies are also more
  115. // ephemeral than Psiphon servers.
  116. // Limitation: RFC5780, "4.1. Source Port Selection" recommends using the
  117. // same source port for NAT discovery _and_ subsequent NAT traveral
  118. // applications, such as WebRTC ICE. It's stated that the discovered NAT
  119. // type may only be valid for the particular tested port.
  120. //
  121. // We don't do this at this time, as we don't want to incur the full
  122. // RFC5780 discovery overhead for every WebRTC dial, and expect that, in
  123. // most typical cases, the network NAT type applies to all ports.
  124. // Furthermore, the UDP conn that owns the tested port may need to be
  125. // closed to interrupt discovery.
  126. // We run the filtering test before the mapping test, and each test uses a
  127. // distinct source port; using the same source port may result in NAT
  128. // state from one test confusing the other test. See also,
  129. // https://github.com/jselbie/stunserver/issues/18:
  130. //
  131. // > running both the behavior test and the filtering test at the
  132. // > same time can cause an incorrect filtering type to be detected.
  133. // > If the filtering is actually "address dependent", the scan will
  134. // > report it as "endpoint independent".
  135. // >
  136. // > The cause appears to be the order in which the tests are being
  137. // > performed, currently "behavior" tests followed by "filtering"
  138. // > tests. The network traffic from the behavior tests having been run
  139. // > causes the router to allow filtering test responses back through
  140. // > that would not have otherwise been allowed... The behavior tests
  141. // > send traffic to the secondary IP of the STUN server, so the
  142. // > filtering tests are allowed to get responses back from that
  143. // > secondary IP.
  144. // >
  145. // > The fix is likely some combination of ...re-order the tests...
  146. // > or use the a different port for the filtering test.
  147. //
  148. // TODO: RFC5780, "4.5 Combining and Ordering Tests", suggests that the
  149. // individual test steps within filtering and mapping could be combined,
  150. // and certain tests may be run concurrently, with the goal of reducing
  151. // the total elapsed test time. However, "care must be taken when
  152. // combining and parallelizing tests, due to the sensitivity of certain
  153. // tests to prior state on the NAT and because some NAT devices have an
  154. // upper limit on how quickly bindings will be allocated."
  155. //
  156. // For now, we stick with a conservative arrangement of tests. Note that,
  157. // in practise, the discoverNATMapping completes much faster
  158. // discoverNATFiltering, and so there's a limited gain from running these
  159. // two top-level tests concurrently.
  160. mappingConn, err := config.WebRTCDialCoordinator.UDPListen(ctx)
  161. if err != nil {
  162. return NATTypeUnknown, errors.Trace(err)
  163. }
  164. defer mappingConn.Close()
  165. filteringConn, err := config.WebRTCDialCoordinator.UDPListen(ctx)
  166. if err != nil {
  167. return NATTypeUnknown, errors.Trace(err)
  168. }
  169. defer filteringConn.Close()
  170. type result struct {
  171. NATType NATType
  172. err error
  173. }
  174. resultChannel := make(chan result, 1)
  175. go func() {
  176. filtering, err := discoverNATFiltering(ctx, filteringConn, serverAddress)
  177. if err != nil {
  178. resultChannel <- result{err: errors.Trace(err)}
  179. return
  180. }
  181. mapping, err := discoverNATMapping(ctx, mappingConn, serverAddress)
  182. if err != nil {
  183. resultChannel <- result{err: errors.Trace(err)}
  184. return
  185. }
  186. resultChannel <- result{NATType: MakeNATType(mapping, filtering)}
  187. return
  188. }()
  189. var r result
  190. select {
  191. case r = <-resultChannel:
  192. case <-ctx.Done():
  193. // Interrupt and await the goroutine
  194. mappingConn.Close()
  195. filteringConn.Close()
  196. <-resultChannel
  197. // Don't call STUNServerAddressFailed, since ctx.Done may be due to an
  198. // early dial cancel.
  199. return NATTypeUnknown, errors.Trace(ctx.Err())
  200. }
  201. if r.err != nil {
  202. config.WebRTCDialCoordinator.STUNServerAddressFailed(RFC5780, stunServerAddress)
  203. return NATTypeUnknown, errors.Trace(err)
  204. }
  205. config.WebRTCDialCoordinator.STUNServerAddressSucceeded(RFC5780, stunServerAddress)
  206. return r.NATType, nil
  207. }
  208. func discoverPortMappingTypes(
  209. ctx context.Context,
  210. logger common.Logger) (PortMappingTypes, error) {
  211. portMappingTypes, err := probePortMapping(ctx, logger)
  212. if err != nil {
  213. return nil, errors.Trace(err)
  214. }
  215. return portMappingTypes, nil
  216. }