2021-08-03 22:28:13 +00:00
// 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 (
2021-08-03 22:29:53 +00:00
"context"
2021-08-03 22:28:13 +00:00
"crypto/rand"
"encoding/binary"
2021-08-03 22:29:53 +00:00
"fmt"
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
"net/netip"
2021-08-03 22:29:53 +00:00
"time"
2021-08-03 22:28:13 +00:00
2022-07-25 03:08:42 +00:00
"tailscale.com/net/netaddr"
2021-08-03 22:28:13 +00:00
)
// References:
//
// https://www.rfc-editor.org/rfc/pdfrfc/rfc6887.txt.pdf
// https://tools.ietf.org/html/rfc6887
2021-11-23 22:43:04 +00:00
//go:generate go run tailscale.com/cmd/addlicense -year 2021 -file pcpresultcode_string.go go run golang.org/x/tools/cmd/stringer -type=pcpResultCode -trimprefix=pcpCode
type pcpResultCode uint8
2021-08-03 22:28:13 +00:00
// PCP constants
const (
2021-08-09 19:52:15 +00:00
pcpVersion = 2
pcpDefaultPort = 5351
2021-08-03 22:28:13 +00:00
pcpMapLifetimeSec = 7200 // TODO does the RFC recommend anything? This is taken from PMP.
2021-11-23 22:43:04 +00:00
pcpCodeOK pcpResultCode = 0
pcpCodeNotAuthorized pcpResultCode = 2
2021-11-23 21:49:47 +00:00
// 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.
2021-11-23 22:43:04 +00:00
pcpCodeAddressMismatch pcpResultCode = 12
2021-08-03 22:28:13 +00:00
pcpOpReply = 0x80 // OR'd into request's op code on response
pcpOpAnnounce = 0
pcpOpMap = 1
pcpUDPMapping = 17 // portmap UDP
pcpTCPMapping = 6 // portmap TCP
)
2021-08-03 22:29:53 +00:00
type pcpMapping struct {
2021-08-09 19:52:15 +00:00
c * Client
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
gw netip . AddrPort
internal netip . AddrPort
external netip . AddrPort
2021-08-03 22:29:53 +00:00
renewAfter time . Time
goodUntil time . Time
2021-08-04 23:51:10 +00:00
// TODO should this also contain an epoch?
// Doesn't seem to be used elsewhere, but can use it for validation at some point.
2021-08-03 22:29:53 +00:00
}
2021-08-03 22:28:13 +00:00
2021-08-03 22:29:53 +00:00
func ( p * pcpMapping ) GoodUntil ( ) time . Time { return p . goodUntil }
func ( p * pcpMapping ) RenewAfter ( ) time . Time { return p . renewAfter }
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
func ( p * pcpMapping ) External ( ) netip . AddrPort { return p . external }
2021-08-03 22:29:53 +00:00
func ( p * pcpMapping ) Release ( ctx context . Context ) {
2021-08-09 19:52:15 +00:00
uc , err := p . c . listenPacket ( ctx , "udp4" , ":0" )
2021-08-03 22:29:53 +00:00
if err != nil {
return
}
defer uc . Close ( )
2022-07-25 03:08:42 +00:00
pkt := buildPCPRequestMappingPacket ( p . internal . Addr ( ) , p . internal . Port ( ) , p . external . Port ( ) , 0 , p . external . Addr ( ) )
uc . WriteToUDPAddrPort ( pkt , p . gw )
2021-08-03 22:29:53 +00:00
}
2021-08-03 22:28:13 +00:00
2021-08-03 22:29:53 +00:00
// 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.
2021-08-05 19:33:13 +00:00
// If prevExternalIP is not known, it should be set to 0.0.0.0.
func buildPCPRequestMappingPacket (
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
myIP netip . Addr ,
2021-08-05 19:33:13 +00:00
localPort , prevPort uint16 ,
lifetimeSec uint32 ,
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
prevExternalIP netip . Addr ,
2021-08-05 19:33:13 +00:00
) ( pkt [ ] byte ) {
2021-08-03 22:29:53 +00:00
// 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 )
2021-08-03 22:28:13 +00:00
myIP16 := myIP . As16 ( )
2021-08-03 22:29:53 +00:00
copy ( pkt [ 8 : 24 ] , myIP16 [ : ] )
2021-08-03 22:28:13 +00:00
mapOp := pkt [ 24 : ]
2021-08-03 22:29:53 +00:00
rand . Read ( mapOp [ : 12 ] ) // 96 bit mapping nonce
2021-08-04 23:51:10 +00:00
// TODO: should this be a UDP mapping? It looks like it supports "all protocols" with 0, but
2021-08-03 22:29:53 +00:00
// 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 )
2021-08-05 19:33:13 +00:00
prevExternalIP16 := prevExternalIP . As16 ( )
copy ( mapOp [ 20 : ] , prevExternalIP16 [ : ] )
2021-08-03 22:28:13 +00:00
return pkt
}
2021-08-09 19:52:15 +00:00
// parsePCPMapResponse parses resp into a partially populated pcpMapping.
// In particular, its Client is not populated.
2021-08-03 22:29:53 +00:00
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" )
}
2022-06-11 05:13:24 +00:00
if res . ResultCode == pcpCodeNotAuthorized {
return nil , fmt . Errorf ( "PCP is implemented but not enabled in the router" )
}
2021-08-03 22:29:53 +00:00
if res . ResultCode != pcpCodeOK {
return nil , fmt . Errorf ( "PCP response not ok, code %d" , res . ResultCode )
}
2021-08-04 23:51:10 +00:00
// TODO: don't ignore the nonce and make sure it's the same?
2021-08-03 22:29:53 +00:00
externalPort := binary . BigEndian . Uint16 ( resp [ 42 : 44 ] )
externalIPBytes := [ 16 ] byte { }
copy ( externalIPBytes [ : ] , resp [ 44 : ] )
externalIP := netaddr . IPFrom16 ( externalIPBytes )
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
external := netip . AddrPortFrom ( externalIP , externalPort )
2021-08-03 22:29:53 +00:00
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
}
2021-08-03 22:28:13 +00:00
// pcpAnnounceRequest generates a PCP packet with an ANNOUNCE opcode.
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
func pcpAnnounceRequest ( myIP netip . Addr ) [ ] byte {
2021-08-03 22:28:13 +00:00
// See https://tools.ietf.org/html/rfc6887#section-7.1
pkt := make ( [ ] byte , 24 )
2021-08-03 22:29:53 +00:00
pkt [ 0 ] = pcpVersion
2021-08-03 22:28:13 +00:00
pkt [ 1 ] = pcpOpAnnounce
myIP16 := myIP . As16 ( )
copy ( pkt [ 8 : ] , myIP16 [ : ] )
return pkt
}
type pcpResponse struct {
OpCode uint8
2021-11-23 22:43:04 +00:00
ResultCode pcpResultCode
2021-08-03 22:28:13 +00:00
Lifetime uint32
Epoch uint32
}
func parsePCPResponse ( b [ ] byte ) ( res pcpResponse , ok bool ) {
if len ( b ) < 24 || b [ 0 ] != pcpVersion {
return
}
res . OpCode = b [ 1 ]
2021-11-23 22:43:04 +00:00
res . ResultCode = pcpResultCode ( b [ 3 ] )
2021-08-03 22:28:13 +00:00
res . Lifetime = binary . BigEndian . Uint32 ( b [ 4 : ] )
res . Epoch = binary . BigEndian . Uint32 ( b [ 8 : ] )
return res , true
}