2021-08-05 18:32:13 +01:00
|
|
|
// 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"
|
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 05:14:09 +01:00
|
|
|
"net/netip"
|
2021-08-05 18:32:13 +01:00
|
|
|
"sync"
|
2022-08-04 05:51:02 +01:00
|
|
|
"sync/atomic"
|
2021-08-06 20:01:23 +01:00
|
|
|
"testing"
|
2021-08-05 18:32:13 +01:00
|
|
|
|
2022-07-25 04:08:42 +01:00
|
|
|
"tailscale.com/net/netaddr"
|
2021-08-06 20:01:23 +01:00
|
|
|
"tailscale.com/types/logger"
|
2021-08-05 18:32:13 +01:00
|
|
|
)
|
|
|
|
|
2022-09-25 19:29:55 +01:00
|
|
|
// TestIGD is an IGD (Internet Gateway Device) for testing. It supports fake
|
2021-08-05 18:32:13 +01:00
|
|
|
// 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
|
2021-08-06 20:01:23 +01:00
|
|
|
logf logger.Logf
|
2022-08-04 05:51:02 +01:00
|
|
|
closed atomic.Bool
|
2021-08-06 20:01:23 +01:00
|
|
|
|
|
|
|
// do* will log which packets are sent, but will not reply to unexpected packets.
|
2021-08-05 18:32:13 +01:00
|
|
|
|
|
|
|
doPMP bool
|
|
|
|
doPCP bool
|
2021-08-06 20:01:23 +01:00
|
|
|
doUPnP bool
|
2021-08-05 18:32:13 +01:00
|
|
|
|
|
|
|
mu sync.Mutex // guards below
|
|
|
|
counters igdCounters
|
|
|
|
}
|
|
|
|
|
2021-08-06 20:01:23 +01:00
|
|
|
// TestIGDOptions are options
|
|
|
|
type TestIGDOptions struct {
|
|
|
|
PMP bool
|
|
|
|
PCP bool
|
|
|
|
UPnP bool // TODO: more options for 3 flavors of UPnP services
|
|
|
|
}
|
|
|
|
|
2021-08-05 18:32:13 +01:00
|
|
|
type igdCounters struct {
|
|
|
|
numUPnPDiscoRecv int32
|
|
|
|
numUPnPOtherUDPRecv int32
|
|
|
|
numUPnPHTTPRecv int32
|
|
|
|
numPMPRecv int32
|
|
|
|
numPMPDiscoRecv int32
|
|
|
|
numPCPRecv int32
|
|
|
|
numPCPDiscoRecv int32
|
2021-08-06 20:01:23 +01:00
|
|
|
numPCPMapRecv int32
|
|
|
|
numPCPOtherRecv int32
|
2021-08-05 18:32:13 +01:00
|
|
|
numPMPPublicAddrRecv int32
|
|
|
|
numPMPBogusRecv int32
|
2021-08-06 20:01:23 +01:00
|
|
|
|
|
|
|
numFailedWrites int32
|
|
|
|
invalidPCPMapPkt int32
|
2021-08-05 18:32:13 +01:00
|
|
|
}
|
|
|
|
|
2021-08-06 20:01:23 +01:00
|
|
|
func NewTestIGD(logf logger.Logf, t TestIGDOptions) (*TestIGD, error) {
|
2021-08-05 18:32:13 +01:00
|
|
|
d := &TestIGD{
|
2021-08-06 20:01:23 +01:00
|
|
|
doPMP: t.PMP,
|
|
|
|
doPCP: t.PCP,
|
|
|
|
doUPnP: t.UPnP,
|
2021-08-05 18:32:13 +01:00
|
|
|
}
|
2022-03-16 23:27:57 +00:00
|
|
|
d.logf = func(msg string, args ...any) {
|
2021-12-01 18:02:15 +00:00
|
|
|
// Don't log after the device has closed;
|
|
|
|
// stray trailing logging angers testing.T.Logf.
|
2022-08-04 05:51:02 +01:00
|
|
|
if d.closed.Load() {
|
2021-12-01 18:02:15 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
logf(msg, args...)
|
|
|
|
}
|
2021-08-05 18:32:13 +01:00
|
|
|
var err error
|
2021-08-09 20:52:15 +01:00
|
|
|
if d.upnpConn, err = testListenUDP(); err != nil {
|
2021-08-05 18:32:13 +01:00
|
|
|
return nil, err
|
|
|
|
}
|
2021-08-09 20:52:15 +01:00
|
|
|
if d.pxpConn, err = testListenUDP(); err != nil {
|
|
|
|
d.upnpConn.Close()
|
2021-08-05 18:32:13 +01:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
d.ts = httptest.NewServer(http.HandlerFunc(d.serveUPnPHTTP))
|
|
|
|
go d.serveUPnPDiscovery()
|
|
|
|
go d.servePxP()
|
|
|
|
return d, nil
|
|
|
|
}
|
|
|
|
|
2021-08-09 20:52:15 +01:00
|
|
|
func testListenUDP() (net.PacketConn, error) {
|
|
|
|
return net.ListenPacket("udp4", "127.0.0.1:0")
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *TestIGD) TestPxPPort() uint16 {
|
|
|
|
return uint16(d.pxpConn.LocalAddr().(*net.UDPAddr).Port)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *TestIGD) TestUPnPPort() uint16 {
|
|
|
|
return uint16(d.upnpConn.LocalAddr().(*net.UDPAddr).Port)
|
|
|
|
}
|
|
|
|
|
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 05:14:09 +01:00
|
|
|
func testIPAndGateway() (gw, ip netip.Addr, ok bool) {
|
2021-08-06 20:01:23 +01:00
|
|
|
return netaddr.IPv4(127, 0, 0, 1), netaddr.IPv4(1, 2, 3, 4), true
|
|
|
|
}
|
|
|
|
|
2021-08-05 18:32:13 +01:00
|
|
|
func (d *TestIGD) Close() error {
|
2022-08-04 05:51:02 +01:00
|
|
|
d.closed.Store(true)
|
2021-08-05 18:32:13 +01:00
|
|
|
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 {
|
2022-08-04 05:51:02 +01:00
|
|
|
if !d.closed.Load() {
|
2021-08-18 22:39:12 +01:00
|
|
|
d.logf("serveUPnP failed: %v", err)
|
|
|
|
}
|
2021-08-05 18:32:13 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
pkt := buf[:n]
|
|
|
|
if bytes.Equal(pkt, uPnPPacket) { // a super lazy "parse"
|
|
|
|
d.inc(&d.counters.numUPnPDiscoRecv)
|
2022-08-04 05:29:11 +01:00
|
|
|
resPkt := fmt.Appendf(nil, "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")
|
2021-08-06 20:01:23 +01:00
|
|
|
if d.doUPnP {
|
|
|
|
_, err = d.upnpConn.WriteTo(resPkt, src)
|
|
|
|
if err != nil {
|
|
|
|
d.inc(&d.counters.numFailedWrites)
|
|
|
|
}
|
|
|
|
}
|
2021-08-05 18:32:13 +01:00
|
|
|
} 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 {
|
2022-08-04 05:51:02 +01:00
|
|
|
if !d.closed.Load() {
|
2021-08-18 22:39:12 +01:00
|
|
|
d.logf("servePxP failed: %v", err)
|
|
|
|
}
|
2021-08-05 18:32:13 +01:00
|
|
|
return
|
|
|
|
}
|
2022-08-03 05:48:56 +01:00
|
|
|
src := netaddr.Unmap(a.(*net.UDPAddr).AddrPort())
|
|
|
|
if !src.IsValid() {
|
2021-08-05 18:32:13 +01:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 05:14:09 +01:00
|
|
|
func (d *TestIGD) handlePMPQuery(pkt []byte, src netip.AddrPort) {
|
2021-08-05 18:32:13 +01:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 05:14:09 +01:00
|
|
|
func (d *TestIGD) handlePCPQuery(pkt []byte, src netip.AddrPort) {
|
2021-08-05 18:32:13 +01:00
|
|
|
d.inc(&d.counters.numPCPRecv)
|
2021-08-06 20:01:23 +01:00
|
|
|
if len(pkt) < 24 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
op := pkt[1]
|
|
|
|
pktSrcBytes := [16]byte{}
|
|
|
|
copy(pktSrcBytes[:], pkt[8:24])
|
2022-08-02 21:38:11 +01:00
|
|
|
pktSrc := netip.AddrFrom16(pktSrcBytes).Unmap()
|
2022-07-25 04:08:42 +01:00
|
|
|
if pktSrc != src.Addr() {
|
2021-08-06 20:01:23 +01:00
|
|
|
// 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.
|
2022-07-25 04:08:42 +01:00
|
|
|
d.logf("mismatch of packet source and source IP: got %v, expected %v", pktSrc, src.Addr())
|
2021-08-06 20:01:23 +01:00
|
|
|
}
|
|
|
|
switch op {
|
|
|
|
case pcpOpAnnounce:
|
|
|
|
d.inc(&d.counters.numPCPDiscoRecv)
|
|
|
|
if !d.doPCP {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
resp := buildPCPDiscoResponse(pkt)
|
2022-07-25 04:08:42 +01:00
|
|
|
if _, err := d.pxpConn.WriteTo(resp, net.UDPAddrFromAddrPort(src)); err != nil {
|
2021-08-06 20:01:23 +01:00
|
|
|
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)
|
2022-07-25 04:08:42 +01:00
|
|
|
d.pxpConn.WriteTo(resp, net.UDPAddrFromAddrPort(src))
|
2021-08-06 20:01:23 +01:00
|
|
|
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
|
2021-08-05 18:32:13 +01:00
|
|
|
}
|