discovery.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  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. "net"
  23. "sync"
  24. "time"
  25. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
  26. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
  27. "github.com/pion/stun"
  28. )
  29. const (
  30. discoverNATTimeout = 10 * time.Second
  31. discoverNATRoundTripTimeout = 2 * time.Second
  32. )
  33. // NATDiscoverConfig specifies the configuration for a NATDiscover run.
  34. type NATDiscoverConfig struct {
  35. // Logger is used to log events.
  36. Logger common.Logger
  37. // DialParameters specifies specific STUN and discovery and
  38. // settings, and receives discovery results.
  39. DialParameters DialParameters
  40. // SkipPortMapping indicates whether to skip port mapping type discovery,
  41. // as clients do since they will gather the same stats during the WebRTC
  42. // offer preparation.
  43. SkipPortMapping bool
  44. }
  45. // NATDiscover runs NAT type and port mapping type discovery operations.
  46. //
  47. // Successfuly results are delivered to NATDiscoverConfig.DialParameters
  48. // callbacks, SetNATType and SetPortMappingTypes, which should cache results
  49. // associated with the current network, by network ID.
  50. //
  51. // NAT discovery will invoke DialParameter callbacks
  52. // STUNServerAddressSucceeded and STUNServerAddressFailed, which may be used
  53. // to mark or unmark STUN servers for replay.
  54. func NATDiscover(
  55. ctx context.Context,
  56. config *NATDiscoverConfig) {
  57. // Run discovery until the specified timeout, or ctx is done. NAT and port
  58. // mapping discovery are run concurrently.
  59. discoverCtx, cancelFunc := context.WithTimeout(
  60. ctx, common.ValueOrDefault(config.DialParameters.DiscoverNATTimeout(), discoverNATTimeout))
  61. defer cancelFunc()
  62. discoveryWaitGroup := new(sync.WaitGroup)
  63. if config.DialParameters.NATType().NeedsDiscovery() &&
  64. !config.DialParameters.DisableSTUN() {
  65. discoveryWaitGroup.Add(1)
  66. go func() {
  67. defer discoveryWaitGroup.Done()
  68. natType, err := discoverNATType(discoverCtx, config)
  69. if err == nil {
  70. // Deliver the result. The DialParameters provider may cache
  71. // this result, associated wih the current networkID.
  72. config.DialParameters.SetNATType(natType)
  73. }
  74. config.Logger.WithTraceFields(common.LogFields{
  75. "nat_type": natType.String(),
  76. "error": err,
  77. }).Info("NAT type discovery")
  78. }()
  79. }
  80. if !config.SkipPortMapping &&
  81. config.DialParameters.PortMappingTypes().NeedsDiscovery() &&
  82. !config.DialParameters.DisablePortMapping() {
  83. discoveryWaitGroup.Add(1)
  84. go func() {
  85. defer discoveryWaitGroup.Done()
  86. portMappingTypes, err := discoverPortMappingTypes(
  87. discoverCtx, config.Logger)
  88. if err == nil {
  89. // Deliver the result. The DialParameters provider may cache
  90. // this result, associated wih the current networkID.
  91. config.DialParameters.SetPortMappingTypes(portMappingTypes)
  92. }
  93. config.Logger.WithTraceFields(common.LogFields{
  94. "port_mapping_types": portMappingTypes.String(),
  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.DialParameters.STUNServerAddress(RFC5780)
  106. if stunServerAddress == "" {
  107. return NATTypeUnknown, errors.TraceNew("no RFC5780 STUN server")
  108. }
  109. // The STUN server will observe proxy IP addresses. Enumeration is
  110. // mitigated by using various public STUN servers, including Psiphon STUN
  111. // servers for proxies in non-censored regions. Proxies are also more
  112. // ephemeral than Psiphon servers.
  113. // Limitation: RFC5780, "4.1. Source Port Selection" recommends using the
  114. // same source port for NAT discovery _and_ subsequent NAT traveral
  115. // applications, such as WebRTC ICE. It's stated that the discovered NAT
  116. // type may only be valid for the particular tested port.
  117. //
  118. // We don't do this at this time, as we don't want to incur the full
  119. // RFC5780 discovery overhead for every WebRTC dial, and expect that, in
  120. // most typical cases, the network NAT type applies to all ports.
  121. // Furthermore, the UDP conn that owns the tested port may need to be
  122. // closed to interrupt discovery.
  123. conn, err := config.DialParameters.UDPListen()
  124. if err != nil {
  125. return NATTypeUnknown, errors.Trace(err)
  126. }
  127. defer conn.Close()
  128. type result struct {
  129. NATType NATType
  130. err error
  131. }
  132. resultChannel := make(chan result, 1)
  133. go func() {
  134. serverAddress, err := config.DialParameters.ResolveAddress(
  135. ctx, stunServerAddress)
  136. if err != nil {
  137. resultChannel <- result{err: errors.Trace(err)}
  138. return
  139. }
  140. mapping, err := discoverNATMapping(ctx, conn, serverAddress)
  141. if err != nil {
  142. resultChannel <- result{err: errors.Trace(err)}
  143. return
  144. }
  145. filtering, err := discoverNATFiltering(ctx, conn, serverAddress)
  146. if err != nil {
  147. resultChannel <- result{err: errors.Trace(err)}
  148. return
  149. }
  150. resultChannel <- result{NATType: MakeNATType(mapping, filtering)}
  151. return
  152. }()
  153. var r result
  154. select {
  155. case r = <-resultChannel:
  156. case <-ctx.Done():
  157. r.err = errors.Trace(ctx.Err())
  158. // Interrupt the goroutine
  159. conn.Close()
  160. <-resultChannel
  161. }
  162. if r.err != nil {
  163. if ctx.Err() == nil {
  164. config.DialParameters.STUNServerAddressFailed(RFC5780, stunServerAddress)
  165. }
  166. return NATTypeUnknown, errors.Trace(r.err)
  167. }
  168. config.DialParameters.STUNServerAddressSucceeded(RFC5780, stunServerAddress)
  169. return r.NATType, nil
  170. }
  171. // discoverNATMapping and discoverNATFiltering are modifications of:
  172. // https://github.com/pion/stun/blob/b321a45be43b07685c639943aaa28e6841517799/cmd/stun-nat-behaviour/main.go
  173. // https://github.com/pion/stun/blob/b321a45be43b07685c639943aaa28e6841517799/LICENSE.md:
  174. /*
  175. Copyright 2018 Pion LLC
  176. Permission is hereby granted, free of charge, to any person obtaining a copy
  177. of this software and associated documentation files (the "Software"), to deal
  178. in the Software without restriction, including without limitation the rights
  179. to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  180. copies of the Software, and to permit persons to whom the Software is
  181. furnished to do so, subject to the following conditions:
  182. The above copyright notice and this permission notice shall be included in all
  183. copies or substantial portions of the Software.
  184. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  185. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  186. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  187. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  188. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  189. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  190. SOFTWARE.
  191. */
  192. // RFC5780: 4.3. Determining NAT Mapping Behavior
  193. func discoverNATMapping(
  194. ctx context.Context,
  195. conn net.PacketConn,
  196. serverAddress string) (NATMapping, error) {
  197. // Test I: Regular binding request
  198. request := stun.MustBuild(stun.TransactionID, stun.BindingRequest)
  199. response, _, err := doSTUNRoundTrip(request, conn, serverAddress)
  200. if err != nil {
  201. return NATMappingUnknown, errors.Trace(err)
  202. }
  203. responseFields := parseSTUNMessage(response)
  204. if responseFields.xorAddr == nil || responseFields.otherAddr == nil {
  205. return NATMappingUnknown, errors.TraceNew("NAT discovery not supported")
  206. }
  207. if responseFields.xorAddr.String() == conn.LocalAddr().String() {
  208. return NATMappingEndpointIndependent, nil
  209. }
  210. otherAddress := responseFields.otherAddr
  211. // Test II: Send binding request to the other address but primary port
  212. _, serverPort, err := net.SplitHostPort(serverAddress)
  213. if err != nil {
  214. return NATMappingUnknown, errors.Trace(err)
  215. }
  216. address := net.JoinHostPort(otherAddress.IP.String(), serverPort)
  217. response2, _, err := doSTUNRoundTrip(request, conn, address)
  218. if err != nil {
  219. return NATMappingUnknown, errors.Trace(err)
  220. }
  221. response2Fields := parseSTUNMessage(response2)
  222. if response2Fields.xorAddr.String() == responseFields.xorAddr.String() {
  223. return NATMappingEndpointIndependent, nil
  224. }
  225. // Test III: Send binding request to the other address and port
  226. response3, _, err := doSTUNRoundTrip(request, conn, otherAddress.String())
  227. if err != nil {
  228. return NATMappingUnknown, errors.Trace(err)
  229. }
  230. response3Fields := parseSTUNMessage(response3)
  231. if response3Fields.xorAddr.String() == response2Fields.xorAddr.String() {
  232. return NATMappingAddressDependent, nil
  233. } else {
  234. return NATMappingAddressPortDependent, nil
  235. }
  236. return NATMappingUnknown, nil
  237. }
  238. // RFC5780: 4.4. Determining NAT Filtering Behavior
  239. func discoverNATFiltering(
  240. ctx context.Context,
  241. conn net.PacketConn,
  242. serverAddress string) (NATFiltering, error) {
  243. // Test I: Regular binding request
  244. request := stun.MustBuild(stun.TransactionID, stun.BindingRequest)
  245. response, _, err := doSTUNRoundTrip(request, conn, serverAddress)
  246. if err != nil {
  247. return NATFilteringUnknown, errors.Trace(err)
  248. }
  249. responseFields := parseSTUNMessage(response)
  250. if responseFields.xorAddr == nil || responseFields.otherAddr == nil {
  251. return NATFilteringUnknown, errors.TraceNew("NAT discovery not supported")
  252. }
  253. // Test II: Request to change both IP and port
  254. request = stun.MustBuild(stun.TransactionID, stun.BindingRequest)
  255. request.Add(stun.AttrChangeRequest, []byte{0x00, 0x00, 0x00, 0x06})
  256. response, responseTimeout, err := doSTUNRoundTrip(request, conn, serverAddress)
  257. if err == nil {
  258. return NATFilteringEndpointIndependent, nil
  259. } else if !responseTimeout {
  260. return NATFilteringUnknown, errors.Trace(err)
  261. }
  262. // Test III: Request to change port only
  263. request = stun.MustBuild(stun.TransactionID, stun.BindingRequest)
  264. request.Add(stun.AttrChangeRequest, []byte{0x00, 0x00, 0x00, 0x02})
  265. response, responseTimeout, err = doSTUNRoundTrip(request, conn, serverAddress)
  266. if err == nil {
  267. return NATFilteringAddressDependent, nil
  268. } else if !responseTimeout {
  269. return NATFilteringUnknown, errors.Trace(err)
  270. }
  271. return NATFilteringAddressPortDependent, nil
  272. }
  273. func parseSTUNMessage(message *stun.Message) (ret struct {
  274. xorAddr *stun.XORMappedAddress
  275. otherAddr *stun.OtherAddress
  276. respOrigin *stun.ResponseOrigin
  277. mappedAddr *stun.MappedAddress
  278. software *stun.Software
  279. },
  280. ) {
  281. ret.mappedAddr = &stun.MappedAddress{}
  282. ret.xorAddr = &stun.XORMappedAddress{}
  283. ret.respOrigin = &stun.ResponseOrigin{}
  284. ret.otherAddr = &stun.OtherAddress{}
  285. ret.software = &stun.Software{}
  286. if ret.xorAddr.GetFrom(message) != nil {
  287. ret.xorAddr = nil
  288. }
  289. if ret.otherAddr.GetFrom(message) != nil {
  290. ret.otherAddr = nil
  291. }
  292. if ret.respOrigin.GetFrom(message) != nil {
  293. ret.respOrigin = nil
  294. }
  295. if ret.mappedAddr.GetFrom(message) != nil {
  296. ret.mappedAddr = nil
  297. }
  298. if ret.software.GetFrom(message) != nil {
  299. ret.software = nil
  300. }
  301. return ret
  302. }
  303. // doSTUNRoundTrip returns nil, true, nil on timeout reading a response.
  304. func doSTUNRoundTrip(
  305. request *stun.Message,
  306. conn net.PacketConn,
  307. remoteAddress string) (*stun.Message, bool, error) {
  308. remoteAddr, err := net.ResolveUDPAddr("udp", remoteAddress)
  309. if err != nil {
  310. return nil, false, errors.Trace(err)
  311. }
  312. _ = request.NewTransactionID()
  313. _, err = conn.WriteTo(request.Raw, remoteAddr)
  314. if err != nil {
  315. return nil, false, errors.Trace(err)
  316. }
  317. conn.SetReadDeadline(time.Now().Add(discoverNATRoundTripTimeout))
  318. var buffer [1500]byte
  319. n, _, err := conn.ReadFrom(buffer[:])
  320. if err != nil {
  321. if e, ok := err.(net.Error); ok && e.Timeout() {
  322. return nil, true, errors.Trace(err)
  323. }
  324. return nil, false, errors.Trace(err)
  325. }
  326. response := new(stun.Message)
  327. response.Raw = buffer[:n]
  328. err = response.Decode()
  329. if err != nil {
  330. return nil, false, errors.Trace(err)
  331. }
  332. return response, false, nil
  333. }
  334. func discoverPortMappingTypes(
  335. ctx context.Context,
  336. logger common.Logger) (PortMappingTypes, error) {
  337. portMappingTypes, err := probePortMapping(ctx, logger)
  338. if err != nil {
  339. return nil, errors.Trace(err)
  340. }
  341. return portMappingTypes, nil
  342. }