diff --git a/net/portmapper/igd_test.go b/net/portmapper/igd_test.go new file mode 100644 index 000000000..2defb21f6 --- /dev/null +++ b/net/portmapper/igd_test.go @@ -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 +} diff --git a/net/portmapper/portmapper.go b/net/portmapper/portmapper.go index ba180510c..33fb805ac 100644 --- a/net/portmapper/portmapper.go +++ b/net/portmapper/portmapper.go @@ -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 diff --git a/net/portmapper/portmapper_test.go b/net/portmapper/portmapper_test.go index e25c751d8..7d5d77f1f 100644 --- a/net/portmapper/portmapper_test.go +++ b/net/portmapper/portmapper_test.go @@ -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 +}