Commit c4deb647 authored by Jeromy Johnson's avatar Jeromy Johnson Committed by GitHub
Browse files

Merge pull request #139 from libp2p/feat/extract-nat

extract NAT traversal code to its own lib
parents 6d069ac4 0e7e451d
...@@ -4,7 +4,7 @@ import ( ...@@ -4,7 +4,7 @@ import (
"context" "context"
"sync" "sync"
inat "github.com/libp2p/go-libp2p/p2p/nat" inat "github.com/libp2p/go-libp2p-nat"
goprocess "github.com/jbenet/goprocess" goprocess "github.com/jbenet/goprocess"
lgbl "github.com/libp2p/go-libp2p-loggables" lgbl "github.com/libp2p/go-libp2p-loggables"
......
package nat
import (
"fmt"
"sync"
"time"
"github.com/jbenet/goprocess"
ma "github.com/multiformats/go-multiaddr"
manet "github.com/multiformats/go-multiaddr-net"
)
// 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
// InternalAddr returns the internal address.
InternalAddr() ma.Multiaddr
// ExternalAddr returns the external facing address. If the mapping is not
// established, addr will be nil, and and ErrNoMapping will be returned.
ExternalAddr() (addr ma.Multiaddr, 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
permanent bool
intaddr ma.Multiaddr
proc goprocess.Process
comment string
cached ma.Multiaddr
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) InternalAddr() ma.Multiaddr {
m.Lock()
defer m.Unlock()
return m.intaddr
}
func (m *mapping) ExternalAddr() (ma.Multiaddr, error) {
m.cacheLk.Lock()
ctime := m.cacheTime
cval := m.cached
m.cacheLk.Unlock()
if time.Since(ctime) < CacheTime {
return cval, nil
}
if m.ExternalPort() == 0 { // dont even try right now.
return nil, ErrNoMapping
}
m.nat.natmu.Lock()
ip, err := m.nat.nat.GetExternalAddress()
m.nat.natmu.Unlock()
if err != nil {
return nil, err
}
ipmaddr, err := manet.FromIP(ip)
if err != nil {
return nil, fmt.Errorf("error parsing ip")
}
// call m.ExternalPort again, as mapping may have changed under our feet. (tocttou)
extport := m.ExternalPort()
if extport == 0 {
return nil, ErrNoMapping
}
tcp, err := ma.NewMultiaddr(fmt.Sprintf("/%s/%d", m.Protocol(), extport))
if err != nil {
return nil, err
}
maddr2 := ipmaddr.Encapsulate(tcp)
m.cacheLk.Lock()
m.cached = maddr2
m.cacheTime = time.Now()
m.cacheLk.Unlock()
return maddr2, nil
}
func (m *mapping) Close() error {
return m.proc.Close()
}
package nat
import (
"errors"
"fmt"
"strconv"
"strings"
"sync"
"time"
nat "github.com/fd/go-nat"
logging "github.com/ipfs/go-log"
goprocess "github.com/jbenet/goprocess"
periodic "github.com/jbenet/goprocess/periodic"
ma "github.com/multiformats/go-multiaddr"
manet "github.com/multiformats/go-multiaddr-net"
)
var (
// ErrNoMapping signals no mapping exists for an address
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() *NAT {
nat, err := nat.DiscoverGateway()
if err != nil {
log.Debug("DiscoverGateway error:", err)
return nil
}
addr, err := nat.GetDeviceAddress()
if err != nil {
log.Debug("DiscoverGateway address error:", err)
} else {
log.Debug("DiscoverGateway address:", addr)
}
return newNAT(nat)
}
// 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
proc goprocess.Process // manages nat mappings lifecycle
mappingmu sync.RWMutex // guards mappings
mappings map[*mapping]struct{}
Notifier
}
func newNAT(realNAT nat.NAT) *NAT {
return &NAT{
nat: realNAT,
proc: goprocess.WithParent(goprocess.Background()),
mappings: make(map[*mapping]struct{}),
}
}
// Close shuts down all port mappings. NAT can no longer be used.
func (nat *NAT) Close() error {
return nat.proc.Close()
}
// Process returns the nat's life-cycle manager, for making it listen
// to close signals.
func (nat *NAT) Process() goprocess.Process {
return nat.proc
}
// 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
}
func (nat *NAT) addMapping(m *mapping) {
// make mapping automatically close when nat is closed.
nat.proc.AddChild(m.proc)
nat.mappingmu.Lock()
nat.mappings[m] = struct{}{}
nat.mappingmu.Unlock()
}
func (nat *NAT) rmMapping(m *mapping) {
nat.mappingmu.Lock()
delete(nat.mappings, m)
nat.mappingmu.Unlock()
}
// NewMapping attemps 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(maddr ma.Multiaddr) (Mapping, error) {
if nat == nil {
return nil, fmt.Errorf("no nat available")
}
network, addr, err := manet.DialArgs(maddr)
if err != nil {
return nil, fmt.Errorf("DialArgs failed on addr:", maddr.String())
}
switch network {
case "tcp", "tcp4", "tcp6":
network = "tcp"
case "udp", "udp4", "udp6":
network = "udp"
default:
return nil, fmt.Errorf("transport not supported by NAT: %s", network)
}
intports := strings.Split(addr, ":")[1]
intport, err := strconv.Atoi(intports)
if err != nil {
return nil, err
}
m := &mapping{
nat: nat,
proto: network,
intport: intport,
intaddr: maddr,
}
m.proc = goprocess.WithTeardown(func() error {
nat.rmMapping(m)
return nil
})
nat.addMapping(m)
m.proc.AddChild(periodic.Every(MappingDuration/3, func(worker goprocess.Process) {
nat.establishMapping(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) establishMapping(m *mapping) {
oldport := m.ExternalPort()
log.Debugf("Attempting port map: %s/%d", m.Protocol(), m.InternalPort())
comment := "libp2p"
if m.comment != "" {
comment = "libp2p-" + m.comment
}
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()
failure := func() {
m.setExternalPort(0) // clear mapping
// TODO: log.Event
log.Warningf("failed to establish port mapping: %s", err)
nat.Notifier.notifyAll(func(n Notifiee) {
n.MappingFailed(nat, m, oldport, err)
})
// we do not close if the mapping failed,
// because it may work again next time.
}
if err != nil || newport == 0 {
failure()
return
}
m.setExternalPort(newport)
ext, err := m.ExternalAddr()
if err != nil {
log.Debugf("NAT Mapping addr error: %s %s", m.InternalAddr(), err)
failure()
return
}
log.Debugf("NAT Mapping: %s --> %s", m.InternalAddr(), ext)
if oldport != 0 && newport != oldport {
log.Debugf("failed to renew same port mapping: ch %d -> %d", oldport, newport)
nat.Notifier.notifyAll(func(n Notifiee) {
n.MappingChanged(nat, m, oldport, newport)
})
}
nat.Notifier.notifyAll(func(n Notifiee) {
n.MappingSuccess(nat, m)
})
}
// PortMapAddrs attempts to open (and continue to keep open)
// port mappings for given addrs. This function blocks until
// all addresses have been tried. This allows clients to
// retrieve results immediately after:
//
// nat.PortMapAddrs(addrs)
// mapped := nat.ExternalAddrs()
//
// Some 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) PortMapAddrs(addrs []ma.Multiaddr) {
// spin off addr mappings independently.
var wg sync.WaitGroup
for _, addr := range addrs {
// do all of them concurrently
wg.Add(1)
go func() {
defer wg.Done()
nat.NewMapping(addr)
}()
}
wg.Wait()
}
// MappedAddrs returns address mappings NAT believes have been
// successfully established. Unsuccessful mappings are nil. This is:
//
// map[internalAddr]externalAddr
//
// This set of mappings _may not_ be correct, as NAT devices are finicky.
// Consider this with _best effort_ semantics.
func (nat *NAT) MappedAddrs() map[ma.Multiaddr]ma.Multiaddr {
mappings := nat.Mappings()
addrmap := make(map[ma.Multiaddr]ma.Multiaddr, len(mappings))
for _, m := range mappings {
i := m.InternalAddr()
e, err := m.ExternalAddr()
if err != nil {
addrmap[i] = nil
} else {
addrmap[i] = e
}
}
return addrmap
}
// ExternalAddrs returns a list of addresses that NAT believes have
// been successfully established. Unsuccessful mappings are omitted,
// so nat.ExternalAddrs() may return less addresses than nat.InternalAddrs().
// To see which addresses are mapped, use nat.MappedAddrs().
//
// This set of mappings _may not_ be correct, as NAT devices are finicky.
// Consider this with _best effort_ semantics.
func (nat *NAT) ExternalAddrs() []ma.Multiaddr {
mappings := nat.Mappings()
addrs := make([]ma.Multiaddr, 0, len(mappings))
for _, m := range mappings {
a, err := m.ExternalAddr()
if err != nil {
continue // this mapping not currently successful.
}
addrs = append(addrs, a)
}
return addrs
}
package nat
import (
notifier "github.com/whyrusleeping/go-notifier"
)
// Notifier is an object that assists NAT in notifying listeners.
// It is implemented using thirdparty/notifier
type Notifier struct {
n notifier.Notifier
}
func (n *Notifier) notifyAll(notify func(n Notifiee)) {
n.n.NotifyAll(func(n notifier.Notifiee) {
notify(n.(Notifiee))
})
}
// Notify signs up notifiee to listen to NAT events.
func (n *Notifier) Notify(notifiee Notifiee) {
n.n.Notify(n)
}
// StopNotify stops signaling events to notifiee.
func (n *Notifier) StopNotify(notifiee Notifiee) {
n.n.StopNotify(notifiee)
}
// Notifiee is an interface objects must implement to listen to NAT events.
type Notifiee interface {
// Called every time a successful mapping happens
// Warning: the port mapping may have changed. If that is the
// case, both MappingSuccess and MappingChanged are called.
MappingSuccess(nat *NAT, m Mapping)
// Called when mapping a port succeeds, but the mapping is
// with a different port than an earlier success.
MappingChanged(nat *NAT, m Mapping, oldport int, newport int)
// Called when a port mapping fails. NAT will continue attempting after
// the next period. To stop trying, use: mapping.Close(). After this failure,
// mapping.ExternalPort() will be zero, and nat.ExternalAddrs() will not
// return the address for this mapping. With luck, the next attempt will
// succeed, without the client needing to do anything.
MappingFailed(nat *NAT, m Mapping, oldport int, err error)
}
...@@ -38,11 +38,6 @@ ...@@ -38,11 +38,6 @@
"name": "go-multistream", "name": "go-multistream",
"version": "0.3.1" "version": "0.3.1"
}, },
{
"hash": "QmYabcy8kaP658zZRZHLqRquJ37ycNWr4qhBL25tUodZWc",
"name": "go-nat",
"version": "0.0.0"
},
{ {
"hash": "QmQHGMVmrsgmqUG8ih3puNXUJneSpi13dkcZpzLKkskUkH", "hash": "QmQHGMVmrsgmqUG8ih3puNXUJneSpi13dkcZpzLKkskUkH",
"name": "go-detect-race", "name": "go-detect-race",
...@@ -248,6 +243,12 @@ ...@@ -248,6 +243,12 @@
"hash": "QmeAfPWBWDQq9qjQ5oiWhaFs7oEsfB6FyEj5VxNdc2r34q", "hash": "QmeAfPWBWDQq9qjQ5oiWhaFs7oEsfB6FyEj5VxNdc2r34q",
"name": "go-libp2p-swarm", "name": "go-libp2p-swarm",
"version": "1.0.0" "version": "1.0.0"
},
{
"author": "whyrusleeping",
"hash": "QmPpncQ3L4bC3rnwLBrgEomygs5RbnFejb68GgsecxbMiL",
"name": "go-libp2p-nat",
"version": "0.0.0"
} }
], ],
"gxVersion": "0.4.0", "gxVersion": "0.4.0",
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment