tstest/natlab/vnet: send "unsupported version" NAT-PMP/PCP responses

When receiving a PCP request, a NAT-PMP-only server must reply with
an unsupported version response. Same for a PCP-only server
receiving a NAT-PMP request.

The tailscale client sends both a NAT-PMP and a PCP request when
probing for portmapping support and it correctly handle any
unsupported version response it receives.

Updates #21713

Change-Id: I0ec8b4a6c1de14bcc64b82f1988cde7fc01f5747
Signed-off-by: Francois Marier <francois@tailscale.com>
This commit is contained in:
Francois Marier authored and François Marier committed 2026-10-16 10:13:52 -07:00
1 parent e310c02787
commit 48ac67151e
2 files changed
+98 -30

No files matched your search

+19 -1
View File
@@ -115,6 +115,14 @@ func easyPMP(e *vmtest.Env) *vmtest.Node {
fmt.Sprintf("192.168.%d.1/24", n), vnet.EasyNAT, vnet.NATPMP))
}
func easyPxP(e *vmtest.Env) *vmtest.Node {
n := e.NumNodes()
return e.AddNode(fmt.Sprintf("node-%d", n),
e.AddNetwork(
fmt.Sprintf("2.%d.%d.%d", n, n, n), // public IP
fmt.Sprintf("192.168.%d.1/24", n), vnet.EasyNAT, vnet.PCP, vnet.NATPMP))
}
func hardPCP(e *vmtest.Env) *vmtest.Node {
n := e.NumNodes()
return e.AddNode(fmt.Sprintf("node-%d", n),
@@ -131,6 +139,14 @@ func hardPMP(e *vmtest.Env) *vmtest.Node {
fmt.Sprintf("10.7.%d.1/24", n), vnet.HardNAT, vnet.NATPMP))
}
func hardPxP(e *vmtest.Env) *vmtest.Node {
n := e.NumNodes()
return e.AddNode(fmt.Sprintf("node-%d", n),
e.AddNetwork(
fmt.Sprintf("2.%d.%d.%d", n, n, n), // public IP
fmt.Sprintf("10.7.%d.1/24", n), vnet.HardNAT, vnet.PCP, vnet.NATPMP))
}
func hardNoDERPOrEndpoints(e *vmtest.Env) *vmtest.Node {
n := e.NumNodes()
return e.AddNode(fmt.Sprintf("node-%d", n),
@@ -309,14 +325,16 @@ var types = []nodeType{
{"hard", hard},
{"easyPCP", easyPCP},
{"easyPMP", easyPMP},
{"easyPxP", easyPxP},
{"hardPCP", hardPCP},
{"hardPMP", hardPMP},
{"hardPxP", hardPxP},
{"one2one", one2one},
{"sameLAN", sameLAN},
{"cgnat", cgnat},
}
var pair = flag.String("pair", "", "comma-separated pair of types to test (easy, easyAF, hard, easyPCP, easyPMP, hardPCP, hardPMP, one2one, sameLAN)")
var pair = flag.String("pair", "", "comma-separated pair of types to test (easy, easyAF, hard, easyPCP, easyPMP, easyPxP, hardPCP, hardPMP, hardPxP, one2one, sameLAN)")
func TestPair(t *testing.T) {
t1, t2, ok := strings.Cut(*pair, ",")
+79 -29
View File
@@ -92,6 +92,11 @@ const (
versionNATPMP = 0
versionPCP = 2
resultSuccess = 0
resultUnsuppVersion = 1
opcodeResponseBit = 128
)
func (s *Server) PopulateDERPMapIPs() error {
@@ -2058,17 +2063,8 @@ func (n *network) handleUDPPacketForRouter(ep EthernetPacket, udp *layers.UDP, t
return
}
if dstIP == n.lanIP4.Addr() && isNATPMP(udp) {
n.handleNATPMPRequest(UDPPacket{
Src: netip.AddrPortFrom(srcIP, uint16(udp.SrcPort)),
Dst: netip.AddrPortFrom(dstIP, uint16(udp.DstPort)),
Payload: udp.Payload,
})
return
}
if isPCP(udp) {
n.handlePCPRequest(UDPPacket{
if dstIP == n.lanIP4.Addr() && udp.DstPort == pxpPort && len(udp.Payload) >= 2 { // TODO(fmarier): support IPv6 PCP
n.handlePxPRequest(UDPPacket{
Src: netip.AddrPortFrom(srcIP, uint16(udp.SrcPort)),
Dst: netip.AddrPortFrom(dstIP, uint16(udp.DstPort)),
Payload: udp.Payload,
@@ -2533,14 +2529,6 @@ func isDNSRequest(pkt gopacket.Packet) bool {
return ok && dns.QR == false && len(dns.Questions) > 0
}
func isNATPMP(udp *layers.UDP) bool {
return udp.DstPort == pxpPort && len(udp.Payload) > 0 && udp.Payload[0] == versionNATPMP
}
func isPCP(udp *layers.UDP) bool {
return udp.DstPort == pxpPort && len(udp.Payload) > 0 && udp.Payload[0] == versionPCP
}
func makeSTUNReply(req UDPPacket) (res UDPPacket, ok bool) {
txid, err := stun.ParseBindingRequest(req.Payload)
if err != nil {
@@ -2845,6 +2833,68 @@ func (n *network) createARPResponse(pkt gopacket.Packet) ([]byte, error) {
return buffer.Bytes(), nil
}
func (n *network) handlePxPRequest(req UDPPacket) {
if req.Payload[1]&opcodeResponseBit != 0 {
n.logf("vnet: ignoring NAT-PMP/PCP packet with response bit set % 02x", req.Payload)
return
}
switch req.Payload[0] {
case versionNATPMP:
switch {
case n.svcs.Contains(NATPMP):
n.handleNATPMPRequest(req)
case n.svcs.Contains(PCP):
n.writePCPUnsuppVersion(req)
}
case versionPCP:
switch {
case n.svcs.Contains(PCP):
n.handlePCPRequest(req)
case n.svcs.Contains(NATPMP):
n.writeNATPMPUnsuppVersion(req)
}
default:
n.logf("vnet: ignoring NAT-PMP/PCP packet with unknown version % 02x", req.Payload)
}
}
func (n *network) writeNATPMPUnsuppVersion(req UDPPacket) {
res := make([]byte, 0, 8)
res = append(res,
versionNATPMP,
0, // opcode is 0 as per https://www.rfc-editor.org/rfc/rfc6886#section-3.5
0, resultUnsuppVersion,
)
epoch := uint32(time.Now().Unix())
res = binary.BigEndian.AppendUint32(res, epoch)
n.WriteUDPPacketNoNAT(UDPPacket{
Src: req.Dst,
Dst: req.Src,
Payload: res,
})
}
func (n *network) writePCPUnsuppVersion(req UDPPacket) {
opcode := req.Payload[1]
res := make([]byte, 0, 24)
res = append(res,
versionPCP,
opcodeResponseBit|opcode,
0, // reserved
resultUnsuppVersion,
)
lifetimeSec := uint32(1800) // 30 min
res = binary.BigEndian.AppendUint32(res, lifetimeSec)
epoch := uint32(time.Now().Unix())
res = binary.BigEndian.AppendUint32(res, epoch)
res = append(res, make([]byte, 12)...) // reserved (96 bits)
n.WriteUDPPacketNoNAT(UDPPacket{
Src: req.Dst,
Dst: req.Src,
Payload: res,
})
}
func (n *network) handleNATPMPRequest(req UDPPacket) {
if !n.svcs.Contains(NATPMP) {
return
@@ -2854,14 +2904,14 @@ func (n *network) handleNATPMPRequest(req UDPPacket) {
return
}
if len(req.Payload) == 2 && req.Payload[0] == opcodeANNOUNCE {
if len(req.Payload) == 2 && req.Payload[1] == opcodeANNOUNCE {
// https://www.rfc-editor.org/rfc/rfc6886#section-3.2
res := make([]byte, 0, 12)
res = append(res,
versionNATPMP,
128+opcodeANNOUNCE,
0, 0, // result code success
opcodeResponseBit|opcodeANNOUNCE,
0, resultSuccess,
)
epoch := uint32(time.Now().Unix())
res = binary.BigEndian.AppendUint32(res, epoch)
@@ -2895,8 +2945,8 @@ func (n *network) handleNATPMPRequest(req UDPPacket) {
res := make([]byte, 0, 16)
res = append(res,
versionNATPMP,
128+opcodeMAP,
0, 0, // result code success
opcodeResponseBit|opcodeMAP,
0, resultSuccess,
)
epoch := uint32(time.Now().Unix())
res = binary.BigEndian.AppendUint32(res, epoch)
@@ -2936,9 +2986,9 @@ func (n *network) handlePCPRequest(req UDPPacket) {
res := make([]byte, 0, 24)
res = append(res,
versionPCP,
128+opcodeANNOUNCE,
0, // reserved
0, // result code success
opcodeResponseBit|opcodeANNOUNCE,
0, // reserved
resultSuccess,
0, 0, 0, 0, // lifetime (0 for ANNOUNCE)
)
epoch := uint32(time.Now().Unix())
@@ -2990,9 +3040,9 @@ func (n *network) handlePCPRequest(req UDPPacket) {
res := make([]byte, 0, 60)
res = append(res,
versionPCP,
128+opcodeMAP,
opcodeResponseBit|opcodeMAP,
0, // reserved
0, // result code success
resultSuccess,
)
res = binary.BigEndian.AppendUint32(res, lifetimeSec)
epoch := uint32(time.Now().Unix())