Skip to content

Commit

Permalink
Merge pull request #1513 from libp2p/merge-nat
Browse files Browse the repository at this point in the history
move go-libp2p-nat here
  • Loading branch information
marten-seemann authored May 20, 2022
2 parents b121193 + f656048 commit 4c153be
Show file tree
Hide file tree
Showing 6 changed files with 317 additions and 8 deletions.
3 changes: 1 addition & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ require (
github.com/libp2p/go-libp2p-asn-util v0.1.0
github.com/libp2p/go-libp2p-circuit v0.6.0
github.com/libp2p/go-libp2p-core v0.15.1
github.com/libp2p/go-libp2p-nat v0.1.0
github.com/libp2p/go-libp2p-peerstore v0.6.0
github.com/libp2p/go-libp2p-resource-manager v0.2.1
github.com/libp2p/go-libp2p-testing v0.9.2
github.com/libp2p/go-mplex v0.7.0
github.com/libp2p/go-msgio v0.2.0
github.com/libp2p/go-nat v0.1.0
github.com/libp2p/go-netroute v0.2.0
github.com/libp2p/go-reuseport v0.1.0
github.com/libp2p/go-stream-muxer-multistream v0.4.0
Expand Down Expand Up @@ -83,7 +83,6 @@ require (
github.com/libp2p/go-libp2p-tls v0.4.1 // indirect
github.com/libp2p/go-libp2p-transport-upgrader v0.7.1 // indirect
github.com/libp2p/go-libp2p-yamux v0.9.1 // indirect
github.com/libp2p/go-nat v0.1.0 // indirect
github.com/libp2p/go-openssl v0.0.7 // indirect
github.com/libp2p/go-tcp-transport v0.5.1 // indirect
github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect
Expand Down
4 changes: 0 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,6 @@ github.com/ipfs/go-log v0.0.1/go.mod h1:kL1d2/hzSpI0thNYjiKfjanbVNU+IIGA/WnNESY9
github.com/ipfs/go-log v1.0.4/go.mod h1:oDCg2FkjogeFOhqqb+N39l2RpTNPL6F/StPkB3kPgcs=
github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8=
github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo=
github.com/ipfs/go-log/v2 v2.0.3/go.mod h1:O7P1lJt27vWHhOwQmcFEvlmo49ry2VY2+JfBWFaa9+0=
github.com/ipfs/go-log/v2 v2.0.5/go.mod h1:eZs4Xt4ZUJQFM3DlanGhy7TkwwawCZcSByscwkWG+dw=
github.com/ipfs/go-log/v2 v2.1.1/go.mod h1:2v2nsGfZsvvAJz13SyFzf9ObaqwHiHxsPLEHntrv9KM=
github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g=
Expand Down Expand Up @@ -442,8 +441,6 @@ github.com/libp2p/go-libp2p-core v0.15.1 h1:0RY+Mi/ARK9DgG1g9xVQLb8dDaaU8tCePMtG
github.com/libp2p/go-libp2p-core v0.15.1/go.mod h1:agSaboYM4hzB1cWekgVReqV5M4g5M+2eNNejV+1EEhs=
github.com/libp2p/go-libp2p-mplex v0.4.1/go.mod h1:cmy+3GfqfM1PceHTLL7zQzAAYaryDu6iPSC+CIb094g=
github.com/libp2p/go-libp2p-mplex v0.5.0/go.mod h1:eLImPJLkj3iG5t5lq68w3Vm5NAQ5BcKwrrb2VmOYb3M=
github.com/libp2p/go-libp2p-nat v0.1.0 h1:vigUi2MEN+fwghe5ijpScxtbbDz+L/6y8XwlzYOJgSY=
github.com/libp2p/go-libp2p-nat v0.1.0/go.mod h1:DQzAG+QbDYjN1/C3B6vXucLtz3u9rEonLVPtZVzQqks=
github.com/libp2p/go-libp2p-peerstore v0.4.0/go.mod h1:rDJUFyzEWPpXpEwywkcTYYzDHlwza8riYMaUzaN6hX0=
github.com/libp2p/go-libp2p-peerstore v0.6.0 h1:HJminhQSGISBIRb93N6WK3t6Fa8OOTnHd/VBjL4mY5A=
github.com/libp2p/go-libp2p-peerstore v0.6.0/go.mod h1:DGEmKdXrcYpK9Jha3sS7MhqYdInxJy84bIPtSu65bKc=
Expand Down Expand Up @@ -852,7 +849,6 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
Expand Down
2 changes: 1 addition & 1 deletion p2p/host/basic/basic_host.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/libp2p/go-libp2p/p2p/host/autonat"
"github.com/libp2p/go-libp2p/p2p/host/pstoremanager"
"github.com/libp2p/go-libp2p/p2p/host/relaysvc"
inat "github.com/libp2p/go-libp2p/p2p/net/nat"
relayv2 "github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/relay"
"github.com/libp2p/go-libp2p/p2p/protocol/holepunch"
"github.com/libp2p/go-libp2p/p2p/protocol/identify"
Expand All @@ -28,7 +29,6 @@ import (
"github.com/libp2p/go-libp2p-core/record"

"github.com/libp2p/go-eventbus"
inat "github.com/libp2p/go-libp2p-nat"
"github.com/libp2p/go-netroute"

logging "github.com/ipfs/go-log/v2"
Expand Down
4 changes: 3 additions & 1 deletion p2p/host/basic/natmgr.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import (
"sync"
"time"

inat "github.com/libp2p/go-libp2p/p2p/net/nat"

"github.com/libp2p/go-libp2p-core/network"
inat "github.com/libp2p/go-libp2p-nat"

ma "github.com/multiformats/go-multiaddr"
)

Expand Down
119 changes: 119 additions & 0 deletions p2p/net/nat/mapping.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package nat

import (
"fmt"
"net"
"sync"
"time"
)

// Mapping represents a port mapping in a NAT.
type Mapping interface {
// NAT returns the NAT object this Mapping belongs to.
NAT() *NAT

// Protocol returns the protocol of this port mapping. This is either
// "tcp" or "udp" as no other protocols are likely to be NAT-supported.
Protocol() string

// InternalPort returns the internal device port. Mapping will continue to
// try to map InternalPort() to an external facing port.
InternalPort() int

// ExternalPort returns the external facing port. If the mapping is not
// established, port will be 0
ExternalPort() int

// ExternalAddr returns the external facing address. If the mapping is not
// established, addr will be nil, and and ErrNoMapping will be returned.
ExternalAddr() (addr net.Addr, err error)

// Close closes the port mapping
Close() error
}

// keeps republishing
type mapping struct {
sync.Mutex // guards all fields

nat *NAT
proto string
intport int
extport int

cached net.IP
cacheTime time.Time
cacheLk sync.Mutex
}

func (m *mapping) NAT() *NAT {
m.Lock()
defer m.Unlock()
return m.nat
}

func (m *mapping) Protocol() string {
m.Lock()
defer m.Unlock()
return m.proto
}

func (m *mapping) InternalPort() int {
m.Lock()
defer m.Unlock()
return m.intport
}

func (m *mapping) ExternalPort() int {
m.Lock()
defer m.Unlock()
return m.extport
}

func (m *mapping) setExternalPort(p int) {
m.Lock()
defer m.Unlock()
m.extport = p
}

func (m *mapping) ExternalAddr() (net.Addr, error) {
m.cacheLk.Lock()
defer m.cacheLk.Unlock()
oport := m.ExternalPort()
if oport == 0 {
// dont even try right now.
return nil, ErrNoMapping
}

if time.Since(m.cacheTime) >= CacheTime {
m.nat.natmu.Lock()
cval, err := m.nat.nat.GetExternalAddress()
m.nat.natmu.Unlock()

if err != nil {
return nil, err
}

m.cached = cval
m.cacheTime = time.Now()
}
switch m.Protocol() {
case "tcp":
return &net.TCPAddr{
IP: m.cached,
Port: oport,
}, nil
case "udp":
return &net.UDPAddr{
IP: m.cached,
Port: oport,
}, nil
default:
panic(fmt.Sprintf("invalid protocol %q", m.Protocol()))
}
}

func (m *mapping) Close() error {
m.nat.removeMapping(m)
return nil
}
193 changes: 193 additions & 0 deletions p2p/net/nat/nat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package nat

import (
"context"
"errors"
"fmt"
"sync"
"time"

logging "github.com/ipfs/go-log/v2"

"github.com/libp2p/go-nat"
)

// ErrNoMapping signals no mapping exists for an address
var ErrNoMapping = errors.New("mapping not established")

var log = logging.Logger("nat")

// MappingDuration is a default port mapping duration.
// Port mappings are renewed every (MappingDuration / 3)
const MappingDuration = time.Second * 60

// CacheTime is the time a mapping will cache an external address for
const CacheTime = time.Second * 15

// DiscoverNAT looks for a NAT device in the network and
// returns an object that can manage port mappings.
func DiscoverNAT(ctx context.Context) (*NAT, error) {
natInstance, err := nat.DiscoverGateway(ctx)
if err != nil {
return nil, err
}

// Log the device addr.
addr, err := natInstance.GetDeviceAddress()
if err != nil {
log.Debug("DiscoverGateway address error:", err)
} else {
log.Debug("DiscoverGateway address:", addr)
}

return newNAT(natInstance), nil
}

// NAT is an object that manages address port mappings in
// NATs (Network Address Translators). It is a long-running
// service that will periodically renew port mappings,
// and keep an up-to-date list of all the external addresses.
type NAT struct {
natmu sync.Mutex
nat nat.NAT

refCount sync.WaitGroup
ctx context.Context
ctxCancel context.CancelFunc

mappingmu sync.RWMutex // guards mappings
closed bool
mappings map[*mapping]struct{}
}

func newNAT(realNAT nat.NAT) *NAT {
ctx, cancel := context.WithCancel(context.Background())
return &NAT{
nat: realNAT,
mappings: make(map[*mapping]struct{}),
ctx: ctx,
ctxCancel: cancel,
}
}

// Close shuts down all port mappings. NAT can no longer be used.
func (nat *NAT) Close() error {
nat.mappingmu.Lock()
nat.closed = true
nat.mappingmu.Unlock()

nat.ctxCancel()
nat.refCount.Wait()
return nil
}

// Mappings returns a slice of all NAT mappings
func (nat *NAT) Mappings() []Mapping {
nat.mappingmu.Lock()
maps2 := make([]Mapping, 0, len(nat.mappings))
for m := range nat.mappings {
maps2 = append(maps2, m)
}
nat.mappingmu.Unlock()
return maps2
}

// NewMapping attempts to construct a mapping on protocol and internal port
// It will also periodically renew the mapping until the returned Mapping
// -- or its parent NAT -- is Closed.
//
// May not succeed, and mappings may change over time;
// NAT devices may not respect our port requests, and even lie.
// Clients should not store the mapped results, but rather always
// poll our object for the latest mappings.
func (nat *NAT) NewMapping(protocol string, port int) (Mapping, error) {
if nat == nil {
return nil, fmt.Errorf("no nat available")
}

switch protocol {
case "tcp", "udp":
default:
return nil, fmt.Errorf("invalid protocol: %s", protocol)
}

m := &mapping{
intport: port,
nat: nat,
proto: protocol,
}

nat.mappingmu.Lock()
if nat.closed {
nat.mappingmu.Unlock()
return nil, errors.New("closed")
}
nat.mappings[m] = struct{}{}
nat.refCount.Add(1)
nat.mappingmu.Unlock()
go nat.refreshMappings(m)

// do it once synchronously, so first mapping is done right away, and before exiting,
// allowing users -- in the optimistic case -- to use results right after.
nat.establishMapping(m)
return m, nil
}

func (nat *NAT) removeMapping(m *mapping) {
nat.mappingmu.Lock()
delete(nat.mappings, m)
nat.mappingmu.Unlock()
nat.natmu.Lock()
nat.nat.DeletePortMapping(m.Protocol(), m.InternalPort())
nat.natmu.Unlock()
}

func (nat *NAT) refreshMappings(m *mapping) {
defer nat.refCount.Done()
t := time.NewTicker(MappingDuration / 3)
defer t.Stop()

for {
select {
case <-t.C:
nat.establishMapping(m)
case <-nat.ctx.Done():
m.Close()
return
}
}
}

func (nat *NAT) establishMapping(m *mapping) {
oldport := m.ExternalPort()

log.Debugf("Attempting port map: %s/%d", m.Protocol(), m.InternalPort())
const comment = "libp2p"

nat.natmu.Lock()
newport, err := nat.nat.AddPortMapping(m.Protocol(), m.InternalPort(), comment, MappingDuration)
if err != nil {
// Some hardware does not support mappings with timeout, so try that
newport, err = nat.nat.AddPortMapping(m.Protocol(), m.InternalPort(), comment, 0)
}
nat.natmu.Unlock()

if err != nil || newport == 0 {
m.setExternalPort(0) // clear mapping
// TODO: log.Event
if err != nil {
log.Warnf("failed to establish port mapping: %s", err)
} else {
log.Warnf("failed to establish port mapping: newport = 0")
}
// we do not close if the mapping failed,
// because it may work again next time.
return
}

m.setExternalPort(newport)
log.Debugf("NAT Mapping: %d --> %d (%s)", m.ExternalPort(), m.InternalPort(), m.Protocol())
if oldport != 0 && newport != oldport {
log.Debugf("failed to renew same port mapping: ch %d -> %d", oldport, newport)
}
}

0 comments on commit 4c153be

Please sign in to comment.