mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-27 03:55:36 +00:00
fa3639783c
If the epoch that we see during a Probe is less than the existing epoch, it means that the gateway has either restarted or reset its configuration, and an existing mapping is no longer valid. Reset any saved mapping(s) if we detect this case so that a future createOrGetMapping will not attempt to re-use it. Updates #10597 Signed-off-by: Andrew Dunham <andrew@du.nham.ca> Change-Id: Ie3cddaf625cb94a29885f7a1eeea25dbf6b97b47
176 lines
5.0 KiB
Go
176 lines
5.0 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package portmapper
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"net/netip"
|
|
"time"
|
|
)
|
|
|
|
// References:
|
|
//
|
|
// https://www.rfc-editor.org/rfc/pdfrfc/rfc6887.txt.pdf
|
|
// https://tools.ietf.org/html/rfc6887
|
|
|
|
//go:generate go run tailscale.com/cmd/addlicense -file pcpresultcode_string.go go run golang.org/x/tools/cmd/stringer -type=pcpResultCode -trimprefix=pcpCode
|
|
|
|
type pcpResultCode uint8
|
|
|
|
// PCP constants
|
|
const (
|
|
pcpVersion = 2
|
|
pcpDefaultPort = 5351
|
|
|
|
pcpMapLifetimeSec = 7200 // TODO does the RFC recommend anything? This is taken from PMP.
|
|
|
|
pcpCodeOK pcpResultCode = 0
|
|
pcpCodeNotAuthorized pcpResultCode = 2
|
|
// From RFC 6887:
|
|
// ADDRESS_MISMATCH: The source IP address of the request packet does
|
|
// not match the contents of the PCP Client's IP Address field, due
|
|
// to an unexpected NAT on the path between the PCP client and the
|
|
// PCP-controlled NAT or firewall.
|
|
pcpCodeAddressMismatch pcpResultCode = 12
|
|
|
|
pcpOpReply = 0x80 // OR'd into request's op code on response
|
|
pcpOpAnnounce = 0
|
|
pcpOpMap = 1
|
|
|
|
pcpUDPMapping = 17 // portmap UDP
|
|
pcpTCPMapping = 6 // portmap TCP
|
|
)
|
|
|
|
type pcpMapping struct {
|
|
c *Client
|
|
gw netip.AddrPort
|
|
internal netip.AddrPort
|
|
external netip.AddrPort
|
|
|
|
renewAfter time.Time
|
|
goodUntil time.Time
|
|
|
|
epoch uint32
|
|
}
|
|
|
|
func (p *pcpMapping) MappingType() string { return "pcp" }
|
|
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}",
|
|
p.gw, p.external, p.internal,
|
|
p.renewAfter.Unix(), p.goodUntil.Unix())
|
|
}
|
|
|
|
func (p *pcpMapping) Release(ctx context.Context) {
|
|
uc, err := p.c.listenPacket(ctx, "udp4", ":0")
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer uc.Close()
|
|
pkt := buildPCPRequestMappingPacket(p.internal.Addr(), p.internal.Port(), p.external.Port(), 0, p.external.Addr())
|
|
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.
|
|
// 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.
|
|
func buildPCPRequestMappingPacket(
|
|
myIP netip.Addr,
|
|
localPort, prevPort uint16,
|
|
lifetimeSec uint32,
|
|
prevExternalIP netip.Addr,
|
|
) (pkt []byte) {
|
|
// 24 byte common PCP header + 36 bytes of MAP-specific fields
|
|
pkt = make([]byte, 24+36)
|
|
pkt[0] = pcpVersion
|
|
pkt[1] = pcpOpMap
|
|
binary.BigEndian.PutUint32(pkt[4:8], lifetimeSec)
|
|
myIP16 := myIP.As16()
|
|
copy(pkt[8:24], myIP16[:])
|
|
|
|
mapOp := pkt[24:]
|
|
rand.Read(mapOp[:12]) // 96 bit mapping 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)
|
|
|
|
prevExternalIP16 := prevExternalIP.As16()
|
|
copy(mapOp[20:], prevExternalIP16[:])
|
|
return pkt
|
|
}
|
|
|
|
// parsePCPMapResponse parses resp into a partially populated pcpMapping.
|
|
// In particular, its Client is not populated.
|
|
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 == pcpCodeNotAuthorized {
|
|
return nil, fmt.Errorf("PCP is implemented but not enabled in the router")
|
|
}
|
|
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 := netip.AddrFrom16(externalIPBytes).Unmap()
|
|
|
|
external := netip.AddrPortFrom(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),
|
|
epoch: res.Epoch,
|
|
}
|
|
|
|
return mapping, nil
|
|
}
|
|
|
|
// pcpAnnounceRequest generates a PCP packet with an ANNOUNCE opcode.
|
|
func pcpAnnounceRequest(myIP netip.Addr) []byte {
|
|
// See https://tools.ietf.org/html/rfc6887#section-7.1
|
|
pkt := make([]byte, 24)
|
|
pkt[0] = pcpVersion
|
|
pkt[1] = pcpOpAnnounce
|
|
myIP16 := myIP.As16()
|
|
copy(pkt[8:], myIP16[:])
|
|
return pkt
|
|
}
|
|
|
|
type pcpResponse struct {
|
|
OpCode uint8
|
|
ResultCode pcpResultCode
|
|
Lifetime uint32
|
|
Epoch uint32
|
|
}
|
|
|
|
func parsePCPResponse(b []byte) (res pcpResponse, ok bool) {
|
|
if len(b) < 24 || b[0] != pcpVersion {
|
|
return
|
|
}
|
|
res.OpCode = b[1]
|
|
res.ResultCode = pcpResultCode(b[3])
|
|
res.Lifetime = binary.BigEndian.Uint32(b[4:])
|
|
res.Epoch = binary.BigEndian.Uint32(b[8:])
|
|
return res, true
|
|
}
|