diff --git a/net/portmapper/pcp.go b/net/portmapper/pcp.go index d9f6af175..f4c9ceaaf 100644 --- a/net/portmapper/pcp.go +++ b/net/portmapper/pcp.go @@ -5,10 +5,14 @@ package portmapper import ( + "context" "crypto/rand" "encoding/binary" + "fmt" + "time" "inet.af/netaddr" + "tailscale.com/net/netns" ) // References: @@ -34,41 +38,92 @@ const ( pcpTCPMapping = 6 // portmap TCP ) -// pcpMapRequest generates a PCP packet with a MAP opcode. -func pcpMapRequest(myIP netaddr.IP, mapToLocalPort int, delete bool) []byte { - const udpProtoNumber = 17 - lifetimeSeconds := uint32(1) - if delete { - lifetimeSeconds = 0 +type pcpMapping struct { + gw netaddr.IP + internal netaddr.IPPort + external netaddr.IPPort + + renewAfter time.Time + goodUntil time.Time +} + +func (p *pcpMapping) GoodUntil() time.Time { return p.goodUntil } +func (p *pcpMapping) RenewAfter() time.Time { return p.renewAfter } +func (p *pcpMapping) External() netaddr.IPPort { return p.external } +func (p *pcpMapping) Release(ctx context.Context) { + uc, err := netns.Listener().ListenPacket(ctx, "udp4", ":0") + if err != nil { + return } - const opMap = 1 + defer uc.Close() + pkt := buildPCPRequestMappingPacket(p.internal.IP(), p.internal.Port(), p.external.Port(), 0) + uc.WriteTo(pkt, netaddr.IPPortFrom(p.gw, pcpPort).UDPAddr()) +} - // 24 byte header + 36 byte map opcode - pkt := make([]byte, (32+32+128)/8+(96+8+24+16+16+128)/8) +// buildPCPRequestMappingPacket generates a PCP packet with a MAP opcode. +// To create a packet which deletes a mapping, lifetimeSec should be set to 0. +// If prevPort is not known, it should be set to 0. +func buildPCPRequestMappingPacket(myIP netaddr.IP, localPort, prevPort uint16, lifetimeSec uint32) (pkt []byte) { + // note: lifetimeSec = 0 implies delete the mapping, should that be special-cased here? - // The header (https://tools.ietf.org/html/rfc6887#section-7.1) - pkt[0] = 2 // version - pkt[1] = opMap - binary.BigEndian.PutUint32(pkt[4:8], lifetimeSeconds) + // 24 byte common PCP header + 36 bytes of MAP-specific fields + pkt = make([]byte, 24+36) + pkt[0] = pcpVersion + pkt[1] = pcpOpMap + binary.BigEndian.PutUint32(pkt[4:8], lifetimeSec) myIP16 := myIP.As16() - copy(pkt[8:], myIP16[:]) + copy(pkt[8:24], myIP16[:]) - // The map opcode body (https://tools.ietf.org/html/rfc6887#section-11.1) mapOp := pkt[24:] - rand.Read(mapOp[:12]) // 96 bit mappping nonce - mapOp[12] = udpProtoNumber - binary.BigEndian.PutUint16(mapOp[16:], uint16(mapToLocalPort)) + rand.Read(mapOp[:12]) // 96 bit mapping nonce + + // TODO should this be a UDP mapping? It looks like it supports "all protocols" with 0, but + // also doesn't support a local port then. + mapOp[12] = pcpUDPMapping + binary.BigEndian.PutUint16(mapOp[16:18], localPort) + binary.BigEndian.PutUint16(mapOp[18:20], prevPort) + v4unspec := netaddr.MustParseIP("0.0.0.0") v4unspec16 := v4unspec.As16() copy(mapOp[20:], v4unspec16[:]) return pkt } +func parsePCPMapResponse(resp []byte) (*pcpMapping, error) { + if len(resp) < 60 { + return nil, fmt.Errorf("Does not appear to be PCP MAP response") + } + res, ok := parsePCPResponse(resp[:24]) + if !ok { + return nil, fmt.Errorf("Invalid PCP common header") + } + if res.ResultCode != pcpCodeOK { + return nil, fmt.Errorf("PCP response not ok, code %d", res.ResultCode) + } + // TODO don't ignore the nonce and make sure it's the same? + externalPort := binary.BigEndian.Uint16(resp[42:44]) + externalIPBytes := [16]byte{} + copy(externalIPBytes[:], resp[44:]) + externalIP := netaddr.IPFrom16(externalIPBytes) + + external := netaddr.IPPortFrom(externalIP, externalPort) + + lifetime := time.Second * time.Duration(res.Lifetime) + now := time.Now() + mapping := &pcpMapping{ + external: external, + renewAfter: now.Add(lifetime / 2), + goodUntil: now.Add(lifetime), + } + + return mapping, nil +} + // pcpAnnounceRequest generates a PCP packet with an ANNOUNCE opcode. func pcpAnnounceRequest(myIP netaddr.IP) []byte { // See https://tools.ietf.org/html/rfc6887#section-7.1 pkt := make([]byte, 24) - pkt[0] = pcpVersion // version + pkt[0] = pcpVersion pkt[1] = pcpOpAnnounce myIP16 := myIP.As16() copy(pkt[8:], myIP16[:]) diff --git a/net/portmapper/pcp_test.go b/net/portmapper/pcp_test.go new file mode 100644 index 000000000..4bf859d2c --- /dev/null +++ b/net/portmapper/pcp_test.go @@ -0,0 +1,27 @@ +// 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 ( + "testing" + + "inet.af/netaddr" +) + +var examplePCPMapResponse = []byte{2, 129, 0, 0, 0, 0, 28, 32, 0, 2, 155, 237, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 112, 9, 24, 241, 208, 251, 45, 157, 76, 10, 188, 17, 0, 0, 0, 4, 210, 4, 210, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 135, 180, 175, 246} + +func TestParsePCPMapResponse(t *testing.T) { + mapping, err := parsePCPMapResponse(examplePCPMapResponse) + if err != nil { + t.Fatalf("failed to parse PCP Map Response: %v", err) + } + if mapping == nil { + t.Fatalf("got nil mapping when expected non-nil") + } + expectedAddr := netaddr.MustParseIPPort("135.180.175.246:1234") + if mapping.external != expectedAddr { + t.Errorf("mismatched external address, got: %v, want: %v", mapping.external, expectedAddr) + } +} diff --git a/net/portmapper/portmapper.go b/net/portmapper/portmapper.go index bc38204fb..bbd4175e5 100644 --- a/net/portmapper/portmapper.go +++ b/net/portmapper/portmapper.go @@ -3,7 +3,7 @@ // license that can be found in the LICENSE file. // Package portmapper is a UDP port mapping client. It currently allows for mapping over -// NAT-PMP and UPnP, but will perhaps do PCP later. +// NAT-PMP, UPnP, and PCP. package portmapper import ( @@ -237,6 +237,10 @@ func (c *Client) sawPMPRecentlyLocked() bool { func (c *Client) sawPCPRecently() bool { c.mu.Lock() defer c.mu.Unlock() + return c.sawPCPRecentlyLocked() +} + +func (c *Client) sawPCPRecentlyLocked() bool { return c.pcpSawTime.After(time.Now().Add(-trustServiceStillAvailableDuration)) } @@ -373,15 +377,16 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netaddr.IPPor // find a PMP service, bail out early rather than probing // again. Cuts down latency for most clients. haveRecentPMP := c.sawPMPRecentlyLocked() + haveRecentPCP := c.sawPCPRecentlyLocked() if haveRecentPMP { m.external = m.external.WithIP(c.pmpPubIP) } - if c.lastProbe.After(now.Add(-5*time.Second)) && !haveRecentPMP { + if c.lastProbe.After(now.Add(-5*time.Second)) && !haveRecentPMP && !haveRecentPCP { c.mu.Unlock() // fallback to UPnP portmapping - if mapping, ok := c.getUPnPPortMapping(ctx, gw, internalAddr, prevPort); ok { - return mapping, nil + if external, ok := c.getUPnPPortMapping(ctx, gw, internalAddr, prevPort); ok { + return external, nil } return netaddr.IPPort{}, NoMappingError{ErrNoPortMappingServices} } @@ -399,18 +404,28 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netaddr.IPPor pmpAddr := netaddr.IPPortFrom(gw, pmpPort) pmpAddru := pmpAddr.UDPAddr() + pcpAddr := netaddr.IPPortFrom(gw, pcpPort) + pcpAddru := pcpAddr.UDPAddr() - // Ask for our external address if needed. - if m.external.IP().IsZero() { - if _, err := uc.WriteTo(pmpReqExternalAddrPacket, pmpAddru); err != nil { + // Create a mapping, defaulting to PMP unless only PCP was seen recently. + if !haveRecentPMP && haveRecentPCP { + // Only do PCP mapping in the case when PMP did not appear to be available recently. + pkt := buildPCPRequestMappingPacket(myIP, localPort, prevPort, pcpMapLifetimeSec) + if _, err := uc.WriteTo(pkt, pcpAddru); err != nil { return netaddr.IPPort{}, err } - } + } else { + // Ask for our external address if needed. + if m.external.IP().IsZero() { + if _, err := uc.WriteTo(pmpReqExternalAddrPacket, pmpAddru); err != nil { + return netaddr.IPPort{}, err + } + } - // And ask for a mapping. - pmpReqMapping := buildPMPRequestMappingPacket(localPort, prevPort, pmpMapLifetimeSec) - if _, err := uc.WriteTo(pmpReqMapping, pmpAddru); err != nil { - return netaddr.IPPort{}, err + pkt := buildPMPRequestMappingPacket(localPort, prevPort, pmpMapLifetimeSec) + if _, err := uc.WriteTo(pkt, pmpAddru); err != nil { + return netaddr.IPPort{}, err + } } res := make([]byte, 1500) @@ -432,24 +447,41 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netaddr.IPPor continue } if src == pmpAddr { - pres, ok := parsePMPResponse(res[:n]) - if !ok { - c.logf("unexpected PMP response: % 02x", res[:n]) - continue - } - if pres.ResultCode != 0 { - return netaddr.IPPort{}, NoMappingError{fmt.Errorf("PMP response Op=0x%x,Res=0x%x", pres.OpCode, pres.ResultCode)} - } - if pres.OpCode == pmpOpReply|pmpOpMapPublicAddr { - m.external = m.external.WithIP(pres.PublicAddr) - } - if pres.OpCode == pmpOpReply|pmpOpMapUDP { - m.external = m.external.WithPort(pres.ExternalPort) - d := time.Duration(pres.MappingValidSeconds) * time.Second - now := time.Now() - m.goodUntil = now.Add(d) - m.renewAfter = now.Add(d / 2) // renew in half the time - m.epoch = pres.SecondsSinceEpoch + version := res[0] + switch version { + case pmpVersion: + pres, ok := parsePMPResponse(res[:n]) + if !ok { + c.logf("unexpected PMP response: % 02x", res[:n]) + continue + } + if pres.ResultCode != 0 { + return netaddr.IPPort{}, NoMappingError{fmt.Errorf("PMP response Op=0x%x,Res=0x%x", pres.OpCode, pres.ResultCode)} + } + if pres.OpCode == pmpOpReply|pmpOpMapPublicAddr { + m.external = m.external.WithIP(pres.PublicAddr) + } + if pres.OpCode == pmpOpReply|pmpOpMapUDP { + m.external = m.external.WithPort(pres.ExternalPort) + d := time.Duration(pres.MappingValidSeconds) * time.Second + now := time.Now() + m.goodUntil = now.Add(d) + m.renewAfter = now.Add(d / 2) // renew in half the time + m.epoch = pres.SecondsSinceEpoch + } + case pcpVersion: + pcpMapping, err := parsePCPMapResponse(res[:n]) + if err != nil { + c.logf("failed to get PCP mapping: %v", err) + continue + } + pcpMapping.internal = m.internal + c.mu.Lock() + defer c.mu.Unlock() + c.mapping = pcpMapping + return pcpMapping.external, nil + default: + c.logf("unknown PMP/PCP version number: %d %v", version, res[:n]) } } @@ -470,6 +502,7 @@ const ( pmpMapLifetimeSec = 7200 // RFC recommended 2 hour map duration pmpMapLifetimeDelete = 0 // 0 second lifetime deletes + pmpVersion = 0 pmpOpMapPublicAddr = 0 pmpOpMapUDP = 1 pmpOpReply = 0x80 // OR'd into request's op code on response