portmapper.go 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. //go:build PSIPHON_ENABLE_INPROXY
  2. /*
  3. * Copyright (c) 2023, 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. "fmt"
  24. "sync"
  25. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
  26. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
  27. "tailscale.com/net/portmapper"
  28. "tailscale.com/util/clientmetric"
  29. )
  30. // initPortMapper resets port mapping metrics state associated with the
  31. // current network when the network changes, as indicated by
  32. // WebRTCDialCoordinator.NetworkID. initPortMapper also configures the port
  33. // mapping routines to use WebRTCDialCoordinator.BindToDevice. Varying
  34. // WebRTCDialCoordinator.BindToDevice between dials in a single process is not
  35. // supported.
  36. func initPortMapper(coordinator WebRTCDialCoordinator) {
  37. // It's safe for multiple, concurrent client dials to call
  38. // resetRespondingPortMappingTypes: as long as the network ID does not
  39. // change, calls won't clear any valid port mapping type metrics that
  40. // were just recorded.
  41. resetRespondingPortMappingTypes(coordinator.NetworkID())
  42. // WebRTCDialCoordinator.BindToDevice is set as a global variable in
  43. // tailscale.com/net/portmapper. It's safe to repeatedly call
  44. // setPortMapperBindToDevice here, under the assumption that
  45. // WebRTCDialCoordinator.BindToDevice is the same single, static function
  46. // for all dials. This assumption is true for Psiphon.
  47. setPortMapperBindToDevice(coordinator)
  48. }
  49. // portMapper represents a UDP port mapping from a local port to an external,
  50. // publicly addressable IP and port. Port mapping is implemented using
  51. // tailscale.com/net/portmapper, which probes the local network and gateway
  52. // for UPnP-IGD, NAT-PMP, and PCP port mapping capabilities.
  53. type portMapper struct {
  54. havePortMappingOnce sync.Once
  55. portMappingAddress chan string
  56. client *portmapper.Client
  57. }
  58. // newPortMapper initializes a new port mapper, configured to map to the
  59. // specified localPort. newPortMapper does not initiate any network
  60. // operations (it's safe to call when DisablePortMapping is set).
  61. func newPortMapper(
  62. logger common.Logger,
  63. localPort int) *portMapper {
  64. portMappingLogger := func(format string, args ...any) {
  65. logger.WithTrace().Info("port mapping: " + fmt.Sprintf(format, args))
  66. }
  67. p := &portMapper{
  68. portMappingAddress: make(chan string, 1),
  69. }
  70. // This code assumes assumes tailscale NewClient call does only
  71. // initialization; this is the case as of tailscale.com/net/portmapper
  72. // v1.36.2.
  73. //
  74. // This code further assumes that the onChanged callback passed to
  75. // NewClient will not be invoked until after the
  76. // GetCachedMappingOrStartCreatingOne call in portMapper.start; and so
  77. // the p.client reference within callback will be valid.
  78. client := portmapper.NewClient(portMappingLogger, nil, nil, func() {
  79. p.havePortMappingOnce.Do(func() {
  80. address, ok := p.client.GetCachedMappingOrStartCreatingOne()
  81. if ok {
  82. // With sync.Once and a buffer size of 1, this send won't block.
  83. p.portMappingAddress <- address.String()
  84. } else {
  85. // This is not an expected case; there should be a port
  86. // mapping when NewClient is invoked.
  87. //
  88. // TODO: deliver "" to the channel? Otherwise, receiving on
  89. // portMapper.portMappingExternalAddress will hang, or block
  90. // until a context is done.
  91. portMappingLogger("unexpected missing port mapping")
  92. }
  93. })
  94. })
  95. p.client = client
  96. p.client.SetLocalPort(uint16(localPort))
  97. return p
  98. }
  99. // start initiates the port mapping attempt.
  100. func (p *portMapper) start() {
  101. _, _ = p.client.GetCachedMappingOrStartCreatingOne()
  102. }
  103. // portMappingExternalAddress returns a channel which receives a successful
  104. // port mapping external address, if any.
  105. func (p *portMapper) portMappingExternalAddress() <-chan string {
  106. return p.portMappingAddress
  107. }
  108. // close releases the port mapping
  109. func (p *portMapper) close() error {
  110. return errors.Trace(p.client.Close())
  111. }
  112. // probePortMapping discovers and reports which port mapping protocols are
  113. // supported on this network. probePortMapping does not establish a port mapping.
  114. //
  115. // It is intended that in-proxies amake a blocking call to probePortMapping on
  116. // start up (and after a network change) in order to report fresh port
  117. // mapping type metrics, for matching optimization in the ProxyAnnounce
  118. // request. Clients don't incur the delay of a probe call -- which produces
  119. // no port mapping -- and instead opportunistically grab port mapping type
  120. // metrics via getRespondingPortMappingTypes.
  121. func probePortMapping(
  122. ctx context.Context,
  123. logger common.Logger) (PortMappingTypes, error) {
  124. portMappingLogger := func(format string, args ...any) {
  125. logger.WithTrace().Info("port mapping probe: " + fmt.Sprintf(format, args))
  126. }
  127. client := portmapper.NewClient(portMappingLogger, nil, nil, nil)
  128. defer client.Close()
  129. result, err := client.Probe(ctx)
  130. if err != nil {
  131. return nil, errors.Trace(err)
  132. }
  133. portMappingTypes := PortMappingTypes{}
  134. if result.UPnP {
  135. portMappingTypes = append(portMappingTypes, PortMappingTypeUPnP)
  136. }
  137. if result.PMP {
  138. portMappingTypes = append(portMappingTypes, PortMappingTypePMP)
  139. }
  140. if result.PCP {
  141. portMappingTypes = append(portMappingTypes, PortMappingTypePCP)
  142. }
  143. // An empty lists means discovery is needed or the available port mappings
  144. // are unknown; a list with None indicates that a probe returned no
  145. // supported port mapping types.
  146. if len(portMappingTypes) == 0 {
  147. portMappingTypes = append(portMappingTypes, PortMappingTypeNone)
  148. }
  149. return portMappingTypes, nil
  150. }
  151. var respondingPortMappingTypesMutex sync.Mutex
  152. var respondingPortMappingTypesNetworkID string
  153. // resetRespondingPortMappingTypes clears tailscale.com/net/portmapper global
  154. // metrics fields which indicate which port mapping types are responding on
  155. // the current network. These metrics should be cleared whenever the current
  156. // network changes, as indicated by networkID.
  157. //
  158. // Limitations: there may be edge conditions where a
  159. // tailscale.com/net/portmapper client logs metrics concurrent to
  160. // resetRespondingPortMappingTypes being called with a new networkID. If
  161. // incorrect port mapping type metrics are supported, the Broker may log
  162. // incorrect statistics. However, Broker client/in-proxy matching is based on
  163. // actually established port mappings.
  164. func resetRespondingPortMappingTypes(networkID string) {
  165. respondingPortMappingTypesMutex.Lock()
  166. defer respondingPortMappingTypesMutex.Unlock()
  167. if respondingPortMappingTypesNetworkID != networkID {
  168. // Iterating over all metric fields appears to be the only API available.
  169. for _, metric := range clientmetric.Metrics() {
  170. switch metric.Name() {
  171. case "portmap_upnp_ok", "portmap_pmp_ok", "portmap_pcp_ok":
  172. metric.Set(0)
  173. }
  174. }
  175. respondingPortMappingTypesNetworkID = networkID
  176. }
  177. }
  178. // getRespondingPortMappingTypes returns the port mapping types that responded
  179. // during recent portMapper.start invocations as well as probePortMapping
  180. // invocations. The returned list is used for reporting metrics. See
  181. // resetRespondingPortMappingTypes for considerations due to accessing
  182. // tailscale.com/net/portmapper global metrics fields.
  183. //
  184. // To avoid delays, we do not run probePortMapping for regular client dials,
  185. // and so instead use this tailscale.com/net/portmapper metrics field
  186. // approach.
  187. //
  188. // Limitations: the return value represents all port mapping types that
  189. // responded in this session, since the last network change
  190. // (resetRespondingPortMappingTypes call); and do not indicate which of
  191. // several port mapping types may have been used for a particular dial.
  192. func getRespondingPortMappingTypes(networkID string) PortMappingTypes {
  193. respondingPortMappingTypesMutex.Lock()
  194. defer respondingPortMappingTypesMutex.Unlock()
  195. portMappingTypes := PortMappingTypes{}
  196. if respondingPortMappingTypesNetworkID != networkID {
  197. // The network changed since the last resetRespondingPortMappingTypes
  198. // call, and resetRespondingPortMappingTypes has not yet been called
  199. // again. Ignore the current metrics.
  200. return portMappingTypes
  201. }
  202. // Iterating over all metric fields appears to be the only API available.
  203. for _, metric := range clientmetric.Metrics() {
  204. if metric.Name() == "portmap_upnp_ok" && metric.Value() > 1 {
  205. portMappingTypes = append(portMappingTypes, PortMappingTypeUPnP)
  206. }
  207. if metric.Name() == "portmap_pmp_ok" && metric.Value() > 1 {
  208. portMappingTypes = append(portMappingTypes, PortMappingTypePMP)
  209. }
  210. if metric.Name() == "portmap_pcp_ok" && metric.Value() > 1 {
  211. portMappingTypes = append(portMappingTypes, PortMappingTypePCP)
  212. }
  213. }
  214. return portMappingTypes
  215. }