237 lines
6.2 KiB
Go
237 lines
6.2 KiB
Go
package autonatv2
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
logging "github.com/ipfs/go-log/v2"
|
|
"github.com/libp2p/go-libp2p/core/event"
|
|
"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/p2p/protocol/autonatv2/pb"
|
|
ma "github.com/multiformats/go-multiaddr"
|
|
manet "github.com/multiformats/go-multiaddr/net"
|
|
"golang.org/x/exp/rand"
|
|
"golang.org/x/exp/slices"
|
|
)
|
|
|
|
//go:generate protoc --go_out=. --go_opt=Mpb/autonatv2.proto=./pb pb/autonatv2.proto
|
|
|
|
const (
|
|
ServiceName = "libp2p.autonatv2"
|
|
DialBackProtocol = "/libp2p/autonat/2/dial-back"
|
|
DialProtocol = "/libp2p/autonat/2/dial-request"
|
|
|
|
maxMsgSize = 8192
|
|
streamTimeout = time.Minute
|
|
dialBackStreamTimeout = 5 * time.Second
|
|
dialBackDialTimeout = 30 * time.Second
|
|
dialBackMaxMsgSize = 1024
|
|
minHandshakeSizeBytes = 30_000 // for amplification attack prevention
|
|
maxHandshakeSizeBytes = 100_000
|
|
// maxPeerAddresses is the number of addresses in a dial request the server
|
|
// will inspect, rest are ignored.
|
|
maxPeerAddresses = 50
|
|
)
|
|
|
|
var (
|
|
ErrNoValidPeers = errors.New("no valid peers for autonat v2")
|
|
ErrDialRefused = errors.New("dial refused")
|
|
|
|
log = logging.Logger("autonatv2")
|
|
)
|
|
|
|
// Request is the request to verify reachability of a single address
|
|
type Request struct {
|
|
// Addr is the multiaddr to verify
|
|
Addr ma.Multiaddr
|
|
// SendDialData indicates whether to send dial data if the server requests it for Addr
|
|
SendDialData bool
|
|
}
|
|
|
|
// Result is the result of the CheckReachability call
|
|
type Result struct {
|
|
// Addr is the dialed address
|
|
Addr ma.Multiaddr
|
|
// Reachability of the dialed address
|
|
Reachability network.Reachability
|
|
// Status is the outcome of the dialback
|
|
Status pb.DialStatus
|
|
}
|
|
|
|
// AutoNAT implements the AutoNAT v2 client and server.
|
|
// Users can check reachability for their addresses using the CheckReachability method.
|
|
// The server provides amplification attack prevention and rate limiting.
|
|
type AutoNAT struct {
|
|
host host.Host
|
|
|
|
// for cleanly closing
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
wg sync.WaitGroup
|
|
|
|
srv *server
|
|
cli *client
|
|
|
|
mx sync.Mutex
|
|
peers *peersMap
|
|
// allowPrivateAddrs enables using private and localhost addresses for reachability checks.
|
|
// This is only useful for testing.
|
|
allowPrivateAddrs bool
|
|
}
|
|
|
|
// New returns a new AutoNAT instance.
|
|
// host and dialerHost should have the same dialing capabilities. In case the host doesn't support
|
|
// a transport, dial back requests for address for that transport will be ignored.
|
|
func New(host host.Host, dialerHost host.Host, opts ...AutoNATOption) (*AutoNAT, error) {
|
|
s := defaultSettings()
|
|
for _, o := range opts {
|
|
if err := o(s); err != nil {
|
|
return nil, fmt.Errorf("failed to apply option: %w", err)
|
|
}
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
an := &AutoNAT{
|
|
host: host,
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
srv: newServer(host, dialerHost, s),
|
|
cli: newClient(host),
|
|
allowPrivateAddrs: s.allowPrivateAddrs,
|
|
peers: newPeersMap(),
|
|
}
|
|
return an, nil
|
|
}
|
|
|
|
func (an *AutoNAT) background(sub event.Subscription) {
|
|
for {
|
|
select {
|
|
case <-an.ctx.Done():
|
|
sub.Close()
|
|
an.wg.Done()
|
|
return
|
|
case e := <-sub.Out():
|
|
switch evt := e.(type) {
|
|
case event.EvtPeerProtocolsUpdated:
|
|
an.updatePeer(evt.Peer)
|
|
case event.EvtPeerConnectednessChanged:
|
|
an.updatePeer(evt.Peer)
|
|
case event.EvtPeerIdentificationCompleted:
|
|
an.updatePeer(evt.Peer)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (an *AutoNAT) Start() error {
|
|
// Listen on event.EvtPeerProtocolsUpdated, event.EvtPeerConnectednessChanged
|
|
// event.EvtPeerIdentificationCompleted to maintain our set of autonat supporting peers.
|
|
sub, err := an.host.EventBus().Subscribe([]interface{}{
|
|
new(event.EvtPeerProtocolsUpdated),
|
|
new(event.EvtPeerConnectednessChanged),
|
|
new(event.EvtPeerIdentificationCompleted),
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("event subscription failed: %w", err)
|
|
}
|
|
an.cli.Start()
|
|
an.srv.Start()
|
|
|
|
an.wg.Add(1)
|
|
go an.background(sub)
|
|
return nil
|
|
}
|
|
|
|
func (an *AutoNAT) Close() {
|
|
an.cancel()
|
|
an.wg.Wait()
|
|
an.srv.Close()
|
|
an.cli.Close()
|
|
an.peers = nil
|
|
}
|
|
|
|
// GetReachability makes a single dial request for checking reachability for requested addresses
|
|
func (an *AutoNAT) GetReachability(ctx context.Context, reqs []Request) (Result, error) {
|
|
if !an.allowPrivateAddrs {
|
|
for _, r := range reqs {
|
|
if !manet.IsPublicAddr(r.Addr) {
|
|
return Result{}, fmt.Errorf("private address cannot be verified by autonatv2: %s", r.Addr)
|
|
}
|
|
}
|
|
}
|
|
an.mx.Lock()
|
|
p := an.peers.GetRand()
|
|
an.mx.Unlock()
|
|
if p == "" {
|
|
return Result{}, ErrNoValidPeers
|
|
}
|
|
|
|
res, err := an.cli.GetReachability(ctx, p, reqs)
|
|
if err != nil {
|
|
log.Debugf("reachability check with %s failed, err: %s", p, err)
|
|
return Result{}, fmt.Errorf("reachability check with %s failed: %w", p, err)
|
|
}
|
|
log.Debugf("reachability check with %s successful", p)
|
|
return res, nil
|
|
}
|
|
|
|
func (an *AutoNAT) updatePeer(p peer.ID) {
|
|
an.mx.Lock()
|
|
defer an.mx.Unlock()
|
|
|
|
// There are no ordering gurantees between identify and swarm events. Check peerstore
|
|
// and swarm for the current state
|
|
protos, err := an.host.Peerstore().SupportsProtocols(p, DialProtocol)
|
|
connectedness := an.host.Network().Connectedness(p)
|
|
if err == nil && slices.Contains(protos, DialProtocol) && connectedness == network.Connected {
|
|
an.peers.Put(p)
|
|
} else {
|
|
an.peers.Delete(p)
|
|
}
|
|
}
|
|
|
|
// peersMap provides random access to a set of peers. This is useful when the map iteration order is
|
|
// not sufficiently random.
|
|
type peersMap struct {
|
|
peerIdx map[peer.ID]int
|
|
peers []peer.ID
|
|
}
|
|
|
|
func newPeersMap() *peersMap {
|
|
return &peersMap{
|
|
peerIdx: make(map[peer.ID]int),
|
|
peers: make([]peer.ID, 0),
|
|
}
|
|
}
|
|
|
|
func (p *peersMap) GetRand() peer.ID {
|
|
if len(p.peers) == 0 {
|
|
return ""
|
|
}
|
|
return p.peers[rand.Intn(len(p.peers))]
|
|
}
|
|
|
|
func (p *peersMap) Put(pid peer.ID) {
|
|
if _, ok := p.peerIdx[pid]; ok {
|
|
return
|
|
}
|
|
p.peers = append(p.peers, pid)
|
|
p.peerIdx[pid] = len(p.peers) - 1
|
|
}
|
|
|
|
func (p *peersMap) Delete(pid peer.ID) {
|
|
idx, ok := p.peerIdx[pid]
|
|
if !ok {
|
|
return
|
|
}
|
|
p.peers[idx] = p.peers[len(p.peers)-1]
|
|
p.peerIdx[p.peers[idx]] = idx
|
|
p.peers = p.peers[:len(p.peers)-1]
|
|
delete(p.peerIdx, pid)
|
|
}
|