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. } else {
  98. return NATMappingAddressPortDependent, nil
  99. }
  100. return NATMappingUnknown, nil
  101. }
  102. // RFC5780: 4.4. Determining NAT Filtering Behavior
  103. func discoverNATFiltering(
  104. ctx context.Context,
  105. conn net.PacketConn,
  106. serverAddress string) (NATFiltering, error) {
  107. // Test I: Regular binding request
  108. request := stun.MustBuild(stun.TransactionID, stun.BindingRequest)
  109. response, _, err := doSTUNRoundTrip(request, conn, serverAddress)
  110. if err != nil {
  111. return NATFilteringUnknown, errors.Trace(err)
  112. }
  113. responseFields := parseSTUNMessage(response)
  114. if responseFields.xorAddr == nil || responseFields.otherAddr == nil {
  115. return NATFilteringUnknown, errors.TraceNew("NAT discovery not supported")
  116. }
  117. // Test II: Request to change both IP and port
  118. request = stun.MustBuild(stun.TransactionID, stun.BindingRequest)
  119. request.Add(stun.AttrChangeRequest, []byte{0x00, 0x00, 0x00, 0x06})
  120. response, responseTimeout, err := doSTUNRoundTrip(request, conn, serverAddress)
  121. if err == nil {
  122. return NATFilteringEndpointIndependent, nil
  123. } else if !responseTimeout {
  124. return NATFilteringUnknown, errors.Trace(err)
  125. }
  126. // Test III: Request to change port only
  127. request = stun.MustBuild(stun.TransactionID, stun.BindingRequest)
  128. request.Add(stun.AttrChangeRequest, []byte{0x00, 0x00, 0x00, 0x02})
  129. response, responseTimeout, err = doSTUNRoundTrip(request, conn, serverAddress)
  130. if err == nil {
  131. return NATFilteringAddressDependent, nil
  132. } else if !responseTimeout {
  133. return NATFilteringUnknown, errors.Trace(err)
  134. }
  135. return NATFilteringAddressPortDependent, nil
  136. }
  137. func parseSTUNMessage(message *stun.Message) (ret struct {
  138. xorAddr *stun.XORMappedAddress
  139. otherAddr *stun.OtherAddress
  140. respOrigin *stun.ResponseOrigin
  141. mappedAddr *stun.MappedAddress
  142. software *stun.Software
  143. },
  144. ) {
  145. ret.mappedAddr = &stun.MappedAddress{}
  146. ret.xorAddr = &stun.XORMappedAddress{}
  147. ret.respOrigin = &stun.ResponseOrigin{}
  148. ret.otherAddr = &stun.OtherAddress{}
  149. ret.software = &stun.Software{}
  150. if ret.xorAddr.GetFrom(message) != nil {
  151. ret.xorAddr = nil
  152. }
  153. if ret.otherAddr.GetFrom(message) != nil {
  154. ret.otherAddr = nil
  155. }
  156. if ret.respOrigin.GetFrom(message) != nil {
  157. ret.respOrigin = nil
  158. }
  159. if ret.mappedAddr.GetFrom(message) != nil {
  160. ret.mappedAddr = nil
  161. }
  162. if ret.software.GetFrom(message) != nil {
  163. ret.software = nil
  164. }
  165. return ret
  166. }
  167. // doSTUNRoundTrip returns nil, true, nil on timeout reading a response.
  168. func doSTUNRoundTrip(
  169. request *stun.Message,
  170. conn net.PacketConn,
  171. remoteAddress string) (*stun.Message, bool, error) {
  172. remoteAddr, err := net.ResolveUDPAddr("udp", remoteAddress)
  173. if err != nil {
  174. return nil, false, errors.Trace(err)
  175. }
  176. _ = request.NewTransactionID()
  177. _, err = conn.WriteTo(request.Raw, remoteAddr)
  178. if err != nil {
  179. return nil, false, errors.Trace(err)
  180. }
  181. conn.SetReadDeadline(time.Now().Add(discoverNATRoundTripTimeout))
  182. var buffer [1500]byte
  183. n, _, err := conn.ReadFrom(buffer[:])
  184. if err != nil {
  185. if e, ok := err.(net.Error); ok && e.Timeout() {
  186. return nil, true, errors.Trace(err)
  187. }
  188. return nil, false, errors.Trace(err)
  189. }
  190. response := new(stun.Message)
  191. response.Raw = buffer[:n]
  192. err = response.Decode()
  193. if err != nil {
  194. return nil, false, errors.Trace(err)
  195. }
  196. // Verify that the response packet has the expected transaction ID, to
  197. // partially mitigate against phony injected responses.
  198. if response.TransactionID != request.TransactionID {
  199. return nil, false, errors.TraceNew(
  200. "unexpected response transaction ID")
  201. }
  202. return response, false, nil
  203. }