discovery.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  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. // WebRTCDialCoordinator specifies specific STUN and discovery and
  38. // settings, and receives discovery results.
  39. WebRTCDialCoordinator WebRTCDialCoordinator
  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.WebRTCDialCoordinator
  48. // callbacks, SetNATType and SetPortMappingTypes, which should cache results
  49. // associated with the current network, by network ID.
  50. //
  51. // NAT discovery will invoke WebRTCDialCoordinator 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.WebRTCDialCoordinator.DiscoverNATTimeout(), discoverNATTimeout))
  61. defer cancelFunc()
  62. discoveryWaitGroup := new(sync.WaitGroup)
  63. if config.WebRTCDialCoordinator.NATType().NeedsDiscovery() &&
  64. !config.WebRTCDialCoordinator.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 WebRTCDialCoordinator provider may cache
  71. // this result, associated wih the current networkID.
  72. config.WebRTCDialCoordinator.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.WebRTCDialCoordinator.PortMappingTypes().NeedsDiscovery() &&
  82. !config.WebRTCDialCoordinator.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 WebRTCDialCoordinator provider may cache
  90. // this result, associated wih the current networkID.
  91. config.WebRTCDialCoordinator.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.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. return
  190. }()
  191. var r result
  192. select {
  193. case r = <-resultChannel:
  194. case <-ctx.Done():
  195. // Interrupt and await the goroutine
  196. mappingConn.Close()
  197. filteringConn.Close()
  198. <-resultChannel
  199. // Don't call STUNServerAddressFailed, since ctx.Done may be due to an
  200. // early dial cancel.
  201. return NATTypeUnknown, errors.Trace(ctx.Err())
  202. }
  203. if r.err != nil {
  204. config.WebRTCDialCoordinator.STUNServerAddressFailed(RFC5780, stunServerAddress)
  205. return NATTypeUnknown, errors.Trace(err)
  206. }
  207. config.WebRTCDialCoordinator.STUNServerAddressSucceeded(RFC5780, stunServerAddress)
  208. return r.NATType, nil
  209. }
  210. // discoverNATMapping and discoverNATFiltering are modifications of:
  211. // https://github.com/pion/stun/blob/b321a45be43b07685c639943aaa28e6841517799/cmd/stun-nat-behaviour/main.go
  212. // https://github.com/pion/stun/blob/b321a45be43b07685c639943aaa28e6841517799/LICENSE.md:
  213. /*
  214. Copyright 2018 Pion LLC
  215. Permission is hereby granted, free of charge, to any person obtaining a copy
  216. of this software and associated documentation files (the "Software"), to deal
  217. in the Software without restriction, including without limitation the rights
  218. to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  219. copies of the Software, and to permit persons to whom the Software is
  220. furnished to do so, subject to the following conditions:
  221. The above copyright notice and this permission notice shall be included in all
  222. copies or substantial portions of the Software.
  223. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  224. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  225. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  226. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  227. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  228. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  229. SOFTWARE.
  230. */
  231. // RFC5780: 4.3. Determining NAT Mapping Behavior
  232. func discoverNATMapping(
  233. ctx context.Context,
  234. conn net.PacketConn,
  235. serverAddress string) (NATMapping, error) {
  236. // Test I: Regular binding request
  237. request := stun.MustBuild(stun.TransactionID, stun.BindingRequest)
  238. response, _, err := doSTUNRoundTrip(request, conn, serverAddress)
  239. if err != nil {
  240. return NATMappingUnknown, errors.Trace(err)
  241. }
  242. responseFields := parseSTUNMessage(response)
  243. if responseFields.xorAddr == nil || responseFields.otherAddr == nil {
  244. return NATMappingUnknown, errors.TraceNew("NAT discovery not supported")
  245. }
  246. if responseFields.xorAddr.String() == conn.LocalAddr().String() {
  247. return NATMappingEndpointIndependent, nil
  248. }
  249. otherAddress := responseFields.otherAddr
  250. // Verify that otherAddress, specified by STUN server, is a valid public
  251. // IP before sending a packet to it. This prevents the STUN server
  252. // (or injected response) from redirecting the flow to an internal network.
  253. if common.IsBogon(otherAddress.IP) {
  254. return NATMappingUnknown, errors.TraceNew("OTHER-ADDRESS is bogon")
  255. }
  256. // Test II: Send binding request to the other address but primary port
  257. _, serverPort, err := net.SplitHostPort(serverAddress)
  258. if err != nil {
  259. return NATMappingUnknown, errors.Trace(err)
  260. }
  261. address := net.JoinHostPort(otherAddress.IP.String(), serverPort)
  262. response2, _, err := doSTUNRoundTrip(request, conn, address)
  263. if err != nil {
  264. return NATMappingUnknown, errors.Trace(err)
  265. }
  266. response2Fields := parseSTUNMessage(response2)
  267. if response2Fields.xorAddr.String() == responseFields.xorAddr.String() {
  268. return NATMappingEndpointIndependent, nil
  269. }
  270. // Test III: Send binding request to the other address and port
  271. response3, _, err := doSTUNRoundTrip(request, conn, otherAddress.String())
  272. if err != nil {
  273. return NATMappingUnknown, errors.Trace(err)
  274. }
  275. response3Fields := parseSTUNMessage(response3)
  276. if response3Fields.xorAddr.String() == response2Fields.xorAddr.String() {
  277. return NATMappingAddressDependent, nil
  278. } else {
  279. return NATMappingAddressPortDependent, nil
  280. }
  281. return NATMappingUnknown, nil
  282. }
  283. // RFC5780: 4.4. Determining NAT Filtering Behavior
  284. func discoverNATFiltering(
  285. ctx context.Context,
  286. conn net.PacketConn,
  287. serverAddress string) (NATFiltering, error) {
  288. // Test I: Regular binding request
  289. request := stun.MustBuild(stun.TransactionID, stun.BindingRequest)
  290. response, _, err := doSTUNRoundTrip(request, conn, serverAddress)
  291. if err != nil {
  292. return NATFilteringUnknown, errors.Trace(err)
  293. }
  294. responseFields := parseSTUNMessage(response)
  295. if responseFields.xorAddr == nil || responseFields.otherAddr == nil {
  296. return NATFilteringUnknown, errors.TraceNew("NAT discovery not supported")
  297. }
  298. // Test II: Request to change both IP and port
  299. request = stun.MustBuild(stun.TransactionID, stun.BindingRequest)
  300. request.Add(stun.AttrChangeRequest, []byte{0x00, 0x00, 0x00, 0x06})
  301. response, responseTimeout, err := doSTUNRoundTrip(request, conn, serverAddress)
  302. if err == nil {
  303. return NATFilteringEndpointIndependent, nil
  304. } else if !responseTimeout {
  305. return NATFilteringUnknown, errors.Trace(err)
  306. }
  307. // Test III: Request to change port only
  308. request = stun.MustBuild(stun.TransactionID, stun.BindingRequest)
  309. request.Add(stun.AttrChangeRequest, []byte{0x00, 0x00, 0x00, 0x02})
  310. response, responseTimeout, err = doSTUNRoundTrip(request, conn, serverAddress)
  311. if err == nil {
  312. return NATFilteringAddressDependent, nil
  313. } else if !responseTimeout {
  314. return NATFilteringUnknown, errors.Trace(err)
  315. }
  316. return NATFilteringAddressPortDependent, nil
  317. }
  318. func parseSTUNMessage(message *stun.Message) (ret struct {
  319. xorAddr *stun.XORMappedAddress
  320. otherAddr *stun.OtherAddress
  321. respOrigin *stun.ResponseOrigin
  322. mappedAddr *stun.MappedAddress
  323. software *stun.Software
  324. },
  325. ) {
  326. ret.mappedAddr = &stun.MappedAddress{}
  327. ret.xorAddr = &stun.XORMappedAddress{}
  328. ret.respOrigin = &stun.ResponseOrigin{}
  329. ret.otherAddr = &stun.OtherAddress{}
  330. ret.software = &stun.Software{}
  331. if ret.xorAddr.GetFrom(message) != nil {
  332. ret.xorAddr = nil
  333. }
  334. if ret.otherAddr.GetFrom(message) != nil {
  335. ret.otherAddr = nil
  336. }
  337. if ret.respOrigin.GetFrom(message) != nil {
  338. ret.respOrigin = nil
  339. }
  340. if ret.mappedAddr.GetFrom(message) != nil {
  341. ret.mappedAddr = nil
  342. }
  343. if ret.software.GetFrom(message) != nil {
  344. ret.software = nil
  345. }
  346. return ret
  347. }
  348. // doSTUNRoundTrip returns nil, true, nil on timeout reading a response.
  349. func doSTUNRoundTrip(
  350. request *stun.Message,
  351. conn net.PacketConn,
  352. remoteAddress string) (*stun.Message, bool, error) {
  353. remoteAddr, err := net.ResolveUDPAddr("udp", remoteAddress)
  354. if err != nil {
  355. return nil, false, errors.Trace(err)
  356. }
  357. _ = request.NewTransactionID()
  358. _, err = conn.WriteTo(request.Raw, remoteAddr)
  359. if err != nil {
  360. return nil, false, errors.Trace(err)
  361. }
  362. conn.SetReadDeadline(time.Now().Add(discoverNATRoundTripTimeout))
  363. var buffer [1500]byte
  364. n, _, err := conn.ReadFrom(buffer[:])
  365. if err != nil {
  366. if e, ok := err.(net.Error); ok && e.Timeout() {
  367. return nil, true, errors.Trace(err)
  368. }
  369. return nil, false, errors.Trace(err)
  370. }
  371. response := new(stun.Message)
  372. response.Raw = buffer[:n]
  373. err = response.Decode()
  374. if err != nil {
  375. return nil, false, errors.Trace(err)
  376. }
  377. // Verify that the response packet has the expected transaction ID, to
  378. // partially mitigate against phony injected responses.
  379. if response.TransactionID != request.TransactionID {
  380. return nil, false, errors.TraceNew(
  381. "unexpected response transaction ID")
  382. }
  383. return response, false, nil
  384. }
  385. func discoverPortMappingTypes(
  386. ctx context.Context,
  387. logger common.Logger) (PortMappingTypes, error) {
  388. portMappingTypes, err := probePortMapping(ctx, logger)
  389. if err != nil {
  390. return nil, errors.Trace(err)
  391. }
  392. return portMappingTypes, nil
  393. }