|
@@ -24,8 +24,11 @@ package inproxy
|
|
|
import (
|
|
import (
|
|
|
"context"
|
|
"context"
|
|
|
"fmt"
|
|
"fmt"
|
|
|
|
|
+ "reflect"
|
|
|
|
|
+ "runtime/debug"
|
|
|
"strings"
|
|
"strings"
|
|
|
"sync"
|
|
"sync"
|
|
|
|
|
+ "unsafe"
|
|
|
|
|
|
|
|
"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
|
|
"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
|
|
|
"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
|
|
"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
|
|
@@ -68,10 +71,23 @@ type portMapper struct {
|
|
|
|
|
|
|
|
// newPortMapper initializes a new port mapper, configured to map to the
|
|
// newPortMapper initializes a new port mapper, configured to map to the
|
|
|
// specified localPort. newPortMapper does not initiate any network
|
|
// specified localPort. newPortMapper does not initiate any network
|
|
|
-// operations (it's safe to call when DisablePortMapping is set).
|
|
|
|
|
|
|
+// operations.
|
|
|
|
|
+//
|
|
|
|
|
+// newPortMapper requires a PortMappingProbe initialized by probePortMapping,
|
|
|
|
|
+// as the underlying portmapper.Client.GetCachedMappingOrStartCreatingOne
|
|
|
|
|
+// requires data populated by Client.Probe, such as UPnP service
|
|
|
|
|
+// information.
|
|
|
|
|
+//
|
|
|
|
|
+// Rather that run a full Client.Probe per port mapping, the service data from
|
|
|
|
|
+// one probe run is reused.
|
|
|
func newPortMapper(
|
|
func newPortMapper(
|
|
|
logger common.Logger,
|
|
logger common.Logger,
|
|
|
- localPort int) *portMapper {
|
|
|
|
|
|
|
+ probe *PortMappingProbe,
|
|
|
|
|
+ localPort int) (*portMapper, error) {
|
|
|
|
|
+
|
|
|
|
|
+ if probe == nil {
|
|
|
|
|
+ return nil, errors.TraceNew("missing probe")
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
portMappingLogger := func(format string, args ...any) {
|
|
portMappingLogger := func(format string, args ...any) {
|
|
|
logger.WithTrace().Info(
|
|
logger.WithTrace().Info(
|
|
@@ -93,6 +109,9 @@ func newPortMapper(
|
|
|
// the p.client reference within callback will be valid.
|
|
// the p.client reference within callback will be valid.
|
|
|
|
|
|
|
|
client := portmapper.NewClient(portMappingLogger, nil, nil, nil, func() {
|
|
client := portmapper.NewClient(portMappingLogger, nil, nil, nil, func() {
|
|
|
|
|
+ if !p.client.HaveMapping() {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
p.havePortMappingOnce.Do(func() {
|
|
p.havePortMappingOnce.Do(func() {
|
|
|
address, ok := p.client.GetCachedMappingOrStartCreatingOne()
|
|
address, ok := p.client.GetCachedMappingOrStartCreatingOne()
|
|
|
if ok {
|
|
if ok {
|
|
@@ -116,12 +135,114 @@ func newPortMapper(
|
|
|
|
|
|
|
|
p.client.SetLocalPort(uint16(localPort))
|
|
p.client.SetLocalPort(uint16(localPort))
|
|
|
|
|
|
|
|
- return p
|
|
|
|
|
|
|
+ // Copy the port mapping service data from the input probe.
|
|
|
|
|
+ err := p.cloneProbe(probe)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil, errors.Trace(err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return p, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+var portmapperDependencyVersionCheck bool
|
|
|
|
|
+
|
|
|
|
|
+func init() {
|
|
|
|
|
+ buildInfo, ok := debug.ReadBuildInfo()
|
|
|
|
|
+ if !ok {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ for _, dep := range buildInfo.Deps {
|
|
|
|
|
+ if dep.Path == "tailscale.com" && dep.Version == "v1.58.2" {
|
|
|
|
|
+ portmapperDependencyVersionCheck = true
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// cloneProbe copies the port mapping service data gather by Client.Probe from
|
|
|
|
|
+// the input probe client.
|
|
|
|
|
+func (p *portMapper) cloneProbe(probe *PortMappingProbe) error {
|
|
|
|
|
+
|
|
|
|
|
+ // The required portmapper.Client fields are not exported by
|
|
|
|
|
+ // tailscale/net/portmapper, so unsafe reflection is used to copy the
|
|
|
|
|
+ // values. A simple portmapper.Client struct copy can't be performed as
|
|
|
|
|
+ // the struct contain a sync.Mutex field.
|
|
|
|
|
+ //
|
|
|
|
|
+ // The following is assumed, based on the pinned dependency version:
|
|
|
|
|
+ //
|
|
|
|
|
+ // - portmapper.Client.Probe is synchronous, so once probe.client.Probe is
|
|
|
|
|
+ // complete, it's safe to read its fields
|
|
|
|
|
+ //
|
|
|
|
|
+ // - portmapping.Probe does not create a cached mapping.
|
|
|
|
|
+ //
|
|
|
|
|
+ // - Only Probe populates the copied fields and
|
|
|
|
|
+ // portmapper.Client.GetCachedMappingOrStartCreatingOne merely reads
|
|
|
|
|
+ // them (or clears them, in invalidateMappingsLocked)
|
|
|
|
|
+ //
|
|
|
|
|
+ // We further assume that the caller synchronizes access to the input
|
|
|
|
|
+ // probe, so the probe is idle when cloned
|
|
|
|
|
+ // (see Proxy.networkDiscoveryMutex).
|
|
|
|
|
+ //
|
|
|
|
|
+ // An explicit dependency version pin check is made since potential logic
|
|
|
|
|
+ // changes in future versions of the dependency may break the above
|
|
|
|
|
+ // assumptions while the reflect operation might still succeed.
|
|
|
|
|
+ //
|
|
|
|
|
+ // TODO: fork the dependency to add internal support for shared probe
|
|
|
|
|
+ // state, trim additional tailscale dependencies, use Psiphon's custom
|
|
|
|
|
+ // dialer, and remove globals (see clientmetric.Metrics below).
|
|
|
|
|
+
|
|
|
|
|
+ if !portmapperDependencyVersionCheck {
|
|
|
|
|
+ return errors.TraceNew("dependency version check failed")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ src := reflect.ValueOf(probe.client).Elem()
|
|
|
|
|
+ dst := reflect.ValueOf(p.client).Elem()
|
|
|
|
|
+
|
|
|
|
|
+ shallowCloneField := func(name string) error {
|
|
|
|
|
+ srcField := src.FieldByName(name)
|
|
|
|
|
+ dstField := dst.FieldByName(name)
|
|
|
|
|
+ // Bypass "reflect: reflect.Value.Set using value obtained using
|
|
|
|
|
+ // unexported field" restriction.
|
|
|
|
|
+ srcField = reflect.NewAt(
|
|
|
|
|
+ srcField.Type(), unsafe.Pointer(srcField.UnsafeAddr())).Elem()
|
|
|
|
|
+ dstField = reflect.NewAt(
|
|
|
|
|
+ dstField.Type(), unsafe.Pointer(dstField.UnsafeAddr())).Elem()
|
|
|
|
|
+ if !srcField.CanSet() || !dstField.CanSet() {
|
|
|
|
|
+ return errors.Tracef("%s: cannot set field", name)
|
|
|
|
|
+ }
|
|
|
|
|
+ dstField.Set(srcField)
|
|
|
|
|
+ return nil
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // As of the pinned dependency version,
|
|
|
|
|
+ // portmapper.invalidateMappingsLocked sets uPnPMetas to nil, but doesn't
|
|
|
|
|
+ // write to the original slice elements, so a shallow copy is sufficient.
|
|
|
|
|
+
|
|
|
|
|
+ for _, fieldName := range []string{
|
|
|
|
|
+ "lastMyIP",
|
|
|
|
|
+ "lastGW",
|
|
|
|
|
+ "lastProbe",
|
|
|
|
|
+ "pmpPubIP",
|
|
|
|
|
+ "pmpPubIPTime",
|
|
|
|
|
+ "pmpLastEpoch",
|
|
|
|
|
+ "pcpSawTime",
|
|
|
|
|
+ "pcpLastEpoch",
|
|
|
|
|
+ "uPnPSawTime",
|
|
|
|
|
+ "uPnPMetas",
|
|
|
|
|
+ } {
|
|
|
|
|
+ err := shallowCloneField(fieldName)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return errors.Trace(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return nil
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// start initiates the port mapping attempt.
|
|
// start initiates the port mapping attempt.
|
|
|
func (p *portMapper) start() {
|
|
func (p *portMapper) start() {
|
|
|
p.portMappingLogger("started")
|
|
p.portMappingLogger("started")
|
|
|
|
|
+ // There is no cached mapping at this point.
|
|
|
_, _ = p.client.GetCachedMappingOrStartCreatingOne()
|
|
_, _ = p.client.GetCachedMappingOrStartCreatingOne()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -133,6 +254,11 @@ func (p *portMapper) portMappingExternalAddress() <-chan string {
|
|
|
|
|
|
|
|
// close releases the port mapping
|
|
// close releases the port mapping
|
|
|
func (p *portMapper) close() error {
|
|
func (p *portMapper) close() error {
|
|
|
|
|
+
|
|
|
|
|
+ // TODO: it's not clear whether a concurrent portmapper.Client.createOrGetMapping,
|
|
|
|
|
+ // in progress at the time of the portmapper.Client call, will dispose of
|
|
|
|
|
+ // any created mapping if it completes after Close.
|
|
|
|
|
+
|
|
|
err := p.client.Close()
|
|
err := p.client.Close()
|
|
|
p.portMappingLogger("closed")
|
|
p.portMappingLogger("closed")
|
|
|
return errors.Trace(err)
|
|
return errors.Trace(err)
|
|
@@ -147,18 +273,24 @@ func formatPortMappingLog(format string, args ...any) string {
|
|
|
return fmt.Sprintf(format, args...)
|
|
return fmt.Sprintf(format, args...)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// PortMappingProbe records information about the port mapping services found
|
|
|
|
|
+// in a port mapping service probe.
|
|
|
|
|
+type PortMappingProbe struct {
|
|
|
|
|
+ client *portmapper.Client
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// probePortMapping discovers and reports which port mapping protocols are
|
|
// probePortMapping discovers and reports which port mapping protocols are
|
|
|
-// supported on this network. probePortMapping does not establish a port mapping.
|
|
|
|
|
|
|
+// supported on this network. probePortMapping does not establish a port
|
|
|
|
|
+// mapping. probePortMapping caches a PortMappingProbe for use in subsequent
|
|
|
|
|
+// port mapping establishment.
|
|
|
//
|
|
//
|
|
|
-// It is intended that in-proxies amake a blocking call to probePortMapping on
|
|
|
|
|
-// start up (and after a network change) in order to report fresh port
|
|
|
|
|
-// mapping type metrics, for matching optimization in the ProxyAnnounce
|
|
|
|
|
-// request. Clients don't incur the delay of a probe call -- which produces
|
|
|
|
|
-// no port mapping -- and instead opportunistically grab port mapping type
|
|
|
|
|
-// metrics via getRespondingPortMappingTypes.
|
|
|
|
|
|
|
+// It is intended that in-proxy proxies make a blocking call to
|
|
|
|
|
+// probePortMapping on start up (and after a network change) in order to
|
|
|
|
|
+// report fresh port mapping type metrics, for matching optimization in the
|
|
|
|
|
+// ProxyAnnounce request.
|
|
|
func probePortMapping(
|
|
func probePortMapping(
|
|
|
ctx context.Context,
|
|
ctx context.Context,
|
|
|
- logger common.Logger) (PortMappingTypes, error) {
|
|
|
|
|
|
|
+ logger common.Logger) (PortMappingTypes, *PortMappingProbe, error) {
|
|
|
|
|
|
|
|
portMappingLogger := func(format string, args ...any) {
|
|
portMappingLogger := func(format string, args ...any) {
|
|
|
logger.WithTrace().Info(
|
|
logger.WithTrace().Info(
|
|
@@ -166,11 +298,10 @@ func probePortMapping(
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
client := portmapper.NewClient(portMappingLogger, nil, nil, nil, nil)
|
|
client := portmapper.NewClient(portMappingLogger, nil, nil, nil, nil)
|
|
|
- defer client.Close()
|
|
|
|
|
|
|
|
|
|
result, err := client.Probe(ctx)
|
|
result, err := client.Probe(ctx)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
- return nil, errors.Trace(err)
|
|
|
|
|
|
|
+ return nil, nil, errors.Trace(err)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
portMappingTypes := PortMappingTypes{}
|
|
portMappingTypes := PortMappingTypes{}
|
|
@@ -184,15 +315,30 @@ func probePortMapping(
|
|
|
portMappingTypes = append(portMappingTypes, PortMappingTypePCP)
|
|
portMappingTypes = append(portMappingTypes, PortMappingTypePCP)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // An empty lists means discovery is needed or the available port mappings
|
|
|
|
|
- // are unknown; a list with None indicates that a probe returned no
|
|
|
|
|
- // supported port mapping types.
|
|
|
|
|
|
|
+ var probe *PortMappingProbe
|
|
|
|
|
|
|
|
if len(portMappingTypes) == 0 {
|
|
if len(portMappingTypes) == 0 {
|
|
|
|
|
+
|
|
|
|
|
+ // An empty lists means discovery is needed or the available port mappings
|
|
|
|
|
+ // are unknown; a list with None indicates that a probe returned no
|
|
|
|
|
+ // supported port mapping types.
|
|
|
|
|
+
|
|
|
portMappingTypes = append(portMappingTypes, PortMappingTypeNone)
|
|
portMappingTypes = append(portMappingTypes, PortMappingTypeNone)
|
|
|
|
|
+
|
|
|
|
|
+ } else {
|
|
|
|
|
+
|
|
|
|
|
+ // Return a probe for use in subsequent port mappings only when
|
|
|
|
|
+ // services were found.
|
|
|
|
|
+ //
|
|
|
|
|
+ // It is not necessary to call PortMappingProbe.client.Close, as it is
|
|
|
|
|
+ // not holding open any actual mappings.
|
|
|
|
|
+
|
|
|
|
|
+ probe = &PortMappingProbe{
|
|
|
|
|
+ client: client,
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- return portMappingTypes, nil
|
|
|
|
|
|
|
+ return portMappingTypes, probe, nil
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
var respondingPortMappingTypesMutex sync.Mutex
|
|
var respondingPortMappingTypesMutex sync.Mutex
|