|
@@ -2,8 +2,8 @@
|
|
|
// Use of this source code is governed by a BSD-style
|
|
// Use of this source code is governed by a BSD-style
|
|
|
// license that can be found in the LICENSE file.
|
|
// license that can be found in the LICENSE file.
|
|
|
|
|
|
|
|
-// Package portmapper is a UDP port mapping client. It currently only does
|
|
|
|
|
-// NAT-PMP, but will likely do UPnP and perhaps PCP later.
|
|
|
|
|
|
|
+// Package portmapper is a UDP port mapping client. It currently allows for mapping over
|
|
|
|
|
+// NAT-PMP and UPnP, but will perhaps do PCP later.
|
|
|
package portmapper
|
|
package portmapper
|
|
|
|
|
|
|
|
import (
|
|
import (
|
|
@@ -17,7 +17,6 @@ import (
|
|
|
"sync"
|
|
"sync"
|
|
|
"time"
|
|
"time"
|
|
|
|
|
|
|
|
- "go4.org/mem"
|
|
|
|
|
"inet.af/netaddr"
|
|
"inet.af/netaddr"
|
|
|
"tailscale.com/net/interfaces"
|
|
"tailscale.com/net/interfaces"
|
|
|
"tailscale.com/net/netns"
|
|
"tailscale.com/net/netns"
|
|
@@ -66,15 +65,34 @@ type Client struct {
|
|
|
pcpSawTime time.Time // time we last saw PCP was available
|
|
pcpSawTime time.Time // time we last saw PCP was available
|
|
|
uPnPSawTime time.Time // time we last saw UPnP was available
|
|
uPnPSawTime time.Time // time we last saw UPnP was available
|
|
|
|
|
|
|
|
- localPort uint16
|
|
|
|
|
- pmpMapping *pmpMapping // non-nil if we have a PMP mapping
|
|
|
|
|
|
|
+ localPort uint16
|
|
|
|
|
+
|
|
|
|
|
+ mapping mapping // non-nil if we have a mapping
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// mapping represents a created port-mapping over some protocol. It specifies a lease duration,
|
|
|
|
|
+// how to release the mapping, and whether the map is still valid.
|
|
|
|
|
+//
|
|
|
|
|
+// After a mapping is created, it should be immutable, and thus reads should be safe across
|
|
|
|
|
+// concurrent goroutines.
|
|
|
|
|
+type mapping interface {
|
|
|
|
|
+ // Release will attempt to unmap the established port mapping. It will block until completion,
|
|
|
|
|
+ // but can be called asynchronously. Release should be idempotent, and thus even if called
|
|
|
|
|
+ // multiple times should not cause additional side-effects.
|
|
|
|
|
+ Release(context.Context)
|
|
|
|
|
+ // goodUntil will return the lease time that the mapping is valid for.
|
|
|
|
|
+ GoodUntil() time.Time
|
|
|
|
|
+ // renewAfter returns the earliest time that the mapping should be renewed.
|
|
|
|
|
+ RenewAfter() time.Time
|
|
|
|
|
+ // externalIPPort indicates what port the mapping can be reached from on the outside.
|
|
|
|
|
+ External() netaddr.IPPort
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// HaveMapping reports whether we have a current valid mapping.
|
|
// HaveMapping reports whether we have a current valid mapping.
|
|
|
func (c *Client) HaveMapping() bool {
|
|
func (c *Client) HaveMapping() bool {
|
|
|
c.mu.Lock()
|
|
c.mu.Lock()
|
|
|
defer c.mu.Unlock()
|
|
defer c.mu.Unlock()
|
|
|
- return c.pmpMapping != nil && c.pmpMapping.goodUntil.After(time.Now())
|
|
|
|
|
|
|
+ return c.mapping != nil && c.mapping.GoodUntil().After(time.Now())
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// pmpMapping is an already-created PMP mapping.
|
|
// pmpMapping is an already-created PMP mapping.
|
|
@@ -94,9 +112,13 @@ func (m *pmpMapping) externalValid() bool {
|
|
|
return !m.external.IP().IsZero() && m.external.Port() != 0
|
|
return !m.external.IP().IsZero() && m.external.Port() != 0
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// release does a best effort fire-and-forget release of the PMP mapping m.
|
|
|
|
|
-func (m *pmpMapping) release() {
|
|
|
|
|
- uc, err := netns.Listener().ListenPacket(context.Background(), "udp4", ":0")
|
|
|
|
|
|
|
+func (p *pmpMapping) GoodUntil() time.Time { return p.goodUntil }
|
|
|
|
|
+func (p *pmpMapping) RenewAfter() time.Time { return p.renewAfter }
|
|
|
|
|
+func (p *pmpMapping) External() netaddr.IPPort { return p.external }
|
|
|
|
|
+
|
|
|
|
|
+// Release does a best effort fire-and-forget release of the PMP mapping m.
|
|
|
|
|
+func (m *pmpMapping) Release(ctx context.Context) {
|
|
|
|
|
+ uc, err := netns.Listener().ListenPacket(ctx, "udp4", ":0")
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
@@ -166,7 +188,6 @@ func (c *Client) gatewayAndSelfIP() (gw, myIP netaddr.IP, ok bool) {
|
|
|
gw = netaddr.IP{}
|
|
gw = netaddr.IP{}
|
|
|
myIP = netaddr.IP{}
|
|
myIP = netaddr.IP{}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
c.mu.Lock()
|
|
c.mu.Lock()
|
|
|
defer c.mu.Unlock()
|
|
defer c.mu.Unlock()
|
|
|
|
|
|
|
@@ -179,11 +200,11 @@ func (c *Client) gatewayAndSelfIP() (gw, myIP netaddr.IP, ok bool) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) invalidateMappingsLocked(releaseOld bool) {
|
|
func (c *Client) invalidateMappingsLocked(releaseOld bool) {
|
|
|
- if c.pmpMapping != nil {
|
|
|
|
|
|
|
+ if c.mapping != nil {
|
|
|
if releaseOld {
|
|
if releaseOld {
|
|
|
- c.pmpMapping.release()
|
|
|
|
|
|
|
+ c.mapping.Release(context.Background())
|
|
|
}
|
|
}
|
|
|
- c.pmpMapping = nil
|
|
|
|
|
|
|
+ c.mapping = nil
|
|
|
}
|
|
}
|
|
|
c.pmpPubIP = netaddr.IP{}
|
|
c.pmpPubIP = netaddr.IP{}
|
|
|
c.pmpPubIPTime = time.Time{}
|
|
c.pmpPubIPTime = time.Time{}
|
|
@@ -262,12 +283,12 @@ func (c *Client) GetCachedMappingOrStartCreatingOne() (external netaddr.IPPort,
|
|
|
|
|
|
|
|
// Do we have an existing mapping that's valid?
|
|
// Do we have an existing mapping that's valid?
|
|
|
now := time.Now()
|
|
now := time.Now()
|
|
|
- if m := c.pmpMapping; m != nil {
|
|
|
|
|
- if now.Before(m.goodUntil) {
|
|
|
|
|
- if now.After(m.renewAfter) {
|
|
|
|
|
|
|
+ if m := c.mapping; m != nil {
|
|
|
|
|
+ if now.Before(m.GoodUntil()) {
|
|
|
|
|
+ if now.After(m.RenewAfter()) {
|
|
|
c.maybeStartMappingLocked()
|
|
c.maybeStartMappingLocked()
|
|
|
}
|
|
}
|
|
|
- return m.external, true
|
|
|
|
|
|
|
+ return m.External(), true
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -315,9 +336,10 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
|
|
|
|
|
|
|
c.mu.Lock()
|
|
c.mu.Lock()
|
|
|
localPort := c.localPort
|
|
localPort := c.localPort
|
|
|
|
|
+ internalAddr := netaddr.IPPortFrom(myIP, localPort)
|
|
|
m := &pmpMapping{
|
|
m := &pmpMapping{
|
|
|
gw: gw,
|
|
gw: gw,
|
|
|
- internal: netaddr.IPPortFrom(myIP, localPort),
|
|
|
|
|
|
|
+ internal: internalAddr,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// prevPort is the port we had most previously, if any. We try
|
|
// prevPort is the port we had most previously, if any. We try
|
|
@@ -326,13 +348,13 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
|
|
|
|
|
|
|
// Do we have an existing mapping that's valid?
|
|
// Do we have an existing mapping that's valid?
|
|
|
now := time.Now()
|
|
now := time.Now()
|
|
|
- if m := c.pmpMapping; m != nil {
|
|
|
|
|
- if now.Before(m.renewAfter) {
|
|
|
|
|
|
|
+ if m := c.mapping; m != nil {
|
|
|
|
|
+ if now.Before(m.RenewAfter()) {
|
|
|
defer c.mu.Unlock()
|
|
defer c.mu.Unlock()
|
|
|
- return m.external, nil
|
|
|
|
|
|
|
+ return m.External(), nil
|
|
|
}
|
|
}
|
|
|
// The mapping might still be valid, so just try to renew it.
|
|
// The mapping might still be valid, so just try to renew it.
|
|
|
- prevPort = m.external.Port()
|
|
|
|
|
|
|
+ prevPort = m.External().Port()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// If we just did a Probe (e.g. via netchecker) but didn't
|
|
// If we just did a Probe (e.g. via netchecker) but didn't
|
|
@@ -344,6 +366,10 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
|
|
}
|
|
}
|
|
|
if c.lastProbe.After(now.Add(-5*time.Second)) && !haveRecentPMP {
|
|
if c.lastProbe.After(now.Add(-5*time.Second)) && !haveRecentPMP {
|
|
|
c.mu.Unlock()
|
|
c.mu.Unlock()
|
|
|
|
|
+ // fallback to UPnP portmapping
|
|
|
|
|
+ if mapping, ok := c.getUPnPPortMapping(ctx, gw, internalAddr, prevPort); ok {
|
|
|
|
|
+ return mapping, nil
|
|
|
|
|
+ }
|
|
|
return netaddr.IPPort{}, NoMappingError{ErrNoPortMappingServices}
|
|
return netaddr.IPPort{}, NoMappingError{ErrNoPortMappingServices}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -381,6 +407,10 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
|
|
if ctx.Err() == context.Canceled {
|
|
if ctx.Err() == context.Canceled {
|
|
|
return netaddr.IPPort{}, err
|
|
return netaddr.IPPort{}, err
|
|
|
}
|
|
}
|
|
|
|
|
+ // fallback to UPnP portmapping
|
|
|
|
|
+ if mapping, ok := c.getUPnPPortMapping(ctx, gw, internalAddr, prevPort); ok {
|
|
|
|
|
+ return mapping, nil
|
|
|
|
|
+ }
|
|
|
return netaddr.IPPort{}, NoMappingError{ErrNoPortMappingServices}
|
|
return netaddr.IPPort{}, NoMappingError{ErrNoPortMappingServices}
|
|
|
}
|
|
}
|
|
|
srcu := srci.(*net.UDPAddr)
|
|
srcu := srci.(*net.UDPAddr)
|
|
@@ -413,7 +443,7 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
|
|
if m.externalValid() {
|
|
if m.externalValid() {
|
|
|
c.mu.Lock()
|
|
c.mu.Lock()
|
|
|
defer c.mu.Unlock()
|
|
defer c.mu.Unlock()
|
|
|
- c.pmpMapping = m
|
|
|
|
|
|
|
+ c.mapping = m
|
|
|
return m.external, nil
|
|
return m.external, nil
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -530,9 +560,27 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
|
|
defer cancel()
|
|
defer cancel()
|
|
|
defer closeCloserOnContextDone(ctx, uc)()
|
|
defer closeCloserOnContextDone(ctx, uc)()
|
|
|
|
|
|
|
|
|
|
+ if c.sawUPnPRecently() {
|
|
|
|
|
+ res.UPnP = true
|
|
|
|
|
+ } else {
|
|
|
|
|
+ hasUPnP := make(chan bool, 1)
|
|
|
|
|
+ defer func() {
|
|
|
|
|
+ res.UPnP = <-hasUPnP
|
|
|
|
|
+ }()
|
|
|
|
|
+ go func() {
|
|
|
|
|
+ client, err := getUPnPClient(ctx, gw)
|
|
|
|
|
+ if err == nil && client != nil {
|
|
|
|
|
+ hasUPnP <- true
|
|
|
|
|
+ c.mu.Lock()
|
|
|
|
|
+ c.uPnPSawTime = time.Now()
|
|
|
|
|
+ c.mu.Unlock()
|
|
|
|
|
+ }
|
|
|
|
|
+ close(hasUPnP)
|
|
|
|
|
+ }()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
pcpAddr := netaddr.IPPortFrom(gw, pcpPort).UDPAddr()
|
|
pcpAddr := netaddr.IPPortFrom(gw, pcpPort).UDPAddr()
|
|
|
pmpAddr := netaddr.IPPortFrom(gw, pmpPort).UDPAddr()
|
|
pmpAddr := netaddr.IPPortFrom(gw, pmpPort).UDPAddr()
|
|
|
- upnpAddr := netaddr.IPPortFrom(gw, upnpPort).UDPAddr()
|
|
|
|
|
|
|
|
|
|
// Don't send probes to services that we recently learned (for
|
|
// Don't send probes to services that we recently learned (for
|
|
|
// the same gw/myIP) are available. See
|
|
// the same gw/myIP) are available. See
|
|
@@ -547,16 +595,11 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
|
|
} else {
|
|
} else {
|
|
|
uc.WriteTo(pcpAnnounceRequest(myIP), pcpAddr)
|
|
uc.WriteTo(pcpAnnounceRequest(myIP), pcpAddr)
|
|
|
}
|
|
}
|
|
|
- if c.sawUPnPRecently() {
|
|
|
|
|
- res.UPnP = true
|
|
|
|
|
- } else {
|
|
|
|
|
- uc.WriteTo(uPnPPacket, upnpAddr)
|
|
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
buf := make([]byte, 1500)
|
|
buf := make([]byte, 1500)
|
|
|
pcpHeard := false // true when we get any PCP response
|
|
pcpHeard := false // true when we get any PCP response
|
|
|
for {
|
|
for {
|
|
|
- if pcpHeard && res.PMP && res.UPnP {
|
|
|
|
|
|
|
+ if pcpHeard && res.PMP {
|
|
|
// Nothing more to discover.
|
|
// Nothing more to discover.
|
|
|
return res, nil
|
|
return res, nil
|
|
|
}
|
|
}
|
|
@@ -569,13 +612,6 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
|
|
}
|
|
}
|
|
|
port := addr.(*net.UDPAddr).Port
|
|
port := addr.(*net.UDPAddr).Port
|
|
|
switch port {
|
|
switch port {
|
|
|
- case upnpPort:
|
|
|
|
|
- if mem.Contains(mem.B(buf[:n]), mem.S(":InternetGatewayDevice:")) {
|
|
|
|
|
- res.UPnP = true
|
|
|
|
|
- c.mu.Lock()
|
|
|
|
|
- c.uPnPSawTime = time.Now()
|
|
|
|
|
- c.mu.Unlock()
|
|
|
|
|
- }
|
|
|
|
|
case pcpPort: // same as pmpPort
|
|
case pcpPort: // same as pmpPort
|
|
|
if pres, ok := parsePCPResponse(buf[:n]); ok {
|
|
if pres, ok := parsePCPResponse(buf[:n]); ok {
|
|
|
if pres.OpCode == pcpOpReply|pcpOpAnnounce {
|
|
if pres.OpCode == pcpOpReply|pcpOpAnnounce {
|
|
@@ -687,14 +723,4 @@ func parsePCPResponse(b []byte) (res pcpResponse, ok bool) {
|
|
|
return res, true
|
|
return res, true
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const (
|
|
|
|
|
- upnpPort = 1900
|
|
|
|
|
-)
|
|
|
|
|
-
|
|
|
|
|
-var uPnPPacket = []byte("M-SEARCH * HTTP/1.1\r\n" +
|
|
|
|
|
- "HOST: 239.255.255.250:1900\r\n" +
|
|
|
|
|
- "ST: ssdp:all\r\n" +
|
|
|
|
|
- "MAN: \"ssdp:discover\"\r\n" +
|
|
|
|
|
- "MX: 2\r\n\r\n")
|
|
|
|
|
-
|
|
|
|
|
var pmpReqExternalAddrPacket = []byte{0, 0} // version 0, opcode 0 = "Public address request"
|
|
var pmpReqExternalAddrPacket = []byte{0, 0} // version 0, opcode 0 = "Public address request"
|