status-go/vendor/github.com/libp2p/go-libp2p-autonat/autonat.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]
}
}