mirror of
https://github.com/tailscale/tailscale.git
synced 2025-01-08 09:07:44 +00:00
net/portmapper: actually test something in TestProbeIntegration
And use dynamic port numbers in tests, as Linux on GitHub Actions and Windows in general have things running on these ports. Co-Author: Julian Knodt <julianknodt@gmail.com> Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
26c1183941
commit
bdb93c5942
@ -49,10 +49,11 @@ func NewTestIGD() (*TestIGD, error) {
|
|||||||
doUPnP: true,
|
doUPnP: true,
|
||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
if d.upnpConn, err = net.ListenPacket("udp", "127.0.0.1:1900"); err != nil {
|
if d.upnpConn, err = testListenUDP(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if d.pxpConn, err = net.ListenPacket("udp", "127.0.0.1:5351"); err != nil {
|
if d.pxpConn, err = testListenUDP(); err != nil {
|
||||||
|
d.upnpConn.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
d.ts = httptest.NewServer(http.HandlerFunc(d.serveUPnPHTTP))
|
d.ts = httptest.NewServer(http.HandlerFunc(d.serveUPnPHTTP))
|
||||||
@ -61,6 +62,18 @@ func NewTestIGD() (*TestIGD, error) {
|
|||||||
return d, nil
|
return d, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
func (d *TestIGD) Close() error {
|
func (d *TestIGD) Close() error {
|
||||||
d.ts.Close()
|
d.ts.Close()
|
||||||
d.upnpConn.Close()
|
d.upnpConn.Close()
|
||||||
|
@ -12,7 +12,6 @@
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"inet.af/netaddr"
|
"inet.af/netaddr"
|
||||||
"tailscale.com/net/netns"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// References:
|
// References:
|
||||||
@ -23,7 +22,7 @@
|
|||||||
// PCP constants
|
// PCP constants
|
||||||
const (
|
const (
|
||||||
pcpVersion = 2
|
pcpVersion = 2
|
||||||
pcpPort = 5351
|
pcpDefaultPort = 5351
|
||||||
|
|
||||||
pcpMapLifetimeSec = 7200 // TODO does the RFC recommend anything? This is taken from PMP.
|
pcpMapLifetimeSec = 7200 // TODO does the RFC recommend anything? This is taken from PMP.
|
||||||
|
|
||||||
@ -39,7 +38,8 @@
|
|||||||
)
|
)
|
||||||
|
|
||||||
type pcpMapping struct {
|
type pcpMapping struct {
|
||||||
gw netaddr.IP
|
c *Client
|
||||||
|
gw netaddr.IPPort
|
||||||
internal netaddr.IPPort
|
internal netaddr.IPPort
|
||||||
external netaddr.IPPort
|
external netaddr.IPPort
|
||||||
|
|
||||||
@ -54,13 +54,13 @@ func (p *pcpMapping) GoodUntil() time.Time { return p.goodUntil }
|
|||||||
func (p *pcpMapping) RenewAfter() time.Time { return p.renewAfter }
|
func (p *pcpMapping) RenewAfter() time.Time { return p.renewAfter }
|
||||||
func (p *pcpMapping) External() netaddr.IPPort { return p.external }
|
func (p *pcpMapping) External() netaddr.IPPort { return p.external }
|
||||||
func (p *pcpMapping) Release(ctx context.Context) {
|
func (p *pcpMapping) Release(ctx context.Context) {
|
||||||
uc, err := netns.Listener().ListenPacket(ctx, "udp4", ":0")
|
uc, err := p.c.listenPacket(ctx, "udp4", ":0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer uc.Close()
|
defer uc.Close()
|
||||||
pkt := buildPCPRequestMappingPacket(p.internal.IP(), p.internal.Port(), p.external.Port(), 0, p.external.IP())
|
pkt := buildPCPRequestMappingPacket(p.internal.IP(), p.internal.Port(), p.external.Port(), 0, p.external.IP())
|
||||||
uc.WriteTo(pkt, netaddr.IPPortFrom(p.gw, pcpPort).UDPAddr())
|
uc.WriteTo(pkt, p.gw.UDPAddr())
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildPCPRequestMappingPacket generates a PCP packet with a MAP opcode.
|
// buildPCPRequestMappingPacket generates a PCP packet with a MAP opcode.
|
||||||
@ -95,6 +95,8 @@ func buildPCPRequestMappingPacket(
|
|||||||
return pkt
|
return pkt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parsePCPMapResponse parses resp into a partially populated pcpMapping.
|
||||||
|
// In particular, its Client is not populated.
|
||||||
func parsePCPMapResponse(resp []byte) (*pcpMapping, error) {
|
func parsePCPMapResponse(resp []byte) (*pcpMapping, error) {
|
||||||
if len(resp) < 60 {
|
if len(resp) < 60 {
|
||||||
return nil, fmt.Errorf("Does not appear to be PCP MAP response")
|
return nil, fmt.Errorf("Does not appear to be PCP MAP response")
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -55,6 +56,8 @@ type Client struct {
|
|||||||
logf logger.Logf
|
logf logger.Logf
|
||||||
ipAndGateway func() (gw, ip netaddr.IP, ok bool)
|
ipAndGateway func() (gw, ip netaddr.IP, ok bool)
|
||||||
onChange func() // or nil
|
onChange func() // or nil
|
||||||
|
testPxPPort uint16 // if non-zero, pxpPort to use for tests
|
||||||
|
testUPnPPort uint16 // if non-zero, uPnPPort to use for tests
|
||||||
|
|
||||||
mu sync.Mutex // guards following, and all fields thereof
|
mu sync.Mutex // guards following, and all fields thereof
|
||||||
|
|
||||||
@ -113,7 +116,8 @@ func (c *Client) HaveMapping() bool {
|
|||||||
//
|
//
|
||||||
// All fields are immutable once created.
|
// All fields are immutable once created.
|
||||||
type pmpMapping struct {
|
type pmpMapping struct {
|
||||||
gw netaddr.IP
|
c *Client
|
||||||
|
gw netaddr.IPPort
|
||||||
external netaddr.IPPort
|
external netaddr.IPPort
|
||||||
internal netaddr.IPPort
|
internal netaddr.IPPort
|
||||||
renewAfter time.Time // the time at which we want to renew the mapping
|
renewAfter time.Time // the time at which we want to renew the mapping
|
||||||
@ -132,13 +136,13 @@ func (p *pmpMapping) External() netaddr.IPPort { return p.external }
|
|||||||
|
|
||||||
// Release does a best effort fire-and-forget release of the PMP mapping m.
|
// Release does a best effort fire-and-forget release of the PMP mapping m.
|
||||||
func (m *pmpMapping) Release(ctx context.Context) {
|
func (m *pmpMapping) Release(ctx context.Context) {
|
||||||
uc, err := netns.Listener().ListenPacket(ctx, "udp4", ":0")
|
uc, err := m.c.listenPacket(ctx, "udp4", ":0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer uc.Close()
|
defer uc.Close()
|
||||||
pkt := buildPMPRequestMappingPacket(m.internal.Port(), m.external.Port(), pmpMapLifetimeDelete)
|
pkt := buildPMPRequestMappingPacket(m.internal.Port(), m.external.Port(), pmpMapLifetimeDelete)
|
||||||
uc.WriteTo(pkt, netaddr.IPPortFrom(m.gw, pmpPort).UDPAddr())
|
uc.WriteTo(pkt, m.gw.UDPAddr())
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient returns a new portmapping client.
|
// NewClient returns a new portmapping client.
|
||||||
@ -213,6 +217,32 @@ func (c *Client) gatewayAndSelfIP() (gw, myIP netaddr.IP, ok bool) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pxpPort returns the NAT-PMP and PCP port number.
|
||||||
|
// It returns 5351, except for in tests where it varies by run.
|
||||||
|
func (c *Client) pxpPort() uint16 {
|
||||||
|
if c.testPxPPort != 0 {
|
||||||
|
return c.testPxPPort
|
||||||
|
}
|
||||||
|
return pmpDefaultPort
|
||||||
|
}
|
||||||
|
|
||||||
|
// upnpPort returns the UPnP discovery port number.
|
||||||
|
// It returns 1900, except for in tests where it varies by run.
|
||||||
|
func (c *Client) upnpPort() uint16 {
|
||||||
|
if c.testUPnPPort != 0 {
|
||||||
|
return c.testUPnPPort
|
||||||
|
}
|
||||||
|
return upnpDefaultPort
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) listenPacket(ctx context.Context, network, addr string) (net.PacketConn, error) {
|
||||||
|
if (c.testPxPPort != 0 || c.testUPnPPort != 0) && os.Getenv("GITHUB_ACTIONS") == "true" {
|
||||||
|
var lc net.ListenConfig
|
||||||
|
return lc.ListenPacket(ctx, network, addr)
|
||||||
|
}
|
||||||
|
return netns.Listener().ListenPacket(ctx, network, addr)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) invalidateMappingsLocked(releaseOld bool) {
|
func (c *Client) invalidateMappingsLocked(releaseOld bool) {
|
||||||
if c.mapping != nil {
|
if c.mapping != nil {
|
||||||
if releaseOld {
|
if releaseOld {
|
||||||
@ -399,7 +429,8 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
|||||||
// PCP returns all the information necessary for a mapping in a single packet, so we can
|
// PCP returns all the information necessary for a mapping in a single packet, so we can
|
||||||
// construct it upon receiving that packet.
|
// construct it upon receiving that packet.
|
||||||
m := &pmpMapping{
|
m := &pmpMapping{
|
||||||
gw: gw,
|
c: c,
|
||||||
|
gw: netaddr.IPPortFrom(gw, c.pxpPort()),
|
||||||
internal: internalAddr,
|
internal: internalAddr,
|
||||||
}
|
}
|
||||||
if haveRecentPMP {
|
if haveRecentPMP {
|
||||||
@ -415,7 +446,7 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
|||||||
}
|
}
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
|
|
||||||
uc, err := netns.Listener().ListenPacket(ctx, "udp4", ":0")
|
uc, err := c.listenPacket(ctx, "udp4", ":0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return netaddr.IPPort{}, err
|
return netaddr.IPPort{}, err
|
||||||
}
|
}
|
||||||
@ -424,7 +455,7 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
|||||||
uc.SetReadDeadline(time.Now().Add(portMapServiceTimeout))
|
uc.SetReadDeadline(time.Now().Add(portMapServiceTimeout))
|
||||||
defer closeCloserOnContextDone(ctx, uc)()
|
defer closeCloserOnContextDone(ctx, uc)()
|
||||||
|
|
||||||
pxpAddr := netaddr.IPPortFrom(gw, pmpPort)
|
pxpAddr := netaddr.IPPortFrom(gw, c.pxpPort())
|
||||||
pxpAddru := pxpAddr.UDPAddr()
|
pxpAddru := pxpAddr.UDPAddr()
|
||||||
|
|
||||||
preferPCP := !DisablePCP && (DisablePMP || (!haveRecentPMP && haveRecentPCP))
|
preferPCP := !DisablePCP && (DisablePMP || (!haveRecentPMP && haveRecentPCP))
|
||||||
@ -499,8 +530,9 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
|||||||
// PCP should only have a single packet response
|
// PCP should only have a single packet response
|
||||||
return netaddr.IPPort{}, NoMappingError{ErrNoPortMappingServices}
|
return netaddr.IPPort{}, NoMappingError{ErrNoPortMappingServices}
|
||||||
}
|
}
|
||||||
|
pcpMapping.c = c
|
||||||
pcpMapping.internal = m.internal
|
pcpMapping.internal = m.internal
|
||||||
pcpMapping.gw = gw
|
pcpMapping.gw = netaddr.IPPortFrom(gw, c.pxpPort())
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
c.mapping = pcpMapping
|
c.mapping = pcpMapping
|
||||||
@ -524,7 +556,7 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
|||||||
|
|
||||||
// NAT-PMP constants.
|
// NAT-PMP constants.
|
||||||
const (
|
const (
|
||||||
pmpPort = 5351
|
pmpDefaultPort = 5351
|
||||||
pmpMapLifetimeSec = 7200 // RFC recommended 2 hour map duration
|
pmpMapLifetimeSec = 7200 // RFC recommended 2 hour map duration
|
||||||
pmpMapLifetimeDelete = 0 // 0 second lifetime deletes
|
pmpMapLifetimeDelete = 0 // 0 second lifetime deletes
|
||||||
|
|
||||||
@ -622,7 +654,7 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
uc, err := netns.Listener().ListenPacket(context.Background(), "udp4", ":0")
|
uc, err := c.listenPacket(context.Background(), "udp4", ":0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logf("ProbePCP: %v", err)
|
c.logf("ProbePCP: %v", err)
|
||||||
return res, err
|
return res, err
|
||||||
@ -632,9 +664,8 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
defer closeCloserOnContextDone(ctx, uc)()
|
defer closeCloserOnContextDone(ctx, uc)()
|
||||||
|
|
||||||
pcpAddr := netaddr.IPPortFrom(gw, pcpPort).UDPAddr()
|
pxpAddr := netaddr.IPPortFrom(gw, c.pxpPort()).UDPAddr()
|
||||||
pmpAddr := netaddr.IPPortFrom(gw, pmpPort).UDPAddr()
|
upnpAddr := netaddr.IPPortFrom(gw, c.upnpPort()).UDPAddr()
|
||||||
upnpAddr := netaddr.IPPortFrom(gw, upnpPort).UDPAddr()
|
|
||||||
|
|
||||||
// Don't send probes to services that we recently learned (for
|
// Don't send probes to services that we recently learned (for
|
||||||
// the same gw/myIP) are available. See
|
// the same gw/myIP) are available. See
|
||||||
@ -642,12 +673,12 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
|||||||
if c.sawPMPRecently() {
|
if c.sawPMPRecently() {
|
||||||
res.PMP = true
|
res.PMP = true
|
||||||
} else if !DisablePMP {
|
} else if !DisablePMP {
|
||||||
uc.WriteTo(pmpReqExternalAddrPacket, pmpAddr)
|
uc.WriteTo(pmpReqExternalAddrPacket, pxpAddr)
|
||||||
}
|
}
|
||||||
if c.sawPCPRecently() {
|
if c.sawPCPRecently() {
|
||||||
res.PCP = true
|
res.PCP = true
|
||||||
} else if !DisablePCP {
|
} else if !DisablePCP {
|
||||||
uc.WriteTo(pcpAnnounceRequest(myIP), pcpAddr)
|
uc.WriteTo(pcpAnnounceRequest(myIP), pxpAddr)
|
||||||
}
|
}
|
||||||
if c.sawUPnPRecently() {
|
if c.sawUPnPRecently() {
|
||||||
res.UPnP = true
|
res.UPnP = true
|
||||||
@ -669,9 +700,9 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
|||||||
}
|
}
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
port := addr.(*net.UDPAddr).Port
|
port := uint16(addr.(*net.UDPAddr).Port)
|
||||||
switch port {
|
switch port {
|
||||||
case upnpPort:
|
case c.upnpPort():
|
||||||
if mem.Contains(mem.B(buf[:n]), mem.S(":InternetGatewayDevice:")) {
|
if mem.Contains(mem.B(buf[:n]), mem.S(":InternetGatewayDevice:")) {
|
||||||
meta, err := parseUPnPDiscoResponse(buf[:n])
|
meta, err := parseUPnPDiscoResponse(buf[:n])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -686,7 +717,7 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
|||||||
c.uPnPMeta = meta
|
c.uPnPMeta = meta
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
}
|
}
|
||||||
case pcpPort: // same as pmpPort
|
case c.pxpPort(): // same value for PMP and PCP
|
||||||
if pres, ok := parsePCPResponse(buf[:n]); ok {
|
if pres, ok := parsePCPResponse(buf[:n]); ok {
|
||||||
if pres.OpCode == pcpOpReply|pcpOpAnnounce {
|
if pres.OpCode == pcpOpReply|pcpOpAnnounce {
|
||||||
pcpHeard = true
|
pcpHeard = true
|
||||||
@ -729,7 +760,7 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
|||||||
var pmpReqExternalAddrPacket = []byte{pmpVersion, pmpOpMapPublicAddr} // 0, 0
|
var pmpReqExternalAddrPacket = []byte{pmpVersion, pmpOpMapPublicAddr} // 0, 0
|
||||||
|
|
||||||
const (
|
const (
|
||||||
upnpPort = 1900 // for UDP discovery only; TCP port discovered later
|
upnpDefaultPort = 1900 // for UDP discovery only; TCP port discovered later
|
||||||
)
|
)
|
||||||
|
|
||||||
// uPnPPacket is the UPnP UDP discovery packet's request body.
|
// uPnPPacket is the UPnP UDP discovery packet's request body.
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -72,7 +73,9 @@ func TestProbeIntegration(t *testing.T) {
|
|||||||
logf("portmapping changed.")
|
logf("portmapping changed.")
|
||||||
logf("have mapping: %v", c.HaveMapping())
|
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)
|
||||||
c.SetGatewayLookupFunc(func() (gw, self netaddr.IP, ok bool) {
|
c.SetGatewayLookupFunc(func() (gw, self netaddr.IP, ok bool) {
|
||||||
return netaddr.IPv4(127, 0, 0, 1), netaddr.IPv4(1, 2, 3, 4), true
|
return netaddr.IPv4(127, 0, 0, 1), netaddr.IPv4(1, 2, 3, 4), true
|
||||||
})
|
})
|
||||||
@ -81,7 +84,21 @@ func TestProbeIntegration(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Probe: %v", err)
|
t.Fatalf("Probe: %v", err)
|
||||||
}
|
}
|
||||||
|
if !res.UPnP {
|
||||||
|
t.Errorf("didn't detect UPnP")
|
||||||
|
}
|
||||||
|
st := igd.stats()
|
||||||
|
want := igdCounters{
|
||||||
|
numUPnPDiscoRecv: 1,
|
||||||
|
numPMPRecv: 1,
|
||||||
|
numPCPRecv: 1,
|
||||||
|
numPMPPublicAddrRecv: 1,
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(st, want) {
|
||||||
|
t.Errorf("unexpected stats:\n got: %+v\nwant: %+v", st, want)
|
||||||
|
}
|
||||||
|
|
||||||
t.Logf("Probe: %+v", res)
|
t.Logf("Probe: %+v", res)
|
||||||
t.Logf("IGD stats: %+v", igd.stats())
|
t.Logf("IGD stats: %+v", st)
|
||||||
// TODO(bradfitz): finish
|
// TODO(bradfitz): finish
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user