net/portmapper: add PCP integration test
This adds a PCP test to the IGD test server, by hardcoding in a few observed packets from Denton's box. Signed-off-by: julianknodt <julianknodt@gmail.com>pull/2462/head
parent
3700cf9ea4
commit
26b6fe7f02
|
@ -11,8 +11,10 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"sync"
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"inet.af/netaddr"
|
"inet.af/netaddr"
|
||||||
|
"tailscale.com/types/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestIGD is an IGD (Intenet Gateway Device) for testing. It supports fake
|
// TestIGD is an IGD (Intenet Gateway Device) for testing. It supports fake
|
||||||
|
@ -21,15 +23,25 @@ type TestIGD struct {
|
||||||
upnpConn net.PacketConn // for UPnP discovery
|
upnpConn net.PacketConn // for UPnP discovery
|
||||||
pxpConn net.PacketConn // for NAT-PMP and/or PCP
|
pxpConn net.PacketConn // for NAT-PMP and/or PCP
|
||||||
ts *httptest.Server
|
ts *httptest.Server
|
||||||
|
logf logger.Logf
|
||||||
|
|
||||||
|
// do* will log which packets are sent, but will not reply to unexpected packets.
|
||||||
|
|
||||||
doPMP bool
|
doPMP bool
|
||||||
doPCP bool
|
doPCP bool
|
||||||
doUPnP bool // TODO: more options for 3 flavors of UPnP services
|
doUPnP bool
|
||||||
|
|
||||||
mu sync.Mutex // guards below
|
mu sync.Mutex // guards below
|
||||||
counters igdCounters
|
counters igdCounters
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestIGDOptions are options
|
||||||
|
type TestIGDOptions struct {
|
||||||
|
PMP bool
|
||||||
|
PCP bool
|
||||||
|
UPnP bool // TODO: more options for 3 flavors of UPnP services
|
||||||
|
}
|
||||||
|
|
||||||
type igdCounters struct {
|
type igdCounters struct {
|
||||||
numUPnPDiscoRecv int32
|
numUPnPDiscoRecv int32
|
||||||
numUPnPOtherUDPRecv int32
|
numUPnPOtherUDPRecv int32
|
||||||
|
@ -38,15 +50,21 @@ type igdCounters struct {
|
||||||
numPMPDiscoRecv int32
|
numPMPDiscoRecv int32
|
||||||
numPCPRecv int32
|
numPCPRecv int32
|
||||||
numPCPDiscoRecv int32
|
numPCPDiscoRecv int32
|
||||||
|
numPCPMapRecv int32
|
||||||
|
numPCPOtherRecv int32
|
||||||
numPMPPublicAddrRecv int32
|
numPMPPublicAddrRecv int32
|
||||||
numPMPBogusRecv int32
|
numPMPBogusRecv int32
|
||||||
|
|
||||||
|
numFailedWrites int32
|
||||||
|
invalidPCPMapPkt int32
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTestIGD() (*TestIGD, error) {
|
func NewTestIGD(logf logger.Logf, t TestIGDOptions) (*TestIGD, error) {
|
||||||
d := &TestIGD{
|
d := &TestIGD{
|
||||||
doPMP: true,
|
logf: logf,
|
||||||
doPCP: true,
|
doPMP: t.PMP,
|
||||||
doUPnP: true,
|
doPCP: t.PCP,
|
||||||
|
doUPnP: t.UPnP,
|
||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
if d.upnpConn, err = testListenUDP(); err != nil {
|
if d.upnpConn, err = testListenUDP(); err != nil {
|
||||||
|
@ -74,6 +92,10 @@ func (d *TestIGD) TestUPnPPort() uint16 {
|
||||||
return uint16(d.upnpConn.LocalAddr().(*net.UDPAddr).Port)
|
return uint16(d.upnpConn.LocalAddr().(*net.UDPAddr).Port)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testIPAndGateway() (gw, ip netaddr.IP, ok bool) {
|
||||||
|
return netaddr.IPv4(127, 0, 0, 1), netaddr.IPv4(1, 2, 3, 4), true
|
||||||
|
}
|
||||||
|
|
||||||
func (d *TestIGD) Close() error {
|
func (d *TestIGD) Close() error {
|
||||||
d.ts.Close()
|
d.ts.Close()
|
||||||
d.upnpConn.Close()
|
d.upnpConn.Close()
|
||||||
|
@ -102,13 +124,19 @@ func (d *TestIGD) serveUPnPDiscovery() {
|
||||||
for {
|
for {
|
||||||
n, src, err := d.upnpConn.ReadFrom(buf)
|
n, src, err := d.upnpConn.ReadFrom(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
d.logf("serveUPnP failed: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pkt := buf[:n]
|
pkt := buf[:n]
|
||||||
if bytes.Equal(pkt, uPnPPacket) { // a super lazy "parse"
|
if bytes.Equal(pkt, uPnPPacket) { // a super lazy "parse"
|
||||||
d.inc(&d.counters.numUPnPDiscoRecv)
|
d.inc(&d.counters.numUPnPDiscoRecv)
|
||||||
resPkt := []byte(fmt.Sprintf("HTTP/1.1 200 OK\r\nCACHE-CONTROL: max-age=120\r\nST: urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\nUSN: uuid:bee7052b-49e8-3597-b545-55a1e38ac11::urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\nEXT:\r\nSERVER: Tailscale-Test/1.0 UPnP/1.1 MiniUPnPd/2.2.1\r\nLOCATION: %s\r\nOPT: \"http://schemas.upnp.org/upnp/1/0/\"; ns=01\r\n01-NLS: 1627958564\r\nBOOTID.UPNP.ORG: 1627958564\r\nCONFIGID.UPNP.ORG: 1337\r\n\r\n", d.ts.URL+"/rootDesc.xml"))
|
resPkt := []byte(fmt.Sprintf("HTTP/1.1 200 OK\r\nCACHE-CONTROL: max-age=120\r\nST: urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\nUSN: uuid:bee7052b-49e8-3597-b545-55a1e38ac11::urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\nEXT:\r\nSERVER: Tailscale-Test/1.0 UPnP/1.1 MiniUPnPd/2.2.1\r\nLOCATION: %s\r\nOPT: \"http://schemas.upnp.org/upnp/1/0/\"; ns=01\r\n01-NLS: 1627958564\r\nBOOTID.UPNP.ORG: 1627958564\r\nCONFIGID.UPNP.ORG: 1337\r\n\r\n", d.ts.URL+"/rootDesc.xml"))
|
||||||
d.upnpConn.WriteTo(resPkt, src)
|
if d.doUPnP {
|
||||||
|
_, err = d.upnpConn.WriteTo(resPkt, src)
|
||||||
|
if err != nil {
|
||||||
|
d.inc(&d.counters.numFailedWrites)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
d.inc(&d.counters.numUPnPOtherUDPRecv)
|
d.inc(&d.counters.numUPnPOtherUDPRecv)
|
||||||
}
|
}
|
||||||
|
@ -121,6 +149,7 @@ func (d *TestIGD) servePxP() {
|
||||||
for {
|
for {
|
||||||
n, a, err := d.pxpConn.ReadFrom(buf)
|
n, a, err := d.pxpConn.ReadFrom(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
d.logf("servePxP failed: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ua := a.(*net.UDPAddr)
|
ua := a.(*net.UDPAddr)
|
||||||
|
@ -164,5 +193,55 @@ func (d *TestIGD) handlePMPQuery(pkt []byte, src netaddr.IPPort) {
|
||||||
|
|
||||||
func (d *TestIGD) handlePCPQuery(pkt []byte, src netaddr.IPPort) {
|
func (d *TestIGD) handlePCPQuery(pkt []byte, src netaddr.IPPort) {
|
||||||
d.inc(&d.counters.numPCPRecv)
|
d.inc(&d.counters.numPCPRecv)
|
||||||
// TODO
|
if len(pkt) < 24 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
op := pkt[1]
|
||||||
|
pktSrcBytes := [16]byte{}
|
||||||
|
copy(pktSrcBytes[:], pkt[8:24])
|
||||||
|
pktSrc := netaddr.IPFrom16(pktSrcBytes)
|
||||||
|
if pktSrc != src.IP() {
|
||||||
|
// TODO this error isn't fatal but should be rejected by server.
|
||||||
|
// Since it's a test it's difficult to get them the same though.
|
||||||
|
d.logf("mismatch of packet source and source IP: got %v, expected %v", pktSrc, src.IP())
|
||||||
|
}
|
||||||
|
switch op {
|
||||||
|
case pcpOpAnnounce:
|
||||||
|
d.inc(&d.counters.numPCPDiscoRecv)
|
||||||
|
if !d.doPCP {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp := buildPCPDiscoResponse(pkt)
|
||||||
|
if _, err := d.pxpConn.WriteTo(resp, src.UDPAddr()); err != nil {
|
||||||
|
d.inc(&d.counters.numFailedWrites)
|
||||||
|
}
|
||||||
|
case pcpOpMap:
|
||||||
|
if len(pkt) < 60 {
|
||||||
|
d.logf("got too short packet for pcp op map: %v", pkt)
|
||||||
|
d.inc(&d.counters.invalidPCPMapPkt)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
d.inc(&d.counters.numPCPMapRecv)
|
||||||
|
if !d.doPCP {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp := buildPCPMapResponse(pkt)
|
||||||
|
d.pxpConn.WriteTo(resp, src.UDPAddr())
|
||||||
|
default:
|
||||||
|
// unknown op code, ignore it for now.
|
||||||
|
d.inc(&d.counters.numPCPOtherRecv)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestClient(t *testing.T, igd *TestIGD) *Client {
|
||||||
|
var c *Client
|
||||||
|
c = NewClient(t.Logf, func() {
|
||||||
|
t.Logf("port map changed")
|
||||||
|
t.Logf("have mapping: %v", c.HaveMapping())
|
||||||
|
})
|
||||||
|
c.testPxPPort = igd.TestPxPPort()
|
||||||
|
c.testUPnPPort = igd.TestUPnPPort()
|
||||||
|
c.SetGatewayLookupFunc(testIPAndGateway)
|
||||||
|
return c
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
package portmapper
|
package portmapper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/binary"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"inet.af/netaddr"
|
"inet.af/netaddr"
|
||||||
|
@ -25,3 +26,37 @@ func TestParsePCPMapResponse(t *testing.T) {
|
||||||
t.Errorf("mismatched external address, got: %v, want: %v", mapping.external, expectedAddr)
|
t.Errorf("mismatched external address, got: %v, want: %v", mapping.external, expectedAddr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
serverResponseBit = 1 << 7
|
||||||
|
fakeLifetimeSec = 1<<31 - 1
|
||||||
|
)
|
||||||
|
|
||||||
|
func buildPCPDiscoResponse(req []byte) []byte {
|
||||||
|
out := make([]byte, 24)
|
||||||
|
out[0] = pcpVersion
|
||||||
|
out[1] = req[1] | serverResponseBit
|
||||||
|
out[3] = 0
|
||||||
|
// Do not put an epoch time in 8:12, when we start using it, tests that use it should fail.
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildPCPMapResponse(req []byte) []byte {
|
||||||
|
out := make([]byte, 24+36)
|
||||||
|
out[0] = pcpVersion
|
||||||
|
out[1] = req[1] | serverResponseBit
|
||||||
|
out[3] = 0
|
||||||
|
binary.BigEndian.PutUint32(out[4:8], 1<<30)
|
||||||
|
// Do not put an epoch time in 8:12, when we start using it, tests that use it should fail.
|
||||||
|
mapResp := out[24:]
|
||||||
|
mapReq := req[24:]
|
||||||
|
// copy nonce, protocol and internal port
|
||||||
|
copy(mapResp[:13], mapReq[:13])
|
||||||
|
copy(mapResp[16:18], mapReq[16:18])
|
||||||
|
// assign external port
|
||||||
|
binary.BigEndian.PutUint16(mapResp[18:20], 4242)
|
||||||
|
assignedIP := netaddr.IPv4(127, 0, 0, 1)
|
||||||
|
assignedIP16 := assignedIP.As16()
|
||||||
|
copy(mapResp[20:36], assignedIP16[:])
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
|
@ -11,9 +11,6 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"inet.af/netaddr"
|
|
||||||
"tailscale.com/types/logger"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCreateOrGetMapping(t *testing.T) {
|
func TestCreateOrGetMapping(t *testing.T) {
|
||||||
|
@ -61,24 +58,15 @@ func TestClientProbeThenMap(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestProbeIntegration(t *testing.T) {
|
func TestProbeIntegration(t *testing.T) {
|
||||||
igd, err := NewTestIGD()
|
igd, err := NewTestIGD(t.Logf, TestIGDOptions{PMP: true, PCP: true, UPnP: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer igd.Close()
|
defer igd.Close()
|
||||||
|
|
||||||
logf := t.Logf
|
c := newTestClient(t, igd)
|
||||||
var c *Client
|
|
||||||
c = NewClient(logger.WithPrefix(logf, "portmapper: "), func() {
|
|
||||||
logf("portmapping changed.")
|
|
||||||
logf("have mapping: %v", c.HaveMapping())
|
|
||||||
})
|
|
||||||
c.testPxPPort = igd.TestPxPPort()
|
|
||||||
c.testUPnPPort = igd.TestUPnPPort()
|
|
||||||
t.Logf("Listening on pxp=%v, upnp=%v", c.testPxPPort, c.testUPnPPort)
|
t.Logf("Listening on pxp=%v, upnp=%v", c.testPxPPort, c.testUPnPPort)
|
||||||
c.SetGatewayLookupFunc(func() (gw, self netaddr.IP, ok bool) {
|
defer c.Close()
|
||||||
return netaddr.IPv4(127, 0, 0, 1), netaddr.IPv4(1, 2, 3, 4), true
|
|
||||||
})
|
|
||||||
|
|
||||||
res, err := c.Probe(context.Background())
|
res, err := c.Probe(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -92,6 +80,7 @@ func TestProbeIntegration(t *testing.T) {
|
||||||
numUPnPDiscoRecv: 1,
|
numUPnPDiscoRecv: 1,
|
||||||
numPMPRecv: 1,
|
numPMPRecv: 1,
|
||||||
numPCPRecv: 1,
|
numPCPRecv: 1,
|
||||||
|
numPCPDiscoRecv: 1,
|
||||||
numPMPPublicAddrRecv: 1,
|
numPMPPublicAddrRecv: 1,
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(st, want) {
|
if !reflect.DeepEqual(st, want) {
|
||||||
|
@ -102,3 +91,35 @@ func TestProbeIntegration(t *testing.T) {
|
||||||
t.Logf("IGD stats: %+v", st)
|
t.Logf("IGD stats: %+v", st)
|
||||||
// TODO(bradfitz): finish
|
// TODO(bradfitz): finish
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPCPIntegration(t *testing.T) {
|
||||||
|
igd, err := NewTestIGD(t.Logf, TestIGDOptions{PMP: false, PCP: true, UPnP: false})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer igd.Close()
|
||||||
|
|
||||||
|
c := newTestClient(t, igd)
|
||||||
|
defer c.Close()
|
||||||
|
res, err := c.Probe(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("probe failed: %v", err)
|
||||||
|
}
|
||||||
|
if res.UPnP || res.PMP {
|
||||||
|
t.Errorf("probe unexpectedly saw upnp or pmp: %+v", res)
|
||||||
|
}
|
||||||
|
if !res.PCP {
|
||||||
|
t.Fatalf("probe did not see pcp: %+v", res)
|
||||||
|
}
|
||||||
|
|
||||||
|
external, err := c.createOrGetMapping(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to get mapping: %v", err)
|
||||||
|
}
|
||||||
|
if external.IsZero() {
|
||||||
|
t.Errorf("got zero IP, expected non-zero")
|
||||||
|
}
|
||||||
|
if c.mapping == nil {
|
||||||
|
t.Errorf("got nil mapping after successful createOrGetMapping")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue