net/portmapper: add start of self-contained portmapper integration tests

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2021-08-05 10:32:13 -07:00
parent 98d36ee18d
commit e6d4ab2dd6
3 changed files with 186 additions and 1 deletions

155
net/portmapper/igd_test.go Normal file
View File

@ -0,0 +1,155 @@
// 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 (
"bytes"
"fmt"
"net"
"net/http"
"net/http/httptest"
"sync"
"inet.af/netaddr"
)
// TestIGD is an IGD (Intenet Gateway Device) for testing. It supports fake
// implementations of NAT-PMP, PCP, and/or UPnP to test clients against.
type TestIGD struct {
upnpConn net.PacketConn // for UPnP discovery
pxpConn net.PacketConn // for NAT-PMP and/or PCP
ts *httptest.Server
doPMP bool
doPCP bool
doUPnP bool // TODO: more options for 3 flavors of UPnP services
mu sync.Mutex // guards below
counters igdCounters
}
type igdCounters struct {
numUPnPDiscoRecv int32
numUPnPOtherUDPRecv int32
numUPnPHTTPRecv int32
numPMPRecv int32
numPMPDiscoRecv int32
numPCPRecv int32
numPCPDiscoRecv int32
numPMPPublicAddrRecv int32
numPMPBogusRecv int32
}
func NewTestIGD() (*TestIGD, error) {
d := &TestIGD{
doPMP: true,
doPCP: true,
doUPnP: true,
}
var err error
if d.upnpConn, err = net.ListenPacket("udp", "127.0.0.1:1900"); err != nil {
return nil, err
}
if d.pxpConn, err = net.ListenPacket("udp", "127.0.0.1:5351"); err != nil {
return nil, err
}
d.ts = httptest.NewServer(http.HandlerFunc(d.serveUPnPHTTP))
go d.serveUPnPDiscovery()
go d.servePxP()
return d, nil
}
func (d *TestIGD) Close() error {
d.ts.Close()
d.upnpConn.Close()
d.pxpConn.Close()
return nil
}
func (d *TestIGD) inc(p *int32) {
d.mu.Lock()
defer d.mu.Unlock()
(*p)++
}
func (d *TestIGD) stats() igdCounters {
d.mu.Lock()
defer d.mu.Unlock()
return d.counters
}
func (d *TestIGD) serveUPnPHTTP(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r) // TODO
}
func (d *TestIGD) serveUPnPDiscovery() {
buf := make([]byte, 1500)
for {
n, src, err := d.upnpConn.ReadFrom(buf)
if err != nil {
return
}
pkt := buf[:n]
if bytes.Equal(pkt, uPnPPacket) { // a super lazy "parse"
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"))
d.upnpConn.WriteTo(resPkt, src)
} else {
d.inc(&d.counters.numUPnPOtherUDPRecv)
}
}
}
// servePxP serves NAT-PMP and PCP, which share a port number.
func (d *TestIGD) servePxP() {
buf := make([]byte, 1500)
for {
n, a, err := d.pxpConn.ReadFrom(buf)
if err != nil {
return
}
ua := a.(*net.UDPAddr)
src, ok := netaddr.FromStdAddr(ua.IP, ua.Port, ua.Zone)
if !ok {
panic("bogus addr")
}
pkt := buf[:n]
if len(pkt) < 2 {
continue
}
ver := pkt[0]
switch ver {
default:
continue
case pmpVersion:
d.handlePMPQuery(pkt, src)
case pcpVersion:
d.handlePCPQuery(pkt, src)
}
}
}
func (d *TestIGD) handlePMPQuery(pkt []byte, src netaddr.IPPort) {
d.inc(&d.counters.numPMPRecv)
if len(pkt) < 2 {
return
}
op := pkt[1]
switch op {
case pmpOpMapPublicAddr:
if len(pkt) != 2 {
d.inc(&d.counters.numPMPBogusRecv)
return
}
d.inc(&d.counters.numPMPPublicAddrRecv)
}
// TODO
}
func (d *TestIGD) handlePCPQuery(pkt []byte, src netaddr.IPPort) {
d.inc(&d.counters.numPCPRecv)
// TODO
}

View File

@ -726,7 +726,7 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
}
}
var pmpReqExternalAddrPacket = []byte{0, 0} // version 0, opcode 0 = "Public address request"
var pmpReqExternalAddrPacket = []byte{pmpVersion, pmpOpMapPublicAddr} // 0, 0
const (
upnpPort = 1900 // for UDP discovery only; TCP port discovered later

View File

@ -10,6 +10,9 @@ import (
"strconv"
"testing"
"time"
"inet.af/netaddr"
"tailscale.com/types/logger"
)
func TestCreateOrGetMapping(t *testing.T) {
@ -55,3 +58,30 @@ func TestClientProbeThenMap(t *testing.T) {
ext, err := c.createOrGetMapping(context.Background())
t.Logf("createOrGetMapping: %v, %v", ext, err)
}
func TestProbeIntegration(t *testing.T) {
igd, err := NewTestIGD()
if err != nil {
t.Fatal(err)
}
defer igd.Close()
logf := t.Logf
var c *Client
c = NewClient(logger.WithPrefix(logf, "portmapper: "), func() {
logf("portmapping changed.")
logf("have mapping: %v", c.HaveMapping())
})
c.SetGatewayLookupFunc(func() (gw, self netaddr.IP, ok bool) {
return netaddr.IPv4(127, 0, 0, 1), netaddr.IPv4(1, 2, 3, 4), true
})
res, err := c.Probe(context.Background())
if err != nil {
t.Fatalf("Probe: %v", err)
}
t.Logf("Probe: %+v", res)
t.Logf("IGD stats: %+v", igd.stats())
// TODO(bradfitz): finish
}