discoverySTUN.go 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. //go:build PSIPHON_ENABLE_INPROXY
  2. /*
  3. * Copyright (c) 2024, Psiphon Inc.
  4. * All rights reserved.
  5. *
  6. * This program is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU General Public License as published by
  8. * the Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public License
  17. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. *
  19. */
  20. package inproxy
  21. import (
  22. "context"
  23. "net"
  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. // discoverNATMapping and discoverNATFiltering are modifications of:
  30. // https://github.com/pion/stun/blob/b321a45be43b07685c639943aaa28e6841517799/cmd/stun-nat-behaviour/main.go
  31. // https://github.com/pion/stun/blob/b321a45be43b07685c639943aaa28e6841517799/LICENSE.md:
  32. /*
  33. Copyright 2018 Pion LLC
  34. Permission is hereby granted, free of charge, to any person obtaining a copy
  35. of this software and associated documentation files (the "Software"), to deal
  36. in the Software without restriction, including without limitation the rights
  37. to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  38. copies of the Software, and to permit persons to whom the Software is
  39. furnished to do so, subject to the following conditions:
  40. The above copyright notice and this permission notice shall be included in all
  41. copies or substantial portions of the Software.
  42. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  43. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  44. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  45. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  46. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  47. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  48. SOFTWARE.
  49. */
  50. // RFC5780: 4.3. Determining NAT Mapping Behavior
  51. func discoverNATMapping(
  52. ctx context.Context,
  53. conn net.PacketConn,
  54. serverAddress string) (NATMapping, error) {
  55. // Test I: Regular binding request
  56. request := stun.MustBuild(stun.TransactionID, stun.BindingRequest)
  57. response, _, err := doSTUNRoundTrip(request, conn, serverAddress)
  58. if err != nil {
  59. return NATMappingUnknown, errors.Trace(err)
  60. }
  61. responseFields := parseSTUNMessage(response)
  62. if responseFields.xorAddr == nil || responseFields.otherAddr == nil {
  63. return NATMappingUnknown, errors.TraceNew("NAT discovery not supported")
  64. }
  65. if responseFields.xorAddr.String() == conn.LocalAddr().String() {
  66. return NATMappingEndpointIndependent, nil
  67. }
  68. otherAddress := responseFields.otherAddr
  69. // Verify that otherAddress, specified by STUN server, is a valid public
  70. // IP before sending a packet to it. This prevents the STUN server
  71. // (or injected response) from redirecting the flow to an internal network.
  72. if common.IsBogon(otherAddress.IP) {
  73. return NATMappingUnknown, errors.TraceNew("OTHER-ADDRESS is bogon")
  74. }
  75. // Test II: Send binding request to the other address but primary port
  76. _, serverPort, err := net.SplitHostPort(serverAddress)
  77. if err != nil {
  78. return NATMappingUnknown, errors.Trace(err)
  79. }
  80. address := net.JoinHostPort(otherAddress.IP.String(), serverPort)
  81. response2, _, err := doSTUNRoundTrip(request, conn, address)
  82. if err != nil {
  83. return NATMappingUnknown, errors.Trace(err)
  84. }
  85. response2Fields := parseSTUNMessage(response2)
  86. if response2Fields.xorAddr.String() == responseFields.xorAddr.String() {
  87. return NATMappingEndpointIndependent, nil
  88. }
  89. // Test III: Send binding request to the other address and port
  90. response3, _, err := doSTUNRoundTrip(request, conn, otherAddress.String())
  91. if err != nil {
  92. return NATMappingUnknown, errors.Trace(err)
  93. }
  94. response3Fields := parseSTUNMessage(response3)
  95. if response3Fields.xorAddr.String() == response2Fields.xorAddr.String() {
  96. return NATMappingAddressDependent, nil
  97. }
  98. return NATMappingAddressPortDependent, nil
  99. }
  100. // RFC5780: 4.4. Determining NAT Filtering Behavior
  101. func discoverNATFiltering(
  102. ctx context.Context,
  103. conn net.PacketConn,
  104. serverAddress string) (NATFiltering, error) {
  105. // Test I: Regular binding request
  106. request := stun.MustBuild(stun.TransactionID, stun.BindingRequest)
  107. response, _, err := doSTUNRoundTrip(request, conn, serverAddress)
  108. if err != nil {
  109. return NATFilteringUnknown, errors.Trace(err)
  110. }
  111. responseFields := parseSTUNMessage(response)
  112. if responseFields.xorAddr == nil || responseFields.otherAddr == nil {
  113. return NATFilteringUnknown, errors.TraceNew("NAT discovery not supported")
  114. }
  115. // Test II: Request to change both IP and port
  116. request = stun.MustBuild(stun.TransactionID, stun.BindingRequest)
  117. request.Add(stun.AttrChangeRequest, []byte{0x00, 0x00, 0x00, 0x06})
  118. _, responseTimeout, err := doSTUNRoundTrip(request, conn, serverAddress)
  119. if err == nil {
  120. return NATFilteringEndpointIndependent, nil
  121. } else if !responseTimeout {
  122. return NATFilteringUnknown, errors.Trace(err)
  123. }
  124. // Test III: Request to change port only
  125. request = stun.MustBuild(stun.TransactionID, stun.BindingRequest)
  126. request.Add(stun.AttrChangeRequest, []byte{0x00, 0x00, 0x00, 0x02})
  127. _, responseTimeout, err = doSTUNRoundTrip(request, conn, serverAddress)
  128. if err == nil {
  129. return NATFilteringAddressDependent, nil
  130. } else if !responseTimeout {
  131. return NATFilteringUnknown, errors.Trace(err)
  132. }
  133. return NATFilteringAddressPortDependent, nil
  134. }
  135. func parseSTUNMessage(message *stun.Message) (ret struct {
  136. xorAddr *stun.XORMappedAddress
  137. otherAddr *stun.OtherAddress
  138. respOrigin *stun.ResponseOrigin
  139. mappedAddr *stun.MappedAddress
  140. software *stun.Software
  141. },
  142. ) {
  143. ret.mappedAddr = &stun.MappedAddress{}
  144. ret.xorAddr = &stun.XORMappedAddress{}
  145. ret.respOrigin = &stun.ResponseOrigin{}
  146. ret.otherAddr = &stun.OtherAddress{}
  147. ret.software = &stun.Software{}
  148. if ret.xorAddr.GetFrom(message) != nil {
  149. ret.xorAddr = nil
  150. }
  151. if ret.otherAddr.GetFrom(message) != nil {
  152. ret.otherAddr = nil
  153. }
  154. if ret.respOrigin.GetFrom(message) != nil {
  155. ret.respOrigin = nil
  156. }
  157. if ret.mappedAddr.GetFrom(message) != nil {
  158. ret.mappedAddr = nil
  159. }
  160. if ret.software.GetFrom(message) != nil {
  161. ret.software = nil
  162. }
  163. return ret
  164. }
  165. // doSTUNRoundTrip returns nil, true, nil on timeout reading a response.
  166. func doSTUNRoundTrip(
  167. request *stun.Message,
  168. conn net.PacketConn,
  169. remoteAddress string) (*stun.Message, bool, error) {
  170. remoteAddr, err := net.ResolveUDPAddr("udp", remoteAddress)
  171. if err != nil {
  172. return nil, false, errors.Trace(err)
  173. }
  174. _ = request.NewTransactionID()
  175. _, err = conn.WriteTo(request.Raw, remoteAddr)
  176. if err != nil {
  177. return nil, false, errors.Trace(err)
  178. }
  179. err = conn.SetReadDeadline(time.Now().Add(discoverNATRoundTripTimeout))
  180. if err != nil {
  181. return nil, false, errors.Trace(err)
  182. }
  183. var buffer [1500]byte
  184. n, _, err := conn.ReadFrom(buffer[:])
  185. if err != nil {
  186. if e, ok := err.(net.Error); ok && e.Timeout() {
  187. return nil, true, errors.Trace(err)
  188. }
  189. return nil, false, errors.Trace(err)
  190. }
  191. response := new(stun.Message)
  192. response.Raw = buffer[:n]
  193. err = response.Decode()
  194. if err != nil {
  195. return nil, false, errors.Trace(err)
  196. }
  197. // Verify that the response packet has the expected transaction ID, to
  198. // partially mitigate against phony injected responses.
  199. if response.TransactionID != request.TransactionID {
  200. return nil, false, errors.TraceNew(
  201. "unexpected response transaction ID")
  202. }
  203. return response, false, nil
  204. }