diff --git a/config/config.go b/config/config.go index cc689baf7..ba326dd2d 100644 --- a/config/config.go +++ b/config/config.go @@ -24,6 +24,7 @@ import ( "github.com/libp2p/go-libp2p/p2p/host/autonat" "github.com/libp2p/go-libp2p/p2p/host/autorelay" bhost "github.com/libp2p/go-libp2p/p2p/host/basic" + blankhost "github.com/libp2p/go-libp2p/p2p/host/blank" "github.com/libp2p/go-libp2p/p2p/host/eventbus" "github.com/libp2p/go-libp2p/p2p/host/peerstore/pstoremem" rcmgr "github.com/libp2p/go-libp2p/p2p/host/resource-manager" @@ -131,6 +132,13 @@ type Config struct { SwarmOpts []swarm.Option DisableIdentifyAddressDiscovery bool + + EnableAutoNATv2 bool + + UDPBlackHoleSuccessCounter *swarm.BlackHoleSuccessCounter + CustomUDPBlackHoleSuccessCounter bool + IPv6BlackHoleSuccessCounter *swarm.BlackHoleSuccessCounter + CustomIPv6BlackHoleSuccessCounter bool } func (cfg *Config) makeSwarm(eventBus event.Bus, enableMetrics bool) (*swarm.Swarm, error) { @@ -165,7 +173,10 @@ func (cfg *Config) makeSwarm(eventBus event.Bus, enableMetrics bool) (*swarm.Swa return nil, err } - opts := cfg.SwarmOpts + opts := append(cfg.SwarmOpts, + swarm.WithUDPBlackHoleSuccessCounter(cfg.UDPBlackHoleSuccessCounter), + swarm.WithIPv6BlackHoleSuccessCounter(cfg.IPv6BlackHoleSuccessCounter), + ) if cfg.Reporter != nil { opts = append(opts, swarm.WithMetrics(cfg.Reporter)) } @@ -193,6 +204,77 @@ func (cfg *Config) makeSwarm(eventBus event.Bus, enableMetrics bool) (*swarm.Swa return swarm.NewSwarm(pid, cfg.Peerstore, eventBus, opts...) } +func (cfg *Config) makeAutoNATV2Host() (host.Host, error) { + autonatPrivKey, _, err := crypto.GenerateEd25519Key(rand.Reader) + if err != nil { + return nil, err + } + ps, err := pstoremem.NewPeerstore() + if err != nil { + return nil, err + } + + autoNatCfg := Config{ + Transports: cfg.Transports, + Muxers: cfg.Muxers, + SecurityTransports: cfg.SecurityTransports, + Insecure: cfg.Insecure, + PSK: cfg.PSK, + ConnectionGater: cfg.ConnectionGater, + Reporter: cfg.Reporter, + PeerKey: autonatPrivKey, + Peerstore: ps, + DialRanker: swarm.NoDelayDialRanker, + UDPBlackHoleSuccessCounter: cfg.UDPBlackHoleSuccessCounter, + IPv6BlackHoleSuccessCounter: cfg.IPv6BlackHoleSuccessCounter, + ResourceManager: cfg.ResourceManager, + SwarmOpts: []swarm.Option{ + // Don't update black hole state for failed autonat dials + swarm.WithReadOnlyBlackHoleDetector(), + }, + } + fxopts, err := autoNatCfg.addTransports() + if err != nil { + return nil, err + } + var dialerHost host.Host + fxopts = append(fxopts, + fx.Provide(eventbus.NewBus), + fx.Provide(func(lifecycle fx.Lifecycle, b event.Bus) (*swarm.Swarm, error) { + lifecycle.Append(fx.Hook{ + OnStop: func(context.Context) error { + return ps.Close() + }}) + sw, err := autoNatCfg.makeSwarm(b, false) + return sw, err + }), + fx.Provide(func(sw *swarm.Swarm) *blankhost.BlankHost { + return blankhost.NewBlankHost(sw) + }), + fx.Provide(func(bh *blankhost.BlankHost) host.Host { + return bh + }), + fx.Provide(func() crypto.PrivKey { return autonatPrivKey }), + fx.Provide(func(bh host.Host) peer.ID { return bh.ID() }), + fx.Invoke(func(bh *blankhost.BlankHost) { + dialerHost = bh + }), + ) + app := fx.New(fxopts...) + if err := app.Err(); err != nil { + return nil, err + } + err = app.Start(context.Background()) + if err != nil { + return nil, err + } + go func() { + <-dialerHost.Network().(*swarm.Swarm).Done() + app.Stop(context.Background()) + }() + return dialerHost, nil +} + func (cfg *Config) addTransports() ([]fx.Option, error) { fxopts := []fx.Option{ fx.WithLogger(func() fxevent.Logger { return getFXLogger() }), @@ -291,6 +373,14 @@ func (cfg *Config) addTransports() ([]fx.Option, error) { } func (cfg *Config) newBasicHost(swrm *swarm.Swarm, eventBus event.Bus) (*bhost.BasicHost, error) { + var autonatv2Dialer host.Host + if cfg.EnableAutoNATv2 { + ah, err := cfg.makeAutoNATV2Host() + if err != nil { + return nil, err + } + autonatv2Dialer = ah + } h, err := bhost.NewHost(swrm, &bhost.HostOpts{ EventBus: eventBus, ConnManager: cfg.ConnManager, @@ -306,6 +396,8 @@ func (cfg *Config) newBasicHost(swrm *swarm.Swarm, eventBus event.Bus) (*bhost.B EnableMetrics: !cfg.DisableMetrics, PrometheusRegisterer: cfg.PrometheusRegisterer, DisableIdentifyAddressDiscovery: cfg.DisableIdentifyAddressDiscovery, + EnableAutoNATv2: cfg.EnableAutoNATv2, + AutoNATv2Dialer: autonatv2Dialer, }) if err != nil { return nil, err @@ -488,9 +580,8 @@ func (cfg *Config) addAutoNAT(h *bhost.BasicHost) error { Peerstore: ps, DialRanker: swarm.NoDelayDialRanker, SwarmOpts: []swarm.Option{ - // It is better to disable black hole detection and just attempt a dial for autonat - swarm.WithUDPBlackHoleConfig(false, 0, 0), - swarm.WithIPv6BlackHoleConfig(false, 0, 0), + swarm.WithUDPBlackHoleSuccessCounter(nil), + swarm.WithIPv6BlackHoleSuccessCounter(nil), }, } diff --git a/core/network/network.go b/core/network/network.go index 22efbf235..d2e2bc818 100644 --- a/core/network/network.go +++ b/core/network/network.go @@ -194,6 +194,9 @@ type Dialer interface { // Notify/StopNotify register and unregister a notifiee for signals Notify(Notifiee) StopNotify(Notifiee) + + // CanDial returns whether the dialer can dial peer p at addr + CanDial(p peer.ID, addr ma.Multiaddr) bool } // AddrDelay provides an address along with the delay after which the address diff --git a/defaults.go b/defaults.go index 8f7dc9467..1aba3bd7e 100644 --- a/defaults.go +++ b/defaults.go @@ -10,6 +10,7 @@ import ( rcmgr "github.com/libp2p/go-libp2p/p2p/host/resource-manager" "github.com/libp2p/go-libp2p/p2p/muxer/yamux" "github.com/libp2p/go-libp2p/p2p/net/connmgr" + "github.com/libp2p/go-libp2p/p2p/net/swarm" "github.com/libp2p/go-libp2p/p2p/security/noise" tls "github.com/libp2p/go-libp2p/p2p/security/tls" quic "github.com/libp2p/go-libp2p/p2p/transport/quic" @@ -133,6 +134,18 @@ var DefaultPrometheusRegisterer = func(cfg *Config) error { return cfg.Apply(PrometheusRegisterer(prometheus.DefaultRegisterer)) } +var defaultUDPBlackHoleDetector = func(cfg *Config) error { + // A black hole is a binary property. On a network if UDP dials are blocked, all dials will + // fail. So a low success rate of 5 out 100 dials is good enough. + return cfg.Apply(UDPBlackHoleSuccessCounter(&swarm.BlackHoleSuccessCounter{N: 100, MinSuccesses: 5, Name: "UDP"})) +} + +var defaultIPv6BlackHoleDetector = func(cfg *Config) error { + // A black hole is a binary property. On a network if there is no IPv6 connectivity, all + // dials will fail. So a low success rate of 5 out 100 dials is good enough. + return cfg.Apply(IPv6BlackHoleSuccessCounter(&swarm.BlackHoleSuccessCounter{N: 100, MinSuccesses: 5, Name: "IPv6"})) +} + // Complete list of default options and when to fallback on them. // // Please *DON'T* specify default options any other way. Putting this all here @@ -189,6 +202,18 @@ var defaults = []struct { fallback: func(cfg *Config) bool { return !cfg.DisableMetrics && cfg.PrometheusRegisterer == nil }, opt: DefaultPrometheusRegisterer, }, + { + fallback: func(cfg *Config) bool { + return !cfg.CustomUDPBlackHoleSuccessCounter && cfg.UDPBlackHoleSuccessCounter == nil + }, + opt: defaultUDPBlackHoleDetector, + }, + { + fallback: func(cfg *Config) bool { + return !cfg.CustomIPv6BlackHoleSuccessCounter && cfg.IPv6BlackHoleSuccessCounter == nil + }, + opt: defaultIPv6BlackHoleDetector, + }, } // Defaults configures libp2p to use the default options. Can be combined with diff --git a/libp2p_test.go b/libp2p_test.go index 8a9a8edcb..ea52e5647 100644 --- a/libp2p_test.go +++ b/libp2p_test.go @@ -397,6 +397,12 @@ func TestInsecureConstructor(t *testing.T) { h.Close() } +func TestAutoNATv2Service(t *testing.T) { + h, err := New(EnableAutoNATv2()) + require.NoError(t, err) + h.Close() +} + func TestDisableIdentifyAddressDiscovery(t *testing.T) { h, err := New(DisableIdentifyAddressDiscovery()) require.NoError(t, err) diff --git a/options.go b/options.go index de95251ad..1a8bc5dd5 100644 --- a/options.go +++ b/options.go @@ -609,3 +609,29 @@ func DisableIdentifyAddressDiscovery() Option { return nil } } + +// EnableAutoNATv2 enables autonat v2 +func EnableAutoNATv2() Option { + return func(cfg *Config) error { + cfg.EnableAutoNATv2 = true + return nil + } +} + +// UDPBlackHoleSuccessCounter configures libp2p to use f as the black hole filter for UDP addrs +func UDPBlackHoleSuccessCounter(f *swarm.BlackHoleSuccessCounter) Option { + return func(cfg *Config) error { + cfg.UDPBlackHoleSuccessCounter = f + cfg.CustomUDPBlackHoleSuccessCounter = true + return nil + } +} + +// IPv6BlackHoleSuccessCounter configures libp2p to use f as the black hole filter for IPv6 addrs +func IPv6BlackHoleSuccessCounter(f *swarm.BlackHoleSuccessCounter) Option { + return func(cfg *Config) error { + cfg.IPv6BlackHoleSuccessCounter = f + cfg.CustomIPv6BlackHoleSuccessCounter = true + return nil + } +} diff --git a/p2p/host/basic/basic_host.go b/p2p/host/basic/basic_host.go index 8fc808e6b..766b1b13b 100644 --- a/p2p/host/basic/basic_host.go +++ b/p2p/host/basic/basic_host.go @@ -24,6 +24,7 @@ import ( "github.com/libp2p/go-libp2p/p2p/host/eventbus" "github.com/libp2p/go-libp2p/p2p/host/pstoremanager" "github.com/libp2p/go-libp2p/p2p/host/relaysvc" + "github.com/libp2p/go-libp2p/p2p/protocol/autonatv2" relayv2 "github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/relay" "github.com/libp2p/go-libp2p/p2p/protocol/holepunch" "github.com/libp2p/go-libp2p/p2p/protocol/identify" @@ -105,6 +106,8 @@ type BasicHost struct { caBook peerstore.CertifiedAddrBook autoNat autonat.AutoNAT + + autonatv2 *autonatv2.AutoNAT } var _ host.Host = (*BasicHost)(nil) @@ -167,6 +170,8 @@ type HostOpts struct { // DisableIdentifyAddressDiscovery disables address discovery using peer provided observed addresses in identify DisableIdentifyAddressDiscovery bool + EnableAutoNATv2 bool + AutoNATv2Dialer host.Host } // NewHost constructs a new *BasicHost and activates it by attaching its stream and connection handlers to the given inet.Network. @@ -310,6 +315,13 @@ func NewHost(n network.Network, opts *HostOpts) (*BasicHost, error) { h.pings = ping.NewPingService(h) } + if opts.EnableAutoNATv2 { + h.autonatv2, err = autonatv2.New(h, opts.AutoNATv2Dialer) + if err != nil { + return nil, fmt.Errorf("failed to create autonatv2: %w", err) + } + } + n.SetStreamHandler(h.newStreamHandler) // register to be notified when the network's listen addrs change, @@ -398,6 +410,12 @@ func (h *BasicHost) Start() { h.psManager.Start() h.refCount.Add(1) h.ids.Start() + if h.autonatv2 != nil { + err := h.autonatv2.Start() + if err != nil { + log.Errorf("autonat v2 failed to start: %s", err) + } + } go h.background() } @@ -1100,6 +1118,9 @@ func (h *BasicHost) Close() error { if h.hps != nil { h.hps.Close() } + if h.autonatv2 != nil { + h.autonatv2.Close() + } _ = h.emitters.evtLocalProtocolsUpdated.Close() _ = h.emitters.evtLocalAddrsUpdated.Close() diff --git a/p2p/net/mock/mock_peernet.go b/p2p/net/mock/mock_peernet.go index 2e56b7f2b..0b525d3e6 100644 --- a/p2p/net/mock/mock_peernet.go +++ b/p2p/net/mock/mock_peernet.go @@ -434,3 +434,7 @@ func (pn *peernet) notifyAll(notification func(f network.Notifiee)) { func (pn *peernet) ResourceManager() network.ResourceManager { return &network.NullResourceManager{} } + +func (pn *peernet) CanDial(p peer.ID, addr ma.Multiaddr) bool { + return true +} diff --git a/p2p/net/swarm/black_hole_detector.go b/p2p/net/swarm/black_hole_detector.go index dd7849eea..54782c1c0 100644 --- a/p2p/net/swarm/black_hole_detector.go +++ b/p2p/net/swarm/black_hole_detector.go @@ -29,35 +29,26 @@ func (st blackHoleState) String() string { } } -type blackHoleResult int - -const ( - blackHoleResultAllowed blackHoleResult = iota - blackHoleResultProbing - blackHoleResultBlocked -) - -// blackHoleFilter provides black hole filtering for dials. This filter should be used in -// concert with a UDP of IPv6 address filter to detect UDP or IPv6 black hole. In a black -// holed environments dial requests are blocked and only periodic probes to check the -// state of the black hole are allowed. -// -// Requests are blocked if the number of successes in the last n dials is less than -// minSuccesses. If a request succeeds in Blocked state, the filter state is reset and n -// subsequent requests are allowed before reevaluating black hole state. Dials cancelled -// when some other concurrent dial succeeded are counted as failures. A sufficiently large -// n prevents false negatives in such cases. -type blackHoleFilter struct { - // n serves the dual purpose of being the minimum number of requests after which we - // probe the state of the black hole in blocked state and the minimum number of - // completed dials required before evaluating black hole state. - n int - // minSuccesses is the minimum number of Success required in the last n dials +// BlackHoleSuccessCounter provides black hole filtering for dials. This filter should be used in concert +// with a UDP or IPv6 address filter to detect UDP or IPv6 black hole. In a black holed environment, +// dial requests are refused Requests are blocked if the number of successes in the last N dials is +// less than MinSuccesses. +// If a request succeeds in Blocked state, the filter state is reset and N subsequent requests are +// allowed before reevaluating black hole state. Dials cancelled when some other concurrent dial +// succeeded are counted as failures. A sufficiently large N prevents false negatives in such cases. +type BlackHoleSuccessCounter struct { + // N is + // 1. The minimum number of completed dials required before evaluating black hole state + // 2. the minimum number of requests after which we probe the state of the black hole in + // blocked state + N int + // MinSuccesses is the minimum number of Success required in the last n dials // to consider we are not blocked. - minSuccesses int - // name for the detector. - name string + MinSuccesses int + // Name for the detector. + Name string + mu sync.Mutex // requests counts number of dial requests to peers. We handle request at a peer // level and record results at individual address dial level. requests int @@ -67,22 +58,19 @@ type blackHoleFilter struct { successes int // state is the current state of the detector state blackHoleState - - mu sync.Mutex - metricsTracer MetricsTracer } -// RecordResult records the outcome of a dial. A successful dial will change the state -// of the filter to Allowed. A failed dial only blocks subsequent requests if the success +// RecordResult records the outcome of a dial. A successful dial in Blocked state will change the +// state of the filter to Probing. A failed dial only blocks subsequent requests if the success // fraction over the last n outcomes is less than the minSuccessFraction of the filter. -func (b *blackHoleFilter) RecordResult(success bool) { +func (b *BlackHoleSuccessCounter) RecordResult(success bool) { b.mu.Lock() defer b.mu.Unlock() if b.state == blackHoleStateBlocked && success { // If the call succeeds in a blocked state we reset to allowed. // This is better than slowly accumulating values till we cross the minSuccessFraction - // threshold since a blackhole is a binary property. + // threshold since a black hole is a binary property. b.reset() return } @@ -92,7 +80,7 @@ func (b *blackHoleFilter) RecordResult(success bool) { } b.dialResults = append(b.dialResults, success) - if len(b.dialResults) > b.n { + if len(b.dialResults) > b.N { if b.dialResults[0] { b.successes-- } @@ -100,58 +88,68 @@ func (b *blackHoleFilter) RecordResult(success bool) { } b.updateState() - b.trackMetrics() } // HandleRequest returns the result of applying the black hole filter for the request. -func (b *blackHoleFilter) HandleRequest() blackHoleResult { +func (b *BlackHoleSuccessCounter) HandleRequest() blackHoleState { b.mu.Lock() defer b.mu.Unlock() b.requests++ - b.trackMetrics() - if b.state == blackHoleStateAllowed { - return blackHoleResultAllowed - } else if b.state == blackHoleStateProbing || b.requests%b.n == 0 { - return blackHoleResultProbing + return blackHoleStateAllowed + } else if b.state == blackHoleStateProbing || b.requests%b.N == 0 { + return blackHoleStateProbing } else { - return blackHoleResultBlocked + return blackHoleStateBlocked } } -func (b *blackHoleFilter) reset() { +func (b *BlackHoleSuccessCounter) reset() { b.successes = 0 b.dialResults = b.dialResults[:0] b.requests = 0 b.updateState() } -func (b *blackHoleFilter) updateState() { +func (b *BlackHoleSuccessCounter) updateState() { st := b.state - if len(b.dialResults) < b.n { + if len(b.dialResults) < b.N { b.state = blackHoleStateProbing - } else if b.successes >= b.minSuccesses { + } else if b.successes >= b.MinSuccesses { b.state = blackHoleStateAllowed } else { b.state = blackHoleStateBlocked } if st != b.state { - log.Debugf("%s blackHoleDetector state changed from %s to %s", b.name, st, b.state) + log.Debugf("%s blackHoleDetector state changed from %s to %s", b.Name, st, b.state) } } -func (b *blackHoleFilter) trackMetrics() { - if b.metricsTracer == nil { - return - } +func (b *BlackHoleSuccessCounter) State() blackHoleState { + b.mu.Lock() + defer b.mu.Unlock() + + return b.state +} - nextRequestAllowedAfter := 0 +type blackHoleInfo struct { + name string + state blackHoleState + nextProbeAfter int + successFraction float64 +} + +func (b *BlackHoleSuccessCounter) info() blackHoleInfo { + b.mu.Lock() + defer b.mu.Unlock() + + nextProbeAfter := 0 if b.state == blackHoleStateBlocked { - nextRequestAllowedAfter = b.n - (b.requests % b.n) + nextProbeAfter = b.N - (b.requests % b.N) } successFraction := 0.0 @@ -159,22 +157,27 @@ func (b *blackHoleFilter) trackMetrics() { successFraction = float64(b.successes) / float64(len(b.dialResults)) } - b.metricsTracer.UpdatedBlackHoleFilterState( - b.name, - b.state, - nextRequestAllowedAfter, - successFraction, - ) + return blackHoleInfo{ + name: b.Name, + state: b.state, + nextProbeAfter: nextProbeAfter, + successFraction: successFraction, + } } -// blackHoleDetector provides UDP and IPv6 black hole detection using a `blackHoleFilter` -// for each. For details of the black hole detection logic see `blackHoleFilter`. +// blackHoleDetector provides UDP and IPv6 black hole detection using a `BlackHoleSuccessCounter` for each. +// For details of the black hole detection logic see `BlackHoleSuccessCounter`. +// In Read Only mode, detector doesn't update the state of underlying filters and refuses requests +// when black hole state is unknown. This is useful for Swarms made specifically for services like +// AutoNAT where we care about accurately reporting the reachability of a peer. // -// black hole filtering is done at a peer dial level to ensure that periodic probes to -// detect change of the black hole state are actually dialed and are not skipped -// because of dial prioritisation logic. +// Black hole filtering is done at a peer dial level to ensure that periodic probes to detect change +// of the black hole state are actually dialed and are not skipped because of dial prioritisation +// logic. type blackHoleDetector struct { - udp, ipv6 *blackHoleFilter + udp, ipv6 *BlackHoleSuccessCounter + mt MetricsTracer + readOnly bool } // FilterAddrs filters the peer's addresses removing black holed addresses @@ -192,14 +195,16 @@ func (d *blackHoleDetector) FilterAddrs(addrs []ma.Multiaddr) (valid []ma.Multia } } - udpRes := blackHoleResultAllowed + udpRes := blackHoleStateAllowed if d.udp != nil && hasUDP { - udpRes = d.udp.HandleRequest() + udpRes = d.getFilterState(d.udp) + d.trackMetrics(d.udp) } - ipv6Res := blackHoleResultAllowed + ipv6Res := blackHoleStateAllowed if d.ipv6 != nil && hasIPv6 { - ipv6Res = d.ipv6.HandleRequest() + ipv6Res = d.getFilterState(d.ipv6) + d.trackMetrics(d.ipv6) } blackHoled = make([]ma.Multiaddr, 0, len(addrs)) @@ -210,19 +215,19 @@ func (d *blackHoleDetector) FilterAddrs(addrs []ma.Multiaddr) (valid []ma.Multia return true } // allow all UDP addresses while probing irrespective of IPv6 black hole state - if udpRes == blackHoleResultProbing && isProtocolAddr(a, ma.P_UDP) { + if udpRes == blackHoleStateProbing && isProtocolAddr(a, ma.P_UDP) { return true } // allow all IPv6 addresses while probing irrespective of UDP black hole state - if ipv6Res == blackHoleResultProbing && isProtocolAddr(a, ma.P_IP6) { + if ipv6Res == blackHoleStateProbing && isProtocolAddr(a, ma.P_IP6) { return true } - if udpRes == blackHoleResultBlocked && isProtocolAddr(a, ma.P_UDP) { + if udpRes == blackHoleStateBlocked && isProtocolAddr(a, ma.P_UDP) { blackHoled = append(blackHoled, a) return false } - if ipv6Res == blackHoleResultBlocked && isProtocolAddr(a, ma.P_IP6) { + if ipv6Res == blackHoleStateBlocked && isProtocolAddr(a, ma.P_IP6) { blackHoled = append(blackHoled, a) return false } @@ -231,49 +236,36 @@ func (d *blackHoleDetector) FilterAddrs(addrs []ma.Multiaddr) (valid []ma.Multia ), blackHoled } -// RecordResult updates the state of the relevant `blackHoleFilter`s for addr +// RecordResult updates the state of the relevant BlackHoleSuccessCounters for addr func (d *blackHoleDetector) RecordResult(addr ma.Multiaddr, success bool) { - if !manet.IsPublicAddr(addr) { + if d.readOnly || !manet.IsPublicAddr(addr) { return } if d.udp != nil && isProtocolAddr(addr, ma.P_UDP) { d.udp.RecordResult(success) + d.trackMetrics(d.udp) } if d.ipv6 != nil && isProtocolAddr(addr, ma.P_IP6) { d.ipv6.RecordResult(success) + d.trackMetrics(d.ipv6) } } -// blackHoleConfig is the config used for black hole detection -type blackHoleConfig struct { - // Enabled enables black hole detection - Enabled bool - // N is the size of the sliding window used to evaluate black hole state - N int - // MinSuccesses is the minimum number of successes out of N required to not - // block requests - MinSuccesses int -} - -func newBlackHoleDetector(udpConfig, ipv6Config blackHoleConfig, mt MetricsTracer) *blackHoleDetector { - d := &blackHoleDetector{} - - if udpConfig.Enabled { - d.udp = &blackHoleFilter{ - n: udpConfig.N, - minSuccesses: udpConfig.MinSuccesses, - name: "UDP", - metricsTracer: mt, +func (d *blackHoleDetector) getFilterState(f *BlackHoleSuccessCounter) blackHoleState { + if d.readOnly { + if f.State() != blackHoleStateAllowed { + return blackHoleStateBlocked } + return blackHoleStateAllowed } + return f.HandleRequest() +} - if ipv6Config.Enabled { - d.ipv6 = &blackHoleFilter{ - n: ipv6Config.N, - minSuccesses: ipv6Config.MinSuccesses, - name: "IPv6", - metricsTracer: mt, - } +func (d *blackHoleDetector) trackMetrics(f *BlackHoleSuccessCounter) { + if d.readOnly || d.mt == nil { + return } - return d + // Track metrics only in non readOnly state + info := f.info() + d.mt.UpdatedBlackHoleSuccessCounter(info.name, info.state, info.nextProbeAfter, info.successFraction) } diff --git a/p2p/net/swarm/black_hole_detector_test.go b/p2p/net/swarm/black_hole_detector_test.go index 1ab2cbe58..a38b43f4c 100644 --- a/p2p/net/swarm/black_hole_detector_test.go +++ b/p2p/net/swarm/black_hole_detector_test.go @@ -8,58 +8,70 @@ import ( "github.com/stretchr/testify/require" ) -func TestBlackHoleFilterReset(t *testing.T) { +func TestBlackHoleSuccessCounterReset(t *testing.T) { n := 10 - bhf := &blackHoleFilter{n: n, minSuccesses: 2, name: "test"} + bhf := &BlackHoleSuccessCounter{N: n, MinSuccesses: 2, Name: "test"} var i = 0 // calls up to n should be probing for i = 1; i <= n; i++ { - if bhf.HandleRequest() != blackHoleResultProbing { + if bhf.HandleRequest() != blackHoleStateProbing { t.Fatalf("expected calls up to n to be probes") } + if bhf.State() != blackHoleStateProbing { + t.Fatalf("expected state to be probing got %s", bhf.State()) + } bhf.RecordResult(false) } // after threshold calls every nth call should be a probe for i = n + 1; i < 42; i++ { result := bhf.HandleRequest() - if (i%n == 0 && result != blackHoleResultProbing) || (i%n != 0 && result != blackHoleResultBlocked) { + if (i%n == 0 && result != blackHoleStateProbing) || (i%n != 0 && result != blackHoleStateBlocked) { t.Fatalf("expected every nth dial to be a probe") } + if bhf.State() != blackHoleStateBlocked { + t.Fatalf("expected state to be blocked, got %s", bhf.State()) + } } bhf.RecordResult(true) // check if calls up to n are probes again for i = 0; i < n; i++ { - if bhf.HandleRequest() != blackHoleResultProbing { + if bhf.HandleRequest() != blackHoleStateProbing { t.Fatalf("expected black hole detector state to reset after success") } + if bhf.State() != blackHoleStateProbing { + t.Fatalf("expected state to be probing got %s", bhf.State()) + } bhf.RecordResult(false) } // next call should be blocked - if bhf.HandleRequest() != blackHoleResultBlocked { + if bhf.HandleRequest() != blackHoleStateBlocked { t.Fatalf("expected dial to be blocked") + if bhf.State() != blackHoleStateBlocked { + t.Fatalf("expected state to be blocked, got %s", bhf.State()) + } } } -func TestBlackHoleFilterSuccessFraction(t *testing.T) { +func TestBlackHoleSuccessCounterSuccessFraction(t *testing.T) { n := 10 tests := []struct { minSuccesses, successes int - result blackHoleResult + result blackHoleState }{ - {minSuccesses: 5, successes: 5, result: blackHoleResultAllowed}, - {minSuccesses: 3, successes: 3, result: blackHoleResultAllowed}, - {minSuccesses: 5, successes: 4, result: blackHoleResultBlocked}, - {minSuccesses: 5, successes: 7, result: blackHoleResultAllowed}, - {minSuccesses: 3, successes: 1, result: blackHoleResultBlocked}, - {minSuccesses: 0, successes: 0, result: blackHoleResultAllowed}, - {minSuccesses: 10, successes: 10, result: blackHoleResultAllowed}, + {minSuccesses: 5, successes: 5, result: blackHoleStateAllowed}, + {minSuccesses: 3, successes: 3, result: blackHoleStateAllowed}, + {minSuccesses: 5, successes: 4, result: blackHoleStateBlocked}, + {minSuccesses: 5, successes: 7, result: blackHoleStateAllowed}, + {minSuccesses: 3, successes: 1, result: blackHoleStateBlocked}, + {minSuccesses: 0, successes: 0, result: blackHoleStateAllowed}, + {minSuccesses: 10, successes: 10, result: blackHoleStateAllowed}, } for i, tc := range tests { t.Run(fmt.Sprintf("case-%d", i), func(t *testing.T) { - bhf := blackHoleFilter{n: n, minSuccesses: tc.minSuccesses} + bhf := BlackHoleSuccessCounter{N: n, MinSuccesses: tc.minSuccesses} for i := 0; i < tc.successes; i++ { bhf.RecordResult(true) } @@ -75,9 +87,9 @@ func TestBlackHoleFilterSuccessFraction(t *testing.T) { } func TestBlackHoleDetectorInApplicableAddress(t *testing.T) { - udpConfig := blackHoleConfig{Enabled: true, N: 10, MinSuccesses: 5} - ipv6Config := blackHoleConfig{Enabled: true, N: 10, MinSuccesses: 5} - bhd := newBlackHoleDetector(udpConfig, ipv6Config, nil) + udpF := &BlackHoleSuccessCounter{N: 10, MinSuccesses: 5} + ipv6F := &BlackHoleSuccessCounter{N: 10, MinSuccesses: 5} + bhd := &blackHoleDetector{udp: udpF, ipv6: ipv6F} addrs := []ma.Multiaddr{ ma.StringCast("/ip4/1.2.3.4/tcp/1234"), ma.StringCast("/ip4/1.2.3.4/tcp/1233"), @@ -94,8 +106,8 @@ func TestBlackHoleDetectorInApplicableAddress(t *testing.T) { } func TestBlackHoleDetectorUDPDisabled(t *testing.T) { - ipv6Config := blackHoleConfig{Enabled: true, N: 10, MinSuccesses: 5} - bhd := newBlackHoleDetector(blackHoleConfig{Enabled: false}, ipv6Config, nil) + ipv6F := &BlackHoleSuccessCounter{N: 10, MinSuccesses: 5} + bhd := &blackHoleDetector{ipv6: ipv6F} publicAddr := ma.StringCast("/ip4/1.2.3.4/udp/1234/quic-v1") privAddr := ma.StringCast("/ip4/192.168.1.5/udp/1234/quic-v1") for i := 0; i < 100; i++ { @@ -110,8 +122,8 @@ func TestBlackHoleDetectorUDPDisabled(t *testing.T) { } func TestBlackHoleDetectorIPv6Disabled(t *testing.T) { - udpConfig := blackHoleConfig{Enabled: true, N: 10, MinSuccesses: 5} - bhd := newBlackHoleDetector(udpConfig, blackHoleConfig{Enabled: false}, nil) + udpF := &BlackHoleSuccessCounter{N: 10, MinSuccesses: 5} + bhd := &blackHoleDetector{udp: udpF} publicAddr := ma.StringCast("/ip6/2001::1/tcp/1234") privAddr := ma.StringCast("/ip6/::1/tcp/1234") for i := 0; i < 100; i++ { @@ -128,8 +140,8 @@ func TestBlackHoleDetectorIPv6Disabled(t *testing.T) { func TestBlackHoleDetectorProbes(t *testing.T) { bhd := &blackHoleDetector{ - udp: &blackHoleFilter{n: 2, minSuccesses: 1, name: "udp"}, - ipv6: &blackHoleFilter{n: 3, minSuccesses: 1, name: "ipv6"}, + udp: &BlackHoleSuccessCounter{N: 2, MinSuccesses: 1, Name: "udp"}, + ipv6: &BlackHoleSuccessCounter{N: 3, MinSuccesses: 1, Name: "ipv6"}, } udp6Addr := ma.StringCast("/ip6/2001::1/udp/1234/quic-v1") addrs := []ma.Multiaddr{udp6Addr} @@ -163,8 +175,8 @@ func TestBlackHoleDetectorAddrFiltering(t *testing.T) { makeBHD := func(udpBlocked, ipv6Blocked bool) *blackHoleDetector { bhd := &blackHoleDetector{ - udp: &blackHoleFilter{n: 100, minSuccesses: 10, name: "udp"}, - ipv6: &blackHoleFilter{n: 100, minSuccesses: 10, name: "ipv6"}, + udp: &BlackHoleSuccessCounter{N: 100, MinSuccesses: 10, Name: "udp"}, + ipv6: &BlackHoleSuccessCounter{N: 100, MinSuccesses: 10, Name: "ipv6"}, } for i := 0; i < 100; i++ { bhd.RecordResult(udp4Pub, !udpBlocked) @@ -199,3 +211,35 @@ func TestBlackHoleDetectorAddrFiltering(t *testing.T) { require.ElementsMatch(t, bothBlockedOutput, gotAddrs) require.ElementsMatch(t, bothPublicAddrs, gotRemovedAddrs) } + +func TestBlackHoleDetectorReadOnlyMode(t *testing.T) { + udpF := &BlackHoleSuccessCounter{N: 10, MinSuccesses: 5} + ipv6F := &BlackHoleSuccessCounter{N: 10, MinSuccesses: 5} + bhd := &blackHoleDetector{udp: udpF, ipv6: ipv6F, readOnly: true} + publicAddr := ma.StringCast("/ip4/1.2.3.4/udp/1234/quic-v1") + privAddr := ma.StringCast("/ip6/::1/tcp/1234") + for i := 0; i < 100; i++ { + bhd.RecordResult(publicAddr, true) + } + allAddr := []ma.Multiaddr{privAddr, publicAddr} + // public addr filtered because state is probing + wantAddrs := []ma.Multiaddr{privAddr} + wantRemovedAddrs := []ma.Multiaddr{publicAddr} + + gotAddrs, gotRemovedAddrs := bhd.FilterAddrs(allAddr) + require.ElementsMatch(t, wantAddrs, gotAddrs) + require.ElementsMatch(t, wantRemovedAddrs, gotRemovedAddrs) + + // a non readonly shared state black hole detector + nbhd := &blackHoleDetector{udp: bhd.udp, ipv6: bhd.ipv6, readOnly: false} + for i := 0; i < 100; i++ { + nbhd.RecordResult(publicAddr, true) + } + // no addresses filtered because state is allowed + wantAddrs = []ma.Multiaddr{privAddr, publicAddr} + wantRemovedAddrs = []ma.Multiaddr{} + + gotAddrs, gotRemovedAddrs = bhd.FilterAddrs(allAddr) + require.ElementsMatch(t, wantAddrs, gotAddrs) + require.ElementsMatch(t, wantRemovedAddrs, gotRemovedAddrs) +} diff --git a/p2p/net/swarm/swarm.go b/p2p/net/swarm/swarm.go index 7897277cc..02cff1e88 100644 --- a/p2p/net/swarm/swarm.go +++ b/p2p/net/swarm/swarm.go @@ -112,22 +112,33 @@ func WithDialRanker(d network.DialRanker) Option { } } -// WithUDPBlackHoleConfig configures swarm to use c as the config for UDP black hole detection +// WithUDPBlackHoleSuccessCounter configures swarm to use the provided config for UDP black hole detection // n is the size of the sliding window used to evaluate black hole state // min is the minimum number of successes out of n required to not block requests -func WithUDPBlackHoleConfig(enabled bool, n, min int) Option { +func WithUDPBlackHoleSuccessCounter(f *BlackHoleSuccessCounter) Option { return func(s *Swarm) error { - s.udpBlackHoleConfig = blackHoleConfig{Enabled: enabled, N: n, MinSuccesses: min} + s.udpBHF = f return nil } } -// WithIPv6BlackHoleConfig configures swarm to use c as the config for IPv6 black hole detection +// WithIPv6BlackHoleSuccessCounter configures swarm to use the provided config for IPv6 black hole detection // n is the size of the sliding window used to evaluate black hole state // min is the minimum number of successes out of n required to not block requests -func WithIPv6BlackHoleConfig(enabled bool, n, min int) Option { +func WithIPv6BlackHoleSuccessCounter(f *BlackHoleSuccessCounter) Option { return func(s *Swarm) error { - s.ipv6BlackHoleConfig = blackHoleConfig{Enabled: enabled, N: n, MinSuccesses: min} + s.ipv6BHF = f + return nil + } +} + +// WithReadOnlyBlackHoleDetector configures the swarm to use the black hole detector in +// read only mode. In Read Only mode dial requests are refused in unknown state and +// no updates to the detector state are made. This is useful for services like AutoNAT that +// care about accurately providing reachability info. +func WithReadOnlyBlackHoleDetector() Option { + return func(s *Swarm) error { + s.readOnlyBHD = true return nil } } @@ -203,10 +214,11 @@ type Swarm struct { dialRanker network.DialRanker - udpBlackHoleConfig blackHoleConfig - ipv6BlackHoleConfig blackHoleConfig - bhd *blackHoleDetector connectednessEventEmitter *connectednessEventEmitter + udpBHF *BlackHoleSuccessCounter + ipv6BHF *BlackHoleSuccessCounter + bhd *blackHoleDetector + readOnlyBHD bool } // NewSwarm constructs a Swarm. @@ -230,8 +242,8 @@ func NewSwarm(local peer.ID, peers peerstore.Peerstore, eventBus event.Bus, opts // A black hole is a binary property. On a network if UDP dials are blocked or there is // no IPv6 connectivity, all dials will fail. So a low success rate of 5 out 100 dials // is good enough. - udpBlackHoleConfig: blackHoleConfig{Enabled: true, N: 100, MinSuccesses: 5}, - ipv6BlackHoleConfig: blackHoleConfig{Enabled: true, N: 100, MinSuccesses: 5}, + udpBHF: &BlackHoleSuccessCounter{N: 100, MinSuccesses: 5, Name: "UDP"}, + ipv6BHF: &BlackHoleSuccessCounter{N: 100, MinSuccesses: 5, Name: "IPv6"}, } s.conns.m = make(map[peer.ID][]*Conn) @@ -255,7 +267,12 @@ func NewSwarm(local peer.ID, peers peerstore.Peerstore, eventBus event.Bus, opts s.limiter = newDialLimiter(s.dialAddr) s.backf.init(s.ctx) - s.bhd = newBlackHoleDetector(s.udpBlackHoleConfig, s.ipv6BlackHoleConfig, s.metricsTracer) + s.bhd = &blackHoleDetector{ + udp: s.udpBHF, + ipv6: s.ipv6BHF, + mt: s.metricsTracer, + readOnly: s.readOnlyBHD, + } return s, nil } diff --git a/p2p/net/swarm/swarm_dial.go b/p2p/net/swarm/swarm_dial.go index f639ce16a..446ece450 100644 --- a/p2p/net/swarm/swarm_dial.go +++ b/p2p/net/swarm/swarm_dial.go @@ -416,6 +416,11 @@ func (s *Swarm) dialNextAddr(ctx context.Context, p peer.ID, addr ma.Multiaddr, return nil } +func (s *Swarm) CanDial(p peer.ID, addr ma.Multiaddr) bool { + dialable, _ := s.filterKnownUndialables(p, []ma.Multiaddr{addr}) + return len(dialable) > 0 +} + func (s *Swarm) nonProxyAddr(addr ma.Multiaddr) bool { t := s.TransportForDialing(addr) return !t.Proxy() diff --git a/p2p/net/swarm/swarm_dial_test.go b/p2p/net/swarm/swarm_dial_test.go index 83f94b9d9..f4c33170a 100644 --- a/p2p/net/swarm/swarm_dial_test.go +++ b/p2p/net/swarm/swarm_dial_test.go @@ -364,7 +364,7 @@ func TestBlackHoledAddrBlocked(t *testing.T) { defer s.Close() n := 3 - s.bhd.ipv6 = &blackHoleFilter{n: n, minSuccesses: 1, name: "IPv6"} + s.bhd.ipv6 = &BlackHoleSuccessCounter{N: n, MinSuccesses: 1, Name: "IPv6"} // All dials to this addr will fail. // manet.IsPublic is aggressive for IPv6 addresses. Use a NAT64 address. diff --git a/p2p/net/swarm/swarm_metrics.go b/p2p/net/swarm/swarm_metrics.go index b5c0f2e49..929f3f494 100644 --- a/p2p/net/swarm/swarm_metrics.go +++ b/p2p/net/swarm/swarm_metrics.go @@ -85,7 +85,7 @@ var ( Buckets: []float64{0.001, 0.01, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.75, 1, 2}, }, ) - blackHoleFilterState = prometheus.NewGaugeVec( + blackHoleSuccessCounterState = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: metricNamespace, Name: "black_hole_filter_state", @@ -93,7 +93,7 @@ var ( }, []string{"name"}, ) - blackHoleFilterSuccessFraction = prometheus.NewGaugeVec( + blackHoleSuccessCounterSuccessFraction = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: metricNamespace, Name: "black_hole_filter_success_fraction", @@ -101,7 +101,7 @@ var ( }, []string{"name"}, ) - blackHoleFilterNextRequestAllowedAfter = prometheus.NewGaugeVec( + blackHoleSuccessCounterNextRequestAllowedAfter = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: metricNamespace, Name: "black_hole_filter_next_request_allowed_after", @@ -118,9 +118,9 @@ var ( connHandshakeLatency, dialsPerPeer, dialRankingDelay, - blackHoleFilterSuccessFraction, - blackHoleFilterState, - blackHoleFilterNextRequestAllowedAfter, + blackHoleSuccessCounterSuccessFraction, + blackHoleSuccessCounterState, + blackHoleSuccessCounterNextRequestAllowedAfter, } ) @@ -131,7 +131,7 @@ type MetricsTracer interface { FailedDialing(ma.Multiaddr, error, error) DialCompleted(success bool, totalDials int) DialRankingDelay(d time.Duration) - UpdatedBlackHoleFilterState(name string, state blackHoleState, nextProbeAfter int, successFraction float64) + UpdatedBlackHoleSuccessCounter(name string, state blackHoleState, nextProbeAfter int, successFraction float64) } type metricsTracer struct{} @@ -274,14 +274,14 @@ func (m *metricsTracer) DialRankingDelay(d time.Duration) { dialRankingDelay.Observe(d.Seconds()) } -func (m *metricsTracer) UpdatedBlackHoleFilterState(name string, state blackHoleState, +func (m *metricsTracer) UpdatedBlackHoleSuccessCounter(name string, state blackHoleState, nextProbeAfter int, successFraction float64) { tags := metricshelper.GetStringSlice() defer metricshelper.PutStringSlice(tags) *tags = append(*tags, name) - blackHoleFilterState.WithLabelValues(*tags...).Set(float64(state)) - blackHoleFilterSuccessFraction.WithLabelValues(*tags...).Set(successFraction) - blackHoleFilterNextRequestAllowedAfter.WithLabelValues(*tags...).Set(float64(nextProbeAfter)) + blackHoleSuccessCounterState.WithLabelValues(*tags...).Set(float64(state)) + blackHoleSuccessCounterSuccessFraction.WithLabelValues(*tags...).Set(successFraction) + blackHoleSuccessCounterNextRequestAllowedAfter.WithLabelValues(*tags...).Set(float64(nextProbeAfter)) } diff --git a/p2p/net/swarm/swarm_metrics_test.go b/p2p/net/swarm/swarm_metrics_test.go index e415c55fb..45ce0c2e4 100644 --- a/p2p/net/swarm/swarm_metrics_test.go +++ b/p2p/net/swarm/swarm_metrics_test.go @@ -94,8 +94,8 @@ func TestMetricsNoAllocNoCover(t *testing.T) { "FailedDialing": func() { mt.FailedDialing(randItem(addrs), randItem(errors), randItem(errors)) }, "DialCompleted": func() { mt.DialCompleted(mrand.Intn(2) == 1, mrand.Intn(10)) }, "DialRankingDelay": func() { mt.DialRankingDelay(time.Duration(mrand.Intn(1e10))) }, - "UpdatedBlackHoleFilterState": func() { - mt.UpdatedBlackHoleFilterState( + "UpdatedBlackHoleSuccessCounter": func() { + mt.UpdatedBlackHoleSuccessCounter( randItem(bhfNames), randItem(bhfState), mrand.Intn(100), diff --git a/p2p/net/swarm/testing/testing.go b/p2p/net/swarm/testing/testing.go index 627114f64..2bbe8b27a 100644 --- a/p2p/net/swarm/testing/testing.go +++ b/p2p/net/swarm/testing/testing.go @@ -53,63 +53,63 @@ func (rc realclock) Now() time.Time { } // Option is an option that can be passed when constructing a test swarm. -type Option func(*testing.T, *config) +type Option func(testing.TB, *config) // WithClock sets the clock to use for this swarm func WithClock(clock clock) Option { - return func(_ *testing.T, c *config) { + return func(_ testing.TB, c *config) { c.clock = clock } } func WithSwarmOpts(swarmOpts ...swarm.Option) Option { - return func(_ *testing.T, c *config) { + return func(_ testing.TB, c *config) { c.swarmOpts = swarmOpts } } // OptDisableReuseport disables reuseport in this test swarm. -var OptDisableReuseport Option = func(_ *testing.T, c *config) { +var OptDisableReuseport Option = func(_ testing.TB, c *config) { c.disableReuseport = true } // OptDialOnly prevents the test swarm from listening. -var OptDialOnly Option = func(_ *testing.T, c *config) { +var OptDialOnly Option = func(_ testing.TB, c *config) { c.dialOnly = true } // OptDisableTCP disables TCP. -var OptDisableTCP Option = func(_ *testing.T, c *config) { +var OptDisableTCP Option = func(_ testing.TB, c *config) { c.disableTCP = true } // OptDisableQUIC disables QUIC. -var OptDisableQUIC Option = func(_ *testing.T, c *config) { +var OptDisableQUIC Option = func(_ testing.TB, c *config) { c.disableQUIC = true } // OptConnGater configures the given connection gater on the test func OptConnGater(cg connmgr.ConnectionGater) Option { - return func(_ *testing.T, c *config) { + return func(_ testing.TB, c *config) { c.connectionGater = cg } } // OptPeerPrivateKey configures the peer private key which is then used to derive the public key and peer ID. func OptPeerPrivateKey(sk crypto.PrivKey) Option { - return func(_ *testing.T, c *config) { + return func(_ testing.TB, c *config) { c.sk = sk } } func EventBus(b event.Bus) Option { - return func(_ *testing.T, c *config) { + return func(_ testing.TB, c *config) { c.eventBus = b } } // GenUpgrader creates a new connection upgrader for use with this swarm. -func GenUpgrader(t *testing.T, n *swarm.Swarm, connGater connmgr.ConnectionGater, opts ...tptu.Option) transport.Upgrader { +func GenUpgrader(t testing.TB, n *swarm.Swarm, connGater connmgr.ConnectionGater, opts ...tptu.Option) transport.Upgrader { id := n.LocalPeer() pk := n.Peerstore().PrivKey(id) st := insecure.NewWithIdentity(insecure.ID, id, pk) @@ -120,7 +120,7 @@ func GenUpgrader(t *testing.T, n *swarm.Swarm, connGater connmgr.ConnectionGater } // GenSwarm generates a new test swarm. -func GenSwarm(t *testing.T, opts ...Option) *swarm.Swarm { +func GenSwarm(t testing.TB, opts ...Option) *swarm.Swarm { var cfg config cfg.clock = realclock{} for _, o := range opts { diff --git a/p2p/protocol/autonatv2/autonat.go b/p2p/protocol/autonatv2/autonat.go new file mode 100644 index 000000000..1ae11edbf --- /dev/null +++ b/p2p/protocol/autonatv2/autonat.go @@ -0,0 +1,236 @@ +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) +} diff --git a/p2p/protocol/autonatv2/autonat_test.go b/p2p/protocol/autonatv2/autonat_test.go new file mode 100644 index 000000000..ee97ccc69 --- /dev/null +++ b/p2p/protocol/autonatv2/autonat_test.go @@ -0,0 +1,661 @@ +package autonatv2 + +import ( + "context" + "errors" + "fmt" + "sync/atomic" + "testing" + "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" + bhost "github.com/libp2p/go-libp2p/p2p/host/blank" + "github.com/libp2p/go-libp2p/p2p/host/eventbus" + "github.com/libp2p/go-libp2p/p2p/net/swarm" + swarmt "github.com/libp2p/go-libp2p/p2p/net/swarm/testing" + "github.com/libp2p/go-libp2p/p2p/protocol/autonatv2/pb" + + "github.com/libp2p/go-msgio/pbio" + ma "github.com/multiformats/go-multiaddr" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func newAutoNAT(t testing.TB, dialer host.Host, opts ...AutoNATOption) *AutoNAT { + t.Helper() + b := eventbus.NewBus() + h := bhost.NewBlankHost( + swarmt.GenSwarm(t, swarmt.EventBus(b)), bhost.WithEventBus(b)) + if dialer == nil { + dialer = bhost.NewBlankHost( + swarmt.GenSwarm(t, + swarmt.WithSwarmOpts( + swarm.WithUDPBlackHoleSuccessCounter(nil), + swarm.WithIPv6BlackHoleSuccessCounter(nil)))) + } + an, err := New(h, dialer, opts...) + if err != nil { + t.Error(err) + } + an.Start() + t.Cleanup(an.Close) + return an +} + +func parseAddrs(t *testing.T, msg *pb.Message) []ma.Multiaddr { + t.Helper() + req := msg.GetDialRequest() + addrs := make([]ma.Multiaddr, 0) + for _, ab := range req.Addrs { + a, err := ma.NewMultiaddrBytes(ab) + if err != nil { + t.Error("invalid addr bytes", ab) + } + addrs = append(addrs, a) + } + return addrs +} + +// idAndConnect identifies b to a and connects them +func idAndConnect(t testing.TB, a, b host.Host) { + a.Peerstore().AddAddrs(b.ID(), b.Addrs(), peerstore.PermanentAddrTTL) + a.Peerstore().AddProtocols(b.ID(), DialProtocol) + + err := a.Connect(context.Background(), peer.AddrInfo{ID: b.ID()}) + require.NoError(t, err) +} + +// waitForPeer waits for a to have 1 peer in the peerMap +func waitForPeer(t testing.TB, a *AutoNAT) { + t.Helper() + require.Eventually(t, func() bool { + a.mx.Lock() + defer a.mx.Unlock() + return a.peers.GetRand() != "" + }, 5*time.Second, 100*time.Millisecond) +} + +// idAndWait provides server address and protocol to client +func idAndWait(t testing.TB, cli *AutoNAT, srv *AutoNAT) { + idAndConnect(t, cli.host, srv.host) + waitForPeer(t, cli) +} + +func TestAutoNATPrivateAddr(t *testing.T) { + an := newAutoNAT(t, nil) + res, err := an.GetReachability(context.Background(), []Request{{Addr: ma.StringCast("/ip4/192.168.0.1/udp/10/quic-v1")}}) + require.Equal(t, res, Result{}) + require.Contains(t, err.Error(), "private address cannot be verified by autonatv2") +} + +func TestClientRequest(t *testing.T) { + an := newAutoNAT(t, nil, allowPrivateAddrs) + defer an.Close() + defer an.host.Close() + + b := bhost.NewBlankHost(swarmt.GenSwarm(t)) + defer b.Close() + idAndConnect(t, an.host, b) + waitForPeer(t, an) + + addrs := an.host.Addrs() + addrbs := make([][]byte, len(addrs)) + for i := 0; i < len(addrs); i++ { + addrbs[i] = addrs[i].Bytes() + } + + var receivedRequest atomic.Bool + b.SetStreamHandler(DialProtocol, func(s network.Stream) { + receivedRequest.Store(true) + r := pbio.NewDelimitedReader(s, maxMsgSize) + var msg pb.Message + assert.NoError(t, r.ReadMsg(&msg)) + assert.NotNil(t, msg.GetDialRequest()) + assert.Equal(t, addrbs, msg.GetDialRequest().Addrs) + s.Reset() + }) + + res, err := an.GetReachability(context.Background(), []Request{ + {Addr: addrs[0], SendDialData: true}, {Addr: addrs[1]}, + }) + require.Equal(t, res, Result{}) + require.NotNil(t, err) + require.True(t, receivedRequest.Load()) +} + +func TestClientServerError(t *testing.T) { + an := newAutoNAT(t, nil, allowPrivateAddrs) + defer an.Close() + defer an.host.Close() + + b := bhost.NewBlankHost(swarmt.GenSwarm(t)) + defer b.Close() + idAndConnect(t, an.host, b) + waitForPeer(t, an) + + tests := []struct { + handler func(network.Stream) + errorStr string + }{ + { + handler: func(s network.Stream) { + s.Reset() + }, + errorStr: "stream reset", + }, + { + handler: func(s network.Stream) { + w := pbio.NewDelimitedWriter(s) + assert.NoError(t, w.WriteMsg( + &pb.Message{Msg: &pb.Message_DialRequest{DialRequest: &pb.DialRequest{}}})) + }, + errorStr: "invalid msg type", + }, + { + handler: func(s network.Stream) { + w := pbio.NewDelimitedWriter(s) + assert.NoError(t, w.WriteMsg( + &pb.Message{Msg: &pb.Message_DialResponse{ + DialResponse: &pb.DialResponse{ + Status: pb.DialResponse_E_DIAL_REFUSED, + }, + }}, + )) + }, + errorStr: ErrDialRefused.Error(), + }, + } + + for i, tc := range tests { + t.Run(fmt.Sprintf("test-%d", i), func(t *testing.T) { + b.SetStreamHandler(DialProtocol, tc.handler) + addrs := an.host.Addrs() + res, err := an.GetReachability( + context.Background(), + newTestRequests(addrs, false)) + require.Equal(t, res, Result{}) + require.NotNil(t, err) + require.Contains(t, err.Error(), tc.errorStr) + }) + } +} + +func TestClientDataRequest(t *testing.T) { + an := newAutoNAT(t, nil, allowPrivateAddrs) + defer an.Close() + defer an.host.Close() + + b := bhost.NewBlankHost(swarmt.GenSwarm(t)) + defer b.Close() + idAndConnect(t, an.host, b) + waitForPeer(t, an) + + tests := []struct { + handler func(network.Stream) + name string + }{ + { + name: "provides dial data", + handler: func(s network.Stream) { + r := pbio.NewDelimitedReader(s, maxMsgSize) + var msg pb.Message + assert.NoError(t, r.ReadMsg(&msg)) + w := pbio.NewDelimitedWriter(s) + if err := w.WriteMsg(&pb.Message{ + Msg: &pb.Message_DialDataRequest{ + DialDataRequest: &pb.DialDataRequest{ + AddrIdx: 0, + NumBytes: 10000, + }, + }}, + ); err != nil { + t.Error(err) + s.Reset() + return + } + var dialData []byte + for len(dialData) < 10000 { + if err := r.ReadMsg(&msg); err != nil { + t.Error(err) + s.Reset() + return + } + if msg.GetDialDataResponse() == nil { + t.Errorf("expected to receive msg of type DialDataResponse") + s.Reset() + return + } + dialData = append(dialData, msg.GetDialDataResponse().Data...) + } + s.Reset() + }, + }, + { + name: "low priority addr", + handler: func(s network.Stream) { + r := pbio.NewDelimitedReader(s, maxMsgSize) + var msg pb.Message + assert.NoError(t, r.ReadMsg(&msg)) + w := pbio.NewDelimitedWriter(s) + if err := w.WriteMsg(&pb.Message{ + Msg: &pb.Message_DialDataRequest{ + DialDataRequest: &pb.DialDataRequest{ + AddrIdx: 1, + NumBytes: 10000, + }, + }}, + ); err != nil { + t.Error(err) + s.Reset() + return + } + assert.Error(t, r.ReadMsg(&msg)) + s.Reset() + }, + }, + { + name: "too high dial data request", + handler: func(s network.Stream) { + r := pbio.NewDelimitedReader(s, maxMsgSize) + var msg pb.Message + assert.NoError(t, r.ReadMsg(&msg)) + w := pbio.NewDelimitedWriter(s) + if err := w.WriteMsg(&pb.Message{ + Msg: &pb.Message_DialDataRequest{ + DialDataRequest: &pb.DialDataRequest{ + AddrIdx: 0, + NumBytes: 1 << 32, + }, + }}, + ); err != nil { + t.Error(err) + s.Reset() + return + } + assert.Error(t, r.ReadMsg(&msg)) + s.Reset() + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + b.SetStreamHandler(DialProtocol, tc.handler) + addrs := an.host.Addrs() + + res, err := an.GetReachability( + context.Background(), + []Request{ + {Addr: addrs[0], SendDialData: true}, + {Addr: addrs[1]}, + }) + require.Equal(t, res, Result{}) + require.NotNil(t, err) + }) + } +} + +func TestClientDialBacks(t *testing.T) { + an := newAutoNAT(t, nil, allowPrivateAddrs) + defer an.Close() + defer an.host.Close() + + b := bhost.NewBlankHost(swarmt.GenSwarm(t)) + defer b.Close() + idAndConnect(t, an.host, b) + waitForPeer(t, an) + + dialerHost := bhost.NewBlankHost(swarmt.GenSwarm(t)) + defer dialerHost.Close() + + readReq := func(r pbio.Reader) ([]ma.Multiaddr, uint64, error) { + var msg pb.Message + if err := r.ReadMsg(&msg); err != nil { + return nil, 0, err + } + if msg.GetDialRequest() == nil { + return nil, 0, errors.New("no dial request in msg") + } + addrs := parseAddrs(t, &msg) + return addrs, msg.GetDialRequest().GetNonce(), nil + } + + writeNonce := func(addr ma.Multiaddr, nonce uint64) error { + pid := an.host.ID() + dialerHost.Peerstore().AddAddr(pid, addr, peerstore.PermanentAddrTTL) + defer func() { + dialerHost.Network().ClosePeer(pid) + dialerHost.Peerstore().RemovePeer(pid) + dialerHost.Peerstore().ClearAddrs(pid) + }() + as, err := dialerHost.NewStream(context.Background(), pid, DialBackProtocol) + if err != nil { + return err + } + w := pbio.NewDelimitedWriter(as) + if err := w.WriteMsg(&pb.DialBack{Nonce: nonce}); err != nil { + return err + } + as.CloseWrite() + data := make([]byte, 1) + as.Read(data) + as.Close() + return nil + } + + tests := []struct { + name string + handler func(network.Stream) + success bool + }{ + { + name: "correct dial attempt", + handler: func(s network.Stream) { + r := pbio.NewDelimitedReader(s, maxMsgSize) + w := pbio.NewDelimitedWriter(s) + + addrs, nonce, err := readReq(r) + if err != nil { + s.Reset() + t.Error(err) + return + } + if err := writeNonce(addrs[1], nonce); err != nil { + s.Reset() + t.Error(err) + return + } + w.WriteMsg(&pb.Message{ + Msg: &pb.Message_DialResponse{ + DialResponse: &pb.DialResponse{ + Status: pb.DialResponse_OK, + DialStatus: pb.DialStatus_OK, + AddrIdx: 1, + }, + }, + }) + s.Close() + }, + success: true, + }, + { + name: "no dial attempt", + handler: func(s network.Stream) { + r := pbio.NewDelimitedReader(s, maxMsgSize) + if _, _, err := readReq(r); err != nil { + s.Reset() + t.Error(err) + return + } + resp := &pb.DialResponse{ + Status: pb.DialResponse_OK, + DialStatus: pb.DialStatus_OK, + AddrIdx: 0, + } + w := pbio.NewDelimitedWriter(s) + w.WriteMsg(&pb.Message{ + Msg: &pb.Message_DialResponse{ + DialResponse: resp, + }, + }) + s.Close() + }, + success: false, + }, + { + name: "invalid reported address", + handler: func(s network.Stream) { + r := pbio.NewDelimitedReader(s, maxMsgSize) + addrs, nonce, err := readReq(r) + if err != nil { + s.Reset() + t.Error(err) + return + } + + if err := writeNonce(addrs[1], nonce); err != nil { + s.Reset() + t.Error(err) + return + } + + w := pbio.NewDelimitedWriter(s) + w.WriteMsg(&pb.Message{ + Msg: &pb.Message_DialResponse{ + DialResponse: &pb.DialResponse{ + Status: pb.DialResponse_OK, + DialStatus: pb.DialStatus_OK, + AddrIdx: 0, + }, + }, + }) + s.Close() + }, + success: false, + }, + { + name: "invalid nonce", + handler: func(s network.Stream) { + r := pbio.NewDelimitedReader(s, maxMsgSize) + addrs, nonce, err := readReq(r) + if err != nil { + s.Reset() + t.Error(err) + return + } + if err := writeNonce(addrs[0], nonce-1); err != nil { + s.Reset() + t.Error(err) + return + } + w := pbio.NewDelimitedWriter(s) + w.WriteMsg(&pb.Message{ + Msg: &pb.Message_DialResponse{ + DialResponse: &pb.DialResponse{ + Status: pb.DialResponse_OK, + DialStatus: pb.DialStatus_OK, + AddrIdx: 0, + }, + }, + }) + s.Close() + }, + success: false, + }, + { + name: "invalid addr index", + handler: func(s network.Stream) { + r := pbio.NewDelimitedReader(s, maxMsgSize) + _, _, err := readReq(r) + if err != nil { + s.Reset() + t.Error(err) + return + } + w := pbio.NewDelimitedWriter(s) + w.WriteMsg(&pb.Message{ + Msg: &pb.Message_DialResponse{ + DialResponse: &pb.DialResponse{ + Status: pb.DialResponse_OK, + DialStatus: pb.DialStatus_OK, + AddrIdx: 10, + }, + }, + }) + s.Close() + }, + success: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + addrs := an.host.Addrs() + b.SetStreamHandler(DialProtocol, tc.handler) + res, err := an.GetReachability( + context.Background(), + []Request{ + {Addr: addrs[0], SendDialData: true}, + {Addr: addrs[1]}, + }) + if !tc.success { + require.Error(t, err) + require.Equal(t, Result{}, res) + } else { + require.NoError(t, err) + require.Equal(t, res.Reachability, network.ReachabilityPublic) + require.Equal(t, res.Status, pb.DialStatus_OK) + } + }) + } +} + +func TestEventSubscription(t *testing.T) { + an := newAutoNAT(t, nil) + defer an.host.Close() + + b := bhost.NewBlankHost(swarmt.GenSwarm(t)) + defer b.Close() + c := bhost.NewBlankHost(swarmt.GenSwarm(t)) + defer c.Close() + + idAndConnect(t, an.host, b) + require.Eventually(t, func() bool { + an.mx.Lock() + defer an.mx.Unlock() + return len(an.peers.peers) == 1 + }, 5*time.Second, 100*time.Millisecond) + + idAndConnect(t, an.host, c) + require.Eventually(t, func() bool { + an.mx.Lock() + defer an.mx.Unlock() + return len(an.peers.peers) == 2 + }, 5*time.Second, 100*time.Millisecond) + + an.host.Network().ClosePeer(b.ID()) + require.Eventually(t, func() bool { + an.mx.Lock() + defer an.mx.Unlock() + return len(an.peers.peers) == 1 + }, 5*time.Second, 100*time.Millisecond) + + an.host.Network().ClosePeer(c.ID()) + require.Eventually(t, func() bool { + an.mx.Lock() + defer an.mx.Unlock() + return len(an.peers.peers) == 0 + }, 5*time.Second, 100*time.Millisecond) +} + +func TestPeersMap(t *testing.T) { + emptyPeerID := peer.ID("") + + t.Run("single_item", func(t *testing.T) { + p := newPeersMap() + p.Put("peer1") + p.Delete("peer1") + p.Put("peer1") + require.Equal(t, peer.ID("peer1"), p.GetRand()) + p.Delete("peer1") + require.Equal(t, emptyPeerID, p.GetRand()) + }) + + t.Run("multiple_items", func(t *testing.T) { + p := newPeersMap() + require.Equal(t, emptyPeerID, p.GetRand()) + + allPeers := make(map[peer.ID]bool) + for i := 0; i < 20; i++ { + pid := peer.ID(fmt.Sprintf("peer-%d", i)) + allPeers[pid] = true + p.Put(pid) + } + foundPeers := make(map[peer.ID]bool) + for i := 0; i < 1000; i++ { + pid := p.GetRand() + require.NotEqual(t, emptyPeerID, p) + require.True(t, allPeers[pid]) + foundPeers[pid] = true + if len(foundPeers) == len(allPeers) { + break + } + } + for pid := range allPeers { + p.Delete(pid) + } + require.Equal(t, emptyPeerID, p.GetRand()) + }) +} + +func TestAreAddrsConsistency(t *testing.T) { + c := &client{ + normalizeMultiaddr: func(a ma.Multiaddr) ma.Multiaddr { + for { + rest, l := ma.SplitLast(a) + if _, err := l.ValueForProtocol(ma.P_CERTHASH); err != nil { + return a + } + a = rest + } + }, + } + tests := []struct { + name string + localAddr ma.Multiaddr + dialAddr ma.Multiaddr + success bool + }{ + { + name: "simple match", + localAddr: ma.StringCast("/ip4/192.168.0.1/tcp/12345"), + dialAddr: ma.StringCast("/ip4/1.2.3.4/tcp/23232"), + success: true, + }, + { + name: "nat64", + localAddr: ma.StringCast("/ip6/1::1/tcp/12345"), + dialAddr: ma.StringCast("/ip4/1.2.3.4/tcp/23232"), + success: false, + }, + { + name: "simple mismatch", + localAddr: ma.StringCast("/ip4/192.168.0.1/tcp/12345"), + dialAddr: ma.StringCast("/ip4/1.2.3.4/udp/23232/quic-v1"), + success: false, + }, + { + name: "quic-vs-webtransport", + localAddr: ma.StringCast("/ip4/192.168.0.1/udp/12345/quic-v1"), + dialAddr: ma.StringCast("/ip4/1.2.3.4/udp/123/quic-v1/webtransport"), + success: false, + }, + { + name: "webtransport-certhash", + localAddr: ma.StringCast("/ip4/192.168.0.1/udp/12345/quic-v1/webtransport"), + dialAddr: ma.StringCast("/ip4/1.2.3.4/udp/123/quic-v1/webtransport/certhash/uEgNmb28"), + success: true, + }, + { + name: "dns", + localAddr: ma.StringCast("/dns/lib.p2p/udp/12345/quic-v1"), + dialAddr: ma.StringCast("/ip6/1::1/udp/123/quic-v1/"), + success: false, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if c.areAddrsConsistent(tc.localAddr, tc.dialAddr) != tc.success { + wantStr := "match" + if !tc.success { + wantStr = "mismatch" + } + t.Errorf("expected %s between\nlocal addr: %s\ndial addr: %s", wantStr, tc.localAddr, tc.dialAddr) + } + }) + } + +} diff --git a/p2p/protocol/autonatv2/client.go b/p2p/protocol/autonatv2/client.go new file mode 100644 index 000000000..f93cc3137 --- /dev/null +++ b/p2p/protocol/autonatv2/client.go @@ -0,0 +1,342 @@ +package autonatv2 + +import ( + "context" + "fmt" + "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/p2p/protocol/autonatv2/pb" + "github.com/libp2p/go-msgio/pbio" + ma "github.com/multiformats/go-multiaddr" + "golang.org/x/exp/rand" +) + +//go:generate protoc --go_out=. --go_opt=Mpb/autonatv2.proto=./pb pb/autonatv2.proto + +// client implements the client for making dial requests for AutoNAT v2. It verifies successful +// dials and provides an option to send data for dial requests. +type client struct { + host host.Host + dialData []byte + normalizeMultiaddr func(ma.Multiaddr) ma.Multiaddr + + mu sync.Mutex + // dialBackQueues maps nonce to the channel for providing the local multiaddr of the connection + // the nonce was received on + dialBackQueues map[uint64]chan ma.Multiaddr +} + +type normalizeMultiaddrer interface { + NormalizeMultiaddr(ma.Multiaddr) ma.Multiaddr +} + +func newClient(h host.Host) *client { + normalizeMultiaddr := func(a ma.Multiaddr) ma.Multiaddr { return a } + if hn, ok := h.(normalizeMultiaddrer); ok { + normalizeMultiaddr = hn.NormalizeMultiaddr + } + return &client{ + host: h, + dialData: make([]byte, 4000), + normalizeMultiaddr: normalizeMultiaddr, + dialBackQueues: make(map[uint64]chan ma.Multiaddr), + } +} + +func (ac *client) Start() { + ac.host.SetStreamHandler(DialBackProtocol, ac.handleDialBack) +} + +func (ac *client) Close() { + ac.host.RemoveStreamHandler(DialBackProtocol) +} + +// GetReachability verifies address reachability with a AutoNAT v2 server p. +func (ac *client) GetReachability(ctx context.Context, p peer.ID, reqs []Request) (Result, error) { + ctx, cancel := context.WithTimeout(ctx, streamTimeout) + defer cancel() + + s, err := ac.host.NewStream(ctx, p, DialProtocol) + if err != nil { + return Result{}, fmt.Errorf("open %s stream failed: %w", DialProtocol, err) + } + + if err := s.Scope().SetService(ServiceName); err != nil { + s.Reset() + return Result{}, fmt.Errorf("attach stream %s to service %s failed: %w", DialProtocol, ServiceName, err) + } + + if err := s.Scope().ReserveMemory(maxMsgSize, network.ReservationPriorityAlways); err != nil { + s.Reset() + return Result{}, fmt.Errorf("failed to reserve memory for stream %s: %w", DialProtocol, err) + } + defer s.Scope().ReleaseMemory(maxMsgSize) + + s.SetDeadline(time.Now().Add(streamTimeout)) + defer s.Close() + + nonce := rand.Uint64() + ch := make(chan ma.Multiaddr, 1) + ac.mu.Lock() + ac.dialBackQueues[nonce] = ch + ac.mu.Unlock() + defer func() { + ac.mu.Lock() + delete(ac.dialBackQueues, nonce) + ac.mu.Unlock() + }() + + msg := newDialRequest(reqs, nonce) + w := pbio.NewDelimitedWriter(s) + if err := w.WriteMsg(&msg); err != nil { + s.Reset() + return Result{}, fmt.Errorf("dial request write failed: %w", err) + } + + r := pbio.NewDelimitedReader(s, maxMsgSize) + if err := r.ReadMsg(&msg); err != nil { + s.Reset() + return Result{}, fmt.Errorf("dial msg read failed: %w", err) + } + + switch { + case msg.GetDialResponse() != nil: + break + // provide dial data if appropriate + case msg.GetDialDataRequest() != nil: + if err := ac.validateDialDataRequest(reqs, &msg); err != nil { + s.Reset() + return Result{}, fmt.Errorf("invalid dial data request: %w", err) + } + // dial data request is valid and we want to send data + if err := sendDialData(ac.dialData, int(msg.GetDialDataRequest().GetNumBytes()), w, &msg); err != nil { + s.Reset() + return Result{}, fmt.Errorf("dial data send failed: %w", err) + } + if err := r.ReadMsg(&msg); err != nil { + s.Reset() + return Result{}, fmt.Errorf("dial response read failed: %w", err) + } + if msg.GetDialResponse() == nil { + s.Reset() + return Result{}, fmt.Errorf("invalid response type: %T", msg.Msg) + } + default: + s.Reset() + return Result{}, fmt.Errorf("invalid msg type: %T", msg.Msg) + } + + resp := msg.GetDialResponse() + if resp.GetStatus() != pb.DialResponse_OK { + // E_DIAL_REFUSED has implication for deciding future address verificiation priorities + // wrap a distinct error for convenient errors.Is usage + if resp.GetStatus() == pb.DialResponse_E_DIAL_REFUSED { + return Result{}, fmt.Errorf("dial request failed: %w", ErrDialRefused) + } + return Result{}, fmt.Errorf("dial request failed: response status %d %s", resp.GetStatus(), + pb.DialResponse_ResponseStatus_name[int32(resp.GetStatus())]) + } + if resp.GetDialStatus() == pb.DialStatus_UNUSED { + return Result{}, fmt.Errorf("invalid response: invalid dial status UNUSED") + } + if int(resp.AddrIdx) >= len(reqs) { + return Result{}, fmt.Errorf("invalid response: addr index out of range: %d [0-%d)", resp.AddrIdx, len(reqs)) + } + + // wait for nonce from the server + var dialBackAddr ma.Multiaddr + if resp.GetDialStatus() == pb.DialStatus_OK { + timer := time.NewTimer(dialBackStreamTimeout) + select { + case at := <-ch: + dialBackAddr = at + case <-ctx.Done(): + case <-timer.C: + } + timer.Stop() + } + return ac.newResult(resp, reqs, dialBackAddr) +} + +func (ac *client) validateDialDataRequest(reqs []Request, msg *pb.Message) error { + idx := int(msg.GetDialDataRequest().AddrIdx) + if idx >= len(reqs) { // invalid address index + return fmt.Errorf("addr index out of range: %d [0-%d)", idx, len(reqs)) + } + if msg.GetDialDataRequest().NumBytes > maxHandshakeSizeBytes { // data request is too high + return fmt.Errorf("requested data too high: %d", msg.GetDialDataRequest().NumBytes) + } + if !reqs[idx].SendDialData { // low priority addr + return fmt.Errorf("low priority addr: %s index %d", reqs[idx].Addr, idx) + } + return nil +} + +func (ac *client) newResult(resp *pb.DialResponse, reqs []Request, dialBackAddr ma.Multiaddr) (Result, error) { + idx := int(resp.AddrIdx) + addr := reqs[idx].Addr + + var rch network.Reachability + switch resp.DialStatus { + case pb.DialStatus_OK: + if !ac.areAddrsConsistent(dialBackAddr, addr) { + // the server is misinforming us about the address it successfully dialed + // either we received no dialback or the address on the dialback is inconsistent with + // what the server is telling us + return Result{}, fmt.Errorf("invalid response: dialBackAddr: %s, respAddr: %s", dialBackAddr, addr) + } + rch = network.ReachabilityPublic + case pb.DialStatus_E_DIAL_ERROR: + rch = network.ReachabilityPrivate + case pb.DialStatus_E_DIAL_BACK_ERROR: + if ac.areAddrsConsistent(dialBackAddr, addr) { + // We received the dial back but the server claims the dial back errored. + // As long as we received the correct nonce in dial back it is safe to assume + // that we are public. + rch = network.ReachabilityPublic + } else { + rch = network.ReachabilityUnknown + } + default: + // Unexpected response code. Discard the response and fail. + log.Warnf("invalid status code received in response for addr %s: %d", addr, resp.DialStatus) + return Result{}, fmt.Errorf("invalid response: invalid status code for addr %s: %d", addr, resp.DialStatus) + } + + return Result{ + Addr: addr, + Reachability: rch, + Status: resp.DialStatus, + }, nil +} + +func sendDialData(dialData []byte, numBytes int, w pbio.Writer, msg *pb.Message) (err error) { + ddResp := &pb.DialDataResponse{Data: dialData} + *msg = pb.Message{ + Msg: &pb.Message_DialDataResponse{ + DialDataResponse: ddResp, + }, + } + for remain := numBytes; remain > 0; { + if remain < len(ddResp.Data) { + ddResp.Data = ddResp.Data[:remain] + } + if err := w.WriteMsg(msg); err != nil { + return fmt.Errorf("write failed: %w", err) + } + remain -= len(dialData) + } + return nil +} + +func newDialRequest(reqs []Request, nonce uint64) pb.Message { + addrbs := make([][]byte, len(reqs)) + for i, r := range reqs { + addrbs[i] = r.Addr.Bytes() + } + return pb.Message{ + Msg: &pb.Message_DialRequest{ + DialRequest: &pb.DialRequest{ + Addrs: addrbs, + Nonce: nonce, + }, + }, + } +} + +// handleDialBack receives the nonce on the dial-back stream +func (ac *client) handleDialBack(s network.Stream) { + if err := s.Scope().SetService(ServiceName); err != nil { + log.Debugf("failed to attach stream to service %s: %w", ServiceName, err) + s.Reset() + return + } + + if err := s.Scope().ReserveMemory(dialBackMaxMsgSize, network.ReservationPriorityAlways); err != nil { + log.Debugf("failed to reserve memory for stream %s: %w", DialBackProtocol, err) + s.Reset() + return + } + defer s.Scope().ReleaseMemory(dialBackMaxMsgSize) + + s.SetDeadline(time.Now().Add(dialBackStreamTimeout)) + defer s.Close() + + r := pbio.NewDelimitedReader(s, dialBackMaxMsgSize) + var msg pb.DialBack + if err := r.ReadMsg(&msg); err != nil { + log.Debugf("failed to read dialback msg from %s: %s", s.Conn().RemotePeer(), err) + s.Reset() + return + } + nonce := msg.GetNonce() + + ac.mu.Lock() + ch := ac.dialBackQueues[nonce] + ac.mu.Unlock() + if ch == nil { + log.Debugf("dialback received with invalid nonce: localAdds: %s peer: %s nonce: %d", s.Conn().LocalMultiaddr(), s.Conn().RemotePeer(), nonce) + s.Reset() + return + } + select { + case ch <- s.Conn().LocalMultiaddr(): + default: + log.Debugf("multiple dialbacks received: localAddr: %s peer: %s", s.Conn().LocalMultiaddr(), s.Conn().RemotePeer()) + s.Reset() + return + } + w := pbio.NewDelimitedWriter(s) + res := pb.DialBackResponse{} + if err := w.WriteMsg(&res); err != nil { + log.Debugf("failed to write dialback response: %s", err) + s.Reset() + } +} + +func (ac *client) areAddrsConsistent(connLocalAddr, dialedAddr ma.Multiaddr) bool { + if connLocalAddr == nil || dialedAddr == nil { + return false + } + connLocalAddr = ac.normalizeMultiaddr(connLocalAddr) + dialedAddr = ac.normalizeMultiaddr(dialedAddr) + + localProtos := connLocalAddr.Protocols() + externalProtos := dialedAddr.Protocols() + if len(localProtos) != len(externalProtos) { + return false + } + for i := 0; i < len(localProtos); i++ { + if i == 0 { + switch externalProtos[i].Code { + case ma.P_DNS, ma.P_DNSADDR: + if localProtos[i].Code == ma.P_IP4 || localProtos[i].Code == ma.P_IP6 { + continue + } + return false + case ma.P_DNS4: + if localProtos[i].Code == ma.P_IP4 { + continue + } + return false + case ma.P_DNS6: + if localProtos[i].Code == ma.P_IP6 { + continue + } + return false + } + if localProtos[i].Code != externalProtos[i].Code { + return false + } + } else { + if localProtos[i].Code != externalProtos[i].Code { + return false + } + } + } + return true +} diff --git a/p2p/protocol/autonatv2/msg_reader.go b/p2p/protocol/autonatv2/msg_reader.go new file mode 100644 index 000000000..87849a55a --- /dev/null +++ b/p2p/protocol/autonatv2/msg_reader.go @@ -0,0 +1,38 @@ +package autonatv2 + +import ( + "io" + + "github.com/multiformats/go-varint" +) + +// msgReader reads a varint prefixed message from R without any buffering +type msgReader struct { + R io.Reader + Buf []byte +} + +func (m *msgReader) ReadByte() (byte, error) { + buf := m.Buf[:1] + _, err := m.R.Read(buf) + return buf[0], err +} + +func (m *msgReader) ReadMsg() ([]byte, error) { + sz, err := varint.ReadUvarint(m) + if err != nil { + return nil, err + } + if sz > uint64(len(m.Buf)) { + return nil, io.ErrShortBuffer + } + n := 0 + for n < int(sz) { + nr, err := m.R.Read(m.Buf[n:sz]) + if err != nil { + return nil, err + } + n += nr + } + return m.Buf[:sz], nil +} diff --git a/p2p/protocol/autonatv2/options.go b/p2p/protocol/autonatv2/options.go new file mode 100644 index 000000000..5cdb0fe92 --- /dev/null +++ b/p2p/protocol/autonatv2/options.go @@ -0,0 +1,56 @@ +package autonatv2 + +import "time" + +// autoNATSettings is used to configure AutoNAT +type autoNATSettings struct { + allowPrivateAddrs bool + serverRPM int + serverPerPeerRPM int + serverDialDataRPM int + dataRequestPolicy dataRequestPolicyFunc + now func() time.Time + amplificatonAttackPreventionDialWait time.Duration +} + +func defaultSettings() *autoNATSettings { + return &autoNATSettings{ + allowPrivateAddrs: false, + serverRPM: 60, // 1 every second + serverPerPeerRPM: 12, // 1 every 5 seconds + serverDialDataRPM: 12, // 1 every 5 seconds + dataRequestPolicy: amplificationAttackPrevention, + amplificatonAttackPreventionDialWait: 3 * time.Second, + now: time.Now, + } +} + +type AutoNATOption func(s *autoNATSettings) error + +func WithServerRateLimit(rpm, perPeerRPM, dialDataRPM int) AutoNATOption { + return func(s *autoNATSettings) error { + s.serverRPM = rpm + s.serverPerPeerRPM = perPeerRPM + s.serverDialDataRPM = dialDataRPM + return nil + } +} + +func withDataRequestPolicy(drp dataRequestPolicyFunc) AutoNATOption { + return func(s *autoNATSettings) error { + s.dataRequestPolicy = drp + return nil + } +} + +func allowPrivateAddrs(s *autoNATSettings) error { + s.allowPrivateAddrs = true + return nil +} + +func withAmplificationAttackPreventionDialWait(d time.Duration) AutoNATOption { + return func(s *autoNATSettings) error { + s.amplificatonAttackPreventionDialWait = d + return nil + } +} diff --git a/p2p/protocol/autonatv2/pb/autonatv2.pb.go b/p2p/protocol/autonatv2/pb/autonatv2.pb.go new file mode 100644 index 000000000..5c3ea8089 --- /dev/null +++ b/p2p/protocol/autonatv2/pb/autonatv2.pb.go @@ -0,0 +1,818 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.30.0 +// protoc v4.25.3 +// source: pb/autonatv2.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 DialStatus int32 + +const ( + DialStatus_UNUSED DialStatus = 0 + DialStatus_E_DIAL_ERROR DialStatus = 100 + DialStatus_E_DIAL_BACK_ERROR DialStatus = 101 + DialStatus_OK DialStatus = 200 +) + +// Enum value maps for DialStatus. +var ( + DialStatus_name = map[int32]string{ + 0: "UNUSED", + 100: "E_DIAL_ERROR", + 101: "E_DIAL_BACK_ERROR", + 200: "OK", + } + DialStatus_value = map[string]int32{ + "UNUSED": 0, + "E_DIAL_ERROR": 100, + "E_DIAL_BACK_ERROR": 101, + "OK": 200, + } +) + +func (x DialStatus) Enum() *DialStatus { + p := new(DialStatus) + *p = x + return p +} + +func (x DialStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (DialStatus) Descriptor() protoreflect.EnumDescriptor { + return file_pb_autonatv2_proto_enumTypes[0].Descriptor() +} + +func (DialStatus) Type() protoreflect.EnumType { + return &file_pb_autonatv2_proto_enumTypes[0] +} + +func (x DialStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use DialStatus.Descriptor instead. +func (DialStatus) EnumDescriptor() ([]byte, []int) { + return file_pb_autonatv2_proto_rawDescGZIP(), []int{0} +} + +type DialResponse_ResponseStatus int32 + +const ( + DialResponse_E_INTERNAL_ERROR DialResponse_ResponseStatus = 0 + DialResponse_E_REQUEST_REJECTED DialResponse_ResponseStatus = 100 + DialResponse_E_DIAL_REFUSED DialResponse_ResponseStatus = 101 + DialResponse_OK DialResponse_ResponseStatus = 200 +) + +// Enum value maps for DialResponse_ResponseStatus. +var ( + DialResponse_ResponseStatus_name = map[int32]string{ + 0: "E_INTERNAL_ERROR", + 100: "E_REQUEST_REJECTED", + 101: "E_DIAL_REFUSED", + 200: "OK", + } + DialResponse_ResponseStatus_value = map[string]int32{ + "E_INTERNAL_ERROR": 0, + "E_REQUEST_REJECTED": 100, + "E_DIAL_REFUSED": 101, + "OK": 200, + } +) + +func (x DialResponse_ResponseStatus) Enum() *DialResponse_ResponseStatus { + p := new(DialResponse_ResponseStatus) + *p = x + return p +} + +func (x DialResponse_ResponseStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (DialResponse_ResponseStatus) Descriptor() protoreflect.EnumDescriptor { + return file_pb_autonatv2_proto_enumTypes[1].Descriptor() +} + +func (DialResponse_ResponseStatus) Type() protoreflect.EnumType { + return &file_pb_autonatv2_proto_enumTypes[1] +} + +func (x DialResponse_ResponseStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use DialResponse_ResponseStatus.Descriptor instead. +func (DialResponse_ResponseStatus) EnumDescriptor() ([]byte, []int) { + return file_pb_autonatv2_proto_rawDescGZIP(), []int{3, 0} +} + +type DialBackResponse_DialBackStatus int32 + +const ( + DialBackResponse_OK DialBackResponse_DialBackStatus = 0 +) + +// Enum value maps for DialBackResponse_DialBackStatus. +var ( + DialBackResponse_DialBackStatus_name = map[int32]string{ + 0: "OK", + } + DialBackResponse_DialBackStatus_value = map[string]int32{ + "OK": 0, + } +) + +func (x DialBackResponse_DialBackStatus) Enum() *DialBackResponse_DialBackStatus { + p := new(DialBackResponse_DialBackStatus) + *p = x + return p +} + +func (x DialBackResponse_DialBackStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (DialBackResponse_DialBackStatus) Descriptor() protoreflect.EnumDescriptor { + return file_pb_autonatv2_proto_enumTypes[2].Descriptor() +} + +func (DialBackResponse_DialBackStatus) Type() protoreflect.EnumType { + return &file_pb_autonatv2_proto_enumTypes[2] +} + +func (x DialBackResponse_DialBackStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use DialBackResponse_DialBackStatus.Descriptor instead. +func (DialBackResponse_DialBackStatus) EnumDescriptor() ([]byte, []int) { + return file_pb_autonatv2_proto_rawDescGZIP(), []int{6, 0} +} + +type Message struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Types that are assignable to Msg: + // + // *Message_DialRequest + // *Message_DialResponse + // *Message_DialDataRequest + // *Message_DialDataResponse + Msg isMessage_Msg `protobuf_oneof:"msg"` +} + +func (x *Message) Reset() { + *x = Message{} + if protoimpl.UnsafeEnabled { + mi := &file_pb_autonatv2_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_autonatv2_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_autonatv2_proto_rawDescGZIP(), []int{0} +} + +func (m *Message) GetMsg() isMessage_Msg { + if m != nil { + return m.Msg + } + return nil +} + +func (x *Message) GetDialRequest() *DialRequest { + if x, ok := x.GetMsg().(*Message_DialRequest); ok { + return x.DialRequest + } + return nil +} + +func (x *Message) GetDialResponse() *DialResponse { + if x, ok := x.GetMsg().(*Message_DialResponse); ok { + return x.DialResponse + } + return nil +} + +func (x *Message) GetDialDataRequest() *DialDataRequest { + if x, ok := x.GetMsg().(*Message_DialDataRequest); ok { + return x.DialDataRequest + } + return nil +} + +func (x *Message) GetDialDataResponse() *DialDataResponse { + if x, ok := x.GetMsg().(*Message_DialDataResponse); ok { + return x.DialDataResponse + } + return nil +} + +type isMessage_Msg interface { + isMessage_Msg() +} + +type Message_DialRequest struct { + DialRequest *DialRequest `protobuf:"bytes,1,opt,name=dialRequest,proto3,oneof"` +} + +type Message_DialResponse struct { + DialResponse *DialResponse `protobuf:"bytes,2,opt,name=dialResponse,proto3,oneof"` +} + +type Message_DialDataRequest struct { + DialDataRequest *DialDataRequest `protobuf:"bytes,3,opt,name=dialDataRequest,proto3,oneof"` +} + +type Message_DialDataResponse struct { + DialDataResponse *DialDataResponse `protobuf:"bytes,4,opt,name=dialDataResponse,proto3,oneof"` +} + +func (*Message_DialRequest) isMessage_Msg() {} + +func (*Message_DialResponse) isMessage_Msg() {} + +func (*Message_DialDataRequest) isMessage_Msg() {} + +func (*Message_DialDataResponse) isMessage_Msg() {} + +type DialRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Addrs [][]byte `protobuf:"bytes,1,rep,name=addrs,proto3" json:"addrs,omitempty"` + Nonce uint64 `protobuf:"fixed64,2,opt,name=nonce,proto3" json:"nonce,omitempty"` +} + +func (x *DialRequest) Reset() { + *x = DialRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_pb_autonatv2_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DialRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DialRequest) ProtoMessage() {} + +func (x *DialRequest) ProtoReflect() protoreflect.Message { + mi := &file_pb_autonatv2_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 DialRequest.ProtoReflect.Descriptor instead. +func (*DialRequest) Descriptor() ([]byte, []int) { + return file_pb_autonatv2_proto_rawDescGZIP(), []int{1} +} + +func (x *DialRequest) GetAddrs() [][]byte { + if x != nil { + return x.Addrs + } + return nil +} + +func (x *DialRequest) GetNonce() uint64 { + if x != nil { + return x.Nonce + } + return 0 +} + +type DialDataRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + AddrIdx uint32 `protobuf:"varint,1,opt,name=addrIdx,proto3" json:"addrIdx,omitempty"` + NumBytes uint64 `protobuf:"varint,2,opt,name=numBytes,proto3" json:"numBytes,omitempty"` +} + +func (x *DialDataRequest) Reset() { + *x = DialDataRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_pb_autonatv2_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DialDataRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DialDataRequest) ProtoMessage() {} + +func (x *DialDataRequest) ProtoReflect() protoreflect.Message { + mi := &file_pb_autonatv2_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 DialDataRequest.ProtoReflect.Descriptor instead. +func (*DialDataRequest) Descriptor() ([]byte, []int) { + return file_pb_autonatv2_proto_rawDescGZIP(), []int{2} +} + +func (x *DialDataRequest) GetAddrIdx() uint32 { + if x != nil { + return x.AddrIdx + } + return 0 +} + +func (x *DialDataRequest) GetNumBytes() uint64 { + if x != nil { + return x.NumBytes + } + return 0 +} + +type DialResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Status DialResponse_ResponseStatus `protobuf:"varint,1,opt,name=status,proto3,enum=autonatv2.pb.DialResponse_ResponseStatus" json:"status,omitempty"` + AddrIdx uint32 `protobuf:"varint,2,opt,name=addrIdx,proto3" json:"addrIdx,omitempty"` + DialStatus DialStatus `protobuf:"varint,3,opt,name=dialStatus,proto3,enum=autonatv2.pb.DialStatus" json:"dialStatus,omitempty"` +} + +func (x *DialResponse) Reset() { + *x = DialResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_pb_autonatv2_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DialResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DialResponse) ProtoMessage() {} + +func (x *DialResponse) ProtoReflect() protoreflect.Message { + mi := &file_pb_autonatv2_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 DialResponse.ProtoReflect.Descriptor instead. +func (*DialResponse) Descriptor() ([]byte, []int) { + return file_pb_autonatv2_proto_rawDescGZIP(), []int{3} +} + +func (x *DialResponse) GetStatus() DialResponse_ResponseStatus { + if x != nil { + return x.Status + } + return DialResponse_E_INTERNAL_ERROR +} + +func (x *DialResponse) GetAddrIdx() uint32 { + if x != nil { + return x.AddrIdx + } + return 0 +} + +func (x *DialResponse) GetDialStatus() DialStatus { + if x != nil { + return x.DialStatus + } + return DialStatus_UNUSED +} + +type DialDataResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` +} + +func (x *DialDataResponse) Reset() { + *x = DialDataResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_pb_autonatv2_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DialDataResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DialDataResponse) ProtoMessage() {} + +func (x *DialDataResponse) ProtoReflect() protoreflect.Message { + mi := &file_pb_autonatv2_proto_msgTypes[4] + 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 DialDataResponse.ProtoReflect.Descriptor instead. +func (*DialDataResponse) Descriptor() ([]byte, []int) { + return file_pb_autonatv2_proto_rawDescGZIP(), []int{4} +} + +func (x *DialDataResponse) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +type DialBack struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Nonce uint64 `protobuf:"fixed64,1,opt,name=nonce,proto3" json:"nonce,omitempty"` +} + +func (x *DialBack) Reset() { + *x = DialBack{} + if protoimpl.UnsafeEnabled { + mi := &file_pb_autonatv2_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DialBack) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DialBack) ProtoMessage() {} + +func (x *DialBack) ProtoReflect() protoreflect.Message { + mi := &file_pb_autonatv2_proto_msgTypes[5] + 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 DialBack.ProtoReflect.Descriptor instead. +func (*DialBack) Descriptor() ([]byte, []int) { + return file_pb_autonatv2_proto_rawDescGZIP(), []int{5} +} + +func (x *DialBack) GetNonce() uint64 { + if x != nil { + return x.Nonce + } + return 0 +} + +type DialBackResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Status DialBackResponse_DialBackStatus `protobuf:"varint,1,opt,name=status,proto3,enum=autonatv2.pb.DialBackResponse_DialBackStatus" json:"status,omitempty"` +} + +func (x *DialBackResponse) Reset() { + *x = DialBackResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_pb_autonatv2_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DialBackResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DialBackResponse) ProtoMessage() {} + +func (x *DialBackResponse) ProtoReflect() protoreflect.Message { + mi := &file_pb_autonatv2_proto_msgTypes[6] + 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 DialBackResponse.ProtoReflect.Descriptor instead. +func (*DialBackResponse) Descriptor() ([]byte, []int) { + return file_pb_autonatv2_proto_rawDescGZIP(), []int{6} +} + +func (x *DialBackResponse) GetStatus() DialBackResponse_DialBackStatus { + if x != nil { + return x.Status + } + return DialBackResponse_OK +} + +var File_pb_autonatv2_proto protoreflect.FileDescriptor + +var file_pb_autonatv2_proto_rawDesc = []byte{ + 0x0a, 0x12, 0x70, 0x62, 0x2f, 0x61, 0x75, 0x74, 0x6f, 0x6e, 0x61, 0x74, 0x76, 0x32, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0c, 0x61, 0x75, 0x74, 0x6f, 0x6e, 0x61, 0x74, 0x76, 0x32, 0x2e, + 0x70, 0x62, 0x22, 0xaa, 0x02, 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x3d, + 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x6e, 0x61, 0x74, 0x76, 0x32, 0x2e, + 0x70, 0x62, 0x2e, 0x44, 0x69, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, + 0x52, 0x0b, 0x64, 0x69, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x40, 0x0a, + 0x0c, 0x64, 0x69, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x6e, 0x61, 0x74, 0x76, 0x32, 0x2e, + 0x70, 0x62, 0x2e, 0x44, 0x69, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, + 0x00, 0x52, 0x0c, 0x64, 0x69, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x49, 0x0a, 0x0f, 0x64, 0x69, 0x61, 0x6c, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x6e, + 0x61, 0x74, 0x76, 0x32, 0x2e, 0x70, 0x62, 0x2e, 0x44, 0x69, 0x61, 0x6c, 0x44, 0x61, 0x74, 0x61, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0f, 0x64, 0x69, 0x61, 0x6c, 0x44, + 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x4c, 0x0a, 0x10, 0x64, 0x69, + 0x61, 0x6c, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x6e, 0x61, 0x74, 0x76, 0x32, + 0x2e, 0x70, 0x62, 0x2e, 0x44, 0x69, 0x61, 0x6c, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x10, 0x64, 0x69, 0x61, 0x6c, 0x44, 0x61, 0x74, 0x61, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x05, 0x0a, 0x03, 0x6d, 0x73, 0x67, 0x22, + 0x39, 0x0a, 0x0b, 0x44, 0x69, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, + 0x0a, 0x05, 0x61, 0x64, 0x64, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x05, 0x61, + 0x64, 0x64, 0x72, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x06, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x22, 0x47, 0x0a, 0x0f, 0x44, 0x69, + 0x61, 0x6c, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, + 0x07, 0x61, 0x64, 0x64, 0x72, 0x49, 0x64, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, + 0x61, 0x64, 0x64, 0x72, 0x49, 0x64, 0x78, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x75, 0x6d, 0x42, 0x79, + 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x6e, 0x75, 0x6d, 0x42, 0x79, + 0x74, 0x65, 0x73, 0x22, 0x82, 0x02, 0x0a, 0x0c, 0x44, 0x69, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x29, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x6e, 0x61, 0x74, 0x76, 0x32, + 0x2e, 0x70, 0x62, 0x2e, 0x44, 0x69, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 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, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x49, + 0x64, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x49, 0x64, + 0x78, 0x12, 0x38, 0x0a, 0x0a, 0x64, 0x69, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x6e, 0x61, 0x74, 0x76, + 0x32, 0x2e, 0x70, 0x62, 0x2e, 0x44, 0x69, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, + 0x0a, 0x64, 0x69, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x5b, 0x0a, 0x0e, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x14, 0x0a, + 0x10, 0x45, 0x5f, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x5f, 0x45, 0x52, 0x52, 0x4f, + 0x52, 0x10, 0x00, 0x12, 0x16, 0x0a, 0x12, 0x45, 0x5f, 0x52, 0x45, 0x51, 0x55, 0x45, 0x53, 0x54, + 0x5f, 0x52, 0x45, 0x4a, 0x45, 0x43, 0x54, 0x45, 0x44, 0x10, 0x64, 0x12, 0x12, 0x0a, 0x0e, 0x45, + 0x5f, 0x44, 0x49, 0x41, 0x4c, 0x5f, 0x52, 0x45, 0x46, 0x55, 0x53, 0x45, 0x44, 0x10, 0x65, 0x12, + 0x07, 0x0a, 0x02, 0x4f, 0x4b, 0x10, 0xc8, 0x01, 0x22, 0x26, 0x0a, 0x10, 0x44, 0x69, 0x61, 0x6c, + 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, + 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, + 0x22, 0x20, 0x0a, 0x08, 0x44, 0x69, 0x61, 0x6c, 0x42, 0x61, 0x63, 0x6b, 0x12, 0x14, 0x0a, 0x05, + 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x06, 0x52, 0x05, 0x6e, 0x6f, 0x6e, + 0x63, 0x65, 0x22, 0x73, 0x0a, 0x10, 0x44, 0x69, 0x61, 0x6c, 0x42, 0x61, 0x63, 0x6b, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2d, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x6e, 0x61, 0x74, + 0x76, 0x32, 0x2e, 0x70, 0x62, 0x2e, 0x44, 0x69, 0x61, 0x6c, 0x42, 0x61, 0x63, 0x6b, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x44, 0x69, 0x61, 0x6c, 0x42, 0x61, 0x63, 0x6b, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x18, 0x0a, + 0x0e, 0x44, 0x69, 0x61, 0x6c, 0x42, 0x61, 0x63, 0x6b, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, + 0x06, 0x0a, 0x02, 0x4f, 0x4b, 0x10, 0x00, 0x2a, 0x4a, 0x0a, 0x0a, 0x44, 0x69, 0x61, 0x6c, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0a, 0x0a, 0x06, 0x55, 0x4e, 0x55, 0x53, 0x45, 0x44, 0x10, + 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x45, 0x5f, 0x44, 0x49, 0x41, 0x4c, 0x5f, 0x45, 0x52, 0x52, 0x4f, + 0x52, 0x10, 0x64, 0x12, 0x15, 0x0a, 0x11, 0x45, 0x5f, 0x44, 0x49, 0x41, 0x4c, 0x5f, 0x42, 0x41, + 0x43, 0x4b, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x65, 0x12, 0x07, 0x0a, 0x02, 0x4f, 0x4b, + 0x10, 0xc8, 0x01, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_pb_autonatv2_proto_rawDescOnce sync.Once + file_pb_autonatv2_proto_rawDescData = file_pb_autonatv2_proto_rawDesc +) + +func file_pb_autonatv2_proto_rawDescGZIP() []byte { + file_pb_autonatv2_proto_rawDescOnce.Do(func() { + file_pb_autonatv2_proto_rawDescData = protoimpl.X.CompressGZIP(file_pb_autonatv2_proto_rawDescData) + }) + return file_pb_autonatv2_proto_rawDescData +} + +var file_pb_autonatv2_proto_enumTypes = make([]protoimpl.EnumInfo, 3) +var file_pb_autonatv2_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_pb_autonatv2_proto_goTypes = []interface{}{ + (DialStatus)(0), // 0: autonatv2.pb.DialStatus + (DialResponse_ResponseStatus)(0), // 1: autonatv2.pb.DialResponse.ResponseStatus + (DialBackResponse_DialBackStatus)(0), // 2: autonatv2.pb.DialBackResponse.DialBackStatus + (*Message)(nil), // 3: autonatv2.pb.Message + (*DialRequest)(nil), // 4: autonatv2.pb.DialRequest + (*DialDataRequest)(nil), // 5: autonatv2.pb.DialDataRequest + (*DialResponse)(nil), // 6: autonatv2.pb.DialResponse + (*DialDataResponse)(nil), // 7: autonatv2.pb.DialDataResponse + (*DialBack)(nil), // 8: autonatv2.pb.DialBack + (*DialBackResponse)(nil), // 9: autonatv2.pb.DialBackResponse +} +var file_pb_autonatv2_proto_depIdxs = []int32{ + 4, // 0: autonatv2.pb.Message.dialRequest:type_name -> autonatv2.pb.DialRequest + 6, // 1: autonatv2.pb.Message.dialResponse:type_name -> autonatv2.pb.DialResponse + 5, // 2: autonatv2.pb.Message.dialDataRequest:type_name -> autonatv2.pb.DialDataRequest + 7, // 3: autonatv2.pb.Message.dialDataResponse:type_name -> autonatv2.pb.DialDataResponse + 1, // 4: autonatv2.pb.DialResponse.status:type_name -> autonatv2.pb.DialResponse.ResponseStatus + 0, // 5: autonatv2.pb.DialResponse.dialStatus:type_name -> autonatv2.pb.DialStatus + 2, // 6: autonatv2.pb.DialBackResponse.status:type_name -> autonatv2.pb.DialBackResponse.DialBackStatus + 7, // [7:7] is the sub-list for method output_type + 7, // [7:7] is the sub-list for method input_type + 7, // [7:7] is the sub-list for extension type_name + 7, // [7:7] is the sub-list for extension extendee + 0, // [0:7] is the sub-list for field type_name +} + +func init() { file_pb_autonatv2_proto_init() } +func file_pb_autonatv2_proto_init() { + if File_pb_autonatv2_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_pb_autonatv2_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_autonatv2_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DialRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_pb_autonatv2_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DialDataRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_pb_autonatv2_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DialResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_pb_autonatv2_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DialDataResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_pb_autonatv2_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DialBack); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_pb_autonatv2_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DialBackResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_pb_autonatv2_proto_msgTypes[0].OneofWrappers = []interface{}{ + (*Message_DialRequest)(nil), + (*Message_DialResponse)(nil), + (*Message_DialDataRequest)(nil), + (*Message_DialDataResponse)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_pb_autonatv2_proto_rawDesc, + NumEnums: 3, + NumMessages: 7, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_pb_autonatv2_proto_goTypes, + DependencyIndexes: file_pb_autonatv2_proto_depIdxs, + EnumInfos: file_pb_autonatv2_proto_enumTypes, + MessageInfos: file_pb_autonatv2_proto_msgTypes, + }.Build() + File_pb_autonatv2_proto = out.File + file_pb_autonatv2_proto_rawDesc = nil + file_pb_autonatv2_proto_goTypes = nil + file_pb_autonatv2_proto_depIdxs = nil +} diff --git a/p2p/protocol/autonatv2/pb/autonatv2.proto b/p2p/protocol/autonatv2/pb/autonatv2.proto new file mode 100644 index 000000000..64dca1138 --- /dev/null +++ b/p2p/protocol/autonatv2/pb/autonatv2.proto @@ -0,0 +1,64 @@ +syntax = "proto3"; + +package autonatv2.pb; + +message Message { + oneof msg { + DialRequest dialRequest = 1; + DialResponse dialResponse = 2; + DialDataRequest dialDataRequest = 3; + DialDataResponse dialDataResponse = 4; + } +} + +message DialRequest { + repeated bytes addrs = 1; + fixed64 nonce = 2; +} + + +message DialDataRequest { + uint32 addrIdx = 1; + uint64 numBytes = 2; +} + + +enum DialStatus { + UNUSED = 0; + E_DIAL_ERROR = 100; + E_DIAL_BACK_ERROR = 101; + OK = 200; +} + + +message DialResponse { + enum ResponseStatus { + E_INTERNAL_ERROR = 0; + E_REQUEST_REJECTED = 100; + E_DIAL_REFUSED = 101; + OK = 200; + } + + ResponseStatus status = 1; + uint32 addrIdx = 2; + DialStatus dialStatus = 3; +} + + +message DialDataResponse { + bytes data = 1; +} + + +message DialBack { + fixed64 nonce = 1; +} + + +message DialBackResponse { + enum DialBackStatus { + OK = 0; + } + + DialBackStatus status = 1; +} \ No newline at end of file diff --git a/p2p/protocol/autonatv2/server.go b/p2p/protocol/autonatv2/server.go new file mode 100644 index 000000000..12a6fa786 --- /dev/null +++ b/p2p/protocol/autonatv2/server.go @@ -0,0 +1,449 @@ +package autonatv2 + +import ( + "context" + "fmt" + "io" + "sync" + "time" + + pool "github.com/libp2p/go-buffer-pool" + "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" + "github.com/libp2p/go-libp2p/p2p/protocol/autonatv2/pb" + "github.com/libp2p/go-msgio/pbio" + + "math/rand" + + ma "github.com/multiformats/go-multiaddr" + manet "github.com/multiformats/go-multiaddr/net" +) + +type dataRequestPolicyFunc = func(s network.Stream, dialAddr ma.Multiaddr) bool + +// server implements the AutoNATv2 server. +// It can ask client to provide dial data before attempting the requested dial. +// It rate limits requests on a global level, per peer level and on whether the request requires dial data. +type server struct { + host host.Host + dialerHost host.Host + limiter *rateLimiter + + // dialDataRequestPolicy is used to determine whether dialing the address requires receiving + // dial data. It is set to amplification attack prevention by default. + dialDataRequestPolicy dataRequestPolicyFunc + amplificatonAttackPreventionDialWait time.Duration + + // for tests + now func() time.Time + allowPrivateAddrs bool +} + +func newServer(host, dialer host.Host, s *autoNATSettings) *server { + return &server{ + dialerHost: dialer, + host: host, + dialDataRequestPolicy: s.dataRequestPolicy, + amplificatonAttackPreventionDialWait: s.amplificatonAttackPreventionDialWait, + allowPrivateAddrs: s.allowPrivateAddrs, + limiter: &rateLimiter{ + RPM: s.serverRPM, + PerPeerRPM: s.serverPerPeerRPM, + DialDataRPM: s.serverDialDataRPM, + now: s.now, + }, + now: s.now, + } +} + +// Enable attaches the stream handler to the host. +func (as *server) Start() { + as.host.SetStreamHandler(DialProtocol, as.handleDialRequest) +} + +func (as *server) Close() { + as.host.RemoveStreamHandler(DialProtocol) + as.dialerHost.Close() + as.limiter.Close() +} + +// handleDialRequest is the dial-request protocol stream handler +func (as *server) handleDialRequest(s network.Stream) { + if err := s.Scope().SetService(ServiceName); err != nil { + s.Reset() + log.Debugf("failed to attach stream to service %s: %w", ServiceName, err) + return + } + + if err := s.Scope().ReserveMemory(maxMsgSize, network.ReservationPriorityAlways); err != nil { + s.Reset() + log.Debugf("failed to reserve memory for stream %s: %w", DialProtocol, err) + return + } + defer s.Scope().ReleaseMemory(maxMsgSize) + + deadline := as.now().Add(streamTimeout) + ctx, cancel := context.WithDeadline(context.Background(), deadline) + defer cancel() + s.SetDeadline(as.now().Add(streamTimeout)) + defer s.Close() + + p := s.Conn().RemotePeer() + + var msg pb.Message + w := pbio.NewDelimitedWriter(s) + // Check for rate limit before parsing the request + if !as.limiter.Accept(p) { + msg = pb.Message{ + Msg: &pb.Message_DialResponse{ + DialResponse: &pb.DialResponse{ + Status: pb.DialResponse_E_REQUEST_REJECTED, + }, + }, + } + if err := w.WriteMsg(&msg); err != nil { + s.Reset() + log.Debugf("failed to write request rejected response to %s: %s", p, err) + return + } + log.Debugf("rejected request from %s: rate limit exceeded", p) + return + } + defer as.limiter.CompleteRequest(p) + + r := pbio.NewDelimitedReader(s, maxMsgSize) + if err := r.ReadMsg(&msg); err != nil { + s.Reset() + log.Debugf("failed to read request from %s: %s", p, err) + return + } + if msg.GetDialRequest() == nil { + s.Reset() + log.Debugf("invalid message type from %s: %T expected: DialRequest", p, msg.Msg) + return + } + + // parse peer's addresses + var dialAddr ma.Multiaddr + var addrIdx int + for i, ab := range msg.GetDialRequest().GetAddrs() { + if i >= maxPeerAddresses { + break + } + a, err := ma.NewMultiaddrBytes(ab) + if err != nil { + continue + } + if !as.allowPrivateAddrs && !manet.IsPublicAddr(a) { + continue + } + if !as.dialerHost.Network().CanDial(p, a) { + continue + } + dialAddr = a + addrIdx = i + break + } + // No dialable address + if dialAddr == nil { + msg = pb.Message{ + Msg: &pb.Message_DialResponse{ + DialResponse: &pb.DialResponse{ + Status: pb.DialResponse_E_DIAL_REFUSED, + }, + }, + } + if err := w.WriteMsg(&msg); err != nil { + s.Reset() + log.Debugf("failed to write dial refused response to %s: %s", p, err) + return + } + return + } + + nonce := msg.GetDialRequest().Nonce + + isDialDataRequired := as.dialDataRequestPolicy(s, dialAddr) + if isDialDataRequired && !as.limiter.AcceptDialDataRequest(p) { + msg = pb.Message{ + Msg: &pb.Message_DialResponse{ + DialResponse: &pb.DialResponse{ + Status: pb.DialResponse_E_REQUEST_REJECTED, + }, + }, + } + if err := w.WriteMsg(&msg); err != nil { + s.Reset() + log.Debugf("failed to write request rejected response to %s: %s", p, err) + return + } + log.Debugf("rejected request from %s: rate limit exceeded", p) + return + } + + if isDialDataRequired { + if err := getDialData(w, s, &msg, addrIdx); err != nil { + s.Reset() + log.Debugf("%s refused dial data request: %s", p, err) + return + } + // wait for a bit to prevent thundering herd style attacks on a victim + waitTime := time.Duration(rand.Intn(int(as.amplificatonAttackPreventionDialWait) + 1)) // the range is [0, n) + t := time.NewTimer(waitTime) + defer t.Stop() + select { + case <-ctx.Done(): + s.Reset() + log.Debugf("rejecting request without dialing: %s %p ", p, ctx.Err()) + return + case <-t.C: + } + } + + dialStatus := as.dialBack(ctx, s.Conn().RemotePeer(), dialAddr, nonce) + msg = pb.Message{ + Msg: &pb.Message_DialResponse{ + DialResponse: &pb.DialResponse{ + Status: pb.DialResponse_OK, + DialStatus: dialStatus, + AddrIdx: uint32(addrIdx), + }, + }, + } + if err := w.WriteMsg(&msg); err != nil { + s.Reset() + log.Debugf("failed to write response to %s: %s", p, err) + return + } +} + +// getDialData gets data from the client for dialing the address +func getDialData(w pbio.Writer, s network.Stream, msg *pb.Message, addrIdx int) error { + numBytes := minHandshakeSizeBytes + rand.Intn(maxHandshakeSizeBytes-minHandshakeSizeBytes) + *msg = pb.Message{ + Msg: &pb.Message_DialDataRequest{ + DialDataRequest: &pb.DialDataRequest{ + AddrIdx: uint32(addrIdx), + NumBytes: uint64(numBytes), + }, + }, + } + if err := w.WriteMsg(msg); err != nil { + return fmt.Errorf("dial data write: %w", err) + } + // pbio.Reader that we used so far on this stream is buffered. But at this point + // there is nothing unread on the stream. So it is safe to use the raw stream to + // read, reducing allocations. + return readDialData(numBytes, s) +} + +func readDialData(numBytes int, r io.Reader) error { + mr := &msgReader{R: r, Buf: pool.Get(maxMsgSize)} + defer pool.Put(mr.Buf) + for remain := numBytes; remain > 0; { + msg, err := mr.ReadMsg() + if err != nil { + return fmt.Errorf("dial data read: %w", err) + } + // protobuf format is: + // (oneof dialDataResponse:)(dial data:) + bytesLen := len(msg) + bytesLen -= 2 // fieldTag + varint first byte + if bytesLen > 127 { + bytesLen -= 1 // varint second byte + } + bytesLen -= 2 // second fieldTag + varint first byte + if bytesLen > 127 { + bytesLen -= 1 // varint second byte + } + if bytesLen > 0 { + remain -= bytesLen + } + // Check if the peer is not sending too little data forcing us to just do a lot of compute + if bytesLen < 100 && remain > 0 { + return fmt.Errorf("dial data msg too small: %d", bytesLen) + } + } + return nil +} + +func (as *server) dialBack(ctx context.Context, p peer.ID, addr ma.Multiaddr, nonce uint64) pb.DialStatus { + ctx, cancel := context.WithTimeout(ctx, dialBackDialTimeout) + ctx = network.WithForceDirectDial(ctx, "autonatv2") + as.dialerHost.Peerstore().AddAddr(p, addr, peerstore.TempAddrTTL) + defer func() { + cancel() + as.dialerHost.Network().ClosePeer(p) + as.dialerHost.Peerstore().ClearAddrs(p) + as.dialerHost.Peerstore().RemovePeer(p) + }() + + err := as.dialerHost.Connect(ctx, peer.AddrInfo{ID: p}) + if err != nil { + return pb.DialStatus_E_DIAL_ERROR + } + + s, err := as.dialerHost.NewStream(ctx, p, DialBackProtocol) + if err != nil { + return pb.DialStatus_E_DIAL_BACK_ERROR + } + + defer s.Close() + s.SetDeadline(as.now().Add(dialBackStreamTimeout)) + + w := pbio.NewDelimitedWriter(s) + if err := w.WriteMsg(&pb.DialBack{Nonce: nonce}); err != nil { + s.Reset() + return pb.DialStatus_E_DIAL_BACK_ERROR + } + + // Since the underlying connection is on a separate dialer, it'll be closed after this + // function returns. Connection close will drop all the queued writes. To ensure message + // delivery, do a CloseWrite and read a byte from the stream. The peer actually sends a + // response of type DialBackResponse but we only care about the fact that the DialBack + // message has reached the peer. So we ignore that message on the read side. + s.CloseWrite() + s.SetDeadline(as.now().Add(5 * time.Second)) // 5 is a magic number + b := make([]byte, 1) // Read 1 byte here because 0 len reads are free to return (0, nil) immediately + s.Read(b) + + return pb.DialStatus_OK +} + +// rateLimiter implements a sliding window rate limit of requests per minute. It allows 1 concurrent request +// per peer. It rate limits requests globally, at a peer level and depending on whether it requires dial data. +type rateLimiter struct { + // PerPeerRPM is the rate limit per peer + PerPeerRPM int + // RPM is the global rate limit + RPM int + // DialDataRPM is the rate limit for requests that require dial data + DialDataRPM int + + mu sync.Mutex + closed bool + reqs []entry + peerReqs map[peer.ID][]time.Time + dialDataReqs []time.Time + // ongoingReqs tracks in progress requests. This is used to disallow multiple concurrent requests by the + // same peer + // TODO: Should we allow a few concurrent requests per peer? + ongoingReqs map[peer.ID]struct{} + + now func() time.Time // for tests +} + +type entry struct { + PeerID peer.ID + Time time.Time +} + +func (r *rateLimiter) Accept(p peer.ID) bool { + r.mu.Lock() + defer r.mu.Unlock() + if r.closed { + return false + } + if r.peerReqs == nil { + r.peerReqs = make(map[peer.ID][]time.Time) + r.ongoingReqs = make(map[peer.ID]struct{}) + } + + nw := r.now() + r.cleanup(nw) + + if _, ok := r.ongoingReqs[p]; ok { + return false + } + if len(r.reqs) >= r.RPM || len(r.peerReqs[p]) >= r.PerPeerRPM { + return false + } + + r.ongoingReqs[p] = struct{}{} + r.reqs = append(r.reqs, entry{PeerID: p, Time: nw}) + r.peerReqs[p] = append(r.peerReqs[p], nw) + return true +} + +func (r *rateLimiter) AcceptDialDataRequest(p peer.ID) bool { + r.mu.Lock() + defer r.mu.Unlock() + if r.closed { + return false + } + if r.peerReqs == nil { + r.peerReqs = make(map[peer.ID][]time.Time) + r.ongoingReqs = make(map[peer.ID]struct{}) + } + nw := r.now() + r.cleanup(nw) + if len(r.dialDataReqs) >= r.DialDataRPM { + return false + } + r.dialDataReqs = append(r.dialDataReqs, nw) + return true +} + +// cleanup removes stale requests. +// +// This is fast enough in rate limited cases and the state is small enough to +// clean up quickly when blocking requests. +func (r *rateLimiter) cleanup(now time.Time) { + idx := len(r.reqs) + for i, e := range r.reqs { + if now.Sub(e.Time) >= time.Minute { + pi := len(r.peerReqs[e.PeerID]) + for j, t := range r.peerReqs[e.PeerID] { + if now.Sub(t) < time.Minute { + pi = j + break + } + } + r.peerReqs[e.PeerID] = r.peerReqs[e.PeerID][pi:] + if len(r.peerReqs[e.PeerID]) == 0 { + delete(r.peerReqs, e.PeerID) + } + } else { + idx = i + break + } + } + r.reqs = r.reqs[idx:] + + idx = len(r.dialDataReqs) + for i, t := range r.dialDataReqs { + if now.Sub(t) < time.Minute { + idx = i + break + } + } + r.dialDataReqs = r.dialDataReqs[idx:] +} + +func (r *rateLimiter) CompleteRequest(p peer.ID) { + r.mu.Lock() + defer r.mu.Unlock() + delete(r.ongoingReqs, p) +} + +func (r *rateLimiter) Close() { + r.mu.Lock() + defer r.mu.Unlock() + r.closed = true + r.peerReqs = nil + r.ongoingReqs = nil + r.dialDataReqs = nil +} + +// amplificationAttackPrevention is a dialDataRequestPolicy which requests data when the peer's observed +// IP address is different from the dial back IP address +func amplificationAttackPrevention(s network.Stream, dialAddr ma.Multiaddr) bool { + connIP, err := manet.ToIP(s.Conn().RemoteMultiaddr()) + if err != nil { + return true + } + dialIP, _ := manet.ToIP(s.Conn().LocalMultiaddr()) // must be an IP multiaddr + return !connIP.Equal(dialIP) +} diff --git a/p2p/protocol/autonatv2/server_test.go b/p2p/protocol/autonatv2/server_test.go new file mode 100644 index 000000000..0b40f2753 --- /dev/null +++ b/p2p/protocol/autonatv2/server_test.go @@ -0,0 +1,484 @@ +package autonatv2 + +import ( + "bytes" + "context" + "fmt" + "io" + "math" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/core/test" + bhost "github.com/libp2p/go-libp2p/p2p/host/blank" + "github.com/libp2p/go-libp2p/p2p/net/swarm" + swarmt "github.com/libp2p/go-libp2p/p2p/net/swarm/testing" + "github.com/libp2p/go-libp2p/p2p/protocol/autonatv2/pb" + "github.com/libp2p/go-msgio/pbio" + ma "github.com/multiformats/go-multiaddr" + "github.com/multiformats/go-varint" + "github.com/stretchr/testify/require" +) + +func newTestRequests(addrs []ma.Multiaddr, sendDialData bool) (reqs []Request) { + reqs = make([]Request, len(addrs)) + for i := 0; i < len(addrs); i++ { + reqs[i] = Request{Addr: addrs[i], SendDialData: sendDialData} + } + return +} + +func TestServerInvalidAddrsRejected(t *testing.T) { + c := newAutoNAT(t, nil, allowPrivateAddrs, withAmplificationAttackPreventionDialWait(0)) + defer c.Close() + defer c.host.Close() + + t.Run("no transport", func(t *testing.T) { + dialer := bhost.NewBlankHost(swarmt.GenSwarm(t, swarmt.OptDisableQUIC, swarmt.OptDisableTCP)) + an := newAutoNAT(t, dialer, allowPrivateAddrs) + defer an.Close() + defer an.host.Close() + + idAndWait(t, c, an) + + res, err := c.GetReachability(context.Background(), newTestRequests(c.host.Addrs(), true)) + require.ErrorIs(t, err, ErrDialRefused) + require.Equal(t, Result{}, res) + }) + + t.Run("black holed addr", func(t *testing.T) { + dialer := bhost.NewBlankHost(swarmt.GenSwarm( + t, swarmt.WithSwarmOpts(swarm.WithReadOnlyBlackHoleDetector()))) + an := newAutoNAT(t, dialer) + defer an.Close() + defer an.host.Close() + + idAndWait(t, c, an) + + res, err := c.GetReachability(context.Background(), + []Request{{ + Addr: ma.StringCast("/ip4/1.2.3.4/udp/1234/quic-v1"), + SendDialData: true, + }}) + require.ErrorIs(t, err, ErrDialRefused) + require.Equal(t, Result{}, res) + }) + + t.Run("private addrs", func(t *testing.T) { + an := newAutoNAT(t, nil) + defer an.Close() + defer an.host.Close() + + idAndWait(t, c, an) + + res, err := c.GetReachability(context.Background(), newTestRequests(c.host.Addrs(), true)) + require.ErrorIs(t, err, ErrDialRefused) + require.Equal(t, Result{}, res) + }) + + t.Run("relay addrs", func(t *testing.T) { + an := newAutoNAT(t, nil) + defer an.Close() + defer an.host.Close() + + idAndWait(t, c, an) + + res, err := c.GetReachability(context.Background(), newTestRequests( + []ma.Multiaddr{ma.StringCast(fmt.Sprintf("/ip4/1.2.3.4/tcp/1/p2p/%s/p2p-circuit/p2p/%s", c.host.ID(), c.srv.dialerHost.ID()))}, true)) + require.ErrorIs(t, err, ErrDialRefused) + require.Equal(t, Result{}, res) + }) + + t.Run("no addr", func(t *testing.T) { + _, err := c.GetReachability(context.Background(), nil) + require.Error(t, err) + }) + + t.Run("too many address", func(t *testing.T) { + dialer := bhost.NewBlankHost(swarmt.GenSwarm(t, swarmt.OptDisableTCP)) + an := newAutoNAT(t, dialer, allowPrivateAddrs) + defer an.Close() + defer an.host.Close() + + var addrs []ma.Multiaddr + for i := 0; i < 100; i++ { + addrs = append(addrs, ma.StringCast(fmt.Sprintf("/ip4/127.0.0.1/tcp/%d", 2000+i))) + } + addrs = append(addrs, c.host.Addrs()...) + // The dial should still fail because we have too many addresses that the server cannot dial + idAndWait(t, c, an) + + res, err := c.GetReachability(context.Background(), newTestRequests(addrs, true)) + require.ErrorIs(t, err, ErrDialRefused) + require.Equal(t, Result{}, res) + }) + + t.Run("msg too large", func(t *testing.T) { + dialer := bhost.NewBlankHost(swarmt.GenSwarm(t, swarmt.OptDisableTCP)) + an := newAutoNAT(t, dialer, allowPrivateAddrs) + defer an.Close() + defer an.host.Close() + + var addrs []ma.Multiaddr + for i := 0; i < 10000; i++ { + addrs = append(addrs, ma.StringCast(fmt.Sprintf("/ip4/127.0.0.1/tcp/%d", 2000+i))) + } + addrs = append(addrs, c.host.Addrs()...) + // The dial should still fail because we have too many addresses that the server cannot dial + idAndWait(t, c, an) + + res, err := c.GetReachability(context.Background(), newTestRequests(addrs, true)) + require.ErrorIs(t, err, network.ErrReset) + require.Equal(t, Result{}, res) + }) + +} + +func TestServerDataRequest(t *testing.T) { + // server will skip all tcp addresses + dialer := bhost.NewBlankHost(swarmt.GenSwarm(t, swarmt.OptDisableTCP)) + // ask for dial data for quic address + an := newAutoNAT(t, dialer, allowPrivateAddrs, withDataRequestPolicy( + func(s network.Stream, dialAddr ma.Multiaddr) bool { + if _, err := dialAddr.ValueForProtocol(ma.P_QUIC_V1); err == nil { + return true + } + return false + }), + WithServerRateLimit(10, 10, 10), + withAmplificationAttackPreventionDialWait(0), + ) + defer an.Close() + defer an.host.Close() + + c := newAutoNAT(t, nil, allowPrivateAddrs) + defer c.Close() + defer c.host.Close() + + idAndWait(t, c, an) + + var quicAddr, tcpAddr ma.Multiaddr + for _, a := range c.host.Addrs() { + if _, err := a.ValueForProtocol(ma.P_QUIC_V1); err == nil { + quicAddr = a + } else if _, err := a.ValueForProtocol(ma.P_TCP); err == nil { + tcpAddr = a + } + } + + _, err := c.GetReachability(context.Background(), []Request{{Addr: tcpAddr, SendDialData: true}, {Addr: quicAddr}}) + require.Error(t, err) + + res, err := c.GetReachability(context.Background(), []Request{{Addr: quicAddr, SendDialData: true}, {Addr: tcpAddr}}) + require.NoError(t, err) + + require.Equal(t, Result{ + Addr: quicAddr, + Reachability: network.ReachabilityPublic, + Status: pb.DialStatus_OK, + }, res) + + // Small messages should be rejected for dial data + c.cli.dialData = c.cli.dialData[:10] + _, err = c.GetReachability(context.Background(), []Request{{Addr: quicAddr, SendDialData: true}, {Addr: tcpAddr}}) + require.Error(t, err) +} +func TestServerDataRequestJitter(t *testing.T) { + // server will skip all tcp addresses + dialer := bhost.NewBlankHost(swarmt.GenSwarm(t, swarmt.OptDisableTCP)) + // ask for dial data for quic address + an := newAutoNAT(t, dialer, allowPrivateAddrs, withDataRequestPolicy( + func(s network.Stream, dialAddr ma.Multiaddr) bool { + if _, err := dialAddr.ValueForProtocol(ma.P_QUIC_V1); err == nil { + return true + } + return false + }), + WithServerRateLimit(10, 10, 10), + withAmplificationAttackPreventionDialWait(5*time.Second), + ) + defer an.Close() + defer an.host.Close() + + c := newAutoNAT(t, nil, allowPrivateAddrs) + defer c.Close() + defer c.host.Close() + + idAndWait(t, c, an) + + var quicAddr, tcpAddr ma.Multiaddr + for _, a := range c.host.Addrs() { + if _, err := a.ValueForProtocol(ma.P_QUIC_V1); err == nil { + quicAddr = a + } else if _, err := a.ValueForProtocol(ma.P_TCP); err == nil { + tcpAddr = a + } + } + + for i := 0; i < 10; i++ { + st := time.Now() + res, err := c.GetReachability(context.Background(), []Request{{Addr: quicAddr, SendDialData: true}, {Addr: tcpAddr}}) + took := time.Since(st) + require.NoError(t, err) + + require.Equal(t, Result{ + Addr: quicAddr, + Reachability: network.ReachabilityPublic, + Status: pb.DialStatus_OK, + }, res) + if took > 500*time.Millisecond { + return + } + } + t.Fatalf("expected server to delay at least 1 dial") +} + +func TestServerDial(t *testing.T) { + an := newAutoNAT(t, nil, WithServerRateLimit(10, 10, 10), allowPrivateAddrs) + defer an.Close() + defer an.host.Close() + + c := newAutoNAT(t, nil, allowPrivateAddrs) + defer c.Close() + defer c.host.Close() + + idAndWait(t, c, an) + + unreachableAddr := ma.StringCast("/ip4/1.2.3.4/tcp/2") + hostAddrs := c.host.Addrs() + + t.Run("unreachable addr", func(t *testing.T) { + res, err := c.GetReachability(context.Background(), + append([]Request{{Addr: unreachableAddr, SendDialData: true}}, newTestRequests(hostAddrs, false)...)) + require.NoError(t, err) + require.Equal(t, Result{ + Addr: unreachableAddr, + Reachability: network.ReachabilityPrivate, + Status: pb.DialStatus_E_DIAL_ERROR, + }, res) + }) + + t.Run("reachable addr", func(t *testing.T) { + res, err := c.GetReachability(context.Background(), newTestRequests(c.host.Addrs(), false)) + require.NoError(t, err) + require.Equal(t, Result{ + Addr: hostAddrs[0], + Reachability: network.ReachabilityPublic, + Status: pb.DialStatus_OK, + }, res) + for _, addr := range c.host.Addrs() { + res, err := c.GetReachability(context.Background(), newTestRequests([]ma.Multiaddr{addr}, false)) + require.NoError(t, err) + require.Equal(t, Result{ + Addr: addr, + Reachability: network.ReachabilityPublic, + Status: pb.DialStatus_OK, + }, res) + } + }) + + t.Run("dialback error", func(t *testing.T) { + c.host.RemoveStreamHandler(DialBackProtocol) + res, err := c.GetReachability(context.Background(), newTestRequests(c.host.Addrs(), false)) + require.NoError(t, err) + require.Equal(t, Result{ + Addr: hostAddrs[0], + Reachability: network.ReachabilityUnknown, + Status: pb.DialStatus_E_DIAL_BACK_ERROR, + }, res) + }) +} + +func TestRateLimiter(t *testing.T) { + cl := test.NewMockClock() + r := rateLimiter{RPM: 3, PerPeerRPM: 2, DialDataRPM: 1, now: cl.Now} + + require.True(t, r.Accept("peer1")) + + cl.AdvanceBy(10 * time.Second) + require.False(t, r.Accept("peer1")) // first request is still active + r.CompleteRequest("peer1") + + require.True(t, r.Accept("peer1")) + r.CompleteRequest("peer1") + + cl.AdvanceBy(10 * time.Second) + require.False(t, r.Accept("peer1")) + + cl.AdvanceBy(10 * time.Second) + require.True(t, r.Accept("peer2")) + r.CompleteRequest("peer2") + + cl.AdvanceBy(10 * time.Second) + require.False(t, r.Accept("peer3")) + + cl.AdvanceBy(21 * time.Second) // first request expired + require.True(t, r.Accept("peer1")) + r.CompleteRequest("peer1") + + cl.AdvanceBy(10 * time.Second) + require.True(t, r.Accept("peer3")) + r.CompleteRequest("peer3") + + cl.AdvanceBy(50 * time.Second) + require.True(t, r.Accept("peer3")) + r.CompleteRequest("peer3") + + cl.AdvanceBy(1 * time.Second) + require.False(t, r.Accept("peer3")) + + cl.AdvanceBy(10 * time.Second) + require.True(t, r.Accept("peer3")) +} + +func TestRateLimiterStress(t *testing.T) { + cl := test.NewMockClock() + for i := 0; i < 10; i++ { + r := rateLimiter{RPM: 20 + i, PerPeerRPM: 10 + i, DialDataRPM: i, now: cl.Now} + + peers := make([]peer.ID, 10+i) + for i := 0; i < len(peers); i++ { + peers[i] = peer.ID(fmt.Sprintf("peer-%d", i)) + } + peerSuccesses := make([]atomic.Int64, len(peers)) + var success, dialDataSuccesses atomic.Int64 + var wg sync.WaitGroup + for k := 0; k < 5; k++ { + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < 2*60; i++ { + for j, p := range peers { + if r.Accept(p) { + success.Add(1) + peerSuccesses[j].Add(1) + } + if r.AcceptDialDataRequest(p) { + dialDataSuccesses.Add(1) + } + r.CompleteRequest(p) + } + cl.AdvanceBy(time.Second) + } + }() + } + wg.Wait() + if int(success.Load()) > 10*r.RPM || int(success.Load()) < 9*r.RPM { + t.Fatalf("invalid successes, %d, expected %d-%d", success.Load(), 9*r.RPM, 10*r.RPM) + } + if int(dialDataSuccesses.Load()) > 10*r.DialDataRPM || int(dialDataSuccesses.Load()) < 9*r.DialDataRPM { + t.Fatalf("invalid dial data successes, %d expected %d-%d", dialDataSuccesses.Load(), 9*r.DialDataRPM, 10*r.DialDataRPM) + } + for i := range peerSuccesses { + // We cannot check the lower bound because some peers would be hitting the global rpm limit + if int(peerSuccesses[i].Load()) > 10*r.PerPeerRPM { + t.Fatalf("too many per peer successes, PerPeerRPM=%d", r.PerPeerRPM) + } + } + cl.AdvanceBy(1 * time.Minute) + require.True(t, r.Accept(peers[0])) + // Assert lengths to check that we are cleaning up correctly + require.Equal(t, len(r.reqs), 1) + require.Equal(t, len(r.peerReqs), 1) + require.Equal(t, len(r.peerReqs[peers[0]]), 1) + require.Equal(t, len(r.dialDataReqs), 0) + require.Equal(t, len(r.ongoingReqs), 1) + } +} + +func TestReadDialData(t *testing.T) { + for N := 30_000; N < 30_010; N++ { + for msgSize := 100; msgSize < 256; msgSize++ { + r, w := io.Pipe() + msg := &pb.Message{} + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + mw := pbio.NewDelimitedWriter(w) + err := sendDialData(make([]byte, msgSize), N, mw, msg) + if err != nil { + t.Error(err) + } + mw.Close() + }() + err := readDialData(N, r) + require.NoError(t, err) + wg.Wait() + } + + for msgSize := 1000; msgSize < 1256; msgSize++ { + r, w := io.Pipe() + msg := &pb.Message{} + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + mw := pbio.NewDelimitedWriter(w) + err := sendDialData(make([]byte, msgSize), N, mw, msg) + if err != nil { + t.Error(err) + } + mw.Close() + }() + err := readDialData(N, r) + require.NoError(t, err) + wg.Wait() + } + } +} + +func FuzzServerDialRequest(f *testing.F) { + a := newAutoNAT(f, nil, allowPrivateAddrs, WithServerRateLimit(math.MaxInt32, math.MaxInt32, math.MaxInt32)) + c := newAutoNAT(f, nil) + idAndWait(f, c, a) + // reduce the streamTimeout before running this. TODO: fix this + f.Fuzz(func(t *testing.T, data []byte) { + s, err := c.host.NewStream(context.Background(), a.host.ID(), DialProtocol) + if err != nil { + t.Fatal(err) + } + s.SetDeadline(time.Now().Add(10 * time.Second)) + s.Write(data) + buf := make([]byte, 64) + s.Read(buf) // We only care that server didn't panic + s, err = c.host.NewStream(context.Background(), a.host.ID(), DialProtocol) + if err != nil { + t.Fatal(err) + } + + n := varint.PutUvarint(buf, uint64(len(data))) + s.SetDeadline(time.Now().Add(10 * time.Second)) + s.Write(buf[:n]) + s.Write(data) + s.Read(buf) // We only care that server didn't panic + s.Reset() + }) +} + +func FuzzReadDialData(f *testing.F) { + f.Fuzz(func(t *testing.T, numBytes int, data []byte) { + readDialData(numBytes, bytes.NewReader(data)) + }) +} + +func BenchmarkDialData(b *testing.B) { + b.ReportAllocs() + const N = 100_000 + streamBuffer := make([]byte, 2*N) + buf := bytes.NewBuffer(streamBuffer[:0]) + dialData := make([]byte, 4000) + msg := &pb.Message{} + w := pbio.NewDelimitedWriter(buf) + err := sendDialData(dialData, N, w, msg) + require.NoError(b, err) + dialDataBuf := buf.Bytes() + for i := 0; i < b.N; i++ { + err = readDialData(N, bytes.NewReader(dialDataBuf)) + require.NoError(b, err) + } +}