mirror of
https://github.com/tailscale/tailscale.git
synced 2026-11-06 09:33:17 -05:00
tstest/natlab/vnet: move PCP/NAT-PMP code to portmapper.go (#22145)
portmapper.go was introduced for UPnP in commit
8ed43f1f2a.
This also adds a unit test which exercises the incomplete
natlab server implementation of these portmapping protocols.
Updates #21713
Change-Id: I33b61ccbe8c1d8d99d56aa357ae24777be3f5739
Signed-off-by: Francois Marier <francois@tailscale.com>
This commit is contained in:
1 parent
6fa10dae73
commit
ca4e1972fa
4 files changed
+574
-337
No files matched your search
@@ -5,11 +5,33 @@ package vnet
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"math/rand/v2"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
|
||||
// PxP packets
|
||||
const (
|
||||
opcodeANNOUNCE = 0 // external address lookup in NAT-PMP
|
||||
opcodeMAP = 1 // MAP UDP in NAT-PMP
|
||||
|
||||
protocolUDP = 17
|
||||
|
||||
versionNATPMP = 0
|
||||
versionPCP = 2
|
||||
|
||||
resultSuccess = 0
|
||||
resultUnsuppVersion = 1
|
||||
|
||||
opcodeResponseBit = 128
|
||||
)
|
||||
|
||||
// router's UPnP HTTP server, as advertised in its SSDP responses
|
||||
@@ -134,3 +156,324 @@ func (n *network) handleUPnPControl(w http.ResponseWriter, r *http.Request) {
|
||||
`<s:Body><u:%sResponse xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:2">%s</u:%sResponse></s:Body></s:Envelope>`,
|
||||
name, out, name)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if len(req.Payload) < 2 || req.Payload[0] != versionNATPMP {
|
||||
n.logf("vnet: ignoring invalid NAT-PMP packet % 02x", req.Payload)
|
||||
return
|
||||
}
|
||||
|
||||
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,
|
||||
opcodeResponseBit|opcodeANNOUNCE,
|
||||
0, resultSuccess,
|
||||
)
|
||||
epoch := uint32(time.Now().Unix())
|
||||
res = binary.BigEndian.AppendUint32(res, epoch)
|
||||
wan4 := n.wanIP4.As4()
|
||||
res = append(res, wan4[:]...)
|
||||
n.WriteUDPPacketNoNAT(UDPPacket{
|
||||
Src: req.Dst,
|
||||
Dst: req.Src,
|
||||
Payload: res,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Payload) == 12 && req.Payload[1] == opcodeMAP {
|
||||
// https://www.rfc-editor.org/rfc/rfc6886#section-3.3
|
||||
// "00 01 00 00 ed 40 00 00 00 00 1c 20" =>
|
||||
// 00 ver
|
||||
// 01 op=map UDP
|
||||
// 00 00 reserved (0 in request; in response, this is the result code)
|
||||
// ed 40 internal port 60736
|
||||
// 00 00 suggested external port
|
||||
// 00 00 1c 20 suggested lifetime in seconds (7200 sec = 2 hours)
|
||||
internalPort := binary.BigEndian.Uint16(req.Payload[4:6])
|
||||
wantExtPort := binary.BigEndian.Uint16(req.Payload[6:8])
|
||||
lifetimeSec := binary.BigEndian.Uint32(req.Payload[8:12])
|
||||
gotPort, ok := n.doPortMap(req.Src.Addr(), internalPort, wantExtPort, int(lifetimeSec), NATPMP)
|
||||
if !ok {
|
||||
// TODO(fmarier): handle portmap releases properly
|
||||
n.logf("NAT-PMP map request for %v:%d failed", req.Src.Addr(), internalPort)
|
||||
return
|
||||
}
|
||||
res := make([]byte, 0, 16)
|
||||
res = append(res,
|
||||
versionNATPMP,
|
||||
opcodeResponseBit|opcodeMAP,
|
||||
0, resultSuccess,
|
||||
)
|
||||
epoch := uint32(time.Now().Unix())
|
||||
res = binary.BigEndian.AppendUint32(res, epoch)
|
||||
res = binary.BigEndian.AppendUint16(res, internalPort)
|
||||
res = binary.BigEndian.AppendUint16(res, gotPort)
|
||||
res = binary.BigEndian.AppendUint32(res, lifetimeSec)
|
||||
n.WriteUDPPacketNoNAT(UDPPacket{
|
||||
Src: req.Dst,
|
||||
Dst: req.Src,
|
||||
Payload: res,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
n.logf("TODO: handle NAT-PMP packet % 02x", req.Payload)
|
||||
}
|
||||
|
||||
func (n *network) handlePCPRequest(req UDPPacket) {
|
||||
if !n.svcs.Contains(PCP) {
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Payload) < 24 || req.Payload[0] != versionPCP {
|
||||
n.logf("vnet: ignoring invalid PCP packet % 02x", req.Payload)
|
||||
return
|
||||
}
|
||||
// TODO(fmarier): check that client address in the packet matches req.Src
|
||||
|
||||
if len(req.Payload) == 24 && req.Payload[1] == opcodeANNOUNCE {
|
||||
// https://www.rfc-editor.org/rfc/rfc6887#section-14
|
||||
// "02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ff ff 0a 07 01 66" =>
|
||||
// 02 ver
|
||||
// 00 op=announce
|
||||
// 00 00 reserved
|
||||
// 00 00 00 00 requested lifetime in seconds
|
||||
// 00 00 00 00 00 00 00 00 00 00 ff ff 0a 07 01 66 client IP (10.7.1.102)
|
||||
res := make([]byte, 0, 24)
|
||||
res = append(res,
|
||||
versionPCP,
|
||||
opcodeResponseBit|opcodeANNOUNCE,
|
||||
0, // reserved
|
||||
resultSuccess,
|
||||
0, 0, 0, 0, // lifetime (0 for ANNOUNCE)
|
||||
)
|
||||
epoch := uint32(time.Now().Unix())
|
||||
res = binary.BigEndian.AppendUint32(res, epoch)
|
||||
res = append(res, make([]byte, 12)...) // reserved (96 bits)
|
||||
|
||||
// "02 80 00 00 00 00 00 00 6a c9 62 fb 00 00 00 00 00 00 00 00 00 00 00 00" =>
|
||||
// 02 ver
|
||||
// 80 op=announce+128
|
||||
// 00 reserved
|
||||
// 00 result=success
|
||||
// 00 00 00 00 lifetime in seconds
|
||||
// 6a c9 62 fb epoch (2026-10-09T21:56:11 UTC)
|
||||
// 00 00 00 00 00 00 00 00 00 00 00 00 reserved
|
||||
n.WriteUDPPacketNoNAT(UDPPacket{
|
||||
Src: req.Dst,
|
||||
Dst: req.Src,
|
||||
Payload: res,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Payload) == 60 && req.Payload[1] == opcodeMAP && req.Payload[36] == protocolUDP {
|
||||
// https://www.rfc-editor.org/rfc/rfc6887#section-11
|
||||
// "02 01 00 00 00 00 1c 20 00 00 00 00 00 00 00 00 00 00 ff ff 0a 07 01 66 95 bc b5 d9 3d 07 ec bb c8 b3 bd 4f 11 00 00 00 9f 40 00 00 00 00 00 00 00 00 00 00 00 00 ff ff 00 00 00 00" =>
|
||||
// 02 ver
|
||||
// 01 op=map
|
||||
// 00 00 reserved
|
||||
// 00 00 1c 20 requested lifetime in seconds (7200 sec = 2 hours)
|
||||
// 00 00 00 00 00 00 00 00 00 00 ff ff 0a 07 01 66 client IP (10.7.1.102)
|
||||
// 95 bc b5 d9 3d 07 ec bb c8 b3 bd 4f nonce
|
||||
// 11 protocol (17 = UDP)
|
||||
// 00 00 00 reserved
|
||||
// 9f 40 internal port 40768
|
||||
// 00 00 suggested external port
|
||||
// 00 00 00 00 00 00 00 00 00 00 ff ff 00 00 00 00 suggested external IP
|
||||
lifetimeSec := binary.BigEndian.Uint32(req.Payload[4:8])
|
||||
internalPort := binary.BigEndian.Uint16(req.Payload[40:42])
|
||||
// TODO(fmarier): reject wildcard mappings (internalPort = 0)
|
||||
wantExtPort := binary.BigEndian.Uint16(req.Payload[42:44])
|
||||
// TODO(fmarier): reject wantedExtAddr that doesn't match IP version
|
||||
nonce := req.Payload[24:36]
|
||||
// TODO(fmarier): pass the nonce into doPortmap so that it can be checked on renewals/deletions
|
||||
gotPort, ok := n.doPortMap(req.Src.Addr(), internalPort, wantExtPort, int(lifetimeSec), PCP)
|
||||
if !ok {
|
||||
// TODO(fmarier): handle portmap releases
|
||||
n.logf("PCP map request for %v:%d failed", req.Src.Addr(), internalPort)
|
||||
return
|
||||
}
|
||||
res := make([]byte, 0, 60)
|
||||
res = append(res,
|
||||
versionPCP,
|
||||
opcodeResponseBit|opcodeMAP,
|
||||
0, // reserved
|
||||
resultSuccess,
|
||||
)
|
||||
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)
|
||||
res = append(res, nonce...)
|
||||
res = append(res,
|
||||
protocolUDP,
|
||||
0, 0, 0, // reserved
|
||||
)
|
||||
res = binary.BigEndian.AppendUint16(res, internalPort)
|
||||
res = binary.BigEndian.AppendUint16(res, gotPort)
|
||||
wan4 := n.wanIP4.As16()
|
||||
res = append(res, wan4[:]...) // TODO(fmarier): handle non-zero requested external addresses
|
||||
|
||||
// "02 81 00 00 00 00 1c 20 6a c9 62 fb 00 00 00 00 00 00 00 00 00 00 00 00 95 bc b5 d9 3d 07 ec bb c8 b3 bd 4f 11 00 00 00 9f 40 e6 59 00 00 00 00 00 00 00 00 00 00 ff ff 02 01 01 01" =>
|
||||
// 02 vers
|
||||
// 81 op=map+128
|
||||
// 00 reserved
|
||||
// 00 result=success
|
||||
// 00 00 1c 20 lifetime in seconds (7200 sec = 2 hours)
|
||||
// 6a c9 62 fb epoch (2026-10-09T21:56:11 UTC)
|
||||
// 00 00 00 00 00 00 00 00 00 00 00 00 reserved
|
||||
// 95 bc b5 d9 3d 07 ec bb c8 b3 bd 4f nonce
|
||||
// 11 protocol (17 = UDP)
|
||||
// 00 00 00 reserved
|
||||
// 9f 40 internal port 40768
|
||||
// e6 59 external port 58969
|
||||
// 00 00 00 00 00 00 00 00 00 00 ff ff 02 01 01 01 external address (2.1.1.1)
|
||||
n.WriteUDPPacketNoNAT(UDPPacket{
|
||||
Src: req.Dst,
|
||||
Dst: req.Src,
|
||||
Payload: res,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
n.logf("TODO: handle PCP packet % 02x", req.Payload)
|
||||
}
|
||||
|
||||
type PortMapping struct {
|
||||
dst netip.AddrPort // LAN IP:port
|
||||
expiry time.Time
|
||||
Service NetworkService
|
||||
}
|
||||
|
||||
func (n *network) portMappings() map[netip.AddrPort]PortMapping {
|
||||
n.natMu.Lock()
|
||||
defer n.natMu.Unlock()
|
||||
now := time.Now()
|
||||
ret := maps.Clone(n.portMap)
|
||||
maps.DeleteFunc(ret, func(_ netip.AddrPort, pm PortMapping) bool {
|
||||
return now.After(pm.expiry)
|
||||
})
|
||||
return ret
|
||||
}
|
||||
|
||||
func (n *network) doPortMap(src netip.Addr, dstLANPort, wantExtPort uint16, sec int, service NetworkService) (gotPort uint16, ok bool) {
|
||||
n.natMu.Lock()
|
||||
defer n.natMu.Unlock()
|
||||
|
||||
if !n.svcs.Contains(service) {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
wanAP := netip.AddrPortFrom(n.wanIP4, wantExtPort)
|
||||
dst := netip.AddrPortFrom(src, dstLANPort)
|
||||
|
||||
if sec == 0 {
|
||||
lanAP, ok := n.portMap[wanAP]
|
||||
if ok && lanAP.dst.Addr() == src {
|
||||
n.logf("vnet: released %v mapping from %v to %v", service, wanAP, dst)
|
||||
delete(n.portMap, wanAP)
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// See if they already have a mapping and extend expiry if so.
|
||||
// TODO(fmarier): renewing with a different protocol overwrites the
|
||||
// service that created the mapping, so PortMappings only reports the
|
||||
// most recent one.
|
||||
for k, v := range n.portMap {
|
||||
if v.dst == dst {
|
||||
n.portMap[k] = PortMapping{
|
||||
dst: dst,
|
||||
expiry: time.Now().Add(time.Duration(sec) * time.Second),
|
||||
Service: service,
|
||||
}
|
||||
n.logf("vnet: renewed %v mapping from %v to %v", service, wanAP, dst)
|
||||
return k.Port(), true
|
||||
}
|
||||
}
|
||||
|
||||
for range 20_000 {
|
||||
if wanAP.Port() > 0 && !n.natTable.IsPublicPortUsed(wanAP) {
|
||||
mak.Set(&n.portMap, wanAP, PortMapping{
|
||||
dst: dst,
|
||||
expiry: time.Now().Add(time.Duration(sec) * time.Second),
|
||||
Service: service,
|
||||
})
|
||||
n.logf("vnet: allocated %v mapping from %v to %v", service, wanAP, dst)
|
||||
return wanAP.Port(), true
|
||||
}
|
||||
wantExtPort = rand.N(uint16(32<<10)) + 32<<10
|
||||
wanAP = netip.AddrPortFrom(n.wanIP4, wantExtPort)
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// These unit tests exist to catch regressions in the natlab portmapping server
|
||||
// implementations. See tstest/natlab/vmtest/portmapper_test.go for the VM
|
||||
// tests which verify that the tailscale client works.
|
||||
|
||||
package vnet
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/util/must"
|
||||
)
|
||||
|
||||
func newPortmapNetwork(t *testing.T, svc NetworkService) (*Server, *network) {
|
||||
t.Helper()
|
||||
var c Config
|
||||
nw := c.AddNetwork("2.1.1.1", "192.168.0.1/24", svc)
|
||||
c.AddNode(nw)
|
||||
s, err := New(&c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.StopUnsolicitedRAsForTest()
|
||||
t.Cleanup(s.Close)
|
||||
return s, nw.network
|
||||
}
|
||||
|
||||
func mustHex(s string) []byte {
|
||||
return must.Get(hex.DecodeString(strings.ReplaceAll(s, " ", "")))
|
||||
}
|
||||
|
||||
// checkReply checks that got matches want, which is in "% x" format.
|
||||
// Bytes written as "??" in want vary between runs (e.g. epoch, allocated
|
||||
// port) and aren't compared.
|
||||
func checkReply(t *testing.T, got []byte, want string) {
|
||||
t.Helper()
|
||||
gotHex, wantHex := strings.Fields(fmt.Sprintf("% x", got)), strings.Fields(want)
|
||||
if len(gotHex) != len(wantHex) {
|
||||
t.Fatalf("got % x\nwant %s", got, want)
|
||||
}
|
||||
for i, w := range wantHex {
|
||||
if w != "??" && w != gotHex[i] {
|
||||
t.Errorf("got % x\nwant %s", got, want)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkMapping(t *testing.T, n *network, extPort uint16, want netip.AddrPort) {
|
||||
t.Helper()
|
||||
n.natMu.Lock()
|
||||
defer n.natMu.Unlock()
|
||||
if m := n.portMap[netip.AddrPortFrom(n.wanIP4, extPort)]; m.dst != want {
|
||||
t.Errorf("mapping for external port %d is to %v, want %v", extPort, m.dst, want)
|
||||
}
|
||||
}
|
||||
|
||||
type soapResponse struct {
|
||||
ExternalIP netip.Addr `xml:"NewExternalIPAddress"`
|
||||
ReservedPort uint16 `xml:"NewReservedPort"`
|
||||
}
|
||||
|
||||
func soapRequest(t *testing.T, n *network, action, args string) soapResponse {
|
||||
t.Helper()
|
||||
body := fmt.Sprintf(`<?xml version="1.0"?>
|
||||
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<s:Body>
|
||||
<u:%[1]s xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:2">%[2]s</u:%[1]s>
|
||||
</s:Body>
|
||||
</s:Envelope>`, action, args)
|
||||
rec := httptest.NewRecorder()
|
||||
n.upnpHandler().ServeHTTP(rec, httptest.NewRequest("POST", "/ctl/IPConn", strings.NewReader(body)))
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("%s: status %d; body:\n%s", action, rec.Code, rec.Body)
|
||||
}
|
||||
|
||||
var env struct {
|
||||
XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Envelope"`
|
||||
Body struct {
|
||||
Response soapResponse `xml:",any"`
|
||||
} `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"`
|
||||
}
|
||||
if err := xml.Unmarshal(rec.Body.Bytes(), &env); err != nil {
|
||||
t.Fatalf("%s: parsing response: %v\n%s", action, err, rec.Body)
|
||||
}
|
||||
return env.Body.Response
|
||||
}
|
||||
|
||||
var (
|
||||
pcpAnnounceReq = mustHex("02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ff ff c0 a8 00 65") // PCP ANNOUNCE from 192.168.0.101
|
||||
natpmpAddrReq = mustHex("00 00") // NAT-PMP external address request
|
||||
)
|
||||
|
||||
func TestNATPMP(t *testing.T) {
|
||||
s, n := newPortmapNetwork(t, NATPMP)
|
||||
t.Run("external-address", func(t *testing.T) {
|
||||
got := sendUDPToRouter(t, s, pxpPort, natpmpAddrReq)
|
||||
// success, external address 2.1.1.1
|
||||
checkReply(t, got, "00 80 00 00 ?? ?? ?? ?? 02 01 01 01")
|
||||
})
|
||||
t.Run("map-udp", func(t *testing.T) {
|
||||
// MAP UDP of int port 60736 to any ext port for 7200s
|
||||
got := sendUDPToRouter(t, s, pxpPort, mustHex("00 01 00 00 ed 40 00 00 00 00 1c 20"))
|
||||
// success, MAP UDP of int port 60736 for 7200s
|
||||
checkReply(t, got, "00 81 00 00 ?? ?? ?? ?? ed 40 ?? ?? 00 00 1c 20")
|
||||
checkMapping(t, n, binary.BigEndian.Uint16(got[10:12]), netip.AddrPortFrom(clientIPv4(1), 60736))
|
||||
})
|
||||
t.Run("request-unsupported", func(t *testing.T) {
|
||||
got := sendUDPToRouter(t, s, pxpPort, pcpAnnounceReq)
|
||||
// NAT-PMP unsupported version
|
||||
checkReply(t, got, "00 00 00 01 ?? ?? ?? ??")
|
||||
})
|
||||
}
|
||||
|
||||
func TestPCP(t *testing.T) {
|
||||
s, n := newPortmapNetwork(t, PCP)
|
||||
t.Run("announce", func(t *testing.T) {
|
||||
got := sendUDPToRouter(t, s, pxpPort, pcpAnnounceReq)
|
||||
// ANNOUNCE success
|
||||
checkReply(t, got, "02 80 00 00 00 00 00 00 ?? ?? ?? ?? 00 00 00 00 00 00 00 00 00 00 00 00")
|
||||
})
|
||||
t.Run("map-udp", func(t *testing.T) {
|
||||
// MAP UDP of int 192.168.0.101:40768 to any ext IP and port for 7200s, with nonce 95bcb5d93d07ecbbc8b3bd4f
|
||||
got := sendUDPToRouter(t, s, pxpPort, mustHex("02 01 00 00 00 00 1c 20 00 00 00 00 00 00 00 00 00 00 ff ff c0 a8 00 65 "+
|
||||
"95 bc b5 d9 3d 07 ec bb c8 b3 bd 4f 11 00 00 00 9f 40 00 00 00 00 00 00 00 00 00 00 00 00 ff ff 00 00 00 00"))
|
||||
// success, MAP UDP of int port 40768 to ext IP 2.1.1.1 for 7200s, same nonce
|
||||
checkReply(t, got, "02 81 00 00 00 00 1c 20 ?? ?? ?? ?? 00 00 00 00 00 00 00 00 00 00 00 00 "+
|
||||
"95 bc b5 d9 3d 07 ec bb c8 b3 bd 4f 11 00 00 00 9f 40 ?? ?? 00 00 00 00 00 00 00 00 00 00 ff ff 02 01 01 01")
|
||||
checkMapping(t, n, binary.BigEndian.Uint16(got[42:44]), netip.AddrPortFrom(clientIPv4(1), 40768))
|
||||
})
|
||||
t.Run("request-unsupported", func(t *testing.T) {
|
||||
got := sendUDPToRouter(t, s, pxpPort, natpmpAddrReq)
|
||||
// PCP unsupported version, error lifetime 1800s
|
||||
checkReply(t, got, "02 80 00 01 00 00 07 08 ?? ?? ?? ?? 00 00 00 00 00 00 00 00 00 00 00 00")
|
||||
})
|
||||
}
|
||||
|
||||
func TestUPnP(t *testing.T) {
|
||||
s, n := newPortmapNetwork(t, UPnP)
|
||||
t.Run("ssdp-msearch", func(t *testing.T) {
|
||||
// Same as uPnPPacket in net/portmapper.
|
||||
ssdpSearchReq := []byte("M-SEARCH * HTTP/1.1\r\n" +
|
||||
"HOST: 239.255.255.250:1900\r\n" +
|
||||
"ST: ssdp:all\r\n" +
|
||||
"MAN: \"ssdp:discover\"\r\n" +
|
||||
"MX: 2\r\n\r\n")
|
||||
got := sendUDPToRouter(t, s, ssdpPort, ssdpSearchReq)
|
||||
res, err := http.ReadResponse(bufio.NewReader(bytes.NewReader(got)), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("parsing SSDP reply: %v\n%s", err, got)
|
||||
}
|
||||
if loc, want := res.Header.Get("Location"), "http://192.168.0.1:5000/rootDesc.xml"; loc != want {
|
||||
t.Errorf("Location = %q, want %q", loc, want)
|
||||
}
|
||||
if st := res.Header.Get("St"); !strings.Contains(st, ":InternetGatewayDevice:") {
|
||||
t.Errorf("ST = %q, want an InternetGatewayDevice", st)
|
||||
}
|
||||
})
|
||||
t.Run("root-desc", func(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
n.upnpHandler().ServeHTTP(rec, httptest.NewRequest("GET", "/rootDesc.xml", nil))
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status %d; body:\n%s", rec.Code, rec.Body)
|
||||
}
|
||||
if mt, _, err := mime.ParseMediaType(rec.Header().Get("Content-Type")); err != nil || mt != "text/xml" {
|
||||
t.Errorf("Content-Type = %q, want text/xml", rec.Header().Get("Content-Type"))
|
||||
}
|
||||
if rec.Body.Len() == 0 {
|
||||
t.Error("empty body")
|
||||
}
|
||||
})
|
||||
t.Run("get-external-ip", func(t *testing.T) {
|
||||
res := soapRequest(t, n, "GetExternalIPAddress", "")
|
||||
want := netip.MustParseAddr("2.1.1.1")
|
||||
if res.ExternalIP != want {
|
||||
t.Errorf("NewExternalIPAddress = %v, want %v", res.ExternalIP, want)
|
||||
}
|
||||
})
|
||||
t.Run("add-any-port-mapping", func(t *testing.T) {
|
||||
res := soapRequest(t, n, "AddAnyPortMapping", `
|
||||
<NewExternalPort>40768</NewExternalPort>
|
||||
<NewInternalPort>40768</NewInternalPort>
|
||||
<NewInternalClient>192.168.0.101</NewInternalClient>
|
||||
<NewLeaseDuration>7200</NewLeaseDuration>`)
|
||||
checkMapping(t, n, res.ReservedPort, netip.AddrPortFrom(clientIPv4(1), 40768))
|
||||
})
|
||||
}
|
||||
@@ -83,22 +83,6 @@ const (
|
||||
ssdpPort = 1900
|
||||
)
|
||||
|
||||
// PxP packets
|
||||
const (
|
||||
opcodeANNOUNCE = 0 // external address lookup in NAT-PMP
|
||||
opcodeMAP = 1 // MAP UDP in NAT-PMP
|
||||
|
||||
protocolUDP = 17
|
||||
|
||||
versionNATPMP = 0
|
||||
versionPCP = 2
|
||||
|
||||
resultSuccess = 0
|
||||
resultUnsuppVersion = 1
|
||||
|
||||
opcodeResponseBit = 128
|
||||
)
|
||||
|
||||
func (s *Server) PopulateDERPMapIPs() error {
|
||||
out, err := exec.Command("tailscale", "debug", "derp-map").Output()
|
||||
if err != nil {
|
||||
@@ -674,23 +658,6 @@ func (m MAC) String() string {
|
||||
return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", m[0], m[1], m[2], m[3], m[4], m[5])
|
||||
}
|
||||
|
||||
type PortMapping struct {
|
||||
dst netip.AddrPort // LAN IP:port
|
||||
expiry time.Time
|
||||
Service NetworkService
|
||||
}
|
||||
|
||||
func (n *network) portMappings() map[netip.AddrPort]PortMapping {
|
||||
n.natMu.Lock()
|
||||
defer n.natMu.Unlock()
|
||||
now := time.Now()
|
||||
ret := maps.Clone(n.portMap)
|
||||
maps.DeleteFunc(ret, func(_ netip.AddrPort, pm PortMapping) bool {
|
||||
return now.After(pm.expiry)
|
||||
})
|
||||
return ret
|
||||
}
|
||||
|
||||
// writerFunc is a function that writes an Ethernet frame to a connected client.
|
||||
//
|
||||
// ethFrame is the Ethernet frame to write.
|
||||
@@ -2766,58 +2733,6 @@ func (n *network) IsPublicPortUsed(ap netip.AddrPort) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
func (n *network) doPortMap(src netip.Addr, dstLANPort, wantExtPort uint16, sec int, service NetworkService) (gotPort uint16, ok bool) {
|
||||
n.natMu.Lock()
|
||||
defer n.natMu.Unlock()
|
||||
|
||||
if !n.svcs.Contains(service) {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
wanAP := netip.AddrPortFrom(n.wanIP4, wantExtPort)
|
||||
dst := netip.AddrPortFrom(src, dstLANPort)
|
||||
|
||||
if sec == 0 {
|
||||
lanAP, ok := n.portMap[wanAP]
|
||||
if ok && lanAP.dst.Addr() == src {
|
||||
n.logf("vnet: released %v mapping from %v to %v", service, wanAP, dst)
|
||||
delete(n.portMap, wanAP)
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// See if they already have a mapping and extend expiry if so.
|
||||
// TODO(fmarier): renewing with a different protocol overwrites the
|
||||
// service that created the mapping, so PortMappings only reports the
|
||||
// most recent one.
|
||||
for k, v := range n.portMap {
|
||||
if v.dst == dst {
|
||||
n.portMap[k] = PortMapping{
|
||||
dst: dst,
|
||||
expiry: time.Now().Add(time.Duration(sec) * time.Second),
|
||||
Service: service,
|
||||
}
|
||||
n.logf("vnet: renewed %v mapping from %v to %v", service, wanAP, dst)
|
||||
return k.Port(), true
|
||||
}
|
||||
}
|
||||
|
||||
for range 20_000 {
|
||||
if wanAP.Port() > 0 && !n.natTable.IsPublicPortUsed(wanAP) {
|
||||
mak.Set(&n.portMap, wanAP, PortMapping{
|
||||
dst: dst,
|
||||
expiry: time.Now().Add(time.Duration(sec) * time.Second),
|
||||
Service: service,
|
||||
})
|
||||
n.logf("vnet: allocated %v mapping from %v to %v", service, wanAP, dst)
|
||||
return wanAP.Port(), true
|
||||
}
|
||||
wantExtPort = rand.N(uint16(32<<10)) + 32<<10
|
||||
wanAP = netip.AddrPortFrom(n.wanIP4, wantExtPort)
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (n *network) createARPResponse(pkt gopacket.Packet) ([]byte, error) {
|
||||
ethLayer, ok := pkt.Layer(layers.LayerTypeEthernet).(*layers.Ethernet)
|
||||
if !ok {
|
||||
@@ -2867,258 +2782,6 @@ 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
|
||||
}
|
||||
if len(req.Payload) < 2 || req.Payload[0] != versionNATPMP {
|
||||
n.logf("vnet: ignoring invalid NAT-PMP packet % 02x", req.Payload)
|
||||
return
|
||||
}
|
||||
|
||||
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,
|
||||
opcodeResponseBit|opcodeANNOUNCE,
|
||||
0, resultSuccess,
|
||||
)
|
||||
epoch := uint32(time.Now().Unix())
|
||||
res = binary.BigEndian.AppendUint32(res, epoch)
|
||||
wan4 := n.wanIP4.As4()
|
||||
res = append(res, wan4[:]...)
|
||||
n.WriteUDPPacketNoNAT(UDPPacket{
|
||||
Src: req.Dst,
|
||||
Dst: req.Src,
|
||||
Payload: res,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Payload) == 12 && req.Payload[1] == opcodeMAP {
|
||||
// https://www.rfc-editor.org/rfc/rfc6886#section-3.3
|
||||
// "00 01 00 00 ed 40 00 00 00 00 1c 20" =>
|
||||
// 00 ver
|
||||
// 01 op=map UDP
|
||||
// 00 00 reserved (0 in request; in response, this is the result code)
|
||||
// ed 40 internal port 60736
|
||||
// 00 00 suggested external port
|
||||
// 00 00 1c 20 suggested lifetime in seconds (7200 sec = 2 hours)
|
||||
internalPort := binary.BigEndian.Uint16(req.Payload[4:6])
|
||||
wantExtPort := binary.BigEndian.Uint16(req.Payload[6:8])
|
||||
lifetimeSec := binary.BigEndian.Uint32(req.Payload[8:12])
|
||||
gotPort, ok := n.doPortMap(req.Src.Addr(), internalPort, wantExtPort, int(lifetimeSec), NATPMP)
|
||||
if !ok {
|
||||
// TODO(fmarier): handle portmap releases properly
|
||||
n.logf("NAT-PMP map request for %v:%d failed", req.Src.Addr(), internalPort)
|
||||
return
|
||||
}
|
||||
res := make([]byte, 0, 16)
|
||||
res = append(res,
|
||||
versionNATPMP,
|
||||
opcodeResponseBit|opcodeMAP,
|
||||
0, resultSuccess,
|
||||
)
|
||||
epoch := uint32(time.Now().Unix())
|
||||
res = binary.BigEndian.AppendUint32(res, epoch)
|
||||
res = binary.BigEndian.AppendUint16(res, internalPort)
|
||||
res = binary.BigEndian.AppendUint16(res, gotPort)
|
||||
res = binary.BigEndian.AppendUint32(res, lifetimeSec)
|
||||
n.WriteUDPPacketNoNAT(UDPPacket{
|
||||
Src: req.Dst,
|
||||
Dst: req.Src,
|
||||
Payload: res,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
n.logf("TODO: handle NAT-PMP packet % 02x", req.Payload)
|
||||
}
|
||||
|
||||
func (n *network) handlePCPRequest(req UDPPacket) {
|
||||
if !n.svcs.Contains(PCP) {
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Payload) < 24 || req.Payload[0] != versionPCP {
|
||||
n.logf("vnet: ignoring invalid PCP packet % 02x", req.Payload)
|
||||
return
|
||||
}
|
||||
// TODO(fmarier): check that client address in the packet matches req.Src
|
||||
|
||||
if len(req.Payload) == 24 && req.Payload[1] == opcodeANNOUNCE {
|
||||
// https://www.rfc-editor.org/rfc/rfc6887#section-14
|
||||
// "02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ff ff 0a 07 01 66" =>
|
||||
// 02 ver
|
||||
// 00 op=announce
|
||||
// 00 00 reserved
|
||||
// 00 00 00 00 requested lifetime in seconds
|
||||
// 00 00 00 00 00 00 00 00 00 00 ff ff 0a 07 01 66 client IP (10.7.1.102)
|
||||
res := make([]byte, 0, 24)
|
||||
res = append(res,
|
||||
versionPCP,
|
||||
opcodeResponseBit|opcodeANNOUNCE,
|
||||
0, // reserved
|
||||
resultSuccess,
|
||||
0, 0, 0, 0, // lifetime (0 for ANNOUNCE)
|
||||
)
|
||||
epoch := uint32(time.Now().Unix())
|
||||
res = binary.BigEndian.AppendUint32(res, epoch)
|
||||
res = append(res, make([]byte, 12)...) // reserved (96 bits)
|
||||
|
||||
// "02 80 00 00 00 00 00 00 6a c9 62 fb 00 00 00 00 00 00 00 00 00 00 00 00" =>
|
||||
// 02 ver
|
||||
// 80 op=announce+128
|
||||
// 00 reserved
|
||||
// 00 result=success
|
||||
// 00 00 00 00 lifetime in seconds
|
||||
// 6a c9 62 fb epoch (2026-10-09T21:56:11 UTC)
|
||||
// 00 00 00 00 00 00 00 00 00 00 00 00 reserved
|
||||
n.WriteUDPPacketNoNAT(UDPPacket{
|
||||
Src: req.Dst,
|
||||
Dst: req.Src,
|
||||
Payload: res,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Payload) == 60 && req.Payload[1] == opcodeMAP && req.Payload[36] == protocolUDP {
|
||||
// https://www.rfc-editor.org/rfc/rfc6887#section-11
|
||||
// "02 01 00 00 00 00 1c 20 00 00 00 00 00 00 00 00 00 00 ff ff 0a 07 01 66 95 bc b5 d9 3d 07 ec bb c8 b3 bd 4f 11 00 00 00 9f 40 00 00 00 00 00 00 00 00 00 00 00 00 ff ff 00 00 00 00" =>
|
||||
// 02 ver
|
||||
// 01 op=map
|
||||
// 00 00 reserved
|
||||
// 00 00 1c 20 requested lifetime in seconds (7200 sec = 2 hours)
|
||||
// 00 00 00 00 00 00 00 00 00 00 ff ff 0a 07 01 66 client IP (10.7.1.102)
|
||||
// 95 bc b5 d9 3d 07 ec bb c8 b3 bd 4f nonce
|
||||
// 11 protocol (17 = UDP)
|
||||
// 00 00 00 reserved
|
||||
// 9f 40 internal port 40768
|
||||
// 00 00 suggested external port
|
||||
// 00 00 00 00 00 00 00 00 00 00 ff ff 00 00 00 00 suggested external IP
|
||||
lifetimeSec := binary.BigEndian.Uint32(req.Payload[4:8])
|
||||
internalPort := binary.BigEndian.Uint16(req.Payload[40:42])
|
||||
// TODO(fmarier): reject wildcard mappings (internalPort = 0)
|
||||
wantExtPort := binary.BigEndian.Uint16(req.Payload[42:44])
|
||||
// TODO(fmarier): reject wantedExtAddr that doesn't match IP version
|
||||
nonce := req.Payload[24:36]
|
||||
// TODO(fmarier): pass the nonce into doPortmap so that it can be checked on renewals/deletions
|
||||
gotPort, ok := n.doPortMap(req.Src.Addr(), internalPort, wantExtPort, int(lifetimeSec), PCP)
|
||||
if !ok {
|
||||
// TODO(fmarier): handle portmap releases
|
||||
n.logf("PCP map request for %v:%d failed", req.Src.Addr(), internalPort)
|
||||
return
|
||||
}
|
||||
res := make([]byte, 0, 60)
|
||||
res = append(res,
|
||||
versionPCP,
|
||||
opcodeResponseBit|opcodeMAP,
|
||||
0, // reserved
|
||||
resultSuccess,
|
||||
)
|
||||
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)
|
||||
res = append(res, nonce...)
|
||||
res = append(res,
|
||||
protocolUDP,
|
||||
0, 0, 0, // reserved
|
||||
)
|
||||
res = binary.BigEndian.AppendUint16(res, internalPort)
|
||||
res = binary.BigEndian.AppendUint16(res, gotPort)
|
||||
wan4 := n.wanIP4.As16()
|
||||
res = append(res, wan4[:]...) // TODO(fmarier): handle non-zero requested external addresses
|
||||
|
||||
// "02 81 00 00 00 00 1c 20 6a c9 62 fb 00 00 00 00 00 00 00 00 00 00 00 00 95 bc b5 d9 3d 07 ec bb c8 b3 bd 4f 11 00 00 00 9f 40 e6 59 00 00 00 00 00 00 00 00 00 00 ff ff 02 01 01 01" =>
|
||||
// 02 vers
|
||||
// 81 op=map+128
|
||||
// 00 reserved
|
||||
// 00 result=success
|
||||
// 00 00 1c 20 lifetime in seconds (7200 sec = 2 hours)
|
||||
// 6a c9 62 fb epoch (2026-10-09T21:56:11 UTC)
|
||||
// 00 00 00 00 00 00 00 00 00 00 00 00 reserved
|
||||
// 95 bc b5 d9 3d 07 ec bb c8 b3 bd 4f nonce
|
||||
// 11 protocol (17 = UDP)
|
||||
// 00 00 00 reserved
|
||||
// 9f 40 internal port 40768
|
||||
// e6 59 external port 58969
|
||||
// 00 00 00 00 00 00 00 00 00 00 ff ff 02 01 01 01 external address (2.1.1.1)
|
||||
n.WriteUDPPacketNoNAT(UDPPacket{
|
||||
Src: req.Dst,
|
||||
Dst: req.Src,
|
||||
Payload: res,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
n.logf("TODO: handle PCP packet % 02x", req.Payload)
|
||||
}
|
||||
|
||||
// UDPPacket is a UDP packet.
|
||||
//
|
||||
// For the purposes of this project, a UDP packet
|
||||
|
||||
@@ -507,6 +507,37 @@ func (se *sideEffects) logf(format string, args ...any) {
|
||||
se.logs = append(se.logs, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// sendUDPToRouter sends payload from node 1 to the router's LAN IP on dstPort
|
||||
// and returns the payload of the router's reply.
|
||||
func sendUDPToRouter(t *testing.T, s *Server, dstPort uint16, payload []byte) []byte {
|
||||
t.Helper()
|
||||
se := newSideEffects(s)
|
||||
routerIP := s.nodes[0].net.lanIP4.Addr()
|
||||
eth := &layers.Ethernet{SrcMAC: nodeMac(1).HWAddr(), DstMAC: routerMac(1).HWAddr()}
|
||||
ip := mkIPLayer(layers.IPProtocolUDP, clientIPv4(1), routerIP)
|
||||
udp := &layers.UDP{SrcPort: 40000, DstPort: layers.UDPPort(dstPort)}
|
||||
if err := s.handleEthernetFrameFromVM(mustPacket(eth, ip, udp, gopacket.Payload(payload))); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(se.got) != 1 {
|
||||
t.Fatalf("got %d reply packets, want 1; logs:\n%s", len(se.got), strings.Join(se.logs, "\n"))
|
||||
}
|
||||
pkt := gopacket.NewPacket(se.got[0].eth, layers.LayerTypeEthernet, gopacket.Default)
|
||||
ip4, _ := pkt.Layer(layers.LayerTypeIPv4).(*layers.IPv4)
|
||||
res, _ := pkt.Layer(layers.LayerTypeUDP).(*layers.UDP)
|
||||
if ip4 == nil || res == nil {
|
||||
t.Fatalf("reply is not IPv4/UDP:\n%v", pkt)
|
||||
}
|
||||
if src, _ := netip.AddrFromSlice(ip4.SrcIP); src != routerIP || uint16(res.SrcPort) != dstPort {
|
||||
// The client drops replies that don't come from the gateway's port.
|
||||
t.Errorf("reply from %v:%d, want %v:%d", src, res.SrcPort, routerIP, dstPort)
|
||||
}
|
||||
if dst, _ := netip.AddrFromSlice(ip4.DstIP); dst != clientIPv4(1) || res.DstPort != udp.SrcPort {
|
||||
t.Errorf("reply to %v:%d, want %v:%d", dst, res.DstPort, clientIPv4(1), udp.SrcPort)
|
||||
}
|
||||
return res.Payload
|
||||
}
|
||||
|
||||
// all aggregates several side effects checkers into one.
|
||||
func all(checks ...func(*sideEffects) error) func(*sideEffects) error {
|
||||
return func(se *sideEffects) error {
|
||||
|
||||
Reference in new issue
Block a user