254 lines
5.6 KiB
Go
254 lines
5.6 KiB
Go
package autonat
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"math/rand"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/libp2p/go-libp2p-core/host"
|
|
"github.com/libp2p/go-libp2p-core/network"
|
|
"github.com/libp2p/go-libp2p-core/peer"
|
|
"github.com/libp2p/go-libp2p-core/peerstore"
|
|
ma "github.com/multiformats/go-multiaddr"
|
|
)
|
|
|
|
// NATStatus is the state of NAT as detected by the ambient service.
|
|
type NATStatus int
|
|
|
|
const (
|
|
// NAT status is unknown; this means that the ambient service has not been
|
|
// able to decide the presence of NAT in the most recent attempt to test
|
|
// dial through known autonat peers. initial state.
|
|
NATStatusUnknown NATStatus = iota
|
|
// NAT status is publicly dialable
|
|
NATStatusPublic
|
|
// NAT status is private network
|
|
NATStatusPrivate
|
|
)
|
|
|
|
var (
|
|
AutoNATBootDelay = 15 * time.Second
|
|
AutoNATRetryInterval = 90 * time.Second
|
|
AutoNATRefreshInterval = 15 * time.Minute
|
|
AutoNATRequestTimeout = 30 * time.Second
|
|
)
|
|
|
|
// AutoNAT is the interface for ambient NAT autodiscovery
|
|
type AutoNAT interface {
|
|
// Status returns the current NAT status
|
|
Status() NATStatus
|
|
// PublicAddr returns the public dial address when NAT status is public and an
|
|
// error otherwise
|
|
PublicAddr() (ma.Multiaddr, error)
|
|
}
|
|
|
|
// AmbientAutoNAT is the implementation of ambient NAT autodiscovery
|
|
type AmbientAutoNAT struct {
|
|
ctx context.Context
|
|
host host.Host
|
|
|
|
getAddrs GetAddrs
|
|
|
|
mx sync.Mutex
|
|
peers map[peer.ID][]ma.Multiaddr
|
|
status NATStatus
|
|
addr ma.Multiaddr
|
|
// Reflects the confidence on of the NATStatus being private, as a single
|
|
// dialback may fail for reasons unrelated to NAT.
|
|
// If it is <3, then multiple autoNAT peers may be contacted for dialback
|
|
// If only a single autoNAT peer is known, then the confidence increases
|
|
// for each failure until it reaches 3.
|
|
confidence int
|
|
}
|
|
|
|
// NewAutoNAT creates a new ambient NAT autodiscovery instance attached to a host
|
|
// If getAddrs is nil, h.Addrs will be used
|
|
func NewAutoNAT(ctx context.Context, h host.Host, getAddrs GetAddrs) AutoNAT {
|
|
if getAddrs == nil {
|
|
getAddrs = h.Addrs
|
|
}
|
|
|
|
as := &AmbientAutoNAT{
|
|
ctx: ctx,
|
|
host: h,
|
|
getAddrs: getAddrs,
|
|
peers: make(map[peer.ID][]ma.Multiaddr),
|
|
status: NATStatusUnknown,
|
|
}
|
|
|
|
h.Network().Notify(as)
|
|
go as.background()
|
|
|
|
return as
|
|
}
|
|
|
|
func (as *AmbientAutoNAT) Status() NATStatus {
|
|
as.mx.Lock()
|
|
defer as.mx.Unlock()
|
|
return as.status
|
|
}
|
|
|
|
func (as *AmbientAutoNAT) PublicAddr() (ma.Multiaddr, error) {
|
|
as.mx.Lock()
|
|
defer as.mx.Unlock()
|
|
|
|
if as.status != NATStatusPublic {
|
|
return nil, errors.New("NAT Status is not public")
|
|
}
|
|
|
|
return as.addr, nil
|
|
}
|
|
|
|
func (as *AmbientAutoNAT) background() {
|
|
// wait a bit for the node to come online and establish some connections
|
|
// before starting autodetection
|
|
select {
|
|
case <-time.After(AutoNATBootDelay):
|
|
case <-as.ctx.Done():
|
|
return
|
|
}
|
|
|
|
for {
|
|
as.autodetect()
|
|
|
|
delay := AutoNATRefreshInterval
|
|
if as.status == NATStatusUnknown {
|
|
delay = AutoNATRetryInterval
|
|
}
|
|
|
|
select {
|
|
case <-time.After(delay):
|
|
case <-as.ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (as *AmbientAutoNAT) autodetect() {
|
|
peers := as.getPeers()
|
|
|
|
if len(peers) == 0 {
|
|
log.Debugf("skipping NAT auto detection; no autonat peers")
|
|
return
|
|
}
|
|
|
|
cli := NewAutoNATClient(as.host, as.getAddrs)
|
|
ctx, cancel := context.WithTimeout(as.ctx, AutoNATRequestTimeout)
|
|
defer cancel()
|
|
|
|
var result struct {
|
|
sync.Mutex
|
|
private int
|
|
public int
|
|
pubaddr ma.Multiaddr
|
|
}
|
|
|
|
probe := 3 - as.confidence
|
|
if probe == 0 {
|
|
probe = 1
|
|
}
|
|
if probe > len(peers) {
|
|
probe = len(peers)
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
for _, pi := range peers[:probe] {
|
|
wg.Add(1)
|
|
go func(pi peer.AddrInfo) {
|
|
defer wg.Done()
|
|
|
|
as.host.Peerstore().AddAddrs(pi.ID, pi.Addrs, peerstore.TempAddrTTL)
|
|
a, err := cli.DialBack(ctx, pi.ID)
|
|
|
|
switch {
|
|
case err == nil:
|
|
log.Debugf("Dialback through %s successful; public address is %s", pi.ID.Pretty(), a.String())
|
|
result.Lock()
|
|
result.public++
|
|
result.pubaddr = a
|
|
result.Unlock()
|
|
|
|
case IsDialError(err):
|
|
log.Debugf("Dialback through %s failed", pi.ID.Pretty())
|
|
result.Lock()
|
|
result.private++
|
|
result.Unlock()
|
|
|
|
default:
|
|
log.Debugf("Dialback error through %s: %s", pi.ID.Pretty(), err)
|
|
}
|
|
}(pi)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
as.mx.Lock()
|
|
if result.public > 0 {
|
|
log.Debugf("NAT status is public")
|
|
if as.status == NATStatusPrivate {
|
|
// we are flipping our NATStatus, so confidence drops to 0
|
|
as.confidence = 0
|
|
} else if as.confidence < 3 {
|
|
as.confidence++
|
|
}
|
|
as.status = NATStatusPublic
|
|
as.addr = result.pubaddr
|
|
} else if result.private > 0 {
|
|
log.Debugf("NAT status is private")
|
|
if as.status == NATStatusPublic {
|
|
// we are flipping our NATStatus, so confidence drops to 0
|
|
as.confidence = 0
|
|
} else if as.confidence < 3 {
|
|
as.confidence++
|
|
}
|
|
as.status = NATStatusPrivate
|
|
as.addr = nil
|
|
} else if as.confidence > 0 {
|
|
// don't just flip to unknown, reduce confidence first
|
|
as.confidence--
|
|
} else {
|
|
log.Debugf("NAT status is unknown")
|
|
as.status = NATStatusUnknown
|
|
as.addr = nil
|
|
}
|
|
as.mx.Unlock()
|
|
}
|
|
|
|
func (as *AmbientAutoNAT) getPeers() []peer.AddrInfo {
|
|
as.mx.Lock()
|
|
defer as.mx.Unlock()
|
|
|
|
if len(as.peers) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var connected, others []peer.AddrInfo
|
|
|
|
for p, addrs := range as.peers {
|
|
if as.host.Network().Connectedness(p) == network.Connected {
|
|
connected = append(connected, peer.AddrInfo{ID: p, Addrs: addrs})
|
|
} else {
|
|
others = append(others, peer.AddrInfo{ID: p, Addrs: addrs})
|
|
}
|
|
}
|
|
|
|
shufflePeers(connected)
|
|
|
|
if len(connected) < 3 {
|
|
shufflePeers(others)
|
|
return append(connected, others...)
|
|
} else {
|
|
return connected
|
|
}
|
|
}
|
|
|
|
func shufflePeers(peers []peer.AddrInfo) {
|
|
for i := range peers {
|
|
j := rand.Intn(i + 1)
|
|
peers[i], peers[j] = peers[j], peers[i]
|
|
}
|
|
}
|