Update
This commit is contained in:
1 parent
3530afb854
commit
6903fcff27
2146 files changed
+112256
-190703
No files matched your search
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build js
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !js
|
||||
|
||||
+25
-11
@@ -1,11 +1,10 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package portmapper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
@@ -26,7 +25,9 @@ const (
|
||||
pcpVersion = 2
|
||||
pcpDefaultPort = 5351
|
||||
|
||||
pcpMapLifetimeSec = 7200 // TODO does the RFC recommend anything? This is taken from PMP.
|
||||
// Use the same lifetime as NAT-PMP given that we don't know how long
|
||||
// the IP assignment is valid for.
|
||||
pcpMapLifetimeSec = 7200
|
||||
|
||||
pcpCodeOK pcpResultCode = 0
|
||||
pcpCodeNotAuthorized pcpResultCode = 2
|
||||
@@ -45,6 +46,10 @@ const (
|
||||
pcpTCPMapping = 6 // portmap TCP
|
||||
)
|
||||
|
||||
// A pcpNonce is a cookie assigned by the router when issuing a mapping, that
|
||||
// the client retains in order to release or update the mapping later.
|
||||
type pcpNonce [12]byte
|
||||
|
||||
type pcpMapping struct {
|
||||
c *Client
|
||||
gw netip.AddrPort
|
||||
@@ -55,6 +60,7 @@ type pcpMapping struct {
|
||||
goodUntil time.Time
|
||||
|
||||
epoch uint32
|
||||
nonce pcpNonce
|
||||
}
|
||||
|
||||
func (p *pcpMapping) MappingType() string { return "pcp" }
|
||||
@@ -62,9 +68,9 @@ func (p *pcpMapping) GoodUntil() time.Time { return p.goodUntil }
|
||||
func (p *pcpMapping) RenewAfter() time.Time { return p.renewAfter }
|
||||
func (p *pcpMapping) External() netip.AddrPort { return p.external }
|
||||
func (p *pcpMapping) MappingDebug() string {
|
||||
return fmt.Sprintf("pcpMapping{gw:%v, external:%v, internal:%v, renewAfter:%d, goodUntil:%d}",
|
||||
return fmt.Sprintf("pcpMapping{gw:%v, external:%v, internal:%v, renewAfter:%d, goodUntil:%d, nonce:%x}",
|
||||
p.gw, p.external, p.internal,
|
||||
p.renewAfter.Unix(), p.goodUntil.Unix())
|
||||
p.renewAfter.Unix(), p.goodUntil.Unix(), p.nonce)
|
||||
}
|
||||
|
||||
func (p *pcpMapping) Release(ctx context.Context) {
|
||||
@@ -73,19 +79,29 @@ func (p *pcpMapping) Release(ctx context.Context) {
|
||||
return
|
||||
}
|
||||
defer uc.Close()
|
||||
pkt := buildPCPRequestMappingPacket(p.internal.Addr(), p.internal.Port(), p.external.Port(), 0, p.external.Addr())
|
||||
// Per RFC 6887 section 15.1 (with Errata ID 3621), a mapping-delete
|
||||
// request (lifetime 0) MUST set the Suggested External Port to zero and
|
||||
// the Suggested External Address to the all-zeros address of the family
|
||||
// being deleted: ::ffff:0.0.0.0 for IPv4, :: for IPv6.
|
||||
zeroExtAddr := netip.IPv4Unspecified()
|
||||
if p.external.Addr().Is6() {
|
||||
zeroExtAddr = netip.IPv6Unspecified()
|
||||
}
|
||||
pkt := buildPCPRequestMappingPacket(p.internal.Addr(), p.internal.Port(), 0, 0, zeroExtAddr, p.nonce)
|
||||
uc.WriteToUDPAddrPort(pkt, p.gw)
|
||||
}
|
||||
|
||||
// buildPCPRequestMappingPacket generates a PCP packet with a MAP opcode.
|
||||
// To create a packet which deletes a mapping, lifetimeSec should be set to 0.
|
||||
// To create a packet which deletes a mapping, lifetimeSec, prevPort and prevExternalIP should be set to 0.
|
||||
// If prevPort is not known, it should be set to 0.
|
||||
// If prevExternalIP is not known, it should be set to 0.0.0.0.
|
||||
// Renewing or deleting a mapping must reuse the nonce from the original response.
|
||||
func buildPCPRequestMappingPacket(
|
||||
myIP netip.Addr,
|
||||
localPort, prevPort uint16,
|
||||
lifetimeSec uint32,
|
||||
prevExternalIP netip.Addr,
|
||||
nonce pcpNonce,
|
||||
) (pkt []byte) {
|
||||
// 24 byte common PCP header + 36 bytes of MAP-specific fields
|
||||
pkt = make([]byte, 24+36)
|
||||
@@ -96,10 +112,8 @@ func buildPCPRequestMappingPacket(
|
||||
copy(pkt[8:24], myIP16[:])
|
||||
|
||||
mapOp := pkt[24:]
|
||||
rand.Read(mapOp[:12]) // 96 bit mapping nonce
|
||||
copy(mapOp[:12], nonce[:])
|
||||
|
||||
// TODO: should this be a UDP mapping? It looks like it supports "all protocols" with 0, but
|
||||
// also doesn't support a local port then.
|
||||
mapOp[12] = pcpUDPMapping
|
||||
binary.BigEndian.PutUint16(mapOp[16:18], localPort)
|
||||
binary.BigEndian.PutUint16(mapOp[18:20], prevPort)
|
||||
@@ -125,7 +139,6 @@ func parsePCPMapResponse(resp []byte) (*pcpMapping, error) {
|
||||
if res.ResultCode != pcpCodeOK {
|
||||
return nil, fmt.Errorf("PCP response not ok, code %d", res.ResultCode)
|
||||
}
|
||||
// TODO: don't ignore the nonce and make sure it's the same?
|
||||
externalPort := binary.BigEndian.Uint16(resp[42:44])
|
||||
externalIPBytes := [16]byte{}
|
||||
copy(externalIPBytes[:], resp[44:])
|
||||
@@ -141,6 +154,7 @@ func parsePCPMapResponse(resp []byte) (*pcpMapping, error) {
|
||||
goodUntil: now.Add(lifetime),
|
||||
epoch: res.Epoch,
|
||||
}
|
||||
copy(mapping.nonce[:], resp[24:36])
|
||||
|
||||
return mapping, nil
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by "stringer -type=pcpResultCode -trimprefix=pcpCode"; DO NOT EDIT.
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by "stringer -type=pmpResultCode -trimprefix=pmpCode"; DO NOT EDIT.
|
||||
|
||||
+23
-2
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package portmapper is a UDP port mapping client. It currently allows for mapping over
|
||||
@@ -6,7 +6,9 @@
|
||||
package portmapper
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -394,6 +396,7 @@ func (c *Client) listenPacket(ctx context.Context, network, addr string) (nettyp
|
||||
func (c *Client) invalidateMappingsLocked(releaseOld bool) {
|
||||
if c.mapping != nil {
|
||||
if releaseOld {
|
||||
c.vlogf("releasing %s mapping", c.mapping.MappingType())
|
||||
c.mapping.Release(context.Background())
|
||||
}
|
||||
c.mapping = nil
|
||||
@@ -668,6 +671,16 @@ func (c *Client) createOrGetMapping(ctx context.Context) (mapping mapping, exter
|
||||
c.vlogf("fallback to UPnP due to no PCP and PMP failed")
|
||||
return nil, netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
|
||||
}
|
||||
|
||||
var pcpNonce pcpNonce
|
||||
if m, ok := c.mapping.(*pcpMapping); ok {
|
||||
// Reuse the existing mapping's nonce so renewals target the same mapping.
|
||||
pcpNonce = m.nonce
|
||||
} else {
|
||||
// New mappings need a random nonce.
|
||||
rand.Read(pcpNonce[:])
|
||||
}
|
||||
|
||||
c.mu.Unlock()
|
||||
|
||||
uc, err := c.listenPacket(ctx, "udp4", ":0")
|
||||
@@ -687,7 +700,7 @@ func (c *Client) createOrGetMapping(ctx context.Context) (mapping mapping, exter
|
||||
if preferPCP {
|
||||
// TODO replace wildcardIP here with previous external if known.
|
||||
// Only do PCP mapping in the case when PMP did not appear to be available recently.
|
||||
pkt := buildPCPRequestMappingPacket(myIP, localPort, prevPort, pcpMapLifetimeSec, wildcardIP)
|
||||
pkt := buildPCPRequestMappingPacket(myIP, localPort, prevPort, pcpMapLifetimeSec, wildcardIP, pcpNonce)
|
||||
if _, err := uc.WriteToUDPAddrPort(pkt, pxpAddr); err != nil {
|
||||
if neterror.TreatAsLostUDP(err) {
|
||||
err = NoMappingError{ErrNoPortMappingServices}
|
||||
@@ -755,6 +768,14 @@ func (c *Client) createOrGetMapping(ctx context.Context) (mapping mapping, exter
|
||||
m.epoch = pres.SecondsSinceEpoch
|
||||
}
|
||||
case pcpVersion:
|
||||
// Ignore responses related to another client.
|
||||
// A response packet is a 24-byte header followed by an operation
|
||||
// specific payload. For a MAP operation, the first 12 bytes of
|
||||
// the payload are the client-generated nonce.
|
||||
if n < 36 || !bytes.Equal(res[24:36], pcpNonce[:]) {
|
||||
c.logf("ignoring PCP response with missing or mismatched nonce")
|
||||
continue
|
||||
}
|
||||
pcpMapping, err := parsePCPMapResponse(res[:n])
|
||||
if err != nil {
|
||||
c.logf("failed to get PCP mapping: %v", err)
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package portmappertype defines the net/portmapper interface, which may or may not be
|
||||
|
||||
+42
-36
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !js
|
||||
@@ -287,13 +287,14 @@ func getUPnPRootDevice(ctx context.Context, logf logger.Logf, debug DebugKnobs,
|
||||
}
|
||||
|
||||
// selectBestService picks the "best" service from the given UPnP root device
|
||||
// to use to create a port mapping. It may return (nil, nil) if no supported
|
||||
// service was found in the provided *goupnp.RootDevice.
|
||||
// to use to create a port mapping. If selecting the service required querying
|
||||
// its external IP, it also returns that address. It may return a nil client if
|
||||
// no supported service was found in the provided *goupnp.RootDevice.
|
||||
//
|
||||
// loc is the parsed location that was used to fetch the given RootDevice.
|
||||
//
|
||||
// The provided ctx is not retained in the returned upnpClient.
|
||||
func selectBestService(ctx context.Context, logf logger.Logf, root *goupnp.RootDevice, loc *url.URL) (client upnpClient, err error) {
|
||||
func selectBestService(ctx context.Context, logf logger.Logf, root *goupnp.RootDevice, loc *url.URL) (client upnpClient, externalIP netip.Addr, err error) {
|
||||
method := "none"
|
||||
defer func() {
|
||||
if client == nil {
|
||||
@@ -339,12 +340,12 @@ func selectBestService(ctx context.Context, logf logger.Logf, root *goupnp.RootD
|
||||
// If we have no clients, then return right now; if we only have one,
|
||||
// just select and return it.
|
||||
if len(clients) == 0 {
|
||||
return nil, nil
|
||||
return nil, netip.Addr{}, nil
|
||||
}
|
||||
if len(clients) == 1 {
|
||||
method = "single"
|
||||
metricUPnPSelectSingle.Add(1)
|
||||
return clients[0], nil
|
||||
return clients[0], netip.Addr{}, nil
|
||||
}
|
||||
|
||||
metricUPnPSelectMultiple.Add(1)
|
||||
@@ -395,7 +396,7 @@ func selectBestService(ctx context.Context, logf logger.Logf, root *goupnp.RootD
|
||||
if !externalIP.IsPrivate() {
|
||||
method = "ext-public"
|
||||
metricUPnPSelectExternalPublic.Add(1)
|
||||
return svc, nil
|
||||
return svc, externalIP, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,11 +416,11 @@ func selectBestService(ctx context.Context, logf logger.Logf, root *goupnp.RootD
|
||||
if hasExtIP {
|
||||
method = "ext-private"
|
||||
metricUPnPSelectExternalPrivate.Add(1)
|
||||
return svc, nil
|
||||
return svc, externalIPs[svc], nil
|
||||
} else if try == 1 {
|
||||
method = "up"
|
||||
metricUPnPSelectUp.Add(1)
|
||||
return svc, nil
|
||||
return svc, netip.Addr{}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -427,7 +428,7 @@ func selectBestService(ctx context.Context, logf logger.Logf, root *goupnp.RootD
|
||||
// Nothing is up, but we have something (length of clients checked
|
||||
// above); just return the first one.
|
||||
metricUPnPSelectNone.Add(1)
|
||||
return clients[0], nil
|
||||
return clients[0], netip.Addr{}, nil
|
||||
}
|
||||
|
||||
// serviceIsConnected returns whether a given UPnP service is connected, based
|
||||
@@ -574,7 +575,7 @@ func (c *Client) getUPnPPortMapping(
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.mapping = upnp
|
||||
c.localPort = externalAddrPort.Port()
|
||||
c.localPort = internal.Port()
|
||||
return upnp.external, true
|
||||
}
|
||||
|
||||
@@ -602,7 +603,7 @@ func (c *Client) tryUPnPPortmapWithDevice(
|
||||
// Select the best mapping service from the given root device. This
|
||||
// makes network requests, and can vary from mapping to mapping if the
|
||||
// upstream device's connection status changes.
|
||||
client, err := selectBestService(ctx, c.logf, rootDev, loc)
|
||||
client, externalIP, err := selectBestService(ctx, c.logf, rootDev, loc)
|
||||
if err != nil {
|
||||
return netip.AddrPort{}, nil, err
|
||||
}
|
||||
@@ -621,6 +622,35 @@ func (c *Client) tryUPnPPortmapWithDevice(
|
||||
return netip.AddrPort{}, nil, fmt.Errorf("no supported UPnP clients")
|
||||
}
|
||||
|
||||
// Obtain and validate the external IP before creating the mapping. In
|
||||
// particular, MikroTik only supports permanent leases, so forgetting a
|
||||
// successfully-created mapping after a later request fails leaks the rule
|
||||
// forever and each retry creates another one.
|
||||
if !externalIP.IsValid() {
|
||||
extIP, err := client.GetExternalIPAddressCtx(ctx)
|
||||
c.vlogf("client.GetExternalIPAddress: %v, %v", extIP, err)
|
||||
if err != nil {
|
||||
return netip.AddrPort{}, nil, err
|
||||
}
|
||||
externalIP, err = netip.ParseAddr(extIP)
|
||||
if err != nil {
|
||||
return netip.AddrPort{}, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Do a bit of validation on the external IP; we've seen cases where
|
||||
// UPnP devices return the public IP 0.0.0.0, which obviously doesn't
|
||||
// work as an endpoint.
|
||||
//
|
||||
// See: https://github.com/tailscale/corp/issues/23538
|
||||
if externalIP.IsUnspecified() {
|
||||
c.logf("UPnP returned unspecified external IP %v", externalIP)
|
||||
return netip.AddrPort{}, nil, fmt.Errorf("UPnP returned unspecified external IP")
|
||||
} else if externalIP.IsLoopback() {
|
||||
c.logf("UPnP returned loopback external IP %v", externalIP)
|
||||
return netip.AddrPort{}, nil, fmt.Errorf("UPnP returned loopback external IP")
|
||||
}
|
||||
|
||||
// Start by trying to make a temporary lease with a duration.
|
||||
var newPort uint16
|
||||
newPort, err = addAnyPortMapping(
|
||||
@@ -662,30 +692,6 @@ func (c *Client) tryUPnPPortmapWithDevice(
|
||||
return netip.AddrPort{}, nil, err
|
||||
}
|
||||
|
||||
// TODO cache this ip somewhere?
|
||||
extIP, err := client.GetExternalIPAddressCtx(ctx)
|
||||
c.vlogf("client.GetExternalIPAddress: %v, %v", extIP, err)
|
||||
if err != nil {
|
||||
return netip.AddrPort{}, nil, err
|
||||
}
|
||||
externalIP, err := netip.ParseAddr(extIP)
|
||||
if err != nil {
|
||||
return netip.AddrPort{}, nil, err
|
||||
}
|
||||
|
||||
// Do a bit of validation on the external IP; we've seen cases where
|
||||
// UPnP devices return the public IP 0.0.0.0, which obviously doesn't
|
||||
// work as an endpoint.
|
||||
//
|
||||
// See: https://github.com/tailscale/corp/issues/23538
|
||||
if externalIP.IsUnspecified() {
|
||||
c.logf("UPnP returned unspecified external IP %v", externalIP)
|
||||
return netip.AddrPort{}, nil, fmt.Errorf("UPnP returned unspecified external IP")
|
||||
} else if externalIP.IsLoopback() {
|
||||
c.logf("UPnP returned loopback external IP %v", externalIP)
|
||||
return netip.AddrPort{}, nil, fmt.Errorf("UPnP returned loopback external IP")
|
||||
}
|
||||
|
||||
return netip.AddrPortFrom(externalIP, newPort), client, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user