mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-21 04:18:38 +00:00
net/portmapper: add pcp portmapping
This adds PCP portmapping, hooking into the existing PMP portmapping. Signed-off-by: julianknodt <julianknodt@gmail.com>
This commit is contained in:
parent
5c98b1b8d0
commit
777b711d96
@ -5,10 +5,14 @@
|
|||||||
package portmapper
|
package portmapper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"inet.af/netaddr"
|
"inet.af/netaddr"
|
||||||
|
"tailscale.com/net/netns"
|
||||||
)
|
)
|
||||||
|
|
||||||
// References:
|
// References:
|
||||||
@ -34,41 +38,92 @@ const (
|
|||||||
pcpTCPMapping = 6 // portmap TCP
|
pcpTCPMapping = 6 // portmap TCP
|
||||||
)
|
)
|
||||||
|
|
||||||
// pcpMapRequest generates a PCP packet with a MAP opcode.
|
type pcpMapping struct {
|
||||||
func pcpMapRequest(myIP netaddr.IP, mapToLocalPort int, delete bool) []byte {
|
gw netaddr.IP
|
||||||
const udpProtoNumber = 17
|
internal netaddr.IPPort
|
||||||
lifetimeSeconds := uint32(1)
|
external netaddr.IPPort
|
||||||
if delete {
|
|
||||||
lifetimeSeconds = 0
|
renewAfter time.Time
|
||||||
|
goodUntil time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pcpMapping) GoodUntil() time.Time { return p.goodUntil }
|
||||||
|
func (p *pcpMapping) RenewAfter() time.Time { return p.renewAfter }
|
||||||
|
func (p *pcpMapping) External() netaddr.IPPort { return p.external }
|
||||||
|
func (p *pcpMapping) Release(ctx context.Context) {
|
||||||
|
uc, err := netns.Listener().ListenPacket(ctx, "udp4", ":0")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
const opMap = 1
|
defer uc.Close()
|
||||||
|
pkt := buildPCPRequestMappingPacket(p.internal.IP(), p.internal.Port(), p.external.Port(), 0)
|
||||||
|
uc.WriteTo(pkt, netaddr.IPPortFrom(p.gw, pcpPort).UDPAddr())
|
||||||
|
}
|
||||||
|
|
||||||
// 24 byte header + 36 byte map opcode
|
// buildPCPRequestMappingPacket generates a PCP packet with a MAP opcode.
|
||||||
pkt := make([]byte, (32+32+128)/8+(96+8+24+16+16+128)/8)
|
// To create a packet which deletes a mapping, lifetimeSec should be set to 0.
|
||||||
|
// If prevPort is not known, it should be set to 0.
|
||||||
|
func buildPCPRequestMappingPacket(myIP netaddr.IP, localPort, prevPort uint16, lifetimeSec uint32) (pkt []byte) {
|
||||||
|
// note: lifetimeSec = 0 implies delete the mapping, should that be special-cased here?
|
||||||
|
|
||||||
// The header (https://tools.ietf.org/html/rfc6887#section-7.1)
|
// 24 byte common PCP header + 36 bytes of MAP-specific fields
|
||||||
pkt[0] = 2 // version
|
pkt = make([]byte, 24+36)
|
||||||
pkt[1] = opMap
|
pkt[0] = pcpVersion
|
||||||
binary.BigEndian.PutUint32(pkt[4:8], lifetimeSeconds)
|
pkt[1] = pcpOpMap
|
||||||
|
binary.BigEndian.PutUint32(pkt[4:8], lifetimeSec)
|
||||||
myIP16 := myIP.As16()
|
myIP16 := myIP.As16()
|
||||||
copy(pkt[8:], myIP16[:])
|
copy(pkt[8:24], myIP16[:])
|
||||||
|
|
||||||
// The map opcode body (https://tools.ietf.org/html/rfc6887#section-11.1)
|
|
||||||
mapOp := pkt[24:]
|
mapOp := pkt[24:]
|
||||||
rand.Read(mapOp[:12]) // 96 bit mappping nonce
|
rand.Read(mapOp[:12]) // 96 bit mapping nonce
|
||||||
mapOp[12] = udpProtoNumber
|
|
||||||
binary.BigEndian.PutUint16(mapOp[16:], uint16(mapToLocalPort))
|
// 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)
|
||||||
|
|
||||||
v4unspec := netaddr.MustParseIP("0.0.0.0")
|
v4unspec := netaddr.MustParseIP("0.0.0.0")
|
||||||
v4unspec16 := v4unspec.As16()
|
v4unspec16 := v4unspec.As16()
|
||||||
copy(mapOp[20:], v4unspec16[:])
|
copy(mapOp[20:], v4unspec16[:])
|
||||||
return pkt
|
return pkt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parsePCPMapResponse(resp []byte) (*pcpMapping, error) {
|
||||||
|
if len(resp) < 60 {
|
||||||
|
return nil, fmt.Errorf("Does not appear to be PCP MAP response")
|
||||||
|
}
|
||||||
|
res, ok := parsePCPResponse(resp[:24])
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("Invalid PCP common header")
|
||||||
|
}
|
||||||
|
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:])
|
||||||
|
externalIP := netaddr.IPFrom16(externalIPBytes)
|
||||||
|
|
||||||
|
external := netaddr.IPPortFrom(externalIP, externalPort)
|
||||||
|
|
||||||
|
lifetime := time.Second * time.Duration(res.Lifetime)
|
||||||
|
now := time.Now()
|
||||||
|
mapping := &pcpMapping{
|
||||||
|
external: external,
|
||||||
|
renewAfter: now.Add(lifetime / 2),
|
||||||
|
goodUntil: now.Add(lifetime),
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapping, nil
|
||||||
|
}
|
||||||
|
|
||||||
// pcpAnnounceRequest generates a PCP packet with an ANNOUNCE opcode.
|
// pcpAnnounceRequest generates a PCP packet with an ANNOUNCE opcode.
|
||||||
func pcpAnnounceRequest(myIP netaddr.IP) []byte {
|
func pcpAnnounceRequest(myIP netaddr.IP) []byte {
|
||||||
// See https://tools.ietf.org/html/rfc6887#section-7.1
|
// See https://tools.ietf.org/html/rfc6887#section-7.1
|
||||||
pkt := make([]byte, 24)
|
pkt := make([]byte, 24)
|
||||||
pkt[0] = pcpVersion // version
|
pkt[0] = pcpVersion
|
||||||
pkt[1] = pcpOpAnnounce
|
pkt[1] = pcpOpAnnounce
|
||||||
myIP16 := myIP.As16()
|
myIP16 := myIP.As16()
|
||||||
copy(pkt[8:], myIP16[:])
|
copy(pkt[8:], myIP16[:])
|
||||||
|
27
net/portmapper/pcp_test.go
Normal file
27
net/portmapper/pcp_test.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package portmapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"inet.af/netaddr"
|
||||||
|
)
|
||||||
|
|
||||||
|
var examplePCPMapResponse = []byte{2, 129, 0, 0, 0, 0, 28, 32, 0, 2, 155, 237, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 112, 9, 24, 241, 208, 251, 45, 157, 76, 10, 188, 17, 0, 0, 0, 4, 210, 4, 210, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 135, 180, 175, 246}
|
||||||
|
|
||||||
|
func TestParsePCPMapResponse(t *testing.T) {
|
||||||
|
mapping, err := parsePCPMapResponse(examplePCPMapResponse)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse PCP Map Response: %v", err)
|
||||||
|
}
|
||||||
|
if mapping == nil {
|
||||||
|
t.Fatalf("got nil mapping when expected non-nil")
|
||||||
|
}
|
||||||
|
expectedAddr := netaddr.MustParseIPPort("135.180.175.246:1234")
|
||||||
|
if mapping.external != expectedAddr {
|
||||||
|
t.Errorf("mismatched external address, got: %v, want: %v", mapping.external, expectedAddr)
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,7 @@
|
|||||||
// 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 allows for mapping over
|
// Package portmapper is a UDP port mapping client. It currently allows for mapping over
|
||||||
// NAT-PMP and UPnP, but will perhaps do PCP later.
|
// NAT-PMP, UPnP, and PCP.
|
||||||
package portmapper
|
package portmapper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -237,6 +237,10 @@ func (c *Client) sawPMPRecentlyLocked() bool {
|
|||||||
func (c *Client) sawPCPRecently() bool {
|
func (c *Client) sawPCPRecently() bool {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
return c.sawPCPRecentlyLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) sawPCPRecentlyLocked() bool {
|
||||||
return c.pcpSawTime.After(time.Now().Add(-trustServiceStillAvailableDuration))
|
return c.pcpSawTime.After(time.Now().Add(-trustServiceStillAvailableDuration))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -373,15 +377,16 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
|||||||
// find a PMP service, bail out early rather than probing
|
// find a PMP service, bail out early rather than probing
|
||||||
// again. Cuts down latency for most clients.
|
// again. Cuts down latency for most clients.
|
||||||
haveRecentPMP := c.sawPMPRecentlyLocked()
|
haveRecentPMP := c.sawPMPRecentlyLocked()
|
||||||
|
haveRecentPCP := c.sawPCPRecentlyLocked()
|
||||||
|
|
||||||
if haveRecentPMP {
|
if haveRecentPMP {
|
||||||
m.external = m.external.WithIP(c.pmpPubIP)
|
m.external = m.external.WithIP(c.pmpPubIP)
|
||||||
}
|
}
|
||||||
if c.lastProbe.After(now.Add(-5*time.Second)) && !haveRecentPMP {
|
if c.lastProbe.After(now.Add(-5*time.Second)) && !haveRecentPMP && !haveRecentPCP {
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
// fallback to UPnP portmapping
|
// fallback to UPnP portmapping
|
||||||
if mapping, ok := c.getUPnPPortMapping(ctx, gw, internalAddr, prevPort); ok {
|
if external, ok := c.getUPnPPortMapping(ctx, gw, internalAddr, prevPort); ok {
|
||||||
return mapping, nil
|
return external, nil
|
||||||
}
|
}
|
||||||
return netaddr.IPPort{}, NoMappingError{ErrNoPortMappingServices}
|
return netaddr.IPPort{}, NoMappingError{ErrNoPortMappingServices}
|
||||||
}
|
}
|
||||||
@ -399,18 +404,28 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
|||||||
|
|
||||||
pmpAddr := netaddr.IPPortFrom(gw, pmpPort)
|
pmpAddr := netaddr.IPPortFrom(gw, pmpPort)
|
||||||
pmpAddru := pmpAddr.UDPAddr()
|
pmpAddru := pmpAddr.UDPAddr()
|
||||||
|
pcpAddr := netaddr.IPPortFrom(gw, pcpPort)
|
||||||
|
pcpAddru := pcpAddr.UDPAddr()
|
||||||
|
|
||||||
// Ask for our external address if needed.
|
// Create a mapping, defaulting to PMP unless only PCP was seen recently.
|
||||||
if m.external.IP().IsZero() {
|
if !haveRecentPMP && haveRecentPCP {
|
||||||
if _, err := uc.WriteTo(pmpReqExternalAddrPacket, pmpAddru); err != nil {
|
// Only do PCP mapping in the case when PMP did not appear to be available recently.
|
||||||
|
pkt := buildPCPRequestMappingPacket(myIP, localPort, prevPort, pcpMapLifetimeSec)
|
||||||
|
if _, err := uc.WriteTo(pkt, pcpAddru); err != nil {
|
||||||
return netaddr.IPPort{}, err
|
return netaddr.IPPort{}, err
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
// Ask for our external address if needed.
|
||||||
|
if m.external.IP().IsZero() {
|
||||||
|
if _, err := uc.WriteTo(pmpReqExternalAddrPacket, pmpAddru); err != nil {
|
||||||
|
return netaddr.IPPort{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// And ask for a mapping.
|
pkt := buildPMPRequestMappingPacket(localPort, prevPort, pmpMapLifetimeSec)
|
||||||
pmpReqMapping := buildPMPRequestMappingPacket(localPort, prevPort, pmpMapLifetimeSec)
|
if _, err := uc.WriteTo(pkt, pmpAddru); err != nil {
|
||||||
if _, err := uc.WriteTo(pmpReqMapping, pmpAddru); err != nil {
|
return netaddr.IPPort{}, err
|
||||||
return netaddr.IPPort{}, err
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res := make([]byte, 1500)
|
res := make([]byte, 1500)
|
||||||
@ -432,24 +447,41 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if src == pmpAddr {
|
if src == pmpAddr {
|
||||||
pres, ok := parsePMPResponse(res[:n])
|
version := res[0]
|
||||||
if !ok {
|
switch version {
|
||||||
c.logf("unexpected PMP response: % 02x", res[:n])
|
case pmpVersion:
|
||||||
continue
|
pres, ok := parsePMPResponse(res[:n])
|
||||||
}
|
if !ok {
|
||||||
if pres.ResultCode != 0 {
|
c.logf("unexpected PMP response: % 02x", res[:n])
|
||||||
return netaddr.IPPort{}, NoMappingError{fmt.Errorf("PMP response Op=0x%x,Res=0x%x", pres.OpCode, pres.ResultCode)}
|
continue
|
||||||
}
|
}
|
||||||
if pres.OpCode == pmpOpReply|pmpOpMapPublicAddr {
|
if pres.ResultCode != 0 {
|
||||||
m.external = m.external.WithIP(pres.PublicAddr)
|
return netaddr.IPPort{}, NoMappingError{fmt.Errorf("PMP response Op=0x%x,Res=0x%x", pres.OpCode, pres.ResultCode)}
|
||||||
}
|
}
|
||||||
if pres.OpCode == pmpOpReply|pmpOpMapUDP {
|
if pres.OpCode == pmpOpReply|pmpOpMapPublicAddr {
|
||||||
m.external = m.external.WithPort(pres.ExternalPort)
|
m.external = m.external.WithIP(pres.PublicAddr)
|
||||||
d := time.Duration(pres.MappingValidSeconds) * time.Second
|
}
|
||||||
now := time.Now()
|
if pres.OpCode == pmpOpReply|pmpOpMapUDP {
|
||||||
m.goodUntil = now.Add(d)
|
m.external = m.external.WithPort(pres.ExternalPort)
|
||||||
m.renewAfter = now.Add(d / 2) // renew in half the time
|
d := time.Duration(pres.MappingValidSeconds) * time.Second
|
||||||
m.epoch = pres.SecondsSinceEpoch
|
now := time.Now()
|
||||||
|
m.goodUntil = now.Add(d)
|
||||||
|
m.renewAfter = now.Add(d / 2) // renew in half the time
|
||||||
|
m.epoch = pres.SecondsSinceEpoch
|
||||||
|
}
|
||||||
|
case pcpVersion:
|
||||||
|
pcpMapping, err := parsePCPMapResponse(res[:n])
|
||||||
|
if err != nil {
|
||||||
|
c.logf("failed to get PCP mapping: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pcpMapping.internal = m.internal
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.mapping = pcpMapping
|
||||||
|
return pcpMapping.external, nil
|
||||||
|
default:
|
||||||
|
c.logf("unknown PMP/PCP version number: %d %v", version, res[:n])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -470,6 +502,7 @@ const (
|
|||||||
pmpMapLifetimeSec = 7200 // RFC recommended 2 hour map duration
|
pmpMapLifetimeSec = 7200 // RFC recommended 2 hour map duration
|
||||||
pmpMapLifetimeDelete = 0 // 0 second lifetime deletes
|
pmpMapLifetimeDelete = 0 // 0 second lifetime deletes
|
||||||
|
|
||||||
|
pmpVersion = 0
|
||||||
pmpOpMapPublicAddr = 0
|
pmpOpMapPublicAddr = 0
|
||||||
pmpOpMapUDP = 1
|
pmpOpMapUDP = 1
|
||||||
pmpOpReply = 0x80 // OR'd into request's op code on response
|
pmpOpReply = 0x80 // OR'd into request's op code on response
|
||||||
|
Loading…
x
Reference in New Issue
Block a user