discovery.go 8.6 KB

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