Integrate BACKBEAT SDK and resolve KACHING license validation

Major integrations and fixes:
- Added BACKBEAT SDK integration for P2P operation timing
- Implemented beat-aware status tracking for distributed operations
- Added Docker secrets support for secure license management
- Resolved KACHING license validation via HTTPS/TLS
- Updated docker-compose configuration for clean stack deployment
- Disabled rollback policies to prevent deployment failures
- Added license credential storage (CHORUS-DEV-MULTI-001)

Technical improvements:
- BACKBEAT P2P operation tracking with phase management
- Enhanced configuration system with file-based secrets
- Improved error handling for license validation
- Clean separation of KACHING and CHORUS deployment stacks

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
anthonyrawlins
2025-09-06 07:56:26 +10:00
parent 543ab216f9
commit 9bdcbe0447
4730 changed files with 1480093 additions and 1916 deletions

View File

@@ -0,0 +1,450 @@
package autonat
import (
"context"
"math/rand"
"sync/atomic"
"time"
"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/host/eventbus"
logging "github.com/ipfs/go-log/v2"
ma "github.com/multiformats/go-multiaddr"
manet "github.com/multiformats/go-multiaddr/net"
)
var log = logging.Logger("autonat")
const maxConfidence = 3
// AmbientAutoNAT is the implementation of ambient NAT autodiscovery
type AmbientAutoNAT struct {
host host.Host
*config
ctx context.Context
ctxCancel context.CancelFunc // is closed when Close is called
backgroundRunning chan struct{} // is closed when the background go routine exits
inboundConn chan network.Conn
dialResponses chan error
// status is an autoNATResult reflecting current status.
status atomic.Pointer[network.Reachability]
// 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
lastInbound time.Time
lastProbeTry time.Time
lastProbe time.Time
recentProbes map[peer.ID]time.Time
service *autoNATService
emitReachabilityChanged event.Emitter
subscriber event.Subscription
}
// StaticAutoNAT is a simple AutoNAT implementation when a single NAT status is desired.
type StaticAutoNAT struct {
host host.Host
reachability network.Reachability
service *autoNATService
}
// New creates a new NAT autodiscovery system attached to a host
func New(h host.Host, options ...Option) (AutoNAT, error) {
var err error
conf := new(config)
conf.host = h
conf.dialPolicy.host = h
if err = defaults(conf); err != nil {
return nil, err
}
if conf.addressFunc == nil {
conf.addressFunc = h.Addrs
}
for _, o := range options {
if err = o(conf); err != nil {
return nil, err
}
}
emitReachabilityChanged, _ := h.EventBus().Emitter(new(event.EvtLocalReachabilityChanged), eventbus.Stateful)
var service *autoNATService
if (!conf.forceReachability || conf.reachability == network.ReachabilityPublic) && conf.dialer != nil {
service, err = newAutoNATService(conf)
if err != nil {
return nil, err
}
service.Enable()
}
if conf.forceReachability {
emitReachabilityChanged.Emit(event.EvtLocalReachabilityChanged{Reachability: conf.reachability})
return &StaticAutoNAT{
host: h,
reachability: conf.reachability,
service: service,
}, nil
}
ctx, cancel := context.WithCancel(context.Background())
as := &AmbientAutoNAT{
ctx: ctx,
ctxCancel: cancel,
backgroundRunning: make(chan struct{}),
host: h,
config: conf,
inboundConn: make(chan network.Conn, 5),
dialResponses: make(chan error, 1),
emitReachabilityChanged: emitReachabilityChanged,
service: service,
recentProbes: make(map[peer.ID]time.Time),
}
reachability := network.ReachabilityUnknown
as.status.Store(&reachability)
subscriber, err := as.host.EventBus().Subscribe(
[]any{new(event.EvtLocalAddressesUpdated), new(event.EvtPeerIdentificationCompleted)},
eventbus.Name("autonat"),
)
if err != nil {
return nil, err
}
as.subscriber = subscriber
h.Network().Notify(as)
go as.background()
return as, nil
}
// Status returns the AutoNAT observed reachability status.
func (as *AmbientAutoNAT) Status() network.Reachability {
s := as.status.Load()
return *s
}
func (as *AmbientAutoNAT) emitStatus() {
status := *as.status.Load()
as.emitReachabilityChanged.Emit(event.EvtLocalReachabilityChanged{Reachability: status})
if as.metricsTracer != nil {
as.metricsTracer.ReachabilityStatus(status)
}
}
func ipInList(candidate ma.Multiaddr, list []ma.Multiaddr) bool {
candidateIP, _ := manet.ToIP(candidate)
for _, i := range list {
if ip, err := manet.ToIP(i); err == nil && ip.Equal(candidateIP) {
return true
}
}
return false
}
func (as *AmbientAutoNAT) background() {
defer close(as.backgroundRunning)
// wait a bit for the node to come online and establish some connections
// before starting autodetection
delay := as.config.bootDelay
subChan := as.subscriber.Out()
defer as.subscriber.Close()
defer as.emitReachabilityChanged.Close()
timer := time.NewTimer(delay)
defer timer.Stop()
timerRunning := true
retryProbe := false
for {
select {
// new inbound connection.
case conn := <-as.inboundConn:
localAddrs := as.host.Addrs()
if manet.IsPublicAddr(conn.RemoteMultiaddr()) &&
!ipInList(conn.RemoteMultiaddr(), localAddrs) {
as.lastInbound = time.Now()
}
case e := <-subChan:
switch e := e.(type) {
case event.EvtLocalAddressesUpdated:
// On local address update, reduce confidence from maximum so that we schedule
// the next probe sooner
if as.confidence == maxConfidence {
as.confidence--
}
case event.EvtPeerIdentificationCompleted:
if s, err := as.host.Peerstore().SupportsProtocols(e.Peer, AutoNATProto); err == nil && len(s) > 0 {
currentStatus := *as.status.Load()
if currentStatus == network.ReachabilityUnknown {
as.tryProbe(e.Peer)
}
}
default:
log.Errorf("unknown event type: %T", e)
}
// probe finished.
case err, ok := <-as.dialResponses:
if !ok {
return
}
if IsDialRefused(err) {
retryProbe = true
} else {
as.handleDialResponse(err)
}
case <-timer.C:
peer := as.getPeerToProbe()
as.tryProbe(peer)
timerRunning = false
retryProbe = false
case <-as.ctx.Done():
return
}
// Drain the timer channel if it hasn't fired in preparation for Resetting it.
if timerRunning && !timer.Stop() {
<-timer.C
}
timer.Reset(as.scheduleProbe(retryProbe))
timerRunning = true
}
}
func (as *AmbientAutoNAT) cleanupRecentProbes() {
fixedNow := time.Now()
for k, v := range as.recentProbes {
if fixedNow.Sub(v) > as.throttlePeerPeriod {
delete(as.recentProbes, k)
}
}
}
// scheduleProbe calculates when the next probe should be scheduled for.
func (as *AmbientAutoNAT) scheduleProbe(retryProbe bool) time.Duration {
// Our baseline is a probe every 'AutoNATRefreshInterval'
// This is modulated by:
// * if we are in an unknown state, have low confidence, or we want to retry because a probe was refused that
// should drop to 'AutoNATRetryInterval'
// * recent inbound connections (implying continued connectivity) should decrease the retry when public
// * recent inbound connections when not public mean we should try more actively to see if we're public.
fixedNow := time.Now()
currentStatus := *as.status.Load()
nextProbe := fixedNow
// Don't look for peers in the peer store more than once per second.
if !as.lastProbeTry.IsZero() {
backoff := as.lastProbeTry.Add(time.Second)
if backoff.After(nextProbe) {
nextProbe = backoff
}
}
if !as.lastProbe.IsZero() {
untilNext := as.config.refreshInterval
if retryProbe {
untilNext = as.config.retryInterval
} else if currentStatus == network.ReachabilityUnknown {
untilNext = as.config.retryInterval
} else if as.confidence < maxConfidence {
untilNext = as.config.retryInterval
} else if currentStatus == network.ReachabilityPublic && as.lastInbound.After(as.lastProbe) {
untilNext *= 2
} else if currentStatus != network.ReachabilityPublic && as.lastInbound.After(as.lastProbe) {
untilNext /= 5
}
if as.lastProbe.Add(untilNext).After(nextProbe) {
nextProbe = as.lastProbe.Add(untilNext)
}
}
if as.metricsTracer != nil {
as.metricsTracer.NextProbeTime(nextProbe)
}
return nextProbe.Sub(fixedNow)
}
// handleDialResponse updates the current status based on dial response.
func (as *AmbientAutoNAT) handleDialResponse(dialErr error) {
var observation network.Reachability
switch {
case dialErr == nil:
observation = network.ReachabilityPublic
case IsDialError(dialErr):
observation = network.ReachabilityPrivate
default:
observation = network.ReachabilityUnknown
}
as.recordObservation(observation)
}
// recordObservation updates NAT status and confidence
func (as *AmbientAutoNAT) recordObservation(observation network.Reachability) {
currentStatus := *as.status.Load()
if observation == network.ReachabilityPublic {
changed := false
if currentStatus != network.ReachabilityPublic {
// Aggressively switch to public from other states ignoring confidence
log.Debugf("NAT status is public")
// we are flipping our NATStatus, so confidence drops to 0
as.confidence = 0
if as.service != nil {
as.service.Enable()
}
changed = true
} else if as.confidence < maxConfidence {
as.confidence++
}
as.status.Store(&observation)
if changed {
as.emitStatus()
}
} else if observation == network.ReachabilityPrivate {
if currentStatus != network.ReachabilityPrivate {
if as.confidence > 0 {
as.confidence--
} else {
log.Debugf("NAT status is private")
// we are flipping our NATStatus, so confidence drops to 0
as.confidence = 0
as.status.Store(&observation)
if as.service != nil {
as.service.Disable()
}
as.emitStatus()
}
} else if as.confidence < maxConfidence {
as.confidence++
as.status.Store(&observation)
}
} 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.Store(&observation)
if currentStatus != network.ReachabilityUnknown {
if as.service != nil {
as.service.Enable()
}
as.emitStatus()
}
}
if as.metricsTracer != nil {
as.metricsTracer.ReachabilityStatusConfidence(as.confidence)
}
}
func (as *AmbientAutoNAT) tryProbe(p peer.ID) bool {
as.lastProbeTry = time.Now()
if p.Validate() != nil {
return false
}
if lastTime, ok := as.recentProbes[p]; ok {
if time.Since(lastTime) < as.throttlePeerPeriod {
return false
}
}
as.cleanupRecentProbes()
info := as.host.Peerstore().PeerInfo(p)
if !as.config.dialPolicy.skipPeer(info.Addrs) {
as.recentProbes[p] = time.Now()
as.lastProbe = time.Now()
go as.probe(&info)
return true
}
return false
}
func (as *AmbientAutoNAT) probe(pi *peer.AddrInfo) {
cli := NewAutoNATClient(as.host, as.config.addressFunc, as.metricsTracer)
ctx, cancel := context.WithTimeout(as.ctx, as.config.requestTimeout)
defer cancel()
err := cli.DialBack(ctx, pi.ID)
log.Debugf("Dialback through peer %s completed: err: %s", pi.ID, err)
select {
case as.dialResponses <- err:
case <-as.ctx.Done():
return
}
}
func (as *AmbientAutoNAT) getPeerToProbe() peer.ID {
peers := as.host.Network().Peers()
if len(peers) == 0 {
return ""
}
candidates := make([]peer.ID, 0, len(peers))
for _, p := range peers {
info := as.host.Peerstore().PeerInfo(p)
// Exclude peers which don't support the autonat protocol.
if proto, err := as.host.Peerstore().SupportsProtocols(p, AutoNATProto); len(proto) == 0 || err != nil {
continue
}
// Exclude peers in backoff.
if lastTime, ok := as.recentProbes[p]; ok {
if time.Since(lastTime) < as.throttlePeerPeriod {
continue
}
}
if as.config.dialPolicy.skipPeer(info.Addrs) {
continue
}
candidates = append(candidates, p)
}
if len(candidates) == 0 {
return ""
}
return candidates[rand.Intn(len(candidates))]
}
func (as *AmbientAutoNAT) Close() error {
as.ctxCancel()
if as.service != nil {
as.service.Disable()
}
<-as.backgroundRunning
return nil
}
// Status returns the AutoNAT observed reachability status.
func (s *StaticAutoNAT) Status() network.Reachability {
return s.reachability
}
func (s *StaticAutoNAT) Close() error {
if s.service != nil {
s.service.Disable()
}
return nil
}

View File

@@ -0,0 +1,122 @@
package autonat
import (
"context"
"fmt"
"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/p2p/host/autonat/pb"
"github.com/libp2p/go-msgio/pbio"
)
// NewAutoNATClient creates a fresh instance of an AutoNATClient
// If addrFunc is nil, h.Addrs will be used
func NewAutoNATClient(h host.Host, addrFunc AddrFunc, mt MetricsTracer) Client {
if addrFunc == nil {
addrFunc = h.Addrs
}
return &client{h: h, addrFunc: addrFunc, mt: mt}
}
type client struct {
h host.Host
addrFunc AddrFunc
mt MetricsTracer
}
// DialBack asks peer p to dial us back on all addresses returned by the addrFunc.
// It blocks until we've received a response from the peer.
//
// Note: A returned error Message_E_DIAL_ERROR does not imply that the server
// actually performed a dial attempt. Servers that run a version < v0.20.0 also
// return Message_E_DIAL_ERROR if the dial was skipped due to the dialPolicy.
func (c *client) DialBack(ctx context.Context, p peer.ID) error {
s, err := c.h.NewStream(ctx, p, AutoNATProto)
if err != nil {
return err
}
if err := s.Scope().SetService(ServiceName); err != nil {
log.Debugf("error attaching stream to autonat service: %s", err)
s.Reset()
return err
}
if err := s.Scope().ReserveMemory(maxMsgSize, network.ReservationPriorityAlways); err != nil {
log.Debugf("error reserving memory for autonat stream: %s", err)
s.Reset()
return err
}
defer s.Scope().ReleaseMemory(maxMsgSize)
s.SetDeadline(time.Now().Add(streamTimeout))
// Might as well just reset the stream. Once we get to this point, we
// don't care about being nice.
defer s.Close()
r := pbio.NewDelimitedReader(s, maxMsgSize)
w := pbio.NewDelimitedWriter(s)
req := newDialMessage(peer.AddrInfo{ID: c.h.ID(), Addrs: c.addrFunc()})
if err := w.WriteMsg(req); err != nil {
s.Reset()
return err
}
var res pb.Message
if err := r.ReadMsg(&res); err != nil {
s.Reset()
return err
}
if res.GetType() != pb.Message_DIAL_RESPONSE {
s.Reset()
return fmt.Errorf("unexpected response: %s", res.GetType().String())
}
status := res.GetDialResponse().GetStatus()
if c.mt != nil {
c.mt.ReceivedDialResponse(status)
}
switch status {
case pb.Message_OK:
return nil
default:
return Error{Status: status, Text: res.GetDialResponse().GetStatusText()}
}
}
// Error wraps errors signalled by AutoNAT services
type Error struct {
Status pb.Message_ResponseStatus
Text string
}
func (e Error) Error() string {
return fmt.Sprintf("AutoNAT error: %s (%s)", e.Text, e.Status.String())
}
// IsDialError returns true if the error was due to a dial back failure
func (e Error) IsDialError() bool {
return e.Status == pb.Message_E_DIAL_ERROR
}
// IsDialRefused returns true if the error was due to a refusal to dial back
func (e Error) IsDialRefused() bool {
return e.Status == pb.Message_E_DIAL_REFUSED
}
// IsDialError returns true if the AutoNAT peer signalled an error dialing back
func IsDialError(e error) bool {
ae, ok := e.(Error)
return ok && ae.IsDialError()
}
// IsDialRefused returns true if the AutoNAT peer signalled refusal to dial back
func IsDialRefused(e error) bool {
ae, ok := e.(Error)
return ok && ae.IsDialRefused()
}

View File

@@ -0,0 +1,95 @@
package autonat
import (
"net"
"github.com/libp2p/go-libp2p/core/host"
ma "github.com/multiformats/go-multiaddr"
manet "github.com/multiformats/go-multiaddr/net"
)
type dialPolicy struct {
allowSelfDials bool
host host.Host
}
// skipDial indicates that a multiaddress isn't worth attempted dialing.
// The same logic is used when the autonat client is considering if
// a remote peer is worth using as a server, and when the server is
// considering if a requested client is worth dialing back.
func (d *dialPolicy) skipDial(addr ma.Multiaddr) bool {
// skip relay addresses
_, err := addr.ValueForProtocol(ma.P_CIRCUIT)
if err == nil {
return true
}
if d.allowSelfDials {
return false
}
// skip private network (unroutable) addresses
if !manet.IsPublicAddr(addr) {
return true
}
candidateIP, err := manet.ToIP(addr)
if err != nil {
return true
}
// Skip dialing addresses we believe are the local node's
for _, localAddr := range d.host.Addrs() {
localIP, err := manet.ToIP(localAddr)
if err != nil {
continue
}
if localIP.Equal(candidateIP) {
return true
}
}
return false
}
// skipPeer indicates that the collection of multiaddresses representing a peer
// isn't worth attempted dialing. If one of the addresses matches an address
// we believe is ours, we exclude the peer, even if there are other valid
// public addresses in the list.
func (d *dialPolicy) skipPeer(addrs []ma.Multiaddr) bool {
localAddrs := d.host.Addrs()
localHosts := make([]net.IP, 0)
for _, lAddr := range localAddrs {
if _, err := lAddr.ValueForProtocol(ma.P_CIRCUIT); err != nil && manet.IsPublicAddr(lAddr) {
lIP, err := manet.ToIP(lAddr)
if err != nil {
continue
}
localHosts = append(localHosts, lIP)
}
}
// if a public IP of the peer is one of ours: skip the peer.
goodPublic := false
for _, addr := range addrs {
if _, err := addr.ValueForProtocol(ma.P_CIRCUIT); err != nil && manet.IsPublicAddr(addr) {
aIP, err := manet.ToIP(addr)
if err != nil {
continue
}
for _, lIP := range localHosts {
if lIP.Equal(aIP) {
return true
}
}
goodPublic = true
}
}
if d.allowSelfDials {
return false
}
return !goodPublic
}

View File

@@ -0,0 +1,31 @@
package autonat
import (
"context"
"io"
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer"
ma "github.com/multiformats/go-multiaddr"
)
// AutoNAT is the interface for NAT autodiscovery
type AutoNAT interface {
// Status returns the current NAT status
Status() network.Reachability
io.Closer
}
// Client is a stateless client interface to AutoNAT peers
type Client interface {
// DialBack requests from a peer providing AutoNAT services to test dial back
// and report the address on a successful connection.
DialBack(ctx context.Context, p peer.ID) error
}
// AddrFunc is a function returning the candidate addresses for the local host.
type AddrFunc func() []ma.Multiaddr
// Option is an Autonat option for configuration
type Option func(*config) error

View File

@@ -0,0 +1,162 @@
package autonat
import (
"time"
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/p2p/host/autonat/pb"
"github.com/libp2p/go-libp2p/p2p/metricshelper"
"github.com/prometheus/client_golang/prometheus"
)
const metricNamespace = "libp2p_autonat"
var (
reachabilityStatus = prometheus.NewGauge(
prometheus.GaugeOpts{
Namespace: metricNamespace,
Name: "reachability_status",
Help: "Current node reachability",
},
)
reachabilityStatusConfidence = prometheus.NewGauge(
prometheus.GaugeOpts{
Namespace: metricNamespace,
Name: "reachability_status_confidence",
Help: "Node reachability status confidence",
},
)
receivedDialResponseTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: metricNamespace,
Name: "received_dial_response_total",
Help: "Count of dial responses for client",
},
[]string{"response_status"},
)
outgoingDialResponseTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: metricNamespace,
Name: "outgoing_dial_response_total",
Help: "Count of dial responses for server",
},
[]string{"response_status"},
)
outgoingDialRefusedTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: metricNamespace,
Name: "outgoing_dial_refused_total",
Help: "Count of dial requests refused by server",
},
[]string{"refusal_reason"},
)
nextProbeTimestamp = prometheus.NewGauge(
prometheus.GaugeOpts{
Namespace: metricNamespace,
Name: "next_probe_timestamp",
Help: "Time of next probe",
},
)
collectors = []prometheus.Collector{
reachabilityStatus,
reachabilityStatusConfidence,
receivedDialResponseTotal,
outgoingDialResponseTotal,
outgoingDialRefusedTotal,
nextProbeTimestamp,
}
)
type MetricsTracer interface {
ReachabilityStatus(status network.Reachability)
ReachabilityStatusConfidence(confidence int)
ReceivedDialResponse(status pb.Message_ResponseStatus)
OutgoingDialResponse(status pb.Message_ResponseStatus)
OutgoingDialRefused(reason string)
NextProbeTime(t time.Time)
}
func getResponseStatus(status pb.Message_ResponseStatus) string {
var s string
switch status {
case pb.Message_OK:
s = "ok"
case pb.Message_E_DIAL_ERROR:
s = "dial error"
case pb.Message_E_DIAL_REFUSED:
s = "dial refused"
case pb.Message_E_BAD_REQUEST:
s = "bad request"
case pb.Message_E_INTERNAL_ERROR:
s = "internal error"
default:
s = "unknown"
}
return s
}
const (
rate_limited = "rate limited"
dial_blocked = "dial blocked"
no_valid_address = "no valid address"
)
type metricsTracer struct{}
var _ MetricsTracer = &metricsTracer{}
type metricsTracerSetting struct {
reg prometheus.Registerer
}
type MetricsTracerOption func(*metricsTracerSetting)
func WithRegisterer(reg prometheus.Registerer) MetricsTracerOption {
return func(s *metricsTracerSetting) {
if reg != nil {
s.reg = reg
}
}
}
func NewMetricsTracer(opts ...MetricsTracerOption) MetricsTracer {
setting := &metricsTracerSetting{reg: prometheus.DefaultRegisterer}
for _, opt := range opts {
opt(setting)
}
metricshelper.RegisterCollectors(setting.reg, collectors...)
return &metricsTracer{}
}
func (mt *metricsTracer) ReachabilityStatus(status network.Reachability) {
reachabilityStatus.Set(float64(status))
}
func (mt *metricsTracer) ReachabilityStatusConfidence(confidence int) {
reachabilityStatusConfidence.Set(float64(confidence))
}
func (mt *metricsTracer) ReceivedDialResponse(status pb.Message_ResponseStatus) {
tags := metricshelper.GetStringSlice()
defer metricshelper.PutStringSlice(tags)
*tags = append(*tags, getResponseStatus(status))
receivedDialResponseTotal.WithLabelValues(*tags...).Inc()
}
func (mt *metricsTracer) OutgoingDialResponse(status pb.Message_ResponseStatus) {
tags := metricshelper.GetStringSlice()
defer metricshelper.PutStringSlice(tags)
*tags = append(*tags, getResponseStatus(status))
outgoingDialResponseTotal.WithLabelValues(*tags...).Inc()
}
func (mt *metricsTracer) OutgoingDialRefused(reason string) {
tags := metricshelper.GetStringSlice()
defer metricshelper.PutStringSlice(tags)
*tags = append(*tags, reason)
outgoingDialRefusedTotal.WithLabelValues(*tags...).Inc()
}
func (mt *metricsTracer) NextProbeTime(t time.Time) {
nextProbeTimestamp.Set(float64(t.Unix()))
}

View File

@@ -0,0 +1,30 @@
package autonat
import (
"github.com/libp2p/go-libp2p/core/network"
ma "github.com/multiformats/go-multiaddr"
manet "github.com/multiformats/go-multiaddr/net"
)
var _ network.Notifiee = (*AmbientAutoNAT)(nil)
// Listen is part of the network.Notifiee interface
func (as *AmbientAutoNAT) Listen(net network.Network, a ma.Multiaddr) {}
// ListenClose is part of the network.Notifiee interface
func (as *AmbientAutoNAT) ListenClose(net network.Network, a ma.Multiaddr) {}
// Connected is part of the network.Notifiee interface
func (as *AmbientAutoNAT) Connected(net network.Network, c network.Conn) {
if c.Stat().Direction == network.DirInbound &&
manet.IsPublicAddr(c.RemoteMultiaddr()) {
select {
case as.inboundConn <- c:
default:
}
}
}
// Disconnected is part of the network.Notifiee interface
func (as *AmbientAutoNAT) Disconnected(net network.Network, c network.Conn) {}

View File

@@ -0,0 +1,153 @@
package autonat
import (
"errors"
"time"
"github.com/libp2p/go-libp2p/core/host"
"github.com/libp2p/go-libp2p/core/network"
)
// config holds configurable options for the autonat subsystem.
type config struct {
host host.Host
addressFunc AddrFunc
dialPolicy dialPolicy
dialer network.Network
forceReachability bool
reachability network.Reachability
metricsTracer MetricsTracer
// client
bootDelay time.Duration
retryInterval time.Duration
refreshInterval time.Duration
requestTimeout time.Duration
throttlePeerPeriod time.Duration
// server
dialTimeout time.Duration
maxPeerAddresses int
throttleGlobalMax int
throttlePeerMax int
throttleResetPeriod time.Duration
throttleResetJitter time.Duration
}
var defaults = func(c *config) error {
c.bootDelay = 15 * time.Second
c.retryInterval = 90 * time.Second
c.refreshInterval = 15 * time.Minute
c.requestTimeout = 30 * time.Second
c.throttlePeerPeriod = 90 * time.Second
c.dialTimeout = 15 * time.Second
c.maxPeerAddresses = 16
c.throttleGlobalMax = 30
c.throttlePeerMax = 3
c.throttleResetPeriod = 1 * time.Minute
c.throttleResetJitter = 15 * time.Second
return nil
}
// EnableService specifies that AutoNAT should be allowed to run a NAT service to help
// other peers determine their own NAT status. The provided Network should not be the
// default network/dialer of the host passed to `New`, as the NAT system will need to
// make parallel connections, and as such will modify both the associated peerstore
// and terminate connections of this dialer. The dialer provided
// should be compatible (TCP/UDP) however with the transports of the libp2p network.
func EnableService(dialer network.Network) Option {
return func(c *config) error {
if dialer == c.host.Network() || dialer.Peerstore() == c.host.Peerstore() {
return errors.New("dialer should not be that of the host")
}
c.dialer = dialer
return nil
}
}
// WithReachability overrides autonat to simply report an over-ridden reachability
// status.
func WithReachability(reachability network.Reachability) Option {
return func(c *config) error {
c.forceReachability = true
c.reachability = reachability
return nil
}
}
// UsingAddresses allows overriding which Addresses the AutoNAT client believes
// are "its own". Useful for testing, or for more exotic port-forwarding
// scenarios where the host may be listening on different ports than it wants
// to externally advertise or verify connectability on.
func UsingAddresses(addrFunc AddrFunc) Option {
return func(c *config) error {
if addrFunc == nil {
return errors.New("invalid address function supplied")
}
c.addressFunc = addrFunc
return nil
}
}
// WithSchedule configures how aggressively probes will be made to verify the
// address of the host. retryInterval indicates how often probes should be made
// when the host lacks confidence about its address, while refreshInterval
// is the schedule of periodic probes when the host believes it knows its
// steady-state reachability.
func WithSchedule(retryInterval, refreshInterval time.Duration) Option {
return func(c *config) error {
c.retryInterval = retryInterval
c.refreshInterval = refreshInterval
return nil
}
}
// WithoutStartupDelay removes the initial delay the NAT subsystem typically
// uses as a buffer for ensuring that connectivity and guesses as to the hosts
// local interfaces have settled down during startup.
func WithoutStartupDelay() Option {
return func(c *config) error {
c.bootDelay = 1
return nil
}
}
// WithoutThrottling indicates that this autonat service should not place
// restrictions on how many peers it is willing to help when acting as
// a server.
func WithoutThrottling() Option {
return func(c *config) error {
c.throttleGlobalMax = 0
return nil
}
}
// WithThrottling specifies how many peers (`amount`) it is willing to help
// ever `interval` amount of time when acting as a server.
func WithThrottling(amount int, interval time.Duration) Option {
return func(c *config) error {
c.throttleGlobalMax = amount
c.throttleResetPeriod = interval
c.throttleResetJitter = interval / 4
return nil
}
}
// WithPeerThrottling specifies a limit for the maximum number of IP checks
// this node will provide to an individual peer in each `interval`.
func WithPeerThrottling(amount int) Option {
return func(c *config) error {
c.throttlePeerMax = amount
return nil
}
}
// WithMetricsTracer uses mt to track autonat metrics
func WithMetricsTracer(mt MetricsTracer) Option {
return func(c *config) error {
c.metricsTracer = mt
return nil
}
}

View File

@@ -0,0 +1,524 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.30.0
// protoc v3.21.12
// source: pb/autonat.proto
package pb
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Message_MessageType int32
const (
Message_DIAL Message_MessageType = 0
Message_DIAL_RESPONSE Message_MessageType = 1
)
// Enum value maps for Message_MessageType.
var (
Message_MessageType_name = map[int32]string{
0: "DIAL",
1: "DIAL_RESPONSE",
}
Message_MessageType_value = map[string]int32{
"DIAL": 0,
"DIAL_RESPONSE": 1,
}
)
func (x Message_MessageType) Enum() *Message_MessageType {
p := new(Message_MessageType)
*p = x
return p
}
func (x Message_MessageType) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (Message_MessageType) Descriptor() protoreflect.EnumDescriptor {
return file_pb_autonat_proto_enumTypes[0].Descriptor()
}
func (Message_MessageType) Type() protoreflect.EnumType {
return &file_pb_autonat_proto_enumTypes[0]
}
func (x Message_MessageType) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Do not use.
func (x *Message_MessageType) UnmarshalJSON(b []byte) error {
num, err := protoimpl.X.UnmarshalJSONEnum(x.Descriptor(), b)
if err != nil {
return err
}
*x = Message_MessageType(num)
return nil
}
// Deprecated: Use Message_MessageType.Descriptor instead.
func (Message_MessageType) EnumDescriptor() ([]byte, []int) {
return file_pb_autonat_proto_rawDescGZIP(), []int{0, 0}
}
type Message_ResponseStatus int32
const (
Message_OK Message_ResponseStatus = 0
Message_E_DIAL_ERROR Message_ResponseStatus = 100
Message_E_DIAL_REFUSED Message_ResponseStatus = 101
Message_E_BAD_REQUEST Message_ResponseStatus = 200
Message_E_INTERNAL_ERROR Message_ResponseStatus = 300
)
// Enum value maps for Message_ResponseStatus.
var (
Message_ResponseStatus_name = map[int32]string{
0: "OK",
100: "E_DIAL_ERROR",
101: "E_DIAL_REFUSED",
200: "E_BAD_REQUEST",
300: "E_INTERNAL_ERROR",
}
Message_ResponseStatus_value = map[string]int32{
"OK": 0,
"E_DIAL_ERROR": 100,
"E_DIAL_REFUSED": 101,
"E_BAD_REQUEST": 200,
"E_INTERNAL_ERROR": 300,
}
)
func (x Message_ResponseStatus) Enum() *Message_ResponseStatus {
p := new(Message_ResponseStatus)
*p = x
return p
}
func (x Message_ResponseStatus) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (Message_ResponseStatus) Descriptor() protoreflect.EnumDescriptor {
return file_pb_autonat_proto_enumTypes[1].Descriptor()
}
func (Message_ResponseStatus) Type() protoreflect.EnumType {
return &file_pb_autonat_proto_enumTypes[1]
}
func (x Message_ResponseStatus) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Do not use.
func (x *Message_ResponseStatus) UnmarshalJSON(b []byte) error {
num, err := protoimpl.X.UnmarshalJSONEnum(x.Descriptor(), b)
if err != nil {
return err
}
*x = Message_ResponseStatus(num)
return nil
}
// Deprecated: Use Message_ResponseStatus.Descriptor instead.
func (Message_ResponseStatus) EnumDescriptor() ([]byte, []int) {
return file_pb_autonat_proto_rawDescGZIP(), []int{0, 1}
}
type Message struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Type *Message_MessageType `protobuf:"varint,1,opt,name=type,enum=autonat.pb.Message_MessageType" json:"type,omitempty"`
Dial *Message_Dial `protobuf:"bytes,2,opt,name=dial" json:"dial,omitempty"`
DialResponse *Message_DialResponse `protobuf:"bytes,3,opt,name=dialResponse" json:"dialResponse,omitempty"`
}
func (x *Message) Reset() {
*x = Message{}
if protoimpl.UnsafeEnabled {
mi := &file_pb_autonat_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Message) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Message) ProtoMessage() {}
func (x *Message) ProtoReflect() protoreflect.Message {
mi := &file_pb_autonat_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Message.ProtoReflect.Descriptor instead.
func (*Message) Descriptor() ([]byte, []int) {
return file_pb_autonat_proto_rawDescGZIP(), []int{0}
}
func (x *Message) GetType() Message_MessageType {
if x != nil && x.Type != nil {
return *x.Type
}
return Message_DIAL
}
func (x *Message) GetDial() *Message_Dial {
if x != nil {
return x.Dial
}
return nil
}
func (x *Message) GetDialResponse() *Message_DialResponse {
if x != nil {
return x.DialResponse
}
return nil
}
type Message_PeerInfo struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id []byte `protobuf:"bytes,1,opt,name=id" json:"id,omitempty"`
Addrs [][]byte `protobuf:"bytes,2,rep,name=addrs" json:"addrs,omitempty"`
}
func (x *Message_PeerInfo) Reset() {
*x = Message_PeerInfo{}
if protoimpl.UnsafeEnabled {
mi := &file_pb_autonat_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Message_PeerInfo) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Message_PeerInfo) ProtoMessage() {}
func (x *Message_PeerInfo) ProtoReflect() protoreflect.Message {
mi := &file_pb_autonat_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Message_PeerInfo.ProtoReflect.Descriptor instead.
func (*Message_PeerInfo) Descriptor() ([]byte, []int) {
return file_pb_autonat_proto_rawDescGZIP(), []int{0, 0}
}
func (x *Message_PeerInfo) GetId() []byte {
if x != nil {
return x.Id
}
return nil
}
func (x *Message_PeerInfo) GetAddrs() [][]byte {
if x != nil {
return x.Addrs
}
return nil
}
type Message_Dial struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Peer *Message_PeerInfo `protobuf:"bytes,1,opt,name=peer" json:"peer,omitempty"`
}
func (x *Message_Dial) Reset() {
*x = Message_Dial{}
if protoimpl.UnsafeEnabled {
mi := &file_pb_autonat_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Message_Dial) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Message_Dial) ProtoMessage() {}
func (x *Message_Dial) ProtoReflect() protoreflect.Message {
mi := &file_pb_autonat_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Message_Dial.ProtoReflect.Descriptor instead.
func (*Message_Dial) Descriptor() ([]byte, []int) {
return file_pb_autonat_proto_rawDescGZIP(), []int{0, 1}
}
func (x *Message_Dial) GetPeer() *Message_PeerInfo {
if x != nil {
return x.Peer
}
return nil
}
type Message_DialResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Status *Message_ResponseStatus `protobuf:"varint,1,opt,name=status,enum=autonat.pb.Message_ResponseStatus" json:"status,omitempty"`
StatusText *string `protobuf:"bytes,2,opt,name=statusText" json:"statusText,omitempty"`
Addr []byte `protobuf:"bytes,3,opt,name=addr" json:"addr,omitempty"`
}
func (x *Message_DialResponse) Reset() {
*x = Message_DialResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_pb_autonat_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Message_DialResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Message_DialResponse) ProtoMessage() {}
func (x *Message_DialResponse) ProtoReflect() protoreflect.Message {
mi := &file_pb_autonat_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Message_DialResponse.ProtoReflect.Descriptor instead.
func (*Message_DialResponse) Descriptor() ([]byte, []int) {
return file_pb_autonat_proto_rawDescGZIP(), []int{0, 2}
}
func (x *Message_DialResponse) GetStatus() Message_ResponseStatus {
if x != nil && x.Status != nil {
return *x.Status
}
return Message_OK
}
func (x *Message_DialResponse) GetStatusText() string {
if x != nil && x.StatusText != nil {
return *x.StatusText
}
return ""
}
func (x *Message_DialResponse) GetAddr() []byte {
if x != nil {
return x.Addr
}
return nil
}
var File_pb_autonat_proto protoreflect.FileDescriptor
var file_pb_autonat_proto_rawDesc = []byte{
0x0a, 0x10, 0x70, 0x62, 0x2f, 0x61, 0x75, 0x74, 0x6f, 0x6e, 0x61, 0x74, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x12, 0x0a, 0x61, 0x75, 0x74, 0x6f, 0x6e, 0x61, 0x74, 0x2e, 0x70, 0x62, 0x22, 0xb5,
0x04, 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x33, 0x0a, 0x04, 0x74, 0x79,
0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x6e,
0x61, 0x74, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x4d, 0x65,
0x73, 0x73, 0x61, 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12,
0x2c, 0x0a, 0x04, 0x64, 0x69, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e,
0x61, 0x75, 0x74, 0x6f, 0x6e, 0x61, 0x74, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61,
0x67, 0x65, 0x2e, 0x44, 0x69, 0x61, 0x6c, 0x52, 0x04, 0x64, 0x69, 0x61, 0x6c, 0x12, 0x44, 0x0a,
0x0c, 0x64, 0x69, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x03, 0x20,
0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x6e, 0x61, 0x74, 0x2e, 0x70, 0x62,
0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x44, 0x69, 0x61, 0x6c, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0c, 0x64, 0x69, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x1a, 0x30, 0x0a, 0x08, 0x50, 0x65, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x12,
0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x64, 0x12,
0x14, 0x0a, 0x05, 0x61, 0x64, 0x64, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x05,
0x61, 0x64, 0x64, 0x72, 0x73, 0x1a, 0x38, 0x0a, 0x04, 0x44, 0x69, 0x61, 0x6c, 0x12, 0x30, 0x0a,
0x04, 0x70, 0x65, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x75,
0x74, 0x6f, 0x6e, 0x61, 0x74, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
0x2e, 0x50, 0x65, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x04, 0x70, 0x65, 0x65, 0x72, 0x1a,
0x7e, 0x0a, 0x0c, 0x44, 0x69, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
0x3a, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32,
0x22, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x6e, 0x61, 0x74, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x73,
0x73, 0x61, 0x67, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, 0x74, 0x61,
0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x73,
0x74, 0x61, 0x74, 0x75, 0x73, 0x54, 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
0x0a, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x54, 0x65, 0x78, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x61,
0x64, 0x64, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x61, 0x64, 0x64, 0x72, 0x22,
0x2a, 0x0a, 0x0b, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x08,
0x0a, 0x04, 0x44, 0x49, 0x41, 0x4c, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x44, 0x49, 0x41, 0x4c,
0x5f, 0x52, 0x45, 0x53, 0x50, 0x4f, 0x4e, 0x53, 0x45, 0x10, 0x01, 0x22, 0x69, 0x0a, 0x0e, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x06, 0x0a,
0x02, 0x4f, 0x4b, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x45, 0x5f, 0x44, 0x49, 0x41, 0x4c, 0x5f,
0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x64, 0x12, 0x12, 0x0a, 0x0e, 0x45, 0x5f, 0x44, 0x49, 0x41,
0x4c, 0x5f, 0x52, 0x45, 0x46, 0x55, 0x53, 0x45, 0x44, 0x10, 0x65, 0x12, 0x12, 0x0a, 0x0d, 0x45,
0x5f, 0x42, 0x41, 0x44, 0x5f, 0x52, 0x45, 0x51, 0x55, 0x45, 0x53, 0x54, 0x10, 0xc8, 0x01, 0x12,
0x15, 0x0a, 0x10, 0x45, 0x5f, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x5f, 0x45, 0x52,
0x52, 0x4f, 0x52, 0x10, 0xac, 0x02,
}
var (
file_pb_autonat_proto_rawDescOnce sync.Once
file_pb_autonat_proto_rawDescData = file_pb_autonat_proto_rawDesc
)
func file_pb_autonat_proto_rawDescGZIP() []byte {
file_pb_autonat_proto_rawDescOnce.Do(func() {
file_pb_autonat_proto_rawDescData = protoimpl.X.CompressGZIP(file_pb_autonat_proto_rawDescData)
})
return file_pb_autonat_proto_rawDescData
}
var file_pb_autonat_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
var file_pb_autonat_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_pb_autonat_proto_goTypes = []interface{}{
(Message_MessageType)(0), // 0: autonat.pb.Message.MessageType
(Message_ResponseStatus)(0), // 1: autonat.pb.Message.ResponseStatus
(*Message)(nil), // 2: autonat.pb.Message
(*Message_PeerInfo)(nil), // 3: autonat.pb.Message.PeerInfo
(*Message_Dial)(nil), // 4: autonat.pb.Message.Dial
(*Message_DialResponse)(nil), // 5: autonat.pb.Message.DialResponse
}
var file_pb_autonat_proto_depIdxs = []int32{
0, // 0: autonat.pb.Message.type:type_name -> autonat.pb.Message.MessageType
4, // 1: autonat.pb.Message.dial:type_name -> autonat.pb.Message.Dial
5, // 2: autonat.pb.Message.dialResponse:type_name -> autonat.pb.Message.DialResponse
3, // 3: autonat.pb.Message.Dial.peer:type_name -> autonat.pb.Message.PeerInfo
1, // 4: autonat.pb.Message.DialResponse.status:type_name -> autonat.pb.Message.ResponseStatus
5, // [5:5] is the sub-list for method output_type
5, // [5:5] is the sub-list for method input_type
5, // [5:5] is the sub-list for extension type_name
5, // [5:5] is the sub-list for extension extendee
0, // [0:5] is the sub-list for field type_name
}
func init() { file_pb_autonat_proto_init() }
func file_pb_autonat_proto_init() {
if File_pb_autonat_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_pb_autonat_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Message); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_pb_autonat_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Message_PeerInfo); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_pb_autonat_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Message_Dial); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_pb_autonat_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Message_DialResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_pb_autonat_proto_rawDesc,
NumEnums: 2,
NumMessages: 4,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_pb_autonat_proto_goTypes,
DependencyIndexes: file_pb_autonat_proto_depIdxs,
EnumInfos: file_pb_autonat_proto_enumTypes,
MessageInfos: file_pb_autonat_proto_msgTypes,
}.Build()
File_pb_autonat_proto = out.File
file_pb_autonat_proto_rawDesc = nil
file_pb_autonat_proto_goTypes = nil
file_pb_autonat_proto_depIdxs = nil
}

View File

@@ -0,0 +1,37 @@
syntax = "proto2";
package autonat.pb;
message Message {
enum MessageType {
DIAL = 0;
DIAL_RESPONSE = 1;
}
enum ResponseStatus {
OK = 0;
E_DIAL_ERROR = 100;
E_DIAL_REFUSED = 101;
E_BAD_REQUEST = 200;
E_INTERNAL_ERROR = 300;
}
message PeerInfo {
optional bytes id = 1;
repeated bytes addrs = 2;
}
message Dial {
optional PeerInfo peer = 1;
}
message DialResponse {
optional ResponseStatus status = 1;
optional string statusText = 2;
optional bytes addr = 3;
}
optional MessageType type = 1;
optional Dial dial = 2;
optional DialResponse dialResponse = 3;
}

View File

@@ -0,0 +1,41 @@
package autonat
import (
"github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/p2p/host/autonat/pb"
ma "github.com/multiformats/go-multiaddr"
)
//go:generate protoc --proto_path=$PWD:$PWD/../../.. --go_out=. --go_opt=Mpb/autonat.proto=./pb pb/autonat.proto
// AutoNATProto identifies the autonat service protocol
const AutoNATProto = "/libp2p/autonat/1.0.0"
func newDialMessage(pi peer.AddrInfo) *pb.Message {
msg := new(pb.Message)
msg.Type = pb.Message_DIAL.Enum()
msg.Dial = new(pb.Message_Dial)
msg.Dial.Peer = new(pb.Message_PeerInfo)
msg.Dial.Peer.Id = []byte(pi.ID)
msg.Dial.Peer.Addrs = make([][]byte, len(pi.Addrs))
for i, addr := range pi.Addrs {
msg.Dial.Peer.Addrs[i] = addr.Bytes()
}
return msg
}
func newDialResponseOK(addr ma.Multiaddr) *pb.Message_DialResponse {
dr := new(pb.Message_DialResponse)
dr.Status = pb.Message_OK.Enum()
dr.Addr = addr.Bytes()
return dr
}
func newDialResponseError(status pb.Message_ResponseStatus, text string) *pb.Message_DialResponse {
dr := new(pb.Message_DialResponse)
dr.Status = status.Enum()
dr.StatusText = &text
return dr
}

View File

@@ -0,0 +1,295 @@
package autonat
import (
"context"
"errors"
"math/rand"
"sync"
"time"
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/core/peerstore"
"github.com/libp2p/go-libp2p/p2p/host/autonat/pb"
"github.com/libp2p/go-msgio/pbio"
ma "github.com/multiformats/go-multiaddr"
)
var streamTimeout = 60 * time.Second
const (
ServiceName = "libp2p.autonat"
maxMsgSize = 4096
)
// AutoNATService provides NAT autodetection services to other peers
type autoNATService struct {
instanceLock sync.Mutex
instance context.CancelFunc
backgroundRunning chan struct{} // closed when background exits
config *config
// rate limiter
mx sync.Mutex
reqs map[peer.ID]int
globalReqs int
}
// NewAutoNATService creates a new AutoNATService instance attached to a host
func newAutoNATService(c *config) (*autoNATService, error) {
if c.dialer == nil {
return nil, errors.New("cannot create NAT service without a network")
}
return &autoNATService{
config: c,
reqs: make(map[peer.ID]int),
}, nil
}
func (as *autoNATService) handleStream(s network.Stream) {
if err := s.Scope().SetService(ServiceName); err != nil {
log.Debugf("error attaching stream to autonat service: %s", err)
s.Reset()
return
}
if err := s.Scope().ReserveMemory(maxMsgSize, network.ReservationPriorityAlways); err != nil {
log.Debugf("error reserving memory for autonat stream: %s", err)
s.Reset()
return
}
defer s.Scope().ReleaseMemory(maxMsgSize)
s.SetDeadline(time.Now().Add(streamTimeout))
defer s.Close()
pid := s.Conn().RemotePeer()
log.Debugf("New stream from %s", pid)
r := pbio.NewDelimitedReader(s, maxMsgSize)
w := pbio.NewDelimitedWriter(s)
var req pb.Message
var res pb.Message
err := r.ReadMsg(&req)
if err != nil {
log.Debugf("Error reading message from %s: %s", pid, err.Error())
s.Reset()
return
}
t := req.GetType()
if t != pb.Message_DIAL {
log.Debugf("Unexpected message from %s: %s (%d)", pid, t.String(), t)
s.Reset()
return
}
dr := as.handleDial(pid, s.Conn().RemoteMultiaddr(), req.GetDial().GetPeer())
res.Type = pb.Message_DIAL_RESPONSE.Enum()
res.DialResponse = dr
err = w.WriteMsg(&res)
if err != nil {
log.Debugf("Error writing response to %s: %s", pid, err.Error())
s.Reset()
return
}
if as.config.metricsTracer != nil {
as.config.metricsTracer.OutgoingDialResponse(res.GetDialResponse().GetStatus())
}
}
func (as *autoNATService) handleDial(p peer.ID, obsaddr ma.Multiaddr, mpi *pb.Message_PeerInfo) *pb.Message_DialResponse {
if mpi == nil {
return newDialResponseError(pb.Message_E_BAD_REQUEST, "missing peer info")
}
mpid := mpi.GetId()
if mpid != nil {
mp, err := peer.IDFromBytes(mpid)
if err != nil {
return newDialResponseError(pb.Message_E_BAD_REQUEST, "bad peer id")
}
if mp != p {
return newDialResponseError(pb.Message_E_BAD_REQUEST, "peer id mismatch")
}
}
addrs := make([]ma.Multiaddr, 0, as.config.maxPeerAddresses)
seen := make(map[string]struct{})
// Don't even try to dial peers with blocked remote addresses. In order to dial a peer, we
// need to know their public IP address, and it needs to be different from our public IP
// address.
if as.config.dialPolicy.skipDial(obsaddr) {
if as.config.metricsTracer != nil {
as.config.metricsTracer.OutgoingDialRefused(dial_blocked)
}
// Note: versions < v0.20.0 return Message_E_DIAL_ERROR here, thus we can not rely on this error code.
return newDialResponseError(pb.Message_E_DIAL_REFUSED, "refusing to dial peer with blocked observed address")
}
// Determine the peer's IP address.
hostIP, _ := ma.SplitFirst(obsaddr)
switch hostIP.Protocol().Code {
case ma.P_IP4, ma.P_IP6:
default:
// This shouldn't be possible as we should skip all addresses that don't include
// public IP addresses.
return newDialResponseError(pb.Message_E_INTERNAL_ERROR, "expected an IP address")
}
// add observed addr to the list of addresses to dial
addrs = append(addrs, obsaddr)
seen[obsaddr.String()] = struct{}{}
for _, maddr := range mpi.GetAddrs() {
addr, err := ma.NewMultiaddrBytes(maddr)
if err != nil {
log.Debugf("Error parsing multiaddr: %s", err.Error())
continue
}
// For security reasons, we _only_ dial the observed IP address.
// Replace other IP addresses with the observed one so we can still try the
// requested ports/transports.
if ip, rest := ma.SplitFirst(addr); !ip.Equal(hostIP) {
// Make sure it's an IP address
switch ip.Protocol().Code {
case ma.P_IP4, ma.P_IP6:
default:
continue
}
addr = hostIP
if rest != nil {
addr = addr.Encapsulate(rest)
}
}
// Make sure we're willing to dial the rest of the address (e.g., not a circuit
// address).
if as.config.dialPolicy.skipDial(addr) {
continue
}
str := addr.String()
_, ok := seen[str]
if ok {
continue
}
addrs = append(addrs, addr)
seen[str] = struct{}{}
if len(addrs) >= as.config.maxPeerAddresses {
break
}
}
if len(addrs) == 0 {
if as.config.metricsTracer != nil {
as.config.metricsTracer.OutgoingDialRefused(no_valid_address)
}
// Note: versions < v0.20.0 return Message_E_DIAL_ERROR here, thus we can not rely on this error code.
return newDialResponseError(pb.Message_E_DIAL_REFUSED, "no dialable addresses")
}
return as.doDial(peer.AddrInfo{ID: p, Addrs: addrs})
}
func (as *autoNATService) doDial(pi peer.AddrInfo) *pb.Message_DialResponse {
// rate limit check
as.mx.Lock()
count := as.reqs[pi.ID]
if count >= as.config.throttlePeerMax || (as.config.throttleGlobalMax > 0 &&
as.globalReqs >= as.config.throttleGlobalMax) {
as.mx.Unlock()
if as.config.metricsTracer != nil {
as.config.metricsTracer.OutgoingDialRefused(rate_limited)
}
return newDialResponseError(pb.Message_E_DIAL_REFUSED, "too many dials")
}
as.reqs[pi.ID] = count + 1
as.globalReqs++
as.mx.Unlock()
ctx, cancel := context.WithTimeout(context.Background(), as.config.dialTimeout)
defer cancel()
as.config.dialer.Peerstore().ClearAddrs(pi.ID)
as.config.dialer.Peerstore().AddAddrs(pi.ID, pi.Addrs, peerstore.TempAddrTTL)
defer func() {
as.config.dialer.Peerstore().ClearAddrs(pi.ID)
as.config.dialer.Peerstore().RemovePeer(pi.ID)
}()
conn, err := as.config.dialer.DialPeer(ctx, pi.ID)
if err != nil {
log.Debugf("error dialing %s: %s", pi.ID, err.Error())
// wait for the context to timeout to avoid leaking timing information
// this renders the service ineffective as a port scanner
<-ctx.Done()
return newDialResponseError(pb.Message_E_DIAL_ERROR, "dial failed")
}
ra := conn.RemoteMultiaddr()
as.config.dialer.ClosePeer(pi.ID)
return newDialResponseOK(ra)
}
// Enable the autoNAT service if it is not running.
func (as *autoNATService) Enable() {
as.instanceLock.Lock()
defer as.instanceLock.Unlock()
if as.instance != nil {
return
}
ctx, cancel := context.WithCancel(context.Background())
as.instance = cancel
as.backgroundRunning = make(chan struct{})
as.config.host.SetStreamHandler(AutoNATProto, as.handleStream)
go as.background(ctx)
}
// Disable the autoNAT service if it is running.
func (as *autoNATService) Disable() {
as.instanceLock.Lock()
defer as.instanceLock.Unlock()
if as.instance != nil {
as.config.host.RemoveStreamHandler(AutoNATProto)
as.instance()
as.instance = nil
<-as.backgroundRunning
}
}
func (as *autoNATService) background(ctx context.Context) {
defer close(as.backgroundRunning)
timer := time.NewTimer(as.config.throttleResetPeriod)
defer timer.Stop()
for {
select {
case <-timer.C:
as.mx.Lock()
as.reqs = make(map[peer.ID]int)
as.globalReqs = 0
as.mx.Unlock()
jitter := rand.Float32() * float32(as.config.throttleResetJitter)
timer.Reset(as.config.throttleResetPeriod + time.Duration(int64(jitter)))
case <-ctx.Done():
return
}
}
}