Merge branch 'master' into uci-golangci-lint

This commit is contained in:
Piotr Galar
2025-06-11 10:42:45 +01:00
committed by GitHub
43 changed files with 3377 additions and 531 deletions

View File

@@ -12,6 +12,9 @@ linters:
- revive - revive
- unused - unused
- prealloc - prealloc
disable:
- errcheck
- staticcheck
disable: disable:
- errcheck - errcheck

View File

@@ -33,6 +33,7 @@ import (
routed "github.com/libp2p/go-libp2p/p2p/host/routed" routed "github.com/libp2p/go-libp2p/p2p/host/routed"
"github.com/libp2p/go-libp2p/p2p/net/swarm" "github.com/libp2p/go-libp2p/p2p/net/swarm"
tptu "github.com/libp2p/go-libp2p/p2p/net/upgrader" tptu "github.com/libp2p/go-libp2p/p2p/net/upgrader"
"github.com/libp2p/go-libp2p/p2p/protocol/autonatv2"
circuitv2 "github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/client" circuitv2 "github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/client"
relayv2 "github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/relay" 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/holepunch"
@@ -379,8 +380,28 @@ func (cfg *Config) addTransports() ([]fx.Option, error) {
fxopts = append(fxopts, cfg.QUICReuse...) fxopts = append(fxopts, cfg.QUICReuse...)
} else { } else {
fxopts = append(fxopts, fxopts = append(fxopts,
fx.Provide(func(key quic.StatelessResetKey, tokenGenerator quic.TokenGeneratorKey, lifecycle fx.Lifecycle) (*quicreuse.ConnManager, error) { fx.Provide(func(key quic.StatelessResetKey, tokenGenerator quic.TokenGeneratorKey, rcmgr network.ResourceManager, lifecycle fx.Lifecycle) (*quicreuse.ConnManager, error) {
var opts []quicreuse.Option opts := []quicreuse.Option{
quicreuse.ConnContext(func(ctx context.Context, clientInfo *quic.ClientInfo) (context.Context, error) {
// even if creating the quic maddr fails, let the rcmgr decide what to do with the connection
addr, err := quicreuse.ToQuicMultiaddr(clientInfo.RemoteAddr, quic.Version1)
if err != nil {
addr = nil
}
scope, err := rcmgr.OpenConnection(network.DirInbound, false, addr)
if err != nil {
return ctx, err
}
ctx = network.WithConnManagementScope(ctx, scope)
context.AfterFunc(ctx, func() {
scope.Done()
})
return ctx, nil
}),
quicreuse.VerifySourceAddress(func(addr net.Addr) bool {
return rcmgr.VerifySourceAddress(addr)
}),
}
if !cfg.DisableMetrics { if !cfg.DisableMetrics {
opts = append(opts, quicreuse.EnableMetrics(cfg.PrometheusRegisterer)) opts = append(opts, quicreuse.EnableMetrics(cfg.PrometheusRegisterer))
} }
@@ -413,15 +434,7 @@ func (cfg *Config) addTransports() ([]fx.Option, error) {
return fxopts, nil return fxopts, nil
} }
func (cfg *Config) newBasicHost(swrm *swarm.Swarm, eventBus event.Bus) (*bhost.BasicHost, error) { func (cfg *Config) newBasicHost(swrm *swarm.Swarm, eventBus event.Bus, an *autonatv2.AutoNAT) (*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{ h, err := bhost.NewHost(swrm, &bhost.HostOpts{
EventBus: eventBus, EventBus: eventBus,
ConnManager: cfg.ConnManager, ConnManager: cfg.ConnManager,
@@ -437,8 +450,7 @@ func (cfg *Config) newBasicHost(swrm *swarm.Swarm, eventBus event.Bus) (*bhost.B
EnableMetrics: !cfg.DisableMetrics, EnableMetrics: !cfg.DisableMetrics,
PrometheusRegisterer: cfg.PrometheusRegisterer, PrometheusRegisterer: cfg.PrometheusRegisterer,
DisableIdentifyAddressDiscovery: cfg.DisableIdentifyAddressDiscovery, DisableIdentifyAddressDiscovery: cfg.DisableIdentifyAddressDiscovery,
EnableAutoNATv2: cfg.EnableAutoNATv2, AutoNATv2: an,
AutoNATv2Dialer: autonatv2Dialer,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@@ -517,6 +529,24 @@ func (cfg *Config) NewNode() (host.Host, error) {
}) })
return sw, nil return sw, nil
}), }),
fx.Provide(func() (*autonatv2.AutoNAT, error) {
if !cfg.EnableAutoNATv2 {
return nil, nil
}
ah, err := cfg.makeAutoNATV2Host()
if err != nil {
return nil, err
}
var mt autonatv2.MetricsTracer
if !cfg.DisableMetrics {
mt = autonatv2.NewMetricsTracer(cfg.PrometheusRegisterer)
}
autoNATv2, err := autonatv2.New(ah, autonatv2.WithMetricsTracer(mt))
if err != nil {
return nil, fmt.Errorf("failed to create autonatv2: %w", err)
}
return autoNATv2, nil
}),
fx.Provide(cfg.newBasicHost), fx.Provide(cfg.newBasicHost),
fx.Provide(func(bh *bhost.BasicHost) identify.IDService { fx.Provide(func(bh *bhost.BasicHost) identify.IDService {
return bh.IDService() return bh.IDService()

View File

@@ -2,6 +2,7 @@ package event
import ( import (
"github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/network"
ma "github.com/multiformats/go-multiaddr"
) )
// EvtLocalReachabilityChanged is an event struct to be emitted when the local's // EvtLocalReachabilityChanged is an event struct to be emitted when the local's
@@ -11,3 +12,13 @@ import (
type EvtLocalReachabilityChanged struct { type EvtLocalReachabilityChanged struct {
Reachability network.Reachability Reachability network.Reachability
} }
// EvtHostReachableAddrsChanged is sent when host's reachable or unreachable addresses change
// Reachable, Unreachable, and Unknown only contain Public IP or DNS addresses
//
// Experimental: This API is unstable. Any changes to this event will be done without a deprecation notice.
type EvtHostReachableAddrsChanged struct {
Reachable []ma.Multiaddr
Unreachable []ma.Multiaddr
Unknown []ma.Multiaddr
}

View File

@@ -10,6 +10,7 @@
package mocknetwork package mocknetwork
import ( import (
net "net"
reflect "reflect" reflect "reflect"
network "github.com/libp2p/go-libp2p/core/network" network "github.com/libp2p/go-libp2p/core/network"
@@ -87,6 +88,20 @@ func (mr *MockResourceManagerMockRecorder) OpenStream(p, dir any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenStream", reflect.TypeOf((*MockResourceManager)(nil).OpenStream), p, dir) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenStream", reflect.TypeOf((*MockResourceManager)(nil).OpenStream), p, dir)
} }
// VerifySourceAddress mocks base method.
func (m *MockResourceManager) VerifySourceAddress(addr net.Addr) bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VerifySourceAddress", addr)
ret0, _ := ret[0].(bool)
return ret0
}
// VerifySourceAddress indicates an expected call of VerifySourceAddress.
func (mr *MockResourceManagerMockRecorder) VerifySourceAddress(addr any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifySourceAddress", reflect.TypeOf((*MockResourceManager)(nil).VerifySourceAddress), addr)
}
// ViewPeer mocks base method. // ViewPeer mocks base method.
func (m *MockResourceManager) ViewPeer(arg0 peer.ID, arg1 func(network.PeerScope) error) error { func (m *MockResourceManager) ViewPeer(arg0 peer.ID, arg1 func(network.PeerScope) error) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()

View File

@@ -1,6 +1,10 @@
package network package network
import ( import (
"context"
"errors"
"net"
"github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/core/protocol" "github.com/libp2p/go-libp2p/core/protocol"
"github.com/multiformats/go-multiaddr" "github.com/multiformats/go-multiaddr"
@@ -87,6 +91,10 @@ type ResourceManager interface {
// the end of the scope's span. // the end of the scope's span.
OpenConnection(dir Direction, usefd bool, endpoint multiaddr.Multiaddr) (ConnManagementScope, error) OpenConnection(dir Direction, usefd bool, endpoint multiaddr.Multiaddr) (ConnManagementScope, error)
// VerifySourceAddress tells the transport to verify the source address for an incoming connection
// before gating the connection with OpenConnection.
VerifySourceAddress(addr net.Addr) bool
// OpenStream creates a new stream scope, initially unnegotiated. // OpenStream creates a new stream scope, initially unnegotiated.
// An unnegotiated stream will be initially unattached to any protocol scope // An unnegotiated stream will be initially unattached to any protocol scope
// and constrained by the transient scope. // and constrained by the transient scope.
@@ -269,9 +277,30 @@ type ScopeStat struct {
Memory int64 Memory int64
} }
// connManagementScopeKey is the key to store Scope in contexts
type connManagementScopeKey struct{}
func WithConnManagementScope(ctx context.Context, scope ConnManagementScope) context.Context {
return context.WithValue(ctx, connManagementScopeKey{}, scope)
}
func UnwrapConnManagementScope(ctx context.Context) (ConnManagementScope, error) {
v := ctx.Value(connManagementScopeKey{})
if v == nil {
return nil, errors.New("context has no ConnManagementScope")
}
scope, ok := v.(ConnManagementScope)
if !ok {
return nil, errors.New("context has no ConnManagementScope")
}
return scope, nil
}
// NullResourceManager is a stub for tests and initialization of default values // NullResourceManager is a stub for tests and initialization of default values
type NullResourceManager struct{} type NullResourceManager struct{}
var _ ResourceManager = (*NullResourceManager)(nil)
var _ ResourceScope = (*NullScope)(nil) var _ ResourceScope = (*NullScope)(nil)
var _ ResourceScopeSpan = (*NullScope)(nil) var _ ResourceScopeSpan = (*NullScope)(nil)
var _ ServiceScope = (*NullScope)(nil) var _ ServiceScope = (*NullScope)(nil)
@@ -306,6 +335,10 @@ func (n *NullResourceManager) OpenConnection(_ Direction, _ bool, _ multiaddr.Mu
func (n *NullResourceManager) OpenStream(_ peer.ID, _ Direction) (StreamManagementScope, error) { func (n *NullResourceManager) OpenStream(_ peer.ID, _ Direction) (StreamManagementScope, error) {
return &NullScope{}, nil return &NullScope{}, nil
} }
func (*NullResourceManager) VerifySourceAddress(_ net.Addr) bool {
return false
}
func (n *NullResourceManager) Close() error { func (n *NullResourceManager) Close() error {
return nil return nil
} }
@@ -324,3 +357,4 @@ func (n *NullScope) ProtocolScope() ProtocolScope { return &NullScop
func (n *NullScope) SetProtocol(_ protocol.ID) error { return nil } func (n *NullScope) SetProtocol(_ protocol.ID) error { return nil }
func (n *NullScope) ServiceScope() ServiceScope { return &NullScope{} } func (n *NullScope) ServiceScope() ServiceScope { return &NullScope{} }
func (n *NullScope) SetService(_ string) error { return nil } func (n *NullScope) SetService(_ string) error { return nil }
func (n *NullScope) VerifySourceAddress(_ net.Addr) bool { return false }

23
go.mod
View File

@@ -53,18 +53,18 @@ require (
github.com/pion/webrtc/v4 v4.0.14 github.com/pion/webrtc/v4 v4.0.14
github.com/prometheus/client_golang v1.21.0 github.com/prometheus/client_golang v1.21.0
github.com/prometheus/client_model v0.6.1 github.com/prometheus/client_model v0.6.1
github.com/quic-go/quic-go v0.50.0 github.com/quic-go/quic-go v0.52.0
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
go.uber.org/fx v1.23.0 go.uber.org/fx v1.23.0
go.uber.org/goleak v1.3.0 go.uber.org/goleak v1.3.0
go.uber.org/mock v0.5.0 go.uber.org/mock v0.5.2
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.0
golang.org/x/crypto v0.35.0 golang.org/x/crypto v0.37.0
golang.org/x/sync v0.11.0 golang.org/x/sync v0.14.0
golang.org/x/sys v0.30.0 golang.org/x/sys v0.33.0
golang.org/x/time v0.11.0 golang.org/x/time v0.11.0
golang.org/x/tools v0.30.0 golang.org/x/tools v0.32.0
google.golang.org/protobuf v1.36.5 google.golang.org/protobuf v1.36.5
) )
@@ -74,7 +74,7 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/francoispqt/gojay v1.2.13 // indirect github.com/francoispqt/gojay v1.2.13 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/google/pprof v0.0.0-20250208200701-d0013a598941 // indirect github.com/google/pprof v0.0.0-20250501235452-c0086092b71a // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
@@ -83,7 +83,7 @@ require (
github.com/minio/sha256-simd v1.0.1 // indirect github.com/minio/sha256-simd v1.0.1 // indirect
github.com/multiformats/go-base36 v0.2.0 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/onsi/ginkgo/v2 v2.22.2 // indirect github.com/onsi/ginkgo/v2 v2.23.4 // indirect
github.com/pion/dtls/v2 v2.2.12 // indirect github.com/pion/dtls/v2 v2.2.12 // indirect
github.com/pion/dtls/v3 v3.0.4 // indirect github.com/pion/dtls/v3 v3.0.4 // indirect
github.com/pion/interceptor v0.1.37 // indirect github.com/pion/interceptor v0.1.37 // indirect
@@ -103,12 +103,13 @@ require (
github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/qpack v0.5.1 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/wlynxg/anet v0.0.5 // indirect github.com/wlynxg/anet v0.0.5 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/dig v1.18.0 // indirect go.uber.org/dig v1.18.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect
golang.org/x/mod v0.23.0 // indirect golang.org/x/mod v0.24.0 // indirect
golang.org/x/net v0.35.0 // indirect golang.org/x/net v0.39.0 // indirect
golang.org/x/text v0.22.0 // indirect golang.org/x/text v0.24.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.4.0 // indirect lukechampine.com/blake3 v1.4.0 // indirect
) )

56
go.sum
View File

@@ -53,16 +53,16 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20250208200701-d0013a598941 h1:43XjGa6toxLpeksjcxs1jIoIyr+vUfOqY2c6HB4bpoc= github.com/google/pprof v0.0.0-20250501235452-c0086092b71a h1:rDA3FfmxwXR+BVKKdz55WwMJ1pD2hJQNW31d+l3mPk4=
github.com/google/pprof v0.0.0-20250208200701-d0013a598941/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/pprof v0.0.0-20250501235452-c0086092b71a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
@@ -180,10 +180,10 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
@@ -232,6 +232,8 @@ github.com/pion/webrtc/v4 v4.0.14/go.mod h1:R3+qTnQTS03UzwDarYecgioNf7DYgTsldxnC
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA= github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA=
github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
@@ -246,8 +248,8 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.50.0 h1:3H/ld1pa3CYhkcc20TPIyG1bNsdhn9qZBGN3b9/UyUo= github.com/quic-go/quic-go v0.52.0 h1:/SlHrCRElyaU6MaEPKqKr9z83sBg2v4FLLvWM+Z47pA=
github.com/quic-go/quic-go v0.50.0/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E= github.com/quic-go/quic-go v0.52.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ=
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 h1:4WFk6u3sOT6pLa1kQ50ZVdm8BQFgJNA117cepZxtLIg= github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 h1:4WFk6u3sOT6pLa1kQ50ZVdm8BQFgJNA117cepZxtLIg=
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66/go.mod h1:Vp72IJajgeOL6ddqrAhmp7IM9zbTcgkQxD/YdxrVwMw= github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66/go.mod h1:Vp72IJajgeOL6ddqrAhmp7IM9zbTcgkQxD/YdxrVwMw=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
@@ -302,6 +304,8 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw= go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw=
go.uber.org/dig v1.18.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= go.uber.org/dig v1.18.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
go.uber.org/fx v1.23.0 h1:lIr/gYWQGfTwGcSXWXu4vP5Ws6iqnNEIY+F/aFzCKTg= go.uber.org/fx v1.23.0 h1:lIr/gYWQGfTwGcSXWXu4vP5Ws6iqnNEIY+F/aFzCKTg=
@@ -309,8 +313,8 @@ go.uber.org/fx v1.23.0/go.mod h1:o/D9n+2mLP6v1EG+qsdT1O8wKopYAsqZasju97SDFCU=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
@@ -330,8 +334,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4= golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4=
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
@@ -344,8 +348,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -367,8 +371,8 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -382,8 +386,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -407,8 +411,8 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -425,8 +429,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
@@ -442,8 +446,8 @@ golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapK
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -2,6 +2,7 @@ package basichost
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net" "net"
"slices" "slices"
@@ -13,6 +14,7 @@ import (
"github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/transport" "github.com/libp2p/go-libp2p/core/transport"
"github.com/libp2p/go-libp2p/p2p/host/basic/internal/backoff" "github.com/libp2p/go-libp2p/p2p/host/basic/internal/backoff"
"github.com/libp2p/go-libp2p/p2p/host/eventbus"
libp2pwebrtc "github.com/libp2p/go-libp2p/p2p/transport/webrtc" libp2pwebrtc "github.com/libp2p/go-libp2p/p2p/transport/webrtc"
libp2pwebtransport "github.com/libp2p/go-libp2p/p2p/transport/webtransport" libp2pwebtransport "github.com/libp2p/go-libp2p/p2p/transport/webtransport"
"github.com/libp2p/go-netroute" "github.com/libp2p/go-netroute"
@@ -27,24 +29,37 @@ type observedAddrsManager interface {
ObservedAddrsFor(local ma.Multiaddr) []ma.Multiaddr ObservedAddrsFor(local ma.Multiaddr) []ma.Multiaddr
} }
type hostAddrs struct {
addrs []ma.Multiaddr
localAddrs []ma.Multiaddr
reachableAddrs []ma.Multiaddr
unreachableAddrs []ma.Multiaddr
unknownAddrs []ma.Multiaddr
relayAddrs []ma.Multiaddr
}
type addrsManager struct { type addrsManager struct {
eventbus event.Bus bus event.Bus
natManager NATManager natManager NATManager
addrsFactory AddrsFactory addrsFactory AddrsFactory
listenAddrs func() []ma.Multiaddr listenAddrs func() []ma.Multiaddr
transportForListening func(ma.Multiaddr) transport.Transport transportForListening func(ma.Multiaddr) transport.Transport
observedAddrsManager observedAddrsManager observedAddrsManager observedAddrsManager
interfaceAddrs *interfaceAddrsCache interfaceAddrs *interfaceAddrsCache
addrsReachabilityTracker *addrsReachabilityTracker
// addrsUpdatedChan is notified when addrs change. This is provided by the caller.
addrsUpdatedChan chan struct{}
// triggerAddrsUpdateChan is used to trigger an addresses update. // triggerAddrsUpdateChan is used to trigger an addresses update.
triggerAddrsUpdateChan chan struct{} triggerAddrsUpdateChan chan struct{}
// addrsUpdatedChan is notified when addresses change. // triggerReachabilityUpdate is notified when reachable addrs are updated.
addrsUpdatedChan chan struct{} triggerReachabilityUpdate chan struct{}
hostReachability atomic.Pointer[network.Reachability] hostReachability atomic.Pointer[network.Reachability]
addrsMx sync.RWMutex // protects fields below addrsMx sync.RWMutex
localAddrs []ma.Multiaddr currentAddrs hostAddrs
relayAddrs []ma.Multiaddr
wg sync.WaitGroup wg sync.WaitGroup
ctx context.Context ctx context.Context
@@ -52,23 +67,25 @@ type addrsManager struct {
} }
func newAddrsManager( func newAddrsManager(
eventbus event.Bus, bus event.Bus,
natmgr NATManager, natmgr NATManager,
addrsFactory AddrsFactory, addrsFactory AddrsFactory,
listenAddrs func() []ma.Multiaddr, listenAddrs func() []ma.Multiaddr,
transportForListening func(ma.Multiaddr) transport.Transport, transportForListening func(ma.Multiaddr) transport.Transport,
observedAddrsManager observedAddrsManager, observedAddrsManager observedAddrsManager,
addrsUpdatedChan chan struct{}, addrsUpdatedChan chan struct{},
client autonatv2Client,
) (*addrsManager, error) { ) (*addrsManager, error) {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
as := &addrsManager{ as := &addrsManager{
eventbus: eventbus, bus: bus,
listenAddrs: listenAddrs, listenAddrs: listenAddrs,
transportForListening: transportForListening, transportForListening: transportForListening,
observedAddrsManager: observedAddrsManager, observedAddrsManager: observedAddrsManager,
natManager: natmgr, natManager: natmgr,
addrsFactory: addrsFactory, addrsFactory: addrsFactory,
triggerAddrsUpdateChan: make(chan struct{}, 1), triggerAddrsUpdateChan: make(chan struct{}, 1),
triggerReachabilityUpdate: make(chan struct{}, 1),
addrsUpdatedChan: addrsUpdatedChan, addrsUpdatedChan: addrsUpdatedChan,
interfaceAddrs: &interfaceAddrsCache{}, interfaceAddrs: &interfaceAddrsCache{},
ctx: ctx, ctx: ctx,
@@ -76,11 +93,23 @@ func newAddrsManager(
} }
unknownReachability := network.ReachabilityUnknown unknownReachability := network.ReachabilityUnknown
as.hostReachability.Store(&unknownReachability) as.hostReachability.Store(&unknownReachability)
if client != nil {
as.addrsReachabilityTracker = newAddrsReachabilityTracker(client, as.triggerReachabilityUpdate, nil)
}
return as, nil return as, nil
} }
func (a *addrsManager) Start() error { func (a *addrsManager) Start() error {
return a.background() // TODO: add Start method to NATMgr
if a.addrsReachabilityTracker != nil {
err := a.addrsReachabilityTracker.Start()
if err != nil {
return fmt.Errorf("error starting addrs reachability tracker: %s", err)
}
}
return a.startBackgroundWorker()
} }
func (a *addrsManager) Close() { func (a *addrsManager) Close() {
@@ -91,10 +120,18 @@ func (a *addrsManager) Close() {
log.Warnf("error closing natmgr: %s", err) log.Warnf("error closing natmgr: %s", err)
} }
} }
if a.addrsReachabilityTracker != nil {
err := a.addrsReachabilityTracker.Close()
if err != nil {
log.Warnf("error closing addrs reachability tracker: %s", err)
}
}
a.wg.Wait() a.wg.Wait()
} }
func (a *addrsManager) NetNotifee() network.Notifiee { func (a *addrsManager) NetNotifee() network.Notifiee {
// Updating addrs in sync provides the nice property that
// host.Addrs() just after host.Network().Listen(x) will return x
return &network.NotifyBundle{ return &network.NotifyBundle{
ListenF: func(network.Network, ma.Multiaddr) { a.triggerAddrsUpdate() }, ListenF: func(network.Network, ma.Multiaddr) { a.triggerAddrsUpdate() },
ListenCloseF: func(network.Network, ma.Multiaddr) { a.triggerAddrsUpdate() }, ListenCloseF: func(network.Network, ma.Multiaddr) { a.triggerAddrsUpdate() },
@@ -102,37 +139,53 @@ func (a *addrsManager) NetNotifee() network.Notifiee {
} }
func (a *addrsManager) triggerAddrsUpdate() { func (a *addrsManager) triggerAddrsUpdate() {
// This is ugly, we update here *and* in the background loop, but this ensures the nice property a.updateAddrs(false, nil)
// that host.Addrs after host.Network().Listen(...) will return the recently added listen address.
a.updateLocalAddrs()
select { select {
case a.triggerAddrsUpdateChan <- struct{}{}: case a.triggerAddrsUpdateChan <- struct{}{}:
default: default:
} }
} }
func (a *addrsManager) background() error { func (a *addrsManager) startBackgroundWorker() error {
autoRelayAddrsSub, err := a.eventbus.Subscribe(new(event.EvtAutoRelayAddrsUpdated)) autoRelayAddrsSub, err := a.bus.Subscribe(new(event.EvtAutoRelayAddrsUpdated), eventbus.Name("addrs-manager"))
if err != nil { if err != nil {
return fmt.Errorf("error subscribing to auto relay addrs: %s", err) return fmt.Errorf("error subscribing to auto relay addrs: %s", err)
} }
autonatReachabilitySub, err := a.eventbus.Subscribe(new(event.EvtLocalReachabilityChanged)) autonatReachabilitySub, err := a.bus.Subscribe(new(event.EvtLocalReachabilityChanged), eventbus.Name("addrs-manager"))
if err != nil { if err != nil {
return fmt.Errorf("error subscribing to autonat reachability: %s", err) err1 := autoRelayAddrsSub.Close()
if err1 != nil {
err1 = fmt.Errorf("error closign autorelaysub: %w", err1)
}
err = fmt.Errorf("error subscribing to autonat reachability: %s", err)
return errors.Join(err, err1)
} }
// ensure that we have the correct address after returning from Start() emitter, err := a.bus.Emitter(new(event.EvtHostReachableAddrsChanged), eventbus.Stateful)
// update local addrs if err != nil {
a.updateLocalAddrs() err1 := autoRelayAddrsSub.Close()
if err1 != nil {
err1 = fmt.Errorf("error closing autorelaysub: %w", err1)
}
err2 := autonatReachabilitySub.Close()
if err2 != nil {
err2 = fmt.Errorf("error closing autonat reachability: %w", err1)
}
err = fmt.Errorf("error subscribing to autonat reachability: %s", err)
return errors.Join(err, err1, err2)
}
var relayAddrs []ma.Multiaddr
// update relay addrs in case we're private // update relay addrs in case we're private
select { select {
case e := <-autoRelayAddrsSub.Out(): case e := <-autoRelayAddrsSub.Out():
if evt, ok := e.(event.EvtAutoRelayAddrsUpdated); ok { if evt, ok := e.(event.EvtAutoRelayAddrsUpdated); ok {
a.updateRelayAddrs(evt.RelayAddrs) relayAddrs = slices.Clone(evt.RelayAddrs)
} }
default: default:
} }
select { select {
case e := <-autonatReachabilitySub.Out(): case e := <-autonatReachabilitySub.Out():
if evt, ok := e.(event.EvtLocalReachabilityChanged); ok { if evt, ok := e.(event.EvtLocalReachabilityChanged); ok {
@@ -140,18 +193,25 @@ func (a *addrsManager) background() error {
} }
default: default:
} }
// update addresses before starting the worker loop. This ensures that any address updates
// before calling addrsManager.Start are correctly reported after Start returns.
a.updateAddrs(true, relayAddrs)
a.wg.Add(1) a.wg.Add(1)
go func() { go a.background(autoRelayAddrsSub, autonatReachabilitySub, emitter, relayAddrs)
return nil
}
func (a *addrsManager) background(autoRelayAddrsSub, autonatReachabilitySub event.Subscription,
emitter event.Emitter, relayAddrs []ma.Multiaddr,
) {
defer a.wg.Done() defer a.wg.Done()
defer func() { defer func() {
err := autoRelayAddrsSub.Close() err := autoRelayAddrsSub.Close()
if err != nil { if err != nil {
log.Warnf("error closing auto relay addrs sub: %s", err) log.Warnf("error closing auto relay addrs sub: %s", err)
} }
}() err = autonatReachabilitySub.Close()
defer func() {
err := autonatReachabilitySub.Close()
if err != nil { if err != nil {
log.Warnf("error closing autonat reachability sub: %s", err) log.Warnf("error closing autonat reachability sub: %s", err)
} }
@@ -159,24 +219,18 @@ func (a *addrsManager) background() error {
ticker := time.NewTicker(addrChangeTickrInterval) ticker := time.NewTicker(addrChangeTickrInterval)
defer ticker.Stop() defer ticker.Stop()
var prev []ma.Multiaddr var previousAddrs hostAddrs
for { for {
a.updateLocalAddrs() currAddrs := a.updateAddrs(true, relayAddrs)
curr := a.Addrs() a.notifyAddrsChanged(emitter, previousAddrs, currAddrs)
if a.areAddrsDifferent(prev, curr) { previousAddrs = currAddrs
log.Debugf("host addresses updated: %s", curr)
select {
case a.addrsUpdatedChan <- struct{}{}:
default:
}
}
prev = curr
select { select {
case <-ticker.C: case <-ticker.C:
case <-a.triggerAddrsUpdateChan: case <-a.triggerAddrsUpdateChan:
case <-a.triggerReachabilityUpdate:
case e := <-autoRelayAddrsSub.Out(): case e := <-autoRelayAddrsSub.Out():
if evt, ok := e.(event.EvtAutoRelayAddrsUpdated); ok { if evt, ok := e.(event.EvtAutoRelayAddrsUpdated); ok {
a.updateRelayAddrs(evt.RelayAddrs) relayAddrs = slices.Clone(evt.RelayAddrs)
} }
case e := <-autonatReachabilitySub.Out(): case e := <-autonatReachabilitySub.Out():
if evt, ok := e.(event.EvtLocalReachabilityChanged); ok { if evt, ok := e.(event.EvtLocalReachabilityChanged); ok {
@@ -186,24 +240,106 @@ func (a *addrsManager) background() error {
return return
} }
} }
}() }
return nil
// updateAddrs updates the addresses of the host and returns the new updated
// addrs
func (a *addrsManager) updateAddrs(updateRelayAddrs bool, relayAddrs []ma.Multiaddr) hostAddrs {
// Must lock while doing both recompute and update as this method is called from
// multiple goroutines.
a.addrsMx.Lock()
defer a.addrsMx.Unlock()
localAddrs := a.getLocalAddrs()
var currReachableAddrs, currUnreachableAddrs, currUnknownAddrs []ma.Multiaddr
if a.addrsReachabilityTracker != nil {
currReachableAddrs, currUnreachableAddrs, currUnknownAddrs = a.getConfirmedAddrs(localAddrs)
}
if !updateRelayAddrs {
relayAddrs = a.currentAddrs.relayAddrs
} else {
// Copy the callers slice
relayAddrs = slices.Clone(relayAddrs)
}
currAddrs := a.getAddrs(slices.Clone(localAddrs), relayAddrs)
a.currentAddrs = hostAddrs{
addrs: append(a.currentAddrs.addrs[:0], currAddrs...),
localAddrs: append(a.currentAddrs.localAddrs[:0], localAddrs...),
reachableAddrs: append(a.currentAddrs.reachableAddrs[:0], currReachableAddrs...),
unreachableAddrs: append(a.currentAddrs.unreachableAddrs[:0], currUnreachableAddrs...),
unknownAddrs: append(a.currentAddrs.unknownAddrs[:0], currUnknownAddrs...),
relayAddrs: append(a.currentAddrs.relayAddrs[:0], relayAddrs...),
}
return hostAddrs{
localAddrs: localAddrs,
addrs: currAddrs,
reachableAddrs: currReachableAddrs,
unreachableAddrs: currUnreachableAddrs,
unknownAddrs: currUnknownAddrs,
relayAddrs: relayAddrs,
}
}
func (a *addrsManager) notifyAddrsChanged(emitter event.Emitter, previous, current hostAddrs) {
if areAddrsDifferent(previous.localAddrs, current.localAddrs) {
log.Debugf("host local addresses updated: %s", current.localAddrs)
if a.addrsReachabilityTracker != nil {
a.addrsReachabilityTracker.UpdateAddrs(current.localAddrs)
}
}
if areAddrsDifferent(previous.addrs, current.addrs) {
log.Debugf("host addresses updated: %s", current.localAddrs)
select {
case a.addrsUpdatedChan <- struct{}{}:
default:
}
}
// We *must* send both reachability changed and addrs changed events from the
// same goroutine to ensure correct ordering
// Consider the events:
// - addr x discovered
// - addr x is reachable
// - addr x removed
// We must send these events in the same order. It'll be confusing for consumers
// if the reachable event is received after the addr removed event.
if areAddrsDifferent(previous.reachableAddrs, current.reachableAddrs) ||
areAddrsDifferent(previous.unreachableAddrs, current.unreachableAddrs) ||
areAddrsDifferent(previous.unknownAddrs, current.unknownAddrs) {
log.Debugf("host reachable addrs updated: %s", current.localAddrs)
if err := emitter.Emit(event.EvtHostReachableAddrsChanged{
Reachable: slices.Clone(current.reachableAddrs),
Unreachable: slices.Clone(current.unreachableAddrs),
Unknown: slices.Clone(current.unknownAddrs),
}); err != nil {
log.Errorf("error sending host reachable addrs changed event: %s", err)
}
}
} }
// Addrs returns the node's dialable addresses both public and private. // Addrs returns the node's dialable addresses both public and private.
// If autorelay is enabled and node reachability is private, it returns // If autorelay is enabled and node reachability is private, it returns
// the node's relay addresses and private network addresses. // the node's relay addresses and private network addresses.
func (a *addrsManager) Addrs() []ma.Multiaddr { func (a *addrsManager) Addrs() []ma.Multiaddr {
addrs := a.DirectAddrs() a.addrsMx.RLock()
directAddrs := slices.Clone(a.currentAddrs.localAddrs)
relayAddrs := slices.Clone(a.currentAddrs.relayAddrs)
a.addrsMx.RUnlock()
return a.getAddrs(directAddrs, relayAddrs)
}
// getAddrs returns the node's dialable addresses. Mutates localAddrs
func (a *addrsManager) getAddrs(localAddrs []ma.Multiaddr, relayAddrs []ma.Multiaddr) []ma.Multiaddr {
addrs := localAddrs
rch := a.hostReachability.Load() rch := a.hostReachability.Load()
if rch != nil && *rch == network.ReachabilityPrivate { if rch != nil && *rch == network.ReachabilityPrivate {
a.addrsMx.RLock()
// Delete public addresses if the node's reachability is private, and we have relay addresses // Delete public addresses if the node's reachability is private, and we have relay addresses
if len(a.relayAddrs) > 0 { if len(relayAddrs) > 0 {
addrs = slices.DeleteFunc(addrs, manet.IsPublicAddr) addrs = slices.DeleteFunc(addrs, manet.IsPublicAddr)
addrs = append(addrs, a.relayAddrs...) addrs = append(addrs, relayAddrs...)
} }
a.addrsMx.RUnlock()
} }
// Make a copy. Consumers can modify the slice elements // Make a copy. Consumers can modify the slice elements
addrs = slices.Clone(a.addrsFactory(addrs)) addrs = slices.Clone(a.addrsFactory(addrs))
@@ -213,7 +349,8 @@ func (a *addrsManager) Addrs() []ma.Multiaddr {
return addrs return addrs
} }
// HolePunchAddrs returns the node's public direct listen addresses for hole punching. // HolePunchAddrs returns all the host's direct public addresses, reachable or unreachable,
// suitable for hole punching.
func (a *addrsManager) HolePunchAddrs() []ma.Multiaddr { func (a *addrsManager) HolePunchAddrs() []ma.Multiaddr {
addrs := a.DirectAddrs() addrs := a.DirectAddrs()
addrs = slices.Clone(a.addrsFactory(addrs)) addrs = slices.Clone(a.addrsFactory(addrs))
@@ -230,26 +367,23 @@ func (a *addrsManager) HolePunchAddrs() []ma.Multiaddr {
func (a *addrsManager) DirectAddrs() []ma.Multiaddr { func (a *addrsManager) DirectAddrs() []ma.Multiaddr {
a.addrsMx.RLock() a.addrsMx.RLock()
defer a.addrsMx.RUnlock() defer a.addrsMx.RUnlock()
return slices.Clone(a.localAddrs) return slices.Clone(a.currentAddrs.localAddrs)
} }
func (a *addrsManager) updateRelayAddrs(addrs []ma.Multiaddr) { // ConfirmedAddrs returns all addresses of the host that are reachable from the internet
a.addrsMx.Lock() func (a *addrsManager) ConfirmedAddrs() (reachable []ma.Multiaddr, unreachable []ma.Multiaddr, unknown []ma.Multiaddr) {
defer a.addrsMx.Unlock() a.addrsMx.RLock()
a.relayAddrs = append(a.relayAddrs[:0], addrs...) defer a.addrsMx.RUnlock()
return slices.Clone(a.currentAddrs.reachableAddrs), slices.Clone(a.currentAddrs.unreachableAddrs), slices.Clone(a.currentAddrs.unknownAddrs)
}
func (a *addrsManager) getConfirmedAddrs(localAddrs []ma.Multiaddr) (reachableAddrs, unreachableAddrs, unknownAddrs []ma.Multiaddr) {
reachableAddrs, unreachableAddrs, unknownAddrs = a.addrsReachabilityTracker.ConfirmedAddrs()
return removeNotInSource(reachableAddrs, localAddrs), removeNotInSource(unreachableAddrs, localAddrs), removeNotInSource(unknownAddrs, localAddrs)
} }
var p2pCircuitAddr = ma.StringCast("/p2p-circuit") var p2pCircuitAddr = ma.StringCast("/p2p-circuit")
func (a *addrsManager) updateLocalAddrs() {
localAddrs := a.getLocalAddrs()
slices.SortFunc(localAddrs, func(a, b ma.Multiaddr) int { return a.Compare(b) })
a.addrsMx.Lock()
a.localAddrs = localAddrs
a.addrsMx.Unlock()
}
func (a *addrsManager) getLocalAddrs() []ma.Multiaddr { func (a *addrsManager) getLocalAddrs() []ma.Multiaddr {
listenAddrs := a.listenAddrs() listenAddrs := a.listenAddrs()
if len(listenAddrs) == 0 { if len(listenAddrs) == 0 {
@@ -260,8 +394,6 @@ func (a *addrsManager) getLocalAddrs() []ma.Multiaddr {
finalAddrs = a.appendPrimaryInterfaceAddrs(finalAddrs, listenAddrs) finalAddrs = a.appendPrimaryInterfaceAddrs(finalAddrs, listenAddrs)
finalAddrs = a.appendNATAddrs(finalAddrs, listenAddrs, a.interfaceAddrs.All()) finalAddrs = a.appendNATAddrs(finalAddrs, listenAddrs, a.interfaceAddrs.All())
finalAddrs = ma.Unique(finalAddrs)
// Remove "/p2p-circuit" addresses from the list. // Remove "/p2p-circuit" addresses from the list.
// The p2p-circuit listener reports its address as just /p2p-circuit. This is // The p2p-circuit listener reports its address as just /p2p-circuit. This is
// useless for dialing. Users need to manage their circuit addresses themselves, // useless for dialing. Users need to manage their circuit addresses themselves,
@@ -278,6 +410,8 @@ func (a *addrsManager) getLocalAddrs() []ma.Multiaddr {
// Add certhashes for /webrtc-direct, /webtransport, etc addresses discovered // Add certhashes for /webrtc-direct, /webtransport, etc addresses discovered
// using identify. // using identify.
finalAddrs = a.addCertHashes(finalAddrs) finalAddrs = a.addCertHashes(finalAddrs)
finalAddrs = ma.Unique(finalAddrs)
slices.SortFunc(finalAddrs, func(a, b ma.Multiaddr) int { return a.Compare(b) })
return finalAddrs return finalAddrs
} }
@@ -408,7 +542,7 @@ func (a *addrsManager) addCertHashes(addrs []ma.Multiaddr) []ma.Multiaddr {
return addrs return addrs
} }
func (a *addrsManager) areAddrsDifferent(prev, current []ma.Multiaddr) bool { func areAddrsDifferent(prev, current []ma.Multiaddr) bool {
// TODO: make the sorted nature of ma.Unique a guarantee in multiaddrs // TODO: make the sorted nature of ma.Unique a guarantee in multiaddrs
prev = ma.Unique(prev) prev = ma.Unique(prev)
current = ma.Unique(current) current = ma.Unique(current)
@@ -547,3 +681,31 @@ func (i *interfaceAddrsCache) updateUnlocked() {
} }
} }
} }
// removeNotInSource removes items from addrs that are not present in source.
// Modifies the addrs slice in place
// addrs and source must be sorted using multiaddr.Compare.
func removeNotInSource(addrs, source []ma.Multiaddr) []ma.Multiaddr {
j := 0
// mark entries not in source as nil
for i, a := range addrs {
// move right as long as a > source[j]
for j < len(source) && a.Compare(source[j]) > 0 {
j++
}
// a is not in source if we've reached the end, or a is lesser
if j == len(source) || a.Compare(source[j]) < 0 {
addrs[i] = nil
}
// a is in source, nothing to do
}
// j is the current element, i is the lowest index nil element
i := 0
for j := range len(addrs) {
if addrs[j] != nil {
addrs[i], addrs[j] = addrs[j], addrs[i]
i++
}
}
return addrs[:i]
}

View File

@@ -1,13 +1,17 @@
package basichost package basichost
import ( import (
"context"
"errors"
"fmt" "fmt"
"slices"
"testing" "testing"
"time" "time"
"github.com/libp2p/go-libp2p/core/event" "github.com/libp2p/go-libp2p/core/event"
"github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/p2p/host/eventbus" "github.com/libp2p/go-libp2p/p2p/host/eventbus"
"github.com/libp2p/go-libp2p/p2p/protocol/autonatv2"
ma "github.com/multiformats/go-multiaddr" ma "github.com/multiformats/go-multiaddr"
manet "github.com/multiformats/go-multiaddr/net" manet "github.com/multiformats/go-multiaddr/net"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -135,7 +139,7 @@ type mockNatManager struct {
GetMappingFunc func(addr ma.Multiaddr) ma.Multiaddr GetMappingFunc func(addr ma.Multiaddr) ma.Multiaddr
} }
func (m *mockNatManager) Close() error { func (*mockNatManager) Close() error {
return nil return nil
} }
@@ -146,7 +150,7 @@ func (m *mockNatManager) GetMapping(addr ma.Multiaddr) ma.Multiaddr {
return m.GetMappingFunc(addr) return m.GetMappingFunc(addr)
} }
func (m *mockNatManager) HasDiscoveredNAT() bool { func (*mockNatManager) HasDiscoveredNAT() bool {
return true return true
} }
@@ -170,6 +174,8 @@ type addrsManagerArgs struct {
AddrsFactory AddrsFactory AddrsFactory AddrsFactory
ObservedAddrsManager observedAddrsManager ObservedAddrsManager observedAddrsManager
ListenAddrs func() []ma.Multiaddr ListenAddrs func() []ma.Multiaddr
AutoNATClient autonatv2Client
Bus event.Bus
} }
type addrsManagerTestCase struct { type addrsManagerTestCase struct {
@@ -179,13 +185,16 @@ type addrsManagerTestCase struct {
} }
func newAddrsManagerTestCase(t *testing.T, args addrsManagerArgs) addrsManagerTestCase { func newAddrsManagerTestCase(t *testing.T, args addrsManagerArgs) addrsManagerTestCase {
eb := eventbus.NewBus() eb := args.Bus
if eb == nil {
eb = eventbus.NewBus()
}
if args.AddrsFactory == nil { if args.AddrsFactory == nil {
args.AddrsFactory = func(addrs []ma.Multiaddr) []ma.Multiaddr { return addrs } args.AddrsFactory = func(addrs []ma.Multiaddr) []ma.Multiaddr { return addrs }
} }
addrsUpdatedChan := make(chan struct{}, 1) addrsUpdatedChan := make(chan struct{}, 1)
am, err := newAddrsManager( am, err := newAddrsManager(
eb, args.NATManager, args.AddrsFactory, args.ListenAddrs, nil, args.ObservedAddrsManager, addrsUpdatedChan, eb, args.NATManager, args.AddrsFactory, args.ListenAddrs, nil, args.ObservedAddrsManager, addrsUpdatedChan, args.AutoNATClient,
) )
require.NoError(t, err) require.NoError(t, err)
@@ -196,6 +205,7 @@ func newAddrsManagerTestCase(t *testing.T, args addrsManagerArgs) addrsManagerTe
rchEm, err := eb.Emitter(new(event.EvtLocalReachabilityChanged), eventbus.Stateful) rchEm, err := eb.Emitter(new(event.EvtLocalReachabilityChanged), eventbus.Stateful)
require.NoError(t, err) require.NoError(t, err)
t.Cleanup(am.Close)
return addrsManagerTestCase{ return addrsManagerTestCase{
addrsManager: am, addrsManager: am,
PushRelay: func(relayAddrs []ma.Multiaddr) { PushRelay: func(relayAddrs []ma.Multiaddr) {
@@ -425,17 +435,113 @@ func TestAddrsManager(t *testing.T) {
}) })
} }
func TestAddrsManagerReachabilityEvent(t *testing.T) {
publicQUIC, _ := ma.NewMultiaddr("/ip4/1.2.3.4/udp/1234/quic-v1")
publicQUIC2, _ := ma.NewMultiaddr("/ip4/1.2.3.4/udp/1235/quic-v1")
publicTCP, _ := ma.NewMultiaddr("/ip4/1.2.3.4/tcp/1234")
bus := eventbus.NewBus()
sub, err := bus.Subscribe(new(event.EvtHostReachableAddrsChanged))
require.NoError(t, err)
defer sub.Close()
am := newAddrsManagerTestCase(t, addrsManagerArgs{
Bus: bus,
// currently they aren't being passed to the reachability tracker
ListenAddrs: func() []ma.Multiaddr { return []ma.Multiaddr{publicQUIC, publicQUIC2, publicTCP} },
AutoNATClient: mockAutoNATClient{
F: func(_ context.Context, reqs []autonatv2.Request) (autonatv2.Result, error) {
if reqs[0].Addr.Equal(publicQUIC) {
return autonatv2.Result{Addr: reqs[0].Addr, Idx: 0, Reachability: network.ReachabilityPublic}, nil
} else if reqs[0].Addr.Equal(publicTCP) || reqs[0].Addr.Equal(publicQUIC2) {
return autonatv2.Result{Addr: reqs[0].Addr, Idx: 0, Reachability: network.ReachabilityPrivate}, nil
}
return autonatv2.Result{}, errors.New("invalid")
},
},
})
initialUnknownAddrs := []ma.Multiaddr{publicQUIC, publicTCP, publicQUIC2}
// First event: all addresses are initially unknown
select {
case e := <-sub.Out():
evt := e.(event.EvtHostReachableAddrsChanged)
require.Empty(t, evt.Reachable)
require.Empty(t, evt.Unreachable)
require.ElementsMatch(t, initialUnknownAddrs, evt.Unknown)
case <-time.After(5 * time.Second):
t.Fatal("expected initial event for reachability change")
}
// Wait for probes to complete and addresses to be classified
reachableAddrs := []ma.Multiaddr{publicQUIC}
unreachableAddrs := []ma.Multiaddr{publicTCP, publicQUIC2}
select {
case e := <-sub.Out():
evt := e.(event.EvtHostReachableAddrsChanged)
require.ElementsMatch(t, reachableAddrs, evt.Reachable)
require.ElementsMatch(t, unreachableAddrs, evt.Unreachable)
require.Empty(t, evt.Unknown)
reachable, unreachable, unknown := am.ConfirmedAddrs()
require.ElementsMatch(t, reachable, reachableAddrs)
require.ElementsMatch(t, unreachable, unreachableAddrs)
require.Empty(t, unknown)
case <-time.After(5 * time.Second):
t.Fatal("expected final event for reachability change after probing")
}
}
func TestRemoveIfNotInSource(t *testing.T) {
var addrs []ma.Multiaddr
for i := 0; i < 10; i++ {
addrs = append(addrs, ma.StringCast(fmt.Sprintf("/ip4/1.2.3.4/tcp/%d", i)))
}
slices.SortFunc(addrs, func(a, b ma.Multiaddr) int { return a.Compare(b) })
cases := []struct {
addrs []ma.Multiaddr
source []ma.Multiaddr
expected []ma.Multiaddr
}{
{},
{addrs: slices.Clone(addrs[:5]), source: nil, expected: nil},
{addrs: nil, source: addrs, expected: nil},
{addrs: []ma.Multiaddr{addrs[0]}, source: []ma.Multiaddr{addrs[0]}, expected: []ma.Multiaddr{addrs[0]}},
{addrs: slices.Clone(addrs), source: []ma.Multiaddr{addrs[0]}, expected: []ma.Multiaddr{addrs[0]}},
{addrs: slices.Clone(addrs), source: slices.Clone(addrs[5:]), expected: slices.Clone(addrs[5:])},
{addrs: slices.Clone(addrs[:5]), source: []ma.Multiaddr{addrs[0], addrs[2], addrs[8]}, expected: []ma.Multiaddr{addrs[0], addrs[2]}},
}
for i, tc := range cases {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
addrs := removeNotInSource(tc.addrs, tc.source)
require.ElementsMatch(t, tc.expected, addrs, "%s\n%s", tc.expected, tc.addrs)
})
}
}
func BenchmarkAreAddrsDifferent(b *testing.B) { func BenchmarkAreAddrsDifferent(b *testing.B) {
var addrs [10]ma.Multiaddr var addrs [10]ma.Multiaddr
for i := 0; i < len(addrs); i++ { for i := 0; i < len(addrs); i++ {
addrs[i] = ma.StringCast(fmt.Sprintf("/ip4/1.1.1.%d/tcp/1", i)) addrs[i] = ma.StringCast(fmt.Sprintf("/ip4/1.1.1.%d/tcp/1", i))
} }
am := &addrsManager{}
b.Run("areAddrsDifferent", func(b *testing.B) { b.Run("areAddrsDifferent", func(b *testing.B) {
b.ReportAllocs() b.ReportAllocs()
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
am.areAddrsDifferent(addrs[:], addrs[:]) areAddrsDifferent(addrs[:], addrs[:])
} }
}) })
} }
func BenchmarkRemoveIfNotInSource(b *testing.B) {
var addrs [10]ma.Multiaddr
for i := 0; i < len(addrs); i++ {
addrs[i] = ma.StringCast(fmt.Sprintf("/ip4/1.1.1.%d/tcp/1", i))
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
removeNotInSource(slices.Clone(addrs[:5]), addrs[:])
}
}

View File

@@ -0,0 +1,671 @@
package basichost
import (
"context"
"errors"
"fmt"
"math"
"slices"
"sync"
"sync/atomic"
"time"
"github.com/benbjohnson/clock"
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/p2p/protocol/autonatv2"
ma "github.com/multiformats/go-multiaddr"
manet "github.com/multiformats/go-multiaddr/net"
)
type autonatv2Client interface {
GetReachability(ctx context.Context, reqs []autonatv2.Request) (autonatv2.Result, error)
}
const (
// maxAddrsPerRequest is the maximum number of addresses to probe in a single request
maxAddrsPerRequest = 10
// maxTrackedAddrs is the maximum number of addresses to track
// 10 addrs per transport for 5 transports
maxTrackedAddrs = 50
// defaultMaxConcurrency is the default number of concurrent workers for reachability checks
defaultMaxConcurrency = 5
// newAddrsProbeDelay is the delay before probing new addr's reachability.
newAddrsProbeDelay = 1 * time.Second
)
// addrsReachabilityTracker tracks reachability for addresses.
// Use UpdateAddrs to provide addresses for tracking reachability.
// reachabilityUpdateCh is notified when reachability for any of the tracked address changes.
type addrsReachabilityTracker struct {
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
client autonatv2Client
// reachabilityUpdateCh is used to notify when reachability may have changed
reachabilityUpdateCh chan struct{}
maxConcurrency int
newAddrsProbeDelay time.Duration
probeManager *probeManager
newAddrs chan []ma.Multiaddr
clock clock.Clock
mx sync.Mutex
reachableAddrs []ma.Multiaddr
unreachableAddrs []ma.Multiaddr
unknownAddrs []ma.Multiaddr
}
// newAddrsReachabilityTracker returns a new addrsReachabilityTracker.
// reachabilityUpdateCh is notified when reachability for any of the tracked address changes.
func newAddrsReachabilityTracker(client autonatv2Client, reachabilityUpdateCh chan struct{}, cl clock.Clock) *addrsReachabilityTracker {
ctx, cancel := context.WithCancel(context.Background())
if cl == nil {
cl = clock.New()
}
return &addrsReachabilityTracker{
ctx: ctx,
cancel: cancel,
client: client,
reachabilityUpdateCh: reachabilityUpdateCh,
probeManager: newProbeManager(cl.Now),
newAddrsProbeDelay: newAddrsProbeDelay,
maxConcurrency: defaultMaxConcurrency,
newAddrs: make(chan []ma.Multiaddr, 1),
clock: cl,
}
}
func (r *addrsReachabilityTracker) UpdateAddrs(addrs []ma.Multiaddr) {
select {
case r.newAddrs <- slices.Clone(addrs):
case <-r.ctx.Done():
}
}
func (r *addrsReachabilityTracker) ConfirmedAddrs() (reachableAddrs, unreachableAddrs, unknownAddrs []ma.Multiaddr) {
r.mx.Lock()
defer r.mx.Unlock()
return slices.Clone(r.reachableAddrs), slices.Clone(r.unreachableAddrs), slices.Clone(r.unknownAddrs)
}
func (r *addrsReachabilityTracker) Start() error {
r.wg.Add(1)
go r.background()
return nil
}
func (r *addrsReachabilityTracker) Close() error {
r.cancel()
r.wg.Wait()
return nil
}
const (
// defaultReachabilityRefreshInterval is the default interval to refresh reachability.
// In steady state, we check for any required probes every refresh interval.
// This doesn't mean we'll probe for any particular address, only that we'll check
// if any address needs to be probed.
defaultReachabilityRefreshInterval = 5 * time.Minute
// maxBackoffInterval is the maximum back off in case we're unable to probe for reachability.
// We may be unable to confirm addresses in case there are no valid peers with autonatv2
// or the autonatv2 subsystem is consistently erroring.
maxBackoffInterval = 5 * time.Minute
// backoffStartInterval is the initial back off in case we're unable to probe for reachability.
backoffStartInterval = 5 * time.Second
)
func (r *addrsReachabilityTracker) background() {
defer r.wg.Done()
// probeTicker is used to trigger probes at regular intervals
probeTicker := r.clock.Ticker(defaultReachabilityRefreshInterval)
defer probeTicker.Stop()
// probeTimer is used to trigger probes at specific times
probeTimer := r.clock.Timer(time.Duration(math.MaxInt64))
defer probeTimer.Stop()
nextProbeTime := time.Time{}
var task reachabilityTask
var backoffInterval time.Duration
var currReachable, currUnreachable, currUnknown, prevReachable, prevUnreachable, prevUnknown []ma.Multiaddr
for {
select {
case <-probeTicker.C:
// don't start a probe if we have a scheduled probe
if task.BackoffCh == nil && nextProbeTime.IsZero() {
task = r.refreshReachability()
}
case <-probeTimer.C:
if task.BackoffCh == nil {
task = r.refreshReachability()
}
nextProbeTime = time.Time{}
case backoff := <-task.BackoffCh:
task = reachabilityTask{}
// On completion, start the next probe immediately, or wait for backoff.
// In case there are no further probes, the reachability tracker will return an empty task,
// which hangs forever. Eventually, we'll refresh again when the ticker fires.
if backoff {
backoffInterval = newBackoffInterval(backoffInterval)
} else {
backoffInterval = -1 * time.Second // negative to trigger next probe immediately
}
nextProbeTime = r.clock.Now().Add(backoffInterval)
case addrs := <-r.newAddrs:
if task.BackoffCh != nil { // cancel running task.
task.Cancel()
<-task.BackoffCh // ignore backoff from cancelled task
task = reachabilityTask{}
}
r.updateTrackedAddrs(addrs)
newAddrsNextTime := r.clock.Now().Add(r.newAddrsProbeDelay)
if nextProbeTime.Before(newAddrsNextTime) {
nextProbeTime = newAddrsNextTime
}
case <-r.ctx.Done():
if task.BackoffCh != nil {
task.Cancel()
<-task.BackoffCh
task = reachabilityTask{}
}
return
}
currReachable, currUnreachable, currUnknown = r.appendConfirmedAddrs(currReachable[:0], currUnreachable[:0], currUnknown[:0])
if areAddrsDifferent(prevReachable, currReachable) || areAddrsDifferent(prevUnreachable, currUnreachable) || areAddrsDifferent(prevUnknown, currUnknown) {
r.notify()
}
prevReachable = append(prevReachable[:0], currReachable...)
prevUnreachable = append(prevUnreachable[:0], currUnreachable...)
prevUnknown = append(prevUnknown[:0], currUnknown...)
if !nextProbeTime.IsZero() {
probeTimer.Reset(nextProbeTime.Sub(r.clock.Now()))
}
}
}
func newBackoffInterval(current time.Duration) time.Duration {
if current <= 0 {
return backoffStartInterval
}
current *= 2
if current > maxBackoffInterval {
return maxBackoffInterval
}
return current
}
func (r *addrsReachabilityTracker) appendConfirmedAddrs(reachable, unreachable, unknown []ma.Multiaddr) (reachableAddrs, unreachableAddrs, unknownAddrs []ma.Multiaddr) {
reachable, unreachable, unknown = r.probeManager.AppendConfirmedAddrs(reachable, unreachable, unknown)
r.mx.Lock()
r.reachableAddrs = append(r.reachableAddrs[:0], reachable...)
r.unreachableAddrs = append(r.unreachableAddrs[:0], unreachable...)
r.unknownAddrs = append(r.unknownAddrs[:0], unknown...)
r.mx.Unlock()
return reachable, unreachable, unknown
}
func (r *addrsReachabilityTracker) notify() {
select {
case r.reachabilityUpdateCh <- struct{}{}:
default:
}
}
func (r *addrsReachabilityTracker) updateTrackedAddrs(addrs []ma.Multiaddr) {
addrs = slices.DeleteFunc(addrs, func(a ma.Multiaddr) bool {
return !manet.IsPublicAddr(a)
})
if len(addrs) > maxTrackedAddrs {
log.Errorf("too many addresses (%d) for addrs reachability tracker; dropping %d", len(addrs), len(addrs)-maxTrackedAddrs)
addrs = addrs[:maxTrackedAddrs]
}
r.probeManager.UpdateAddrs(addrs)
}
type probe = []autonatv2.Request
const probeTimeout = 30 * time.Second
// reachabilityTask is a task to refresh reachability.
// Waiting on the zero value blocks forever.
type reachabilityTask struct {
Cancel context.CancelFunc
// BackoffCh returns whether the caller should backoff before
// refreshing reachability
BackoffCh chan bool
}
func (r *addrsReachabilityTracker) refreshReachability() reachabilityTask {
if len(r.probeManager.GetProbe()) == 0 {
return reachabilityTask{}
}
resCh := make(chan bool, 1)
ctx, cancel := context.WithTimeout(r.ctx, 5*time.Minute)
r.wg.Add(1)
// We run probes provided by addrsTracker. It stops probing when any
// of the following happens:
// - there are no more probes to run
// - context is completed
// - there are too many consecutive failures from the client
// - the client has no valid peers to probe
go func() {
defer r.wg.Done()
defer cancel()
client := &errCountingClient{autonatv2Client: r.client, MaxConsecutiveErrors: maxConsecutiveErrors}
var backoff atomic.Bool
var wg sync.WaitGroup
wg.Add(r.maxConcurrency)
for range r.maxConcurrency {
go func() {
defer wg.Done()
for {
if ctx.Err() != nil {
return
}
reqs := r.probeManager.GetProbe()
if len(reqs) == 0 {
return
}
r.probeManager.MarkProbeInProgress(reqs)
rctx, cancel := context.WithTimeout(ctx, probeTimeout)
res, err := client.GetReachability(rctx, reqs)
cancel()
r.probeManager.CompleteProbe(reqs, res, err)
if isErrorPersistent(err) {
backoff.Store(true)
return
}
}
}()
}
wg.Wait()
resCh <- backoff.Load()
}()
return reachabilityTask{Cancel: cancel, BackoffCh: resCh}
}
var errTooManyConsecutiveFailures = errors.New("too many consecutive failures")
// errCountingClient counts errors from autonatv2Client and wraps the errors in response with a
// errTooManyConsecutiveFailures in case of persistent failures from autonatv2 module.
type errCountingClient struct {
autonatv2Client
MaxConsecutiveErrors int
mx sync.Mutex
consecutiveErrors int
}
func (c *errCountingClient) GetReachability(ctx context.Context, reqs probe) (autonatv2.Result, error) {
res, err := c.autonatv2Client.GetReachability(ctx, reqs)
c.mx.Lock()
defer c.mx.Unlock()
if err != nil && !errors.Is(err, context.Canceled) { // ignore canceled errors, they're not errors from autonatv2
c.consecutiveErrors++
if c.consecutiveErrors > c.MaxConsecutiveErrors {
err = fmt.Errorf("%w:%w", errTooManyConsecutiveFailures, err)
}
if errors.Is(err, autonatv2.ErrPrivateAddrs) {
log.Errorf("private IP addr in autonatv2 request: %s", err)
}
} else {
c.consecutiveErrors = 0
}
return res, err
}
const maxConsecutiveErrors = 20
// isErrorPersistent returns whether the error will repeat on future probes for a while
func isErrorPersistent(err error) bool {
if err == nil {
return false
}
return errors.Is(err, autonatv2.ErrPrivateAddrs) || errors.Is(err, autonatv2.ErrNoPeers) ||
errors.Is(err, errTooManyConsecutiveFailures)
}
const (
// recentProbeInterval is the interval to probe addresses that have been refused
// these are generally addresses with newer transports for which we don't have many peers
// capable of dialing the transport
recentProbeInterval = 10 * time.Minute
// maxConsecutiveRefusals is the maximum number of consecutive refusals for an address after which
// we wait for `recentProbeInterval` before probing again
maxConsecutiveRefusals = 5
// maxRecentDialsPerAddr is the maximum number of dials on an address before we stop probing for the address.
// This is used to prevent infinite probing of an address whose status is indeterminate for any reason.
maxRecentDialsPerAddr = 10
// confidence is the absolute difference between the number of successes and failures for an address
// targetConfidence is the confidence threshold for an address after which we wait for `maxProbeInterval`
// before probing again.
targetConfidence = 3
// minConfidence is the confidence threshold for an address to be considered reachable or unreachable
// confidence is the absolute difference between the number of successes and failures for an address
minConfidence = 2
// maxRecentDialsWindow is the maximum number of recent probe results to consider for a single address
//
// +2 allows for 1 invalid probe result. Consider a string of successes, after which we have a single failure
// and then a success(...S S S S F S). The confidence in the targetConfidence window will be equal to
// targetConfidence, the last F and S cancel each other, and we won't probe again for maxProbeInterval.
maxRecentDialsWindow = targetConfidence + 2
// highConfidenceAddrProbeInterval is the maximum interval between probes for an address
highConfidenceAddrProbeInterval = 1 * time.Hour
// maxProbeResultTTL is the maximum time to keep probe results for an address
maxProbeResultTTL = maxRecentDialsWindow * highConfidenceAddrProbeInterval
)
// probeManager tracks reachability for a set of addresses by periodically probing reachability with autonatv2.
// A Probe is a list of addresses which can be tested for reachability with autonatv2.
// This struct decides the priority order of addresses for testing reachability, and throttles in case there have
// been too many probes for an address in the `ProbeInterval`.
//
// Use the `runProbes` function to execute the probes with an autonatv2 client.
type probeManager struct {
now func() time.Time
mx sync.Mutex
inProgressProbes map[string]int // addr -> count
inProgressProbesTotal int
statuses map[string]*addrStatus
addrs []ma.Multiaddr
}
// newProbeManager creates a new probe manager.
func newProbeManager(now func() time.Time) *probeManager {
return &probeManager{
statuses: make(map[string]*addrStatus),
inProgressProbes: make(map[string]int),
now: now,
}
}
// AppendConfirmedAddrs appends the current confirmed reachable and unreachable addresses.
func (m *probeManager) AppendConfirmedAddrs(reachable, unreachable, unknown []ma.Multiaddr) (reachableAddrs, unreachableAddrs, unknownAddrs []ma.Multiaddr) {
m.mx.Lock()
defer m.mx.Unlock()
for _, a := range m.addrs {
s := m.statuses[string(a.Bytes())]
s.RemoveBefore(m.now().Add(-maxProbeResultTTL)) // cleanup stale results
switch s.Reachability() {
case network.ReachabilityPublic:
reachable = append(reachable, a)
case network.ReachabilityPrivate:
unreachable = append(unreachable, a)
case network.ReachabilityUnknown:
unknown = append(unknown, a)
}
}
return reachable, unreachable, unknown
}
// UpdateAddrs updates the tracked addrs
func (m *probeManager) UpdateAddrs(addrs []ma.Multiaddr) {
m.mx.Lock()
defer m.mx.Unlock()
slices.SortFunc(addrs, func(a, b ma.Multiaddr) int { return a.Compare(b) })
statuses := make(map[string]*addrStatus, len(addrs))
for _, addr := range addrs {
k := string(addr.Bytes())
if _, ok := m.statuses[k]; !ok {
statuses[k] = &addrStatus{Addr: addr}
} else {
statuses[k] = m.statuses[k]
}
}
m.addrs = addrs
m.statuses = statuses
}
// GetProbe returns the next probe. Returns zero value in case there are no more probes.
// Probes that are run against an autonatv2 client should be marked in progress with
// `MarkProbeInProgress` before running.
func (m *probeManager) GetProbe() probe {
m.mx.Lock()
defer m.mx.Unlock()
now := m.now()
for i, a := range m.addrs {
ab := a.Bytes()
pc := m.statuses[string(ab)].RequiredProbeCount(now)
if m.inProgressProbes[string(ab)] >= pc {
continue
}
reqs := make(probe, 0, maxAddrsPerRequest)
reqs = append(reqs, autonatv2.Request{Addr: a, SendDialData: true})
// We have the first(primary) address. Append other addresses, ignoring inprogress probes
// on secondary addresses. The expectation is that the primary address will
// be dialed.
for j := 1; j < len(m.addrs); j++ {
k := (i + j) % len(m.addrs)
ab := m.addrs[k].Bytes()
pc := m.statuses[string(ab)].RequiredProbeCount(now)
if pc == 0 {
continue
}
reqs = append(reqs, autonatv2.Request{Addr: m.addrs[k], SendDialData: true})
if len(reqs) >= maxAddrsPerRequest {
break
}
}
return reqs
}
return nil
}
// MarkProbeInProgress should be called when a probe is started.
// All in progress probes *MUST* be completed with `CompleteProbe`
func (m *probeManager) MarkProbeInProgress(reqs probe) {
if len(reqs) == 0 {
return
}
m.mx.Lock()
defer m.mx.Unlock()
m.inProgressProbes[string(reqs[0].Addr.Bytes())]++
m.inProgressProbesTotal++
}
// InProgressProbes returns the number of probes that are currently in progress.
func (m *probeManager) InProgressProbes() int {
m.mx.Lock()
defer m.mx.Unlock()
return m.inProgressProbesTotal
}
// CompleteProbe should be called when a probe completes.
func (m *probeManager) CompleteProbe(reqs probe, res autonatv2.Result, err error) {
now := m.now()
if len(reqs) == 0 {
// should never happen
return
}
m.mx.Lock()
defer m.mx.Unlock()
// decrement in-progress count for the first address
primaryAddrKey := string(reqs[0].Addr.Bytes())
m.inProgressProbes[primaryAddrKey]--
if m.inProgressProbes[primaryAddrKey] <= 0 {
delete(m.inProgressProbes, primaryAddrKey)
}
m.inProgressProbesTotal--
// nothing to do if the request errored.
if err != nil {
return
}
// Consider only primary address as refused. This increases the number of
// refused probes, but refused probes are cheap for a server as no dials are made.
if res.AllAddrsRefused {
if s, ok := m.statuses[primaryAddrKey]; ok {
s.AddRefusal(now)
}
return
}
dialAddrKey := string(res.Addr.Bytes())
if dialAddrKey != primaryAddrKey {
if s, ok := m.statuses[primaryAddrKey]; ok {
s.AddRefusal(now)
}
}
// record the result for the dialed address
if s, ok := m.statuses[dialAddrKey]; ok {
s.AddOutcome(now, res.Reachability, maxRecentDialsWindow)
}
}
type dialOutcome struct {
Success bool
At time.Time
}
type addrStatus struct {
Addr ma.Multiaddr
lastRefusalTime time.Time
consecutiveRefusals int
dialTimes []time.Time
outcomes []dialOutcome
}
func (s *addrStatus) Reachability() network.Reachability {
rch, _, _ := s.reachabilityAndCounts()
return rch
}
func (s *addrStatus) RequiredProbeCount(now time.Time) int {
if s.consecutiveRefusals >= maxConsecutiveRefusals {
if now.Sub(s.lastRefusalTime) < recentProbeInterval {
return 0
}
// reset every `recentProbeInterval`
s.lastRefusalTime = time.Time{}
s.consecutiveRefusals = 0
}
// Don't probe if we have probed too many times recently
rd := s.recentDialCount(now)
if rd >= maxRecentDialsPerAddr {
return 0
}
return s.requiredProbeCountForConfirmation(now)
}
func (s *addrStatus) requiredProbeCountForConfirmation(now time.Time) int {
reachability, successes, failures := s.reachabilityAndCounts()
confidence := successes - failures
if confidence < 0 {
confidence = -confidence
}
cnt := targetConfidence - confidence
if cnt > 0 {
return cnt
}
// we have enough confirmations; check if we should refresh
// Should never happen. The confidence logic above should require a few probes.
if len(s.outcomes) == 0 {
return 0
}
lastOutcome := s.outcomes[len(s.outcomes)-1]
// If the last probe result is old, we need to retest
if now.Sub(lastOutcome.At) > highConfidenceAddrProbeInterval {
return 1
}
// if the last probe result was different from reachability, probe again.
switch reachability {
case network.ReachabilityPublic:
if !lastOutcome.Success {
return 1
}
case network.ReachabilityPrivate:
if lastOutcome.Success {
return 1
}
default:
// this should never happen
return 1
}
return 0
}
func (s *addrStatus) AddRefusal(now time.Time) {
s.lastRefusalTime = now
s.consecutiveRefusals++
}
func (s *addrStatus) AddOutcome(at time.Time, rch network.Reachability, windowSize int) {
s.lastRefusalTime = time.Time{}
s.consecutiveRefusals = 0
s.dialTimes = append(s.dialTimes, at)
for i, t := range s.dialTimes {
if at.Sub(t) < recentProbeInterval {
s.dialTimes = slices.Delete(s.dialTimes, 0, i)
break
}
}
s.RemoveBefore(at.Add(-maxProbeResultTTL)) // remove old outcomes
success := false
switch rch {
case network.ReachabilityPublic:
success = true
case network.ReachabilityPrivate:
success = false
default:
return // don't store the outcome if reachability is unknown
}
s.outcomes = append(s.outcomes, dialOutcome{At: at, Success: success})
if len(s.outcomes) > windowSize {
s.outcomes = slices.Delete(s.outcomes, 0, len(s.outcomes)-windowSize)
}
}
// RemoveBefore removes outcomes before t
func (s *addrStatus) RemoveBefore(t time.Time) {
end := 0
for ; end < len(s.outcomes); end++ {
if !s.outcomes[end].At.Before(t) {
break
}
}
s.outcomes = slices.Delete(s.outcomes, 0, end)
}
func (s *addrStatus) recentDialCount(now time.Time) int {
cnt := 0
for _, t := range slices.Backward(s.dialTimes) {
if now.Sub(t) > recentProbeInterval {
break
}
cnt++
}
return cnt
}
func (s *addrStatus) reachabilityAndCounts() (rch network.Reachability, successes int, failures int) {
for _, r := range s.outcomes {
if r.Success {
successes++
} else {
failures++
}
}
if successes-failures >= minConfidence {
return network.ReachabilityPublic, successes, failures
}
if failures-successes >= minConfidence {
return network.ReachabilityPrivate, successes, failures
}
return network.ReachabilityUnknown, successes, failures
}

View File

@@ -0,0 +1,942 @@
package basichost
import (
"context"
"encoding/binary"
"errors"
"fmt"
"math/rand"
"net"
"net/netip"
"slices"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/benbjohnson/clock"
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/p2p/protocol/autonatv2"
ma "github.com/multiformats/go-multiaddr"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestProbeManager(t *testing.T) {
pub1 := ma.StringCast("/ip4/1.1.1.1/tcp/1")
pub2 := ma.StringCast("/ip4/1.1.1.2/tcp/1")
pub3 := ma.StringCast("/ip4/1.1.1.3/tcp/1")
cl := clock.NewMock()
nextProbe := func(pm *probeManager) []autonatv2.Request {
reqs := pm.GetProbe()
if len(reqs) != 0 {
pm.MarkProbeInProgress(reqs)
}
return reqs
}
makeNewProbeManager := func(addrs []ma.Multiaddr) *probeManager {
pm := newProbeManager(cl.Now)
pm.UpdateAddrs(addrs)
return pm
}
t.Run("addrs updates", func(t *testing.T) {
pm := newProbeManager(cl.Now)
pm.UpdateAddrs([]ma.Multiaddr{pub1, pub2})
for {
reqs := nextProbe(pm)
if len(reqs) == 0 {
break
}
pm.CompleteProbe(reqs, autonatv2.Result{Addr: reqs[0].Addr, Idx: 0, Reachability: network.ReachabilityPublic}, nil)
}
reachable, _, _ := pm.AppendConfirmedAddrs(nil, nil, nil)
require.Equal(t, reachable, []ma.Multiaddr{pub1, pub2})
pm.UpdateAddrs([]ma.Multiaddr{pub3})
reachable, _, _ = pm.AppendConfirmedAddrs(nil, nil, nil)
require.Empty(t, reachable)
require.Len(t, pm.statuses, 1)
})
t.Run("inprogress", func(t *testing.T) {
pm := makeNewProbeManager([]ma.Multiaddr{pub1, pub2})
reqs1 := pm.GetProbe()
reqs2 := pm.GetProbe()
require.Equal(t, reqs1, reqs2)
for range targetConfidence {
reqs := nextProbe(pm)
require.Equal(t, reqs, []autonatv2.Request{{Addr: pub1, SendDialData: true}, {Addr: pub2, SendDialData: true}})
}
for range targetConfidence {
reqs := nextProbe(pm)
require.Equal(t, reqs, []autonatv2.Request{{Addr: pub2, SendDialData: true}, {Addr: pub1, SendDialData: true}})
}
reqs := pm.GetProbe()
require.Empty(t, reqs)
})
t.Run("refusals", func(t *testing.T) {
pm := makeNewProbeManager([]ma.Multiaddr{pub1, pub2})
var probes [][]autonatv2.Request
for range targetConfidence {
reqs := nextProbe(pm)
require.Equal(t, reqs, []autonatv2.Request{{Addr: pub1, SendDialData: true}, {Addr: pub2, SendDialData: true}})
probes = append(probes, reqs)
}
// first one refused second one successful
for _, p := range probes {
pm.CompleteProbe(p, autonatv2.Result{Addr: pub2, Idx: 1, Reachability: network.ReachabilityPublic}, nil)
}
// the second address is validated!
probes = nil
for range targetConfidence {
reqs := nextProbe(pm)
require.Equal(t, reqs, []autonatv2.Request{{Addr: pub1, SendDialData: true}})
probes = append(probes, reqs)
}
reqs := pm.GetProbe()
require.Empty(t, reqs)
for _, p := range probes {
pm.CompleteProbe(p, autonatv2.Result{AllAddrsRefused: true}, nil)
}
// all requests refused; no more probes for too many refusals
reqs = pm.GetProbe()
require.Empty(t, reqs)
cl.Add(recentProbeInterval)
reqs = pm.GetProbe()
require.Equal(t, reqs, []autonatv2.Request{{Addr: pub1, SendDialData: true}})
})
t.Run("successes", func(t *testing.T) {
pm := makeNewProbeManager([]ma.Multiaddr{pub1, pub2})
for j := 0; j < 2; j++ {
for i := 0; i < targetConfidence; i++ {
reqs := nextProbe(pm)
pm.CompleteProbe(reqs, autonatv2.Result{Addr: reqs[0].Addr, Idx: 0, Reachability: network.ReachabilityPublic}, nil)
}
}
// all addrs confirmed
reqs := pm.GetProbe()
require.Empty(t, reqs)
cl.Add(highConfidenceAddrProbeInterval + time.Millisecond)
reqs = nextProbe(pm)
require.Equal(t, reqs, []autonatv2.Request{{Addr: pub1, SendDialData: true}, {Addr: pub2, SendDialData: true}})
reqs = nextProbe(pm)
require.Equal(t, reqs, []autonatv2.Request{{Addr: pub2, SendDialData: true}, {Addr: pub1, SendDialData: true}})
})
t.Run("throttling on indeterminate reachability", func(t *testing.T) {
pm := makeNewProbeManager([]ma.Multiaddr{pub1, pub2})
reachability := network.ReachabilityPublic
nextReachability := func() network.Reachability {
if reachability == network.ReachabilityPublic {
reachability = network.ReachabilityPrivate
} else {
reachability = network.ReachabilityPublic
}
return reachability
}
// both addresses are indeterminate
for range 2 * maxRecentDialsPerAddr {
reqs := nextProbe(pm)
pm.CompleteProbe(reqs, autonatv2.Result{Addr: reqs[0].Addr, Idx: 0, Reachability: nextReachability()}, nil)
}
reqs := pm.GetProbe()
require.Empty(t, reqs)
cl.Add(recentProbeInterval + time.Millisecond)
reqs = pm.GetProbe()
require.Equal(t, reqs, []autonatv2.Request{{Addr: pub1, SendDialData: true}, {Addr: pub2, SendDialData: true}})
for range 2 * maxRecentDialsPerAddr {
reqs := nextProbe(pm)
pm.CompleteProbe(reqs, autonatv2.Result{Addr: reqs[0].Addr, Idx: 0, Reachability: nextReachability()}, nil)
}
reqs = pm.GetProbe()
require.Empty(t, reqs)
})
t.Run("reachabilityUpdate", func(t *testing.T) {
pm := makeNewProbeManager([]ma.Multiaddr{pub1, pub2})
for range 2 * targetConfidence {
reqs := nextProbe(pm)
if reqs[0].Addr.Equal(pub1) {
pm.CompleteProbe(reqs, autonatv2.Result{Addr: pub1, Idx: 0, Reachability: network.ReachabilityPublic}, nil)
} else {
pm.CompleteProbe(reqs, autonatv2.Result{Addr: pub2, Idx: 0, Reachability: network.ReachabilityPrivate}, nil)
}
}
reachable, unreachable, _ := pm.AppendConfirmedAddrs(nil, nil, nil)
require.Equal(t, reachable, []ma.Multiaddr{pub1})
require.Equal(t, unreachable, []ma.Multiaddr{pub2})
})
t.Run("expiry", func(t *testing.T) {
pm := makeNewProbeManager([]ma.Multiaddr{pub1})
for range 2 * targetConfidence {
reqs := nextProbe(pm)
pm.CompleteProbe(reqs, autonatv2.Result{Addr: pub1, Idx: 0, Reachability: network.ReachabilityPublic}, nil)
}
reachable, unreachable, _ := pm.AppendConfirmedAddrs(nil, nil, nil)
require.Equal(t, reachable, []ma.Multiaddr{pub1})
require.Empty(t, unreachable)
cl.Add(maxProbeResultTTL + 1*time.Second)
reachable, unreachable, _ = pm.AppendConfirmedAddrs(nil, nil, nil)
require.Empty(t, reachable)
require.Empty(t, unreachable)
})
}
type mockAutoNATClient struct {
F func(context.Context, []autonatv2.Request) (autonatv2.Result, error)
}
func (m mockAutoNATClient) GetReachability(ctx context.Context, reqs []autonatv2.Request) (autonatv2.Result, error) {
return m.F(ctx, reqs)
}
var _ autonatv2Client = mockAutoNATClient{}
func TestAddrsReachabilityTracker(t *testing.T) {
pub1 := ma.StringCast("/ip4/1.1.1.1/tcp/1")
pub2 := ma.StringCast("/ip4/1.1.1.2/tcp/1")
pub3 := ma.StringCast("/ip4/1.1.1.3/tcp/1")
pri := ma.StringCast("/ip4/192.168.1.1/tcp/1")
assertFirstEvent := func(t *testing.T, tr *addrsReachabilityTracker, addrs []ma.Multiaddr) {
select {
case <-tr.reachabilityUpdateCh:
case <-time.After(200 * time.Millisecond):
t.Fatal("expected first event quickly")
}
reachable, unreachable, unknown := tr.ConfirmedAddrs()
require.Empty(t, reachable)
require.Empty(t, unreachable)
require.ElementsMatch(t, unknown, addrs, "%s %s", unknown, addrs)
}
newTracker := func(cli mockAutoNATClient, cl clock.Clock) *addrsReachabilityTracker {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
if cl == nil {
cl = clock.New()
}
tr := &addrsReachabilityTracker{
ctx: ctx,
cancel: cancel,
client: cli,
newAddrs: make(chan []ma.Multiaddr, 1),
reachabilityUpdateCh: make(chan struct{}, 1),
maxConcurrency: 3,
newAddrsProbeDelay: 0 * time.Second,
probeManager: newProbeManager(cl.Now),
clock: cl,
}
err := tr.Start()
require.NoError(t, err)
t.Cleanup(func() {
err := tr.Close()
assert.NoError(t, err)
})
return tr
}
t.Run("simple", func(t *testing.T) {
// pub1 reachable, pub2 unreachable, pub3 ignored
mockClient := mockAutoNATClient{
F: func(_ context.Context, reqs []autonatv2.Request) (autonatv2.Result, error) {
for i, req := range reqs {
if req.Addr.Equal(pub1) {
return autonatv2.Result{Addr: pub1, Idx: i, Reachability: network.ReachabilityPublic}, nil
} else if req.Addr.Equal(pub2) {
return autonatv2.Result{Addr: pub2, Idx: i, Reachability: network.ReachabilityPrivate}, nil
}
}
return autonatv2.Result{AllAddrsRefused: true}, nil
},
}
tr := newTracker(mockClient, nil)
tr.UpdateAddrs([]ma.Multiaddr{pub2, pub1, pri})
assertFirstEvent(t, tr, []ma.Multiaddr{pub1, pub2})
select {
case <-tr.reachabilityUpdateCh:
case <-time.After(2 * time.Second):
t.Fatal("expected reachability update")
}
reachable, unreachable, unknown := tr.ConfirmedAddrs()
require.Equal(t, reachable, []ma.Multiaddr{pub1}, "%s %s", reachable, pub1)
require.Equal(t, unreachable, []ma.Multiaddr{pub2}, "%s %s", unreachable, pub2)
require.Empty(t, unknown)
tr.UpdateAddrs([]ma.Multiaddr{pub3, pub1, pub2, pri})
select {
case <-tr.reachabilityUpdateCh:
case <-time.After(2 * time.Second):
t.Fatal("expected reachability update")
}
reachable, unreachable, unknown = tr.ConfirmedAddrs()
t.Logf("Second probe - Reachable: %v, Unreachable: %v, Unknown: %v", reachable, unreachable, unknown)
require.Equal(t, reachable, []ma.Multiaddr{pub1}, "%s %s", reachable, pub1)
require.Equal(t, unreachable, []ma.Multiaddr{pub2}, "%s %s", unreachable, pub2)
require.Equal(t, unknown, []ma.Multiaddr{pub3}, "%s %s", unknown, pub3)
})
t.Run("confirmed addrs ordering", func(t *testing.T) {
mockClient := mockAutoNATClient{
F: func(_ context.Context, reqs []autonatv2.Request) (autonatv2.Result, error) {
return autonatv2.Result{Addr: reqs[0].Addr, Idx: 0, Reachability: network.ReachabilityPublic}, nil
},
}
tr := newTracker(mockClient, nil)
var addrs []ma.Multiaddr
for i := 0; i < 10; i++ {
addrs = append(addrs, ma.StringCast(fmt.Sprintf("/ip4/1.1.1.1/tcp/%d", i)))
}
slices.SortFunc(addrs, func(a, b ma.Multiaddr) int { return -a.Compare(b) }) // sort in reverse order
tr.UpdateAddrs(addrs)
assertFirstEvent(t, tr, addrs)
select {
case <-tr.reachabilityUpdateCh:
case <-time.After(2 * time.Second):
t.Fatal("expected reachability update")
}
reachable, unreachable, _ := tr.ConfirmedAddrs()
require.Empty(t, unreachable)
orderedAddrs := slices.Clone(addrs)
slices.Reverse(orderedAddrs)
require.Equal(t, reachable, orderedAddrs, "%s %s", reachable, addrs)
})
t.Run("backoff", func(t *testing.T) {
notify := make(chan struct{}, 1)
drainNotify := func() bool {
found := false
for {
select {
case <-notify:
found = true
default:
return found
}
}
}
var allow atomic.Bool
mockClient := mockAutoNATClient{
F: func(_ context.Context, reqs []autonatv2.Request) (autonatv2.Result, error) {
select {
case notify <- struct{}{}:
default:
}
if !allow.Load() {
return autonatv2.Result{}, autonatv2.ErrNoPeers
}
if reqs[0].Addr.Equal(pub1) {
return autonatv2.Result{Addr: pub1, Idx: 0, Reachability: network.ReachabilityPublic}, nil
}
return autonatv2.Result{AllAddrsRefused: true}, nil
},
}
cl := clock.NewMock()
tr := newTracker(mockClient, cl)
// update addrs and wait for initial checks
tr.UpdateAddrs([]ma.Multiaddr{pub1})
assertFirstEvent(t, tr, []ma.Multiaddr{pub1})
// need to update clock after the background goroutine processes the new addrs
time.Sleep(100 * time.Millisecond)
cl.Add(1)
time.Sleep(100 * time.Millisecond)
require.True(t, drainNotify()) // check that we did receive probes
backoffInterval := backoffStartInterval
for i := 0; i < 4; i++ {
drainNotify()
cl.Add(backoffInterval / 2)
select {
case <-notify:
t.Fatal("unexpected call")
case <-time.After(50 * time.Millisecond):
}
cl.Add(backoffInterval/2 + 1) // +1 to push it slightly over the backoff interval
backoffInterval *= 2
select {
case <-notify:
case <-time.After(1 * time.Second):
t.Fatal("expected probe")
}
reachable, unreachable, _ := tr.ConfirmedAddrs()
require.Empty(t, reachable)
require.Empty(t, unreachable)
}
allow.Store(true)
drainNotify()
cl.Add(backoffInterval + 1)
select {
case <-tr.reachabilityUpdateCh:
case <-time.After(1 * time.Second):
t.Fatal("unexpected reachability update")
}
reachable, unreachable, _ := tr.ConfirmedAddrs()
require.Equal(t, reachable, []ma.Multiaddr{pub1})
require.Empty(t, unreachable)
})
t.Run("event update", func(t *testing.T) {
// allow minConfidence probes to pass
called := make(chan struct{}, minConfidence)
notify := make(chan struct{})
mockClient := mockAutoNATClient{
F: func(_ context.Context, _ []autonatv2.Request) (autonatv2.Result, error) {
select {
case called <- struct{}{}:
notify <- struct{}{}
return autonatv2.Result{Addr: pub1, Idx: 0, Reachability: network.ReachabilityPublic}, nil
default:
return autonatv2.Result{AllAddrsRefused: true}, nil
}
},
}
tr := newTracker(mockClient, nil)
tr.UpdateAddrs([]ma.Multiaddr{pub1})
assertFirstEvent(t, tr, []ma.Multiaddr{pub1})
for i := 0; i < minConfidence; i++ {
select {
case <-notify:
case <-time.After(1 * time.Second):
t.Fatal("expected call to autonat client")
}
}
select {
case <-tr.reachabilityUpdateCh:
reachable, unreachable, _ := tr.ConfirmedAddrs()
require.Equal(t, reachable, []ma.Multiaddr{pub1})
require.Empty(t, unreachable)
case <-time.After(1 * time.Second):
t.Fatal("expected reachability update")
}
tr.UpdateAddrs([]ma.Multiaddr{pub1}) // same addrs shouldn't get update
select {
case <-tr.reachabilityUpdateCh:
t.Fatal("didn't expect reachability update")
case <-time.After(100 * time.Millisecond):
}
tr.UpdateAddrs([]ma.Multiaddr{pub2})
select {
case <-tr.reachabilityUpdateCh:
reachable, unreachable, _ := tr.ConfirmedAddrs()
require.Empty(t, reachable)
require.Empty(t, unreachable)
case <-time.After(1 * time.Second):
t.Fatal("expected reachability update")
}
})
t.Run("refresh after reset interval", func(t *testing.T) {
notify := make(chan struct{}, 1)
drainNotify := func() bool {
found := false
for {
select {
case <-notify:
found = true
default:
return found
}
}
}
mockClient := mockAutoNATClient{
F: func(_ context.Context, reqs []autonatv2.Request) (autonatv2.Result, error) {
select {
case notify <- struct{}{}:
default:
}
if reqs[0].Addr.Equal(pub1) {
return autonatv2.Result{Addr: pub1, Idx: 0, Reachability: network.ReachabilityPublic}, nil
}
return autonatv2.Result{AllAddrsRefused: true}, nil
},
}
cl := clock.NewMock()
tr := newTracker(mockClient, cl)
// update addrs and wait for initial checks
tr.UpdateAddrs([]ma.Multiaddr{pub1})
assertFirstEvent(t, tr, []ma.Multiaddr{pub1})
// need to update clock after the background goroutine processes the new addrs
time.Sleep(100 * time.Millisecond)
cl.Add(1)
time.Sleep(100 * time.Millisecond)
require.True(t, drainNotify()) // check that we did receive probes
cl.Add(highConfidenceAddrProbeInterval / 2)
select {
case <-notify:
t.Fatal("unexpected call")
case <-time.After(50 * time.Millisecond):
}
cl.Add(highConfidenceAddrProbeInterval/2 + defaultReachabilityRefreshInterval) // defaultResetInterval for the next probe time
select {
case <-notify:
case <-time.After(1 * time.Second):
t.Fatal("expected probe")
}
})
}
func TestRefreshReachability(t *testing.T) {
pub1 := ma.StringCast("/ip4/1.1.1.1/tcp/1")
pub2 := ma.StringCast("/ip4/1.1.1.1/tcp/2")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
newTracker := func(client autonatv2Client, pm *probeManager) *addrsReachabilityTracker {
return &addrsReachabilityTracker{
probeManager: pm,
client: client,
clock: clock.New(),
maxConcurrency: 3,
ctx: ctx,
cancel: cancel,
}
}
t.Run("backoff on ErrNoValidPeers", func(t *testing.T) {
mockClient := mockAutoNATClient{
F: func(_ context.Context, _ []autonatv2.Request) (autonatv2.Result, error) {
return autonatv2.Result{}, autonatv2.ErrNoPeers
},
}
addrTracker := newProbeManager(time.Now)
addrTracker.UpdateAddrs([]ma.Multiaddr{pub1})
r := newTracker(mockClient, addrTracker)
res := r.refreshReachability()
require.True(t, <-res.BackoffCh)
require.Equal(t, addrTracker.InProgressProbes(), 0)
})
t.Run("returns backoff on errTooManyConsecutiveFailures", func(t *testing.T) {
// Create a client that always returns ErrDialRefused
mockClient := mockAutoNATClient{
F: func(_ context.Context, _ []autonatv2.Request) (autonatv2.Result, error) {
return autonatv2.Result{}, errors.New("test error")
},
}
pm := newProbeManager(time.Now)
pm.UpdateAddrs([]ma.Multiaddr{pub1})
r := newTracker(mockClient, pm)
result := r.refreshReachability()
require.True(t, <-result.BackoffCh)
require.Equal(t, pm.InProgressProbes(), 0)
})
t.Run("quits on cancellation", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
block := make(chan struct{})
mockClient := mockAutoNATClient{
F: func(_ context.Context, _ []autonatv2.Request) (autonatv2.Result, error) {
block <- struct{}{}
return autonatv2.Result{}, nil
},
}
pm := newProbeManager(time.Now)
pm.UpdateAddrs([]ma.Multiaddr{pub1})
r := &addrsReachabilityTracker{
ctx: ctx,
cancel: cancel,
client: mockClient,
probeManager: pm,
clock: clock.New(),
}
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
result := r.refreshReachability()
assert.False(t, <-result.BackoffCh)
assert.Equal(t, pm.InProgressProbes(), 0)
}()
cancel()
time.Sleep(50 * time.Millisecond) // wait for the cancellation to be processed
outer:
for i := 0; i < defaultMaxConcurrency; i++ {
select {
case <-block:
default:
break outer
}
}
select {
case <-block:
t.Fatal("expected no more requests")
case <-time.After(50 * time.Millisecond):
}
wg.Wait()
})
t.Run("handles refusals", func(t *testing.T) {
pub1, _ := ma.NewMultiaddr("/ip4/1.1.1.1/tcp/1")
mockClient := mockAutoNATClient{
F: func(_ context.Context, reqs []autonatv2.Request) (autonatv2.Result, error) {
for i, req := range reqs {
if req.Addr.Equal(pub1) {
return autonatv2.Result{Addr: pub1, Idx: i, Reachability: network.ReachabilityPublic}, nil
}
}
return autonatv2.Result{AllAddrsRefused: true}, nil
},
}
pm := newProbeManager(time.Now)
pm.UpdateAddrs([]ma.Multiaddr{pub2, pub1})
r := newTracker(mockClient, pm)
result := r.refreshReachability()
require.False(t, <-result.BackoffCh)
reachable, unreachable, _ := pm.AppendConfirmedAddrs(nil, nil, nil)
require.Equal(t, reachable, []ma.Multiaddr{pub1})
require.Empty(t, unreachable)
require.Equal(t, pm.InProgressProbes(), 0)
})
t.Run("handles completions", func(t *testing.T) {
mockClient := mockAutoNATClient{
F: func(_ context.Context, reqs []autonatv2.Request) (autonatv2.Result, error) {
for i, req := range reqs {
if req.Addr.Equal(pub1) {
return autonatv2.Result{Addr: pub1, Idx: i, Reachability: network.ReachabilityPublic}, nil
}
if req.Addr.Equal(pub2) {
return autonatv2.Result{Addr: pub2, Idx: i, Reachability: network.ReachabilityPrivate}, nil
}
}
return autonatv2.Result{AllAddrsRefused: true}, nil
},
}
pm := newProbeManager(time.Now)
pm.UpdateAddrs([]ma.Multiaddr{pub2, pub1})
r := newTracker(mockClient, pm)
result := r.refreshReachability()
require.False(t, <-result.BackoffCh)
reachable, unreachable, _ := pm.AppendConfirmedAddrs(nil, nil, nil)
require.Equal(t, reachable, []ma.Multiaddr{pub1})
require.Equal(t, unreachable, []ma.Multiaddr{pub2})
require.Equal(t, pm.InProgressProbes(), 0)
})
}
func TestAddrStatusProbeCount(t *testing.T) {
cases := []struct {
inputs string
wantRequiredProbes int
wantReachability network.Reachability
}{
{
inputs: "",
wantRequiredProbes: 3,
wantReachability: network.ReachabilityUnknown,
},
{
inputs: "S",
wantRequiredProbes: 2,
wantReachability: network.ReachabilityUnknown,
},
{
inputs: "SS",
wantRequiredProbes: 1,
wantReachability: network.ReachabilityPublic,
},
{
inputs: "SSS",
wantRequiredProbes: 0,
wantReachability: network.ReachabilityPublic,
},
{
inputs: "SSSSSSSF",
wantRequiredProbes: 1,
wantReachability: network.ReachabilityPublic,
},
{
inputs: "SFSFSSSS",
wantRequiredProbes: 0,
wantReachability: network.ReachabilityPublic,
},
{
inputs: "SSSSSFSF",
wantRequiredProbes: 2,
wantReachability: network.ReachabilityUnknown,
},
{
inputs: "FF",
wantRequiredProbes: 1,
wantReachability: network.ReachabilityPrivate,
},
}
for _, c := range cases {
t.Run(c.inputs, func(t *testing.T) {
now := time.Time{}.Add(1 * time.Second)
ao := addrStatus{}
for _, r := range c.inputs {
if r == 'S' {
ao.AddOutcome(now, network.ReachabilityPublic, 5)
} else {
ao.AddOutcome(now, network.ReachabilityPrivate, 5)
}
now = now.Add(1 * time.Second)
}
require.Equal(t, ao.RequiredProbeCount(now), c.wantRequiredProbes)
require.Equal(t, ao.Reachability(), c.wantReachability)
if c.wantRequiredProbes == 0 {
now = now.Add(highConfidenceAddrProbeInterval + 10*time.Microsecond)
require.Equal(t, ao.RequiredProbeCount(now), 1)
}
now = now.Add(1 * time.Second)
ao.RemoveBefore(now)
require.Len(t, ao.outcomes, 0)
})
}
}
func BenchmarkAddrTracker(b *testing.B) {
cl := clock.NewMock()
t := newProbeManager(cl.Now)
addrs := make([]ma.Multiaddr, 20)
for i := range addrs {
addrs[i] = ma.StringCast(fmt.Sprintf("/ip4/1.1.1.1/tcp/%d", rand.Intn(1000)))
}
t.UpdateAddrs(addrs)
b.ReportAllocs()
b.ResetTimer()
p := t.GetProbe()
for i := 0; i < b.N; i++ {
pp := t.GetProbe()
if len(pp) == 0 {
pp = p
}
t.MarkProbeInProgress(pp)
t.CompleteProbe(pp, autonatv2.Result{Addr: pp[0].Addr, Idx: 0, Reachability: network.ReachabilityPublic}, nil)
}
}
func FuzzAddrsReachabilityTracker(f *testing.F) {
type autonatv2Response struct {
Result autonatv2.Result
Err error
}
newMockClient := func(b []byte) mockAutoNATClient {
count := 0
return mockAutoNATClient{
F: func(_ context.Context, reqs []autonatv2.Request) (autonatv2.Result, error) {
if len(b) == 0 {
return autonatv2.Result{}, nil
}
count = (count + 1) % len(b)
if b[count]%3 == 0 {
// some address confirmed
c1 := (count + 1) % len(b)
c2 := (count + 2) % len(b)
rch := network.Reachability(b[c1] % 3)
n := int(b[c2]) % len(reqs)
return autonatv2.Result{
Addr: reqs[n].Addr,
Idx: n,
Reachability: rch,
}, nil
}
outcomes := []autonatv2Response{
{Result: autonatv2.Result{AllAddrsRefused: true}},
{Err: errors.New("test error")},
{Err: autonatv2.ErrPrivateAddrs},
{Err: autonatv2.ErrNoPeers},
{Result: autonatv2.Result{}, Err: nil},
{Result: autonatv2.Result{Addr: reqs[0].Addr, Idx: 0, Reachability: network.ReachabilityPublic}},
{Result: autonatv2.Result{
Addr: reqs[0].Addr,
Idx: 0,
Reachability: network.ReachabilityPublic,
AllAddrsRefused: true,
}},
{Result: autonatv2.Result{
Addr: reqs[0].Addr,
Idx: len(reqs) - 1, // invalid idx
Reachability: network.ReachabilityPublic,
AllAddrsRefused: false,
}},
}
outcome := outcomes[int(b[count])%len(outcomes)]
return outcome.Result, outcome.Err
},
}
}
// TODO: Move this to go-multiaddrs
getProto := func(protos []byte) ma.Multiaddr {
protoType := 0
if len(protos) > 0 {
protoType = int(protos[0])
}
port1, port2 := 0, 0
if len(protos) > 1 {
port1 = int(protos[1])
}
if len(protos) > 2 {
port2 = int(protos[2])
}
protoTemplates := []string{
"/tcp/%d/",
"/udp/%d/",
"/udp/%d/quic-v1/",
"/udp/%d/quic-v1/tcp/%d",
"/udp/%d/quic-v1/webtransport/",
"/udp/%d/webrtc/",
"/udp/%d/webrtc-direct/",
"/unix/hello/",
}
s := protoTemplates[protoType%len(protoTemplates)]
port1 %= (1 << 16)
if strings.Count(s, "%d") == 1 {
return ma.StringCast(fmt.Sprintf(s, port1))
}
port2 %= (1 << 16)
return ma.StringCast(fmt.Sprintf(s, port1, port2))
}
getIP := func(ips []byte) ma.Multiaddr {
ipType := 0
if len(ips) > 0 {
ipType = int(ips[0])
}
ips = ips[1:]
var x, y int64
split := 128 / 8
if len(ips) < split {
split = len(ips)
}
var b [8]byte
copy(b[:], ips[:split])
x = int64(binary.LittleEndian.Uint64(b[:]))
clear(b[:])
copy(b[:], ips[split:])
y = int64(binary.LittleEndian.Uint64(b[:]))
var ip netip.Addr
switch ipType % 3 {
case 0:
ip = netip.AddrFrom4([4]byte{byte(x), byte(x >> 8), byte(x >> 16), byte(x >> 24)})
return ma.StringCast(fmt.Sprintf("/ip4/%s/", ip))
case 1:
pubIP := net.ParseIP("2005::") // Public IP address
x := int64(binary.LittleEndian.Uint64(pubIP[0:8]))
ip = netip.AddrFrom16([16]byte{
byte(x), byte(x >> 8), byte(x >> 16), byte(x >> 24),
byte(x >> 32), byte(x >> 40), byte(x >> 48), byte(x >> 56),
byte(y), byte(y >> 8), byte(y >> 16), byte(y >> 24),
byte(y >> 32), byte(y >> 40), byte(y >> 48), byte(y >> 56),
})
return ma.StringCast(fmt.Sprintf("/ip6/%s/", ip))
default:
ip := netip.AddrFrom16([16]byte{
byte(x), byte(x >> 8), byte(x >> 16), byte(x >> 24),
byte(x >> 32), byte(x >> 40), byte(x >> 48), byte(x >> 56),
byte(y), byte(y >> 8), byte(y >> 16), byte(y >> 24),
byte(y >> 32), byte(y >> 40), byte(y >> 48), byte(y >> 56),
})
return ma.StringCast(fmt.Sprintf("/ip6/%s/", ip))
}
}
getAddr := func(addrType int, ips, protos []byte) ma.Multiaddr {
switch addrType % 4 {
case 0:
return getIP(ips).Encapsulate(getProto(protos))
case 1:
return getProto(protos)
case 2:
return nil
default:
return getIP(ips).Encapsulate(getProto(protos))
}
}
getDNSAddr := func(hostNameBytes, protos []byte) ma.Multiaddr {
hostName := strings.ReplaceAll(string(hostNameBytes), "\\", "")
hostName = strings.ReplaceAll(hostName, "/", "")
if hostName == "" {
hostName = "localhost"
}
dnsType := 0
if len(hostNameBytes) > 0 {
dnsType = int(hostNameBytes[0])
}
dnsProtos := []string{"dns", "dns4", "dns6", "dnsaddr"}
da := ma.StringCast(fmt.Sprintf("/%s/%s/", dnsProtos[dnsType%len(dnsProtos)], hostName))
return da.Encapsulate(getProto(protos))
}
const maxAddrs = 1000
getAddrs := func(numAddrs int, ips, protos, hostNames []byte) []ma.Multiaddr {
if len(ips) == 0 || len(protos) == 0 || len(hostNames) == 0 {
return nil
}
numAddrs = ((numAddrs % maxAddrs) + maxAddrs) % maxAddrs
addrs := make([]ma.Multiaddr, numAddrs)
ipIdx := 0
protoIdx := 0
for i := range numAddrs {
addrs[i] = getAddr(i, ips[ipIdx:], protos[protoIdx:])
ipIdx = (ipIdx + 1) % len(ips)
protoIdx = (protoIdx + 1) % len(protos)
}
maxDNSAddrs := 10
protoIdx = 0
for i := 0; i < len(hostNames) && i < maxDNSAddrs; i += 2 {
ed := min(i+2, len(hostNames))
addrs = append(addrs, getDNSAddr(hostNames[i:ed], protos[protoIdx:]))
protoIdx = (protoIdx + 1) % len(protos)
}
return addrs
}
cl := clock.NewMock()
f.Fuzz(func(t *testing.T, numAddrs int, ips, protos, hostNames, autonatResponses []byte) {
tr := newAddrsReachabilityTracker(newMockClient(autonatResponses), nil, cl)
require.NoError(t, tr.Start())
tr.UpdateAddrs(getAddrs(numAddrs, ips, protos, hostNames))
// fuzz tests need to finish in 10 seconds for some reason
// https://github.com/golang/go/issues/48157
// https://github.com/golang/go/commit/5d24203c394e6b64c42a9f69b990d94cb6c8aad4#diff-4e3b9481b8794eb058998e2bec389d3db7a23c54e67ac0f7259a3a5d2c79fd04R474-R483
const maxIters = 20
for range maxIters {
cl.Add(5 * time.Minute)
time.Sleep(100 * time.Millisecond)
}
require.NoError(t, tr.Close())
})
}

View File

@@ -156,8 +156,8 @@ type HostOpts struct {
// DisableIdentifyAddressDiscovery disables address discovery using peer provided observed addresses in identify // DisableIdentifyAddressDiscovery disables address discovery using peer provided observed addresses in identify
DisableIdentifyAddressDiscovery bool DisableIdentifyAddressDiscovery bool
EnableAutoNATv2 bool
AutoNATv2Dialer host.Host AutoNATv2 *autonatv2.AutoNAT
} }
// NewHost constructs a new *BasicHost and activates it by attaching its stream and connection handlers to the given inet.Network. // NewHost constructs a new *BasicHost and activates it by attaching its stream and connection handlers to the given inet.Network.
@@ -236,7 +236,16 @@ func NewHost(n network.Network, opts *HostOpts) (*BasicHost, error) {
}); ok { }); ok {
tfl = s.TransportForListening tfl = s.TransportForListening
} }
h.addressManager, err = newAddrsManager(h.eventbus, natmgr, addrFactory, h.Network().ListenAddresses, tfl, h.ids, h.addrsUpdatedChan)
if opts.AutoNATv2 != nil {
h.autonatv2 = opts.AutoNATv2
}
var autonatv2Client autonatv2Client // avoid typed nil errors
if h.autonatv2 != nil {
autonatv2Client = h.autonatv2
}
h.addressManager, err = newAddrsManager(h.eventbus, natmgr, addrFactory, h.Network().ListenAddresses, tfl, h.ids, h.addrsUpdatedChan, autonatv2Client)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create address service: %w", err) return nil, fmt.Errorf("failed to create address service: %w", err)
} }
@@ -283,17 +292,6 @@ func NewHost(n network.Network, opts *HostOpts) (*BasicHost, error) {
h.pings = ping.NewPingService(h) h.pings = ping.NewPingService(h)
} }
if opts.EnableAutoNATv2 {
var mt autonatv2.MetricsTracer
if opts.EnableMetrics {
mt = autonatv2.NewMetricsTracer(opts.PrometheusRegisterer)
}
h.autonatv2, err = autonatv2.New(h, opts.AutoNATv2Dialer, autonatv2.WithMetricsTracer(mt))
if err != nil {
return nil, fmt.Errorf("failed to create autonatv2: %w", err)
}
}
if !h.disableSignedPeerRecord { if !h.disableSignedPeerRecord {
h.signKey = h.Peerstore().PrivKey(h.ID()) h.signKey = h.Peerstore().PrivKey(h.ID())
cab, ok := peerstore.GetCertifiedAddrBook(h.Peerstore()) cab, ok := peerstore.GetCertifiedAddrBook(h.Peerstore())
@@ -320,7 +318,7 @@ func NewHost(n network.Network, opts *HostOpts) (*BasicHost, error) {
func (h *BasicHost) Start() { func (h *BasicHost) Start() {
h.psManager.Start() h.psManager.Start()
if h.autonatv2 != nil { if h.autonatv2 != nil {
err := h.autonatv2.Start() err := h.autonatv2.Start(h)
if err != nil { if err != nil {
log.Errorf("autonat v2 failed to start: %s", err) log.Errorf("autonat v2 failed to start: %s", err)
} }
@@ -754,6 +752,16 @@ func (h *BasicHost) AllAddrs() []ma.Multiaddr {
return h.addressManager.DirectAddrs() return h.addressManager.DirectAddrs()
} }
// ConfirmedAddrs returns all addresses of the host grouped by their reachability
// as verified by autonatv2.
//
// Experimental: This API may change in the future without deprecation.
//
// Requires AutoNATv2 to be enabled.
func (h *BasicHost) ConfirmedAddrs() (reachable []ma.Multiaddr, unreachable []ma.Multiaddr, unknown []ma.Multiaddr) {
return h.addressManager.ConfirmedAddrs()
}
func trimHostAddrList(addrs []ma.Multiaddr, maxSize int) []ma.Multiaddr { func trimHostAddrList(addrs []ma.Multiaddr, maxSize int) []ma.Multiaddr {
totalSize := 0 totalSize := 0
for _, a := range addrs { for _, a := range addrs {
@@ -836,7 +844,6 @@ func (h *BasicHost) Close() error {
if h.cmgr != nil { if h.cmgr != nil {
h.cmgr.Close() h.cmgr.Close()
} }
h.addressManager.Close() h.addressManager.Close()
if h.ids != nil { if h.ids != nil {

View File

@@ -47,6 +47,7 @@ func TestHostSimple(t *testing.T) {
h1.Start() h1.Start()
h2, err := NewHost(swarmt.GenSwarm(t), nil) h2, err := NewHost(swarmt.GenSwarm(t), nil)
require.NoError(t, err) require.NoError(t, err)
defer h2.Close() defer h2.Close()
h2.Start() h2.Start()
@@ -211,6 +212,7 @@ func TestAllAddrs(t *testing.T) {
// no listen addrs // no listen addrs
h, err := NewHost(swarmt.GenSwarm(t, swarmt.OptDialOnly), nil) h, err := NewHost(swarmt.GenSwarm(t, swarmt.OptDialOnly), nil)
require.NoError(t, err) require.NoError(t, err)
h.Start()
defer h.Close() defer h.Close()
require.Nil(t, h.AllAddrs()) require.Nil(t, h.AllAddrs())

View File

@@ -5,6 +5,9 @@ import (
"net/netip" "net/netip"
"slices" "slices"
"sync" "sync"
"time"
"github.com/libp2p/go-libp2p/x/rate"
) )
type ConnLimitPerSubnet struct { type ConnLimitPerSubnet struct {
@@ -283,3 +286,58 @@ func (cl *connLimiter) rmConn(ip netip.Addr) {
} }
} }
} }
// handshakeDuration is a higher end estimate of QUIC handshake time
const handshakeDuration = 5 * time.Second
// sourceAddressRPS is the refill rate for the source address verification rate limiter.
// A spoofed address if not verified will take a connLimiter token for handshakeDuration.
// Slow refill rate here favours increasing latency(because of address verification) in
// exchange for reducing the chances of spoofing successfully causing a DoS.
const sourceAddressRPS = float64(1.0*time.Second) / (2 * float64(handshakeDuration))
// newVerifySourceAddressRateLimiter returns a rate limiter for verifying source addresses.
// The returned limiter allows maxAllowedConns / 2 unverified addresses to begin handshake.
// This ensures that in the event someone is spoofing IPs, 1/2 the maximum allowed connections
// will be able to connect, although they will have increased latency because of address
// verification.
func newVerifySourceAddressRateLimiter(cl *connLimiter) *rate.Limiter {
networkPrefixLimits := make([]rate.PrefixLimit, 0, len(cl.networkPrefixLimitV4)+len(cl.networkPrefixLimitV6))
for _, l := range cl.networkPrefixLimitV4 {
networkPrefixLimits = append(networkPrefixLimits, rate.PrefixLimit{
Prefix: l.Network,
Limit: rate.Limit{RPS: sourceAddressRPS, Burst: l.ConnCount / 2},
})
}
for _, l := range cl.networkPrefixLimitV6 {
networkPrefixLimits = append(networkPrefixLimits, rate.PrefixLimit{
Prefix: l.Network,
Limit: rate.Limit{RPS: sourceAddressRPS, Burst: l.ConnCount / 2},
})
}
ipv4SubnetLimits := make([]rate.SubnetLimit, 0, len(cl.connLimitPerSubnetV4))
for _, l := range cl.connLimitPerSubnetV4 {
ipv4SubnetLimits = append(ipv4SubnetLimits, rate.SubnetLimit{
PrefixLength: l.PrefixLength,
Limit: rate.Limit{RPS: sourceAddressRPS, Burst: l.ConnCount / 2},
})
}
ipv6SubnetLimits := make([]rate.SubnetLimit, 0, len(cl.connLimitPerSubnetV6))
for _, l := range cl.connLimitPerSubnetV6 {
ipv6SubnetLimits = append(ipv6SubnetLimits, rate.SubnetLimit{
PrefixLength: l.PrefixLength,
Limit: rate.Limit{RPS: sourceAddressRPS, Burst: l.ConnCount / 2},
})
}
return &rate.Limiter{
NetworkPrefixLimits: networkPrefixLimits,
SubnetRateLimiter: rate.SubnetLimiter{
IPv4SubnetLimits: ipv4SubnetLimits,
IPv6SubnetLimits: ipv6SubnetLimits,
GracePeriod: 1 * time.Minute,
},
}
}

View File

@@ -5,7 +5,9 @@ import (
"net" "net"
"net/netip" "net/netip"
"testing" "testing"
"time"
"github.com/libp2p/go-libp2p/x/rate"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -228,3 +230,163 @@ func TestSortedNetworkPrefixLimits(t *testing.T) {
} }
require.EqualValues(t, sorted, npLimits) require.EqualValues(t, sorted, npLimits)
} }
func TestNewVerifySourceAddressRateLimiter(t *testing.T) {
testCases := []struct {
name string
cl *connLimiter
expected *rate.Limiter
}{
{
name: "basic configuration",
cl: &connLimiter{
networkPrefixLimitV4: []NetworkPrefixLimit{
{
Network: netip.MustParsePrefix("192.168.0.0/16"),
ConnCount: 10,
},
},
networkPrefixLimitV6: []NetworkPrefixLimit{
{
Network: netip.MustParsePrefix("2001:db8::/32"),
ConnCount: 20,
},
},
connLimitPerSubnetV4: []ConnLimitPerSubnet{
{
PrefixLength: 24,
ConnCount: 5,
},
},
connLimitPerSubnetV6: []ConnLimitPerSubnet{
{
PrefixLength: 56,
ConnCount: 8,
},
},
},
expected: &rate.Limiter{
NetworkPrefixLimits: []rate.PrefixLimit{
{
Prefix: netip.MustParsePrefix("192.168.0.0/16"),
Limit: rate.Limit{RPS: sourceAddressRPS, Burst: 5},
},
{
Prefix: netip.MustParsePrefix("2001:db8::/32"),
Limit: rate.Limit{RPS: sourceAddressRPS, Burst: 10},
},
},
SubnetRateLimiter: rate.SubnetLimiter{
IPv4SubnetLimits: []rate.SubnetLimit{
{
PrefixLength: 24,
Limit: rate.Limit{RPS: sourceAddressRPS, Burst: 2},
},
},
IPv6SubnetLimits: []rate.SubnetLimit{
{
PrefixLength: 56,
Limit: rate.Limit{RPS: sourceAddressRPS, Burst: 4},
},
},
GracePeriod: 1 * time.Minute,
},
},
},
{
name: "empty configuration",
cl: &connLimiter{},
expected: &rate.Limiter{
NetworkPrefixLimits: []rate.PrefixLimit{},
SubnetRateLimiter: rate.SubnetLimiter{
IPv4SubnetLimits: []rate.SubnetLimit{},
IPv6SubnetLimits: []rate.SubnetLimit{},
GracePeriod: 1 * time.Minute,
},
},
},
{
name: "multiple network prefixes",
cl: &connLimiter{
networkPrefixLimitV4: []NetworkPrefixLimit{
{
Network: netip.MustParsePrefix("192.168.0.0/16"),
ConnCount: 10,
},
{
Network: netip.MustParsePrefix("10.0.0.0/8"),
ConnCount: 20,
},
},
connLimitPerSubnetV4: []ConnLimitPerSubnet{
{
PrefixLength: 24,
ConnCount: 5,
},
{
PrefixLength: 16,
ConnCount: 10,
},
},
},
expected: &rate.Limiter{
NetworkPrefixLimits: []rate.PrefixLimit{
{
Prefix: netip.MustParsePrefix("192.168.0.0/16"),
Limit: rate.Limit{RPS: sourceAddressRPS, Burst: 5},
},
{
Prefix: netip.MustParsePrefix("10.0.0.0/8"),
Limit: rate.Limit{RPS: sourceAddressRPS, Burst: 10},
},
},
SubnetRateLimiter: rate.SubnetLimiter{
IPv4SubnetLimits: []rate.SubnetLimit{
{
PrefixLength: 24,
Limit: rate.Limit{RPS: sourceAddressRPS, Burst: 2},
},
{
PrefixLength: 16,
Limit: rate.Limit{RPS: sourceAddressRPS, Burst: 5},
},
},
IPv6SubnetLimits: []rate.SubnetLimit{},
GracePeriod: 1 * time.Minute,
},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actual := newVerifySourceAddressRateLimiter(tc.cl)
require.Equal(t, len(tc.expected.NetworkPrefixLimits), len(actual.NetworkPrefixLimits))
for i, expected := range tc.expected.NetworkPrefixLimits {
actual := actual.NetworkPrefixLimits[i]
require.Equal(t, expected.Prefix, actual.Prefix)
require.Equal(t, expected.RPS, actual.RPS)
require.Equal(t, expected.Burst, actual.Burst)
}
require.Equal(t, len(tc.expected.SubnetRateLimiter.IPv4SubnetLimits), len(actual.SubnetRateLimiter.IPv4SubnetLimits))
for i, expected := range tc.expected.SubnetRateLimiter.IPv4SubnetLimits {
actual := actual.SubnetRateLimiter.IPv4SubnetLimits[i]
require.Equal(t, expected.PrefixLength, actual.PrefixLength)
require.Equal(t, expected.RPS, actual.RPS)
require.Equal(t, expected.Burst, actual.Burst)
}
require.Equal(t, len(tc.expected.SubnetRateLimiter.IPv6SubnetLimits), len(actual.SubnetRateLimiter.IPv6SubnetLimits))
for i, expected := range tc.expected.SubnetRateLimiter.IPv6SubnetLimits {
actual := actual.SubnetRateLimiter.IPv6SubnetLimits[i]
require.Equal(t, expected.PrefixLength, actual.PrefixLength)
require.Equal(t, expected.RPS, actual.RPS)
require.Equal(t, expected.Burst, actual.Burst)
}
require.Equal(t, tc.expected.SubnetRateLimiter.GracePeriod, actual.SubnetRateLimiter.GracePeriod)
})
}
}

View File

@@ -0,0 +1,59 @@
package rcmgr
import (
"net/netip"
"time"
"github.com/libp2p/go-libp2p/x/rate"
)
var defaultIPv4SubnetLimits = []rate.SubnetLimit{
{
PrefixLength: 32,
Limit: rate.Limit{RPS: 0.2, Burst: 2 * defaultMaxConcurrentConns},
},
}
var defaultIPv6SubnetLimits = []rate.SubnetLimit{
{
PrefixLength: 56,
Limit: rate.Limit{RPS: 0.2, Burst: 2 * defaultMaxConcurrentConns},
},
{
PrefixLength: 48,
Limit: rate.Limit{RPS: 0.5, Burst: 10 * defaultMaxConcurrentConns},
},
}
// defaultNetworkPrefixLimits ensure that all connections on localhost always succeed
var defaultNetworkPrefixLimits = []rate.PrefixLimit{
{
Prefix: netip.MustParsePrefix("127.0.0.0/8"),
Limit: rate.Limit{},
},
{
Prefix: netip.MustParsePrefix("::1/128"),
Limit: rate.Limit{},
},
}
// WithConnRateLimiters sets a custom rate limiter for new connections.
// connRateLimiter is used for OpenConnection calls
func WithConnRateLimiters(connRateLimiter *rate.Limiter) Option {
return func(rm *resourceManager) error {
rm.connRateLimiter = connRateLimiter
return nil
}
}
func newConnRateLimiter() *rate.Limiter {
return &rate.Limiter{
NetworkPrefixLimits: defaultNetworkPrefixLimits,
GlobalLimit: rate.Limit{},
SubnetRateLimiter: rate.SubnetLimiter{
IPv4SubnetLimits: defaultIPv4SubnetLimits,
IPv6SubnetLimits: defaultIPv6SubnetLimits,
GracePeriod: 1 * time.Minute,
},
}
}

View File

@@ -2,7 +2,9 @@ package rcmgr
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net"
"net/netip" "net/netip"
"strings" "strings"
"sync" "sync"
@@ -11,6 +13,7 @@ import (
"github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/core/protocol" "github.com/libp2p/go-libp2p/core/protocol"
"github.com/libp2p/go-libp2p/x/rate"
logging "github.com/ipfs/go-log/v2" logging "github.com/ipfs/go-log/v2"
"github.com/multiformats/go-multiaddr" "github.com/multiformats/go-multiaddr"
@@ -23,6 +26,8 @@ type resourceManager struct {
limits Limiter limits Limiter
connLimiter *connLimiter connLimiter *connLimiter
connRateLimiter *rate.Limiter
verifySourceAddressRateLimiter *rate.Limiter
trace *trace trace *trace
metrics *metrics metrics *metrics
@@ -140,6 +145,7 @@ func NewResourceManager(limits Limiter, opts ...Option) (network.ResourceManager
svc: make(map[string]*serviceScope), svc: make(map[string]*serviceScope),
proto: make(map[protocol.ID]*protocolScope), proto: make(map[protocol.ID]*protocolScope),
peer: make(map[peer.ID]*peerScope), peer: make(map[peer.ID]*peerScope),
connRateLimiter: newConnRateLimiter(),
} }
for _, opt := range opts { for _, opt := range opts {
@@ -169,6 +175,7 @@ func NewResourceManager(limits Limiter, opts ...Option) (network.ResourceManager
}) })
} }
} }
r.verifySourceAddressRateLimiter = newVerifySourceAddressRateLimiter(r.connLimiter)
if !r.disableMetrics { if !r.disableMetrics {
var sr TraceReporter var sr TraceReporter
@@ -338,7 +345,22 @@ func (r *resourceManager) nextStreamId() int64 {
return r.streamId return r.streamId
} }
// VerifySourceAddress tells the transport to verify the peer's IP address before
// initiating a handshake.
func (r *resourceManager) VerifySourceAddress(addr net.Addr) bool {
if r.verifySourceAddressRateLimiter == nil {
return false
}
ipPort, err := netip.ParseAddrPort(addr.String())
if err != nil {
return true
}
return !r.verifySourceAddressRateLimiter.Allow(ipPort.Addr())
}
// OpenConnectionNoIP is deprecated and will be removed in the next release // OpenConnectionNoIP is deprecated and will be removed in the next release
//
// Deprecated: Use OpenConnection instead
func (r *resourceManager) OpenConnectionNoIP(dir network.Direction, usefd bool, endpoint multiaddr.Multiaddr) (network.ConnManagementScope, error) { func (r *resourceManager) OpenConnectionNoIP(dir network.Direction, usefd bool, endpoint multiaddr.Multiaddr) (network.ConnManagementScope, error) {
return r.openConnection(dir, usefd, endpoint, netip.Addr{}) return r.openConnection(dir, usefd, endpoint, netip.Addr{})
} }
@@ -358,6 +380,10 @@ func (r *resourceManager) OpenConnection(dir network.Direction, usefd bool, endp
} }
func (r *resourceManager) openConnection(dir network.Direction, usefd bool, endpoint multiaddr.Multiaddr, ip netip.Addr) (network.ConnManagementScope, error) { func (r *resourceManager) openConnection(dir network.Direction, usefd bool, endpoint multiaddr.Multiaddr, ip netip.Addr) (network.ConnManagementScope, error) {
if !r.connRateLimiter.Allow(ip) {
return nil, errors.New("rate limit exceeded")
}
if ip.IsValid() { if ip.IsValid() {
if ok := r.connLimiter.addConn(ip); !ok { if ok := r.connLimiter.addConn(ip); !ok {
return nil, fmt.Errorf("connections per ip limit exceeded for %s", endpoint) return nil, fmt.Errorf("connections per ip limit exceeded for %s", endpoint)

View File

@@ -1,6 +1,7 @@
package rcmgr package rcmgr
import ( import (
"net"
"net/netip" "net/netip"
"testing" "testing"
@@ -8,6 +9,7 @@ import (
"github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/core/protocol" "github.com/libp2p/go-libp2p/core/protocol"
"github.com/libp2p/go-libp2p/core/test" "github.com/libp2p/go-libp2p/core/test"
"github.com/libp2p/go-libp2p/x/rate"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/multiformats/go-multiaddr" "github.com/multiformats/go-multiaddr"
@@ -1111,3 +1113,63 @@ func TestAllowlistAndConnLimiterPlayNice(t *testing.T) {
require.Equal(t, 1, rcmgr.(*resourceManager).connLimiter.networkPrefixLimitV4[0].ConnCount) require.Equal(t, 1, rcmgr.(*resourceManager).connLimiter.networkPrefixLimitV4[0].ConnCount)
}) })
} }
func TestResourceManagerRateLimiting(t *testing.T) {
// Create a resource manager with very low rate limits
limits := DefaultLimits.AutoScale()
limits.system.Conns = 100 // High enough to not be the limiting factor
limits.transient.Conns = 100
// Create limiters with very low RPS
limiter := &rate.Limiter{
GlobalLimit: rate.Limit{RPS: 0.00001, Burst: 2},
}
rcmgr, err := NewResourceManager(NewFixedLimiter(limits), WithConnRateLimiters(limiter))
if err != nil {
t.Fatal(err)
}
defer rcmgr.Close()
addr := multiaddr.StringCast("/ip4/1.2.3.4")
connScope, err := rcmgr.OpenConnection(network.DirInbound, true, addr)
require.NoError(t, err)
connScope.Done()
connScope, err = rcmgr.OpenConnection(network.DirInbound, true, addr)
require.NoError(t, err)
connScope.Done()
_, err = rcmgr.OpenConnection(network.DirInbound, true, addr)
require.ErrorContains(t, err, "rate limit exceeded")
}
func TestVerifySourceAddressRateLimiter(t *testing.T) {
limits := DefaultLimits.AutoScale()
limits.allowlistedSystem.Conns = 100
limits.allowlistedSystem.ConnsInbound = 100
limits.allowlistedSystem.ConnsOutbound = 100
rcmgr, err := NewResourceManager(NewFixedLimiter(limits), WithLimitPerSubnet([]ConnLimitPerSubnet{
{PrefixLength: 32, ConnCount: 2},
}, []ConnLimitPerSubnet{}))
if err != nil {
t.Fatal(err)
}
defer rcmgr.Close()
na1 := &net.UDPAddr{
IP: net.ParseIP("1.2.3.4"),
Port: 1234,
}
require.False(t, rcmgr.VerifySourceAddress(na1))
require.True(t, rcmgr.VerifySourceAddress(na1))
na2 := &net.UDPAddr{
IP: net.ParseIP("1.2.3.5"),
Port: 1234,
}
require.False(t, rcmgr.VerifySourceAddress(na2))
require.True(t, rcmgr.VerifySourceAddress(na2))
}

View File

@@ -4,18 +4,17 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"iter"
"math/rand/v2"
"slices" "slices"
"sync" "sync"
"time" "time"
"math/rand/v2"
logging "github.com/ipfs/go-log/v2" logging "github.com/ipfs/go-log/v2"
"github.com/libp2p/go-libp2p/core/event" "github.com/libp2p/go-libp2p/core/event"
"github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/host"
"github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/p2p/protocol/autonatv2/pb"
ma "github.com/multiformats/go-multiaddr" ma "github.com/multiformats/go-multiaddr"
manet "github.com/multiformats/go-multiaddr/net" manet "github.com/multiformats/go-multiaddr/net"
) )
@@ -35,11 +34,15 @@ const (
// maxPeerAddresses is the number of addresses in a dial request the server // maxPeerAddresses is the number of addresses in a dial request the server
// will inspect, rest are ignored. // will inspect, rest are ignored.
maxPeerAddresses = 50 maxPeerAddresses = 50
defaultThrottlePeerDuration = 2 * time.Minute
) )
var ( var (
ErrNoValidPeers = errors.New("no valid peers for autonat v2") // ErrNoPeers is returned when the client knows no autonatv2 servers.
ErrDialRefused = errors.New("dial refused") ErrNoPeers = errors.New("no peers for autonat v2")
// ErrPrivateAddrs is returned when the request has private IP addresses.
ErrPrivateAddrs = errors.New("private addresses cannot be verified with autonatv2")
log = logging.Logger("autonatv2") log = logging.Logger("autonatv2")
) )
@@ -56,10 +59,12 @@ type Request struct {
type Result struct { type Result struct {
// Addr is the dialed address // Addr is the dialed address
Addr ma.Multiaddr Addr ma.Multiaddr
// Reachability of the dialed address // Idx is the index of the address that was dialed
Idx int
// Reachability is the reachability for `Addr`
Reachability network.Reachability Reachability network.Reachability
// Status is the outcome of the dialback // AllAddrsRefused is true when the server refused to dial all the addresses in the request.
Status pb.DialStatus AllAddrsRefused bool
} }
// AutoNAT implements the AutoNAT v2 client and server. // AutoNAT implements the AutoNAT v2 client and server.
@@ -78,6 +83,10 @@ type AutoNAT struct {
mx sync.Mutex mx sync.Mutex
peers *peersMap peers *peersMap
throttlePeer map[peer.ID]time.Time
// throttlePeerDuration is the duration to wait before making another dial request to the
// same server.
throttlePeerDuration time.Duration
// allowPrivateAddrs enables using private and localhost addresses for reachability checks. // allowPrivateAddrs enables using private and localhost addresses for reachability checks.
// This is only useful for testing. // This is only useful for testing.
allowPrivateAddrs bool allowPrivateAddrs bool
@@ -86,7 +95,7 @@ type AutoNAT struct {
// New returns a new AutoNAT instance. // New returns a new AutoNAT instance.
// host and dialerHost should have the same dialing capabilities. In case the host doesn't support // 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. // 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) { func New(dialerHost host.Host, opts ...AutoNATOption) (*AutoNAT, error) {
s := defaultSettings() s := defaultSettings()
for _, o := range opts { for _, o := range opts {
if err := o(s); err != nil { if err := o(s); err != nil {
@@ -96,18 +105,20 @@ func New(host host.Host, dialerHost host.Host, opts ...AutoNATOption) (*AutoNAT,
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
an := &AutoNAT{ an := &AutoNAT{
host: host,
ctx: ctx, ctx: ctx,
cancel: cancel, cancel: cancel,
srv: newServer(host, dialerHost, s), srv: newServer(dialerHost, s),
cli: newClient(host), cli: newClient(),
allowPrivateAddrs: s.allowPrivateAddrs, allowPrivateAddrs: s.allowPrivateAddrs,
peers: newPeersMap(), peers: newPeersMap(),
throttlePeer: make(map[peer.ID]time.Time),
throttlePeerDuration: s.throttlePeerDuration,
} }
return an, nil return an, nil
} }
func (an *AutoNAT) background(sub event.Subscription) { func (an *AutoNAT) background(sub event.Subscription) {
ticker := time.NewTicker(10 * time.Minute)
for { for {
select { select {
case <-an.ctx.Done(): case <-an.ctx.Done():
@@ -122,12 +133,24 @@ func (an *AutoNAT) background(sub event.Subscription) {
an.updatePeer(evt.Peer) an.updatePeer(evt.Peer)
case event.EvtPeerIdentificationCompleted: case event.EvtPeerIdentificationCompleted:
an.updatePeer(evt.Peer) an.updatePeer(evt.Peer)
default:
log.Errorf("unexpected event: %T", e)
} }
case <-ticker.C:
now := time.Now()
an.mx.Lock()
for p, t := range an.throttlePeer {
if t.Before(now) {
delete(an.throttlePeer, p)
}
}
an.mx.Unlock()
} }
} }
} }
func (an *AutoNAT) Start() error { func (an *AutoNAT) Start(h host.Host) error {
an.host = h
// Listen on event.EvtPeerProtocolsUpdated, event.EvtPeerConnectednessChanged // Listen on event.EvtPeerProtocolsUpdated, event.EvtPeerConnectednessChanged
// event.EvtPeerIdentificationCompleted to maintain our set of autonat supporting peers. // event.EvtPeerIdentificationCompleted to maintain our set of autonat supporting peers.
sub, err := an.host.EventBus().Subscribe([]interface{}{ sub, err := an.host.EventBus().Subscribe([]interface{}{
@@ -138,8 +161,8 @@ func (an *AutoNAT) Start() error {
if err != nil { if err != nil {
return fmt.Errorf("event subscription failed: %w", err) return fmt.Errorf("event subscription failed: %w", err)
} }
an.cli.Start() an.cli.Start(h)
an.srv.Start() an.srv.Start(h)
an.wg.Add(1) an.wg.Add(1)
go an.background(sub) go an.background(sub)
@@ -156,24 +179,48 @@ func (an *AutoNAT) Close() {
// GetReachability makes a single dial request for checking reachability for requested addresses // GetReachability makes a single dial request for checking reachability for requested addresses
func (an *AutoNAT) GetReachability(ctx context.Context, reqs []Request) (Result, error) { func (an *AutoNAT) GetReachability(ctx context.Context, reqs []Request) (Result, error) {
var filteredReqs []Request
if !an.allowPrivateAddrs { if !an.allowPrivateAddrs {
filteredReqs = make([]Request, 0, len(reqs))
for _, r := range reqs { for _, r := range reqs {
if !manet.IsPublicAddr(r.Addr) { if manet.IsPublicAddr(r.Addr) {
return Result{}, fmt.Errorf("private address cannot be verified by autonatv2: %s", r.Addr) filteredReqs = append(filteredReqs, r)
} else {
log.Errorf("private address in reachability check: %s", r.Addr)
} }
} }
if len(filteredReqs) == 0 {
return Result{}, ErrPrivateAddrs
}
} else {
filteredReqs = reqs
} }
an.mx.Lock() an.mx.Lock()
p := an.peers.GetRand() now := time.Now()
var p peer.ID
for pr := range an.peers.Shuffled() {
if t := an.throttlePeer[pr]; t.After(now) {
continue
}
p = pr
an.throttlePeer[p] = time.Now().Add(an.throttlePeerDuration)
break
}
an.mx.Unlock() an.mx.Unlock()
if p == "" { if p == "" {
return Result{}, ErrNoValidPeers return Result{}, ErrNoPeers
} }
res, err := an.cli.GetReachability(ctx, p, filteredReqs)
res, err := an.cli.GetReachability(ctx, p, reqs)
if err != nil { if err != nil {
log.Debugf("reachability check with %s failed, err: %s", p, err) log.Debugf("reachability check with %s failed, err: %s", p, err)
return Result{}, fmt.Errorf("reachability check with %s failed: %w", p, err) return res, fmt.Errorf("reachability check with %s failed: %w", p, err)
}
// restore the correct index in case we'd filtered private addresses
for i, r := range reqs {
if r.Addr.Equal(res.Addr) {
res.Idx = i
break
}
} }
log.Debugf("reachability check with %s successful", p) log.Debugf("reachability check with %s successful", p)
return res, nil return res, nil
@@ -187,7 +234,7 @@ func (an *AutoNAT) updatePeer(p peer.ID) {
// and swarm for the current state // and swarm for the current state
protos, err := an.host.Peerstore().SupportsProtocols(p, DialProtocol) protos, err := an.host.Peerstore().SupportsProtocols(p, DialProtocol)
connectedness := an.host.Network().Connectedness(p) connectedness := an.host.Network().Connectedness(p)
if err == nil && slices.Contains(protos, DialProtocol) && connectedness == network.Connected { if err == nil && connectedness == network.Connected && slices.Contains(protos, DialProtocol) {
an.peers.Put(p) an.peers.Put(p)
} else { } else {
an.peers.Delete(p) an.peers.Delete(p)
@@ -208,28 +255,40 @@ func newPeersMap() *peersMap {
} }
} }
func (p *peersMap) GetRand() peer.ID { // Shuffled iterates over the map in random order
if len(p.peers) == 0 { func (p *peersMap) Shuffled() iter.Seq[peer.ID] {
return "" n := len(p.peers)
start := 0
if n > 0 {
start = rand.IntN(n)
} }
return p.peers[rand.IntN(len(p.peers))] return func(yield func(peer.ID) bool) {
} for i := range n {
if !yield(p.peers[(i+start)%n]) {
func (p *peersMap) Put(pid peer.ID) {
if _, ok := p.peerIdx[pid]; ok {
return return
} }
p.peers = append(p.peers, pid) }
p.peerIdx[pid] = len(p.peers) - 1 }
} }
func (p *peersMap) Delete(pid peer.ID) { func (p *peersMap) Put(id peer.ID) {
idx, ok := p.peerIdx[pid] if _, ok := p.peerIdx[id]; ok {
return
}
p.peers = append(p.peers, id)
p.peerIdx[id] = len(p.peers) - 1
}
func (p *peersMap) Delete(id peer.ID) {
idx, ok := p.peerIdx[id]
if !ok { if !ok {
return return
} }
p.peers[idx] = p.peers[len(p.peers)-1] n := len(p.peers)
p.peerIdx[p.peers[idx]] = idx lastPeer := p.peers[n-1]
p.peers = p.peers[:len(p.peers)-1] p.peers[idx] = lastPeer
delete(p.peerIdx, pid) p.peerIdx[lastPeer] = idx
p.peers[n-1] = ""
p.peers = p.peers[:n-1]
delete(p.peerIdx, id)
} }

View File

@@ -2,8 +2,13 @@ package autonatv2
import ( import (
"context" "context"
"encoding/binary"
"errors" "errors"
"fmt" "fmt"
"math"
"net"
"net/netip"
"strings"
"sync/atomic" "sync/atomic"
"testing" "testing"
"time" "time"
@@ -36,11 +41,12 @@ func newAutoNAT(t testing.TB, dialer host.Host, opts ...AutoNATOption) *AutoNAT
swarm.WithUDPBlackHoleSuccessCounter(nil), swarm.WithUDPBlackHoleSuccessCounter(nil),
swarm.WithIPv6BlackHoleSuccessCounter(nil)))) swarm.WithIPv6BlackHoleSuccessCounter(nil))))
} }
an, err := New(h, dialer, opts...) opts = append([]AutoNATOption{withThrottlePeerDuration(0)}, opts...)
an, err := New(dialer, opts...)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
an.Start() require.NoError(t, an.Start(h))
t.Cleanup(an.Close) t.Cleanup(an.Close)
return an return an
} }
@@ -74,7 +80,7 @@ func waitForPeer(t testing.TB, a *AutoNAT) {
require.Eventually(t, func() bool { require.Eventually(t, func() bool {
a.mx.Lock() a.mx.Lock()
defer a.mx.Unlock() defer a.mx.Unlock()
return a.peers.GetRand() != "" return len(a.peers.peers) != 0
}, 5*time.Second, 100*time.Millisecond) }, 5*time.Second, 100*time.Millisecond)
} }
@@ -88,7 +94,7 @@ func TestAutoNATPrivateAddr(t *testing.T) {
an := newAutoNAT(t, nil) an := newAutoNAT(t, nil)
res, err := an.GetReachability(context.Background(), []Request{{Addr: ma.StringCast("/ip4/192.168.0.1/udp/10/quic-v1")}}) 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.Equal(t, res, Result{})
require.Contains(t, err.Error(), "private address cannot be verified by autonatv2") require.ErrorIs(t, err, ErrPrivateAddrs)
} }
func TestClientRequest(t *testing.T) { func TestClientRequest(t *testing.T) {
@@ -154,19 +160,6 @@ func TestClientServerError(t *testing.T) {
}, },
errorStr: "invalid msg type", 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 { for i, tc := range tests {
@@ -298,6 +291,49 @@ func TestClientDataRequest(t *testing.T) {
} }
} }
func TestAutoNATPrivateAndPublicAddrs(t *testing.T) {
an := newAutoNAT(t, nil)
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()
handler := func(s network.Stream) {
w := pbio.NewDelimitedWriter(s)
r := pbio.NewDelimitedReader(s, maxMsgSize)
var msg pb.Message
assert.NoError(t, r.ReadMsg(&msg))
w.WriteMsg(&pb.Message{
Msg: &pb.Message_DialResponse{
DialResponse: &pb.DialResponse{
Status: pb.DialResponse_OK,
DialStatus: pb.DialStatus_E_DIAL_ERROR,
AddrIdx: 0,
},
},
})
s.Close()
}
b.SetStreamHandler(DialProtocol, handler)
privateAddr := ma.StringCast("/ip4/192.168.0.1/udp/10/quic-v1")
publicAddr := ma.StringCast("/ip4/1.2.3.4/udp/10/quic-v1")
res, err := an.GetReachability(context.Background(),
[]Request{
{Addr: privateAddr},
{Addr: publicAddr},
})
require.NoError(t, err)
require.Equal(t, res.Addr, publicAddr, "%s\n%s", res.Addr, publicAddr)
require.Equal(t, res.Idx, 1)
require.Equal(t, res.Reachability, network.ReachabilityPrivate)
}
func TestClientDialBacks(t *testing.T) { func TestClientDialBacks(t *testing.T) {
an := newAutoNAT(t, nil, allowPrivateAddrs) an := newAutoNAT(t, nil, allowPrivateAddrs)
defer an.Close() defer an.Close()
@@ -507,7 +543,6 @@ func TestClientDialBacks(t *testing.T) {
} else { } else {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, res.Reachability, network.ReachabilityPublic) require.Equal(t, res.Reachability, network.ReachabilityPublic)
require.Equal(t, res.Status, pb.DialStatus_OK)
} }
}) })
} }
@@ -551,46 +586,6 @@ func TestEventSubscription(t *testing.T) {
}, 5*time.Second, 100*time.Millisecond) }, 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) { func TestAreAddrsConsistency(t *testing.T) {
c := &client{ c := &client{
normalizeMultiaddr: func(a ma.Multiaddr) ma.Multiaddr { normalizeMultiaddr: func(a ma.Multiaddr) ma.Multiaddr {
@@ -645,6 +640,12 @@ func TestAreAddrsConsistency(t *testing.T) {
dialAddr: ma.StringCast("/ip6/1::1/udp/123/quic-v1/"), dialAddr: ma.StringCast("/ip6/1::1/udp/123/quic-v1/"),
success: false, success: false,
}, },
{
name: "dns6",
localAddr: ma.StringCast("/dns6/lib.p2p/udp/12345/quic-v1"),
dialAddr: ma.StringCast("/ip4/1.2.3.4/udp/123/quic-v1/"),
success: false,
},
} }
for _, tc := range tests { for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
@@ -658,3 +659,173 @@ func TestAreAddrsConsistency(t *testing.T) {
}) })
} }
} }
func TestPeerMap(t *testing.T) {
pm := newPeersMap()
// Add 1, 2, 3
pm.Put(peer.ID("1"))
pm.Put(peer.ID("2"))
pm.Put(peer.ID("3"))
// Remove 3, 2
pm.Delete(peer.ID("3"))
pm.Delete(peer.ID("2"))
// Add 4
pm.Put(peer.ID("4"))
// Remove 3, 2 again. Should be no op
pm.Delete(peer.ID("3"))
pm.Delete(peer.ID("2"))
contains := []peer.ID{"1", "4"}
elems := make([]peer.ID, 0)
for p := range pm.Shuffled() {
elems = append(elems, p)
}
require.ElementsMatch(t, contains, elems)
}
func FuzzClient(f *testing.F) {
a := newAutoNAT(f, nil, allowPrivateAddrs, WithServerRateLimit(math.MaxInt32, math.MaxInt32, math.MaxInt32, 2))
c := newAutoNAT(f, nil)
idAndWait(f, c, a)
// TODO: Move this to go-multiaddrs
getProto := func(protos []byte) ma.Multiaddr {
protoType := 0
if len(protos) > 0 {
protoType = int(protos[0])
}
port1, port2 := 0, 0
if len(protos) > 1 {
port1 = int(protos[1])
}
if len(protos) > 2 {
port2 = int(protos[2])
}
protoTemplates := []string{
"/tcp/%d/",
"/udp/%d/",
"/udp/%d/quic-v1/",
"/udp/%d/quic-v1/tcp/%d",
"/udp/%d/quic-v1/webtransport/",
"/udp/%d/webrtc/",
"/udp/%d/webrtc-direct/",
"/unix/hello/",
}
s := protoTemplates[protoType%len(protoTemplates)]
port1 %= (1 << 16)
if strings.Count(s, "%d") == 1 {
return ma.StringCast(fmt.Sprintf(s, port1))
}
port2 %= (1 << 16)
return ma.StringCast(fmt.Sprintf(s, port1, port2))
}
getIP := func(ips []byte) ma.Multiaddr {
ipType := 0
if len(ips) > 0 {
ipType = int(ips[0])
}
ips = ips[1:]
var x, y int64
split := 128 / 8
if len(ips) < split {
split = len(ips)
}
var b [8]byte
copy(b[:], ips[:split])
x = int64(binary.LittleEndian.Uint64(b[:]))
clear(b[:])
copy(b[:], ips[split:])
y = int64(binary.LittleEndian.Uint64(b[:]))
var ip netip.Addr
switch ipType % 3 {
case 0:
ip = netip.AddrFrom4([4]byte{byte(x), byte(x >> 8), byte(x >> 16), byte(x >> 24)})
return ma.StringCast(fmt.Sprintf("/ip4/%s/", ip))
case 1:
pubIP := net.ParseIP("2005::") // Public IP address
x := int64(binary.LittleEndian.Uint64(pubIP[0:8]))
ip = netip.AddrFrom16([16]byte{
byte(x), byte(x >> 8), byte(x >> 16), byte(x >> 24),
byte(x >> 32), byte(x >> 40), byte(x >> 48), byte(x >> 56),
byte(y), byte(y >> 8), byte(y >> 16), byte(y >> 24),
byte(y >> 32), byte(y >> 40), byte(y >> 48), byte(y >> 56),
})
return ma.StringCast(fmt.Sprintf("/ip6/%s/", ip))
default:
ip := netip.AddrFrom16([16]byte{
byte(x), byte(x >> 8), byte(x >> 16), byte(x >> 24),
byte(x >> 32), byte(x >> 40), byte(x >> 48), byte(x >> 56),
byte(y), byte(y >> 8), byte(y >> 16), byte(y >> 24),
byte(y >> 32), byte(y >> 40), byte(y >> 48), byte(y >> 56),
})
return ma.StringCast(fmt.Sprintf("/ip6/%s/", ip))
}
}
getAddr := func(addrType int, ips, protos []byte) ma.Multiaddr {
switch addrType % 4 {
case 0:
return getIP(ips).Encapsulate(getProto(protos))
case 1:
return getProto(protos)
case 2:
return nil
default:
return getIP(ips).Encapsulate(getProto(protos))
}
}
getDNSAddr := func(hostNameBytes, protos []byte) ma.Multiaddr {
hostName := strings.ReplaceAll(string(hostNameBytes), "\\", "")
hostName = strings.ReplaceAll(hostName, "/", "")
if hostName == "" {
hostName = "localhost"
}
dnsType := 0
if len(hostNameBytes) > 0 {
dnsType = int(hostNameBytes[0])
}
dnsProtos := []string{"dns", "dns4", "dns6", "dnsaddr"}
da := ma.StringCast(fmt.Sprintf("/%s/%s/", dnsProtos[dnsType%len(dnsProtos)], hostName))
return da.Encapsulate(getProto(protos))
}
const maxAddrs = 100
getAddrs := func(numAddrs int, ips, protos, hostNames []byte) []ma.Multiaddr {
if len(ips) == 0 || len(protos) == 0 || len(hostNames) == 0 {
return nil
}
numAddrs = ((numAddrs % maxAddrs) + maxAddrs) % maxAddrs
addrs := make([]ma.Multiaddr, numAddrs)
ipIdx := 0
protoIdx := 0
for i := range numAddrs {
addrs[i] = getAddr(i, ips[ipIdx:], protos[protoIdx:])
ipIdx = (ipIdx + 1) % len(ips)
protoIdx = (protoIdx + 1) % len(protos)
}
maxDNSAddrs := 10
protoIdx = 0
for i := 0; i < len(hostNames) && i < maxDNSAddrs; i += 2 {
ed := min(i+2, len(hostNames))
addrs = append(addrs, getDNSAddr(hostNames[i:ed], protos[protoIdx:]))
protoIdx = (protoIdx + 1) % len(protos)
}
return addrs
}
// reduce the streamTimeout before running this. TODO: fix this
f.Fuzz(func(_ *testing.T, numAddrs int, ips, protos, hostNames []byte) {
addrs := getAddrs(numAddrs, ips, protos, hostNames)
reqs := make([]Request, len(addrs))
for i, addr := range addrs {
reqs[i] = Request{Addr: addr, SendDialData: true}
}
c.GetReachability(context.Background(), reqs)
})
}

View File

@@ -35,20 +35,20 @@ type normalizeMultiaddrer interface {
NormalizeMultiaddr(ma.Multiaddr) ma.Multiaddr NormalizeMultiaddr(ma.Multiaddr) ma.Multiaddr
} }
func newClient(h host.Host) *client { func newClient() *client {
normalizeMultiaddr := func(a ma.Multiaddr) ma.Multiaddr { return a }
if hn, ok := h.(normalizeMultiaddrer); ok {
normalizeMultiaddr = hn.NormalizeMultiaddr
}
return &client{ return &client{
host: h,
dialData: make([]byte, 4000), dialData: make([]byte, 4000),
normalizeMultiaddr: normalizeMultiaddr,
dialBackQueues: make(map[uint64]chan ma.Multiaddr), dialBackQueues: make(map[uint64]chan ma.Multiaddr),
} }
} }
func (ac *client) Start() { func (ac *client) Start(h host.Host) {
normalizeMultiaddr := func(a ma.Multiaddr) ma.Multiaddr { return a }
if hn, ok := h.(normalizeMultiaddrer); ok {
normalizeMultiaddr = hn.NormalizeMultiaddr
}
ac.host = h
ac.normalizeMultiaddr = normalizeMultiaddr
ac.host.SetStreamHandler(DialBackProtocol, ac.handleDialBack) ac.host.SetStreamHandler(DialBackProtocol, ac.handleDialBack)
} }
@@ -109,9 +109,9 @@ func (ac *client) GetReachability(ctx context.Context, p peer.ID, reqs []Request
break break
// provide dial data if appropriate // provide dial data if appropriate
case msg.GetDialDataRequest() != nil: case msg.GetDialDataRequest() != nil:
if err := ac.validateDialDataRequest(reqs, &msg); err != nil { if err := validateDialDataRequest(reqs, &msg); err != nil {
s.Reset() s.Reset()
return Result{}, fmt.Errorf("invalid dial data request: %w", err) return Result{}, fmt.Errorf("invalid dial data request: %s %w", s.Conn().RemoteMultiaddr(), err)
} }
// dial data request is valid and we want to send data // dial data request is valid and we want to send data
if err := sendDialData(ac.dialData, int(msg.GetDialDataRequest().GetNumBytes()), w, &msg); err != nil { if err := sendDialData(ac.dialData, int(msg.GetDialDataRequest().GetNumBytes()), w, &msg); err != nil {
@@ -136,7 +136,7 @@ func (ac *client) GetReachability(ctx context.Context, p peer.ID, reqs []Request
// E_DIAL_REFUSED has implication for deciding future address verificiation priorities // E_DIAL_REFUSED has implication for deciding future address verificiation priorities
// wrap a distinct error for convenient errors.Is usage // wrap a distinct error for convenient errors.Is usage
if resp.GetStatus() == pb.DialResponse_E_DIAL_REFUSED { if resp.GetStatus() == pb.DialResponse_E_DIAL_REFUSED {
return Result{}, fmt.Errorf("dial request failed: %w", ErrDialRefused) return Result{AllAddrsRefused: true}, nil
} }
return Result{}, fmt.Errorf("dial request failed: response status %d %s", resp.GetStatus(), return Result{}, fmt.Errorf("dial request failed: response status %d %s", resp.GetStatus(),
pb.DialResponse_ResponseStatus_name[int32(resp.GetStatus())]) pb.DialResponse_ResponseStatus_name[int32(resp.GetStatus())])
@@ -147,7 +147,6 @@ func (ac *client) GetReachability(ctx context.Context, p peer.ID, reqs []Request
if int(resp.AddrIdx) >= len(reqs) { if int(resp.AddrIdx) >= len(reqs) {
return Result{}, fmt.Errorf("invalid response: addr index out of range: %d [0-%d)", 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 // wait for nonce from the server
var dialBackAddr ma.Multiaddr var dialBackAddr ma.Multiaddr
if resp.GetDialStatus() == pb.DialStatus_OK { if resp.GetDialStatus() == pb.DialStatus_OK {
@@ -163,7 +162,7 @@ func (ac *client) GetReachability(ctx context.Context, p peer.ID, reqs []Request
return ac.newResult(resp, reqs, dialBackAddr) return ac.newResult(resp, reqs, dialBackAddr)
} }
func (ac *client) validateDialDataRequest(reqs []Request, msg *pb.Message) error { func validateDialDataRequest(reqs []Request, msg *pb.Message) error {
idx := int(msg.GetDialDataRequest().AddrIdx) idx := int(msg.GetDialDataRequest().AddrIdx)
if idx >= len(reqs) { // invalid address index if idx >= len(reqs) { // invalid address index
return fmt.Errorf("addr index out of range: %d [0-%d)", idx, len(reqs)) return fmt.Errorf("addr index out of range: %d [0-%d)", idx, len(reqs))
@@ -179,9 +178,13 @@ func (ac *client) validateDialDataRequest(reqs []Request, msg *pb.Message) error
func (ac *client) newResult(resp *pb.DialResponse, reqs []Request, dialBackAddr ma.Multiaddr) (Result, error) { func (ac *client) newResult(resp *pb.DialResponse, reqs []Request, dialBackAddr ma.Multiaddr) (Result, error) {
idx := int(resp.AddrIdx) idx := int(resp.AddrIdx)
if idx >= len(reqs) {
// This should have been validated by this point, but checking this is cheap.
return Result{}, fmt.Errorf("addrs index(%d) greater than len(reqs)(%d)", idx, len(reqs))
}
addr := reqs[idx].Addr addr := reqs[idx].Addr
var rch network.Reachability rch := network.ReachabilityUnknown //nolint:ineffassign
switch resp.DialStatus { switch resp.DialStatus {
case pb.DialStatus_OK: case pb.DialStatus_OK:
if !ac.areAddrsConsistent(dialBackAddr, addr) { if !ac.areAddrsConsistent(dialBackAddr, addr) {
@@ -191,17 +194,16 @@ func (ac *client) newResult(resp *pb.DialResponse, reqs []Request, dialBackAddr
return Result{}, fmt.Errorf("invalid response: dialBackAddr: %s, respAddr: %s", dialBackAddr, addr) return Result{}, fmt.Errorf("invalid response: dialBackAddr: %s, respAddr: %s", dialBackAddr, addr)
} }
rch = network.ReachabilityPublic rch = network.ReachabilityPublic
case pb.DialStatus_E_DIAL_ERROR:
rch = network.ReachabilityPrivate
case pb.DialStatus_E_DIAL_BACK_ERROR: case pb.DialStatus_E_DIAL_BACK_ERROR:
if ac.areAddrsConsistent(dialBackAddr, addr) { if !ac.areAddrsConsistent(dialBackAddr, addr) {
return Result{}, fmt.Errorf("dial-back stream error: dialBackAddr: %s, respAddr: %s", dialBackAddr, addr)
}
// We received the dial back but the server claims the dial back errored. // 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 // As long as we received the correct nonce in dial back it is safe to assume
// that we are public. // that we are public.
rch = network.ReachabilityPublic rch = network.ReachabilityPublic
} else { case pb.DialStatus_E_DIAL_ERROR:
rch = network.ReachabilityUnknown rch = network.ReachabilityPrivate
}
default: default:
// Unexpected response code. Discard the response and fail. // Unexpected response code. Discard the response and fail.
log.Warnf("invalid status code received in response for addr %s: %d", addr, resp.DialStatus) log.Warnf("invalid status code received in response for addr %s: %d", addr, resp.DialStatus)
@@ -210,8 +212,8 @@ func (ac *client) newResult(resp *pb.DialResponse, reqs []Request, dialBackAddr
return Result{ return Result{
Addr: addr, Addr: addr,
Idx: idx,
Reachability: rch, Reachability: rch,
Status: resp.DialStatus,
}, nil }, nil
} }
@@ -307,7 +309,7 @@ func (ac *client) handleDialBack(s network.Stream) {
} }
func (ac *client) areAddrsConsistent(connLocalAddr, dialedAddr ma.Multiaddr) bool { func (ac *client) areAddrsConsistent(connLocalAddr, dialedAddr ma.Multiaddr) bool {
if connLocalAddr == nil || dialedAddr == nil { if len(connLocalAddr) == 0 || len(dialedAddr) == 0 {
return false return false
} }
connLocalAddr = ac.normalizeMultiaddr(connLocalAddr) connLocalAddr = ac.normalizeMultiaddr(connLocalAddr)
@@ -318,33 +320,32 @@ func (ac *client) areAddrsConsistent(connLocalAddr, dialedAddr ma.Multiaddr) boo
if len(localProtos) != len(externalProtos) { if len(localProtos) != len(externalProtos) {
return false return false
} }
for i := 0; i < len(localProtos); i++ { for i, lp := range localProtos {
ep := externalProtos[i]
if i == 0 { if i == 0 {
switch externalProtos[i].Code { switch ep.Code {
case ma.P_DNS, ma.P_DNSADDR: case ma.P_DNS, ma.P_DNSADDR:
if localProtos[i].Code == ma.P_IP4 || localProtos[i].Code == ma.P_IP6 { if lp.Code == ma.P_IP4 || lp.Code == ma.P_IP6 {
continue continue
} }
return false return false
case ma.P_DNS4: case ma.P_DNS4:
if localProtos[i].Code == ma.P_IP4 { if lp.Code == ma.P_IP4 {
continue continue
} }
return false return false
case ma.P_DNS6: case ma.P_DNS6:
if localProtos[i].Code == ma.P_IP6 { if lp.Code == ma.P_IP6 {
continue continue
} }
return false return false
} }
if localProtos[i].Code != externalProtos[i].Code { if lp.Code != ep.Code {
return false return false
} }
} else { } else if lp.Code != ep.Code {
if localProtos[i].Code != externalProtos[i].Code {
return false return false
} }
} }
}
return true return true
} }

View File

@@ -13,6 +13,7 @@ type autoNATSettings struct {
now func() time.Time now func() time.Time
amplificatonAttackPreventionDialWait time.Duration amplificatonAttackPreventionDialWait time.Duration
metricsTracer MetricsTracer metricsTracer MetricsTracer
throttlePeerDuration time.Duration
} }
func defaultSettings() *autoNATSettings { func defaultSettings() *autoNATSettings {
@@ -25,6 +26,7 @@ func defaultSettings() *autoNATSettings {
dataRequestPolicy: amplificationAttackPrevention, dataRequestPolicy: amplificationAttackPrevention,
amplificatonAttackPreventionDialWait: 3 * time.Second, amplificatonAttackPreventionDialWait: 3 * time.Second,
now: time.Now, now: time.Now,
throttlePeerDuration: defaultThrottlePeerDuration,
} }
} }
@@ -65,3 +67,10 @@ func withAmplificationAttackPreventionDialWait(d time.Duration) AutoNATOption {
return nil return nil
} }
} }
func withThrottlePeerDuration(d time.Duration) AutoNATOption {
return func(s *autoNATSettings) error {
s.throttlePeerDuration = d
return nil
}
}

View File

@@ -59,10 +59,9 @@ type server struct {
allowPrivateAddrs bool allowPrivateAddrs bool
} }
func newServer(host, dialer host.Host, s *autoNATSettings) *server { func newServer(dialer host.Host, s *autoNATSettings) *server {
return &server{ return &server{
dialerHost: dialer, dialerHost: dialer,
host: host,
dialDataRequestPolicy: s.dataRequestPolicy, dialDataRequestPolicy: s.dataRequestPolicy,
amplificatonAttackPreventionDialWait: s.amplificatonAttackPreventionDialWait, amplificatonAttackPreventionDialWait: s.amplificatonAttackPreventionDialWait,
allowPrivateAddrs: s.allowPrivateAddrs, allowPrivateAddrs: s.allowPrivateAddrs,
@@ -79,7 +78,8 @@ func newServer(host, dialer host.Host, s *autoNATSettings) *server {
} }
// Enable attaches the stream handler to the host. // Enable attaches the stream handler to the host.
func (as *server) Start() { func (as *server) Start(h host.Host) {
as.host = h
as.host.SetStreamHandler(DialProtocol, as.handleDialRequest) as.host.SetStreamHandler(DialProtocol, as.handleDialRequest)
} }

View File

@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"io" "io"
"math" "math"
"strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"testing" "testing"
@@ -46,8 +47,8 @@ func TestServerInvalidAddrsRejected(t *testing.T) {
idAndWait(t, c, an) idAndWait(t, c, an)
res, err := c.GetReachability(context.Background(), newTestRequests(c.host.Addrs(), true)) res, err := c.GetReachability(context.Background(), newTestRequests(c.host.Addrs(), true))
require.ErrorIs(t, err, ErrDialRefused) require.NoError(t, err)
require.Equal(t, Result{}, res) require.Equal(t, Result{AllAddrsRefused: true}, res)
}) })
t.Run("black holed addr", func(t *testing.T) { t.Run("black holed addr", func(t *testing.T) {
@@ -64,8 +65,8 @@ func TestServerInvalidAddrsRejected(t *testing.T) {
Addr: ma.StringCast("/ip4/1.2.3.4/udp/1234/quic-v1"), Addr: ma.StringCast("/ip4/1.2.3.4/udp/1234/quic-v1"),
SendDialData: true, SendDialData: true,
}}) }})
require.ErrorIs(t, err, ErrDialRefused) require.NoError(t, err)
require.Equal(t, Result{}, res) require.Equal(t, Result{AllAddrsRefused: true}, res)
}) })
t.Run("private addrs", func(t *testing.T) { t.Run("private addrs", func(t *testing.T) {
@@ -76,8 +77,8 @@ func TestServerInvalidAddrsRejected(t *testing.T) {
idAndWait(t, c, an) idAndWait(t, c, an)
res, err := c.GetReachability(context.Background(), newTestRequests(c.host.Addrs(), true)) res, err := c.GetReachability(context.Background(), newTestRequests(c.host.Addrs(), true))
require.ErrorIs(t, err, ErrDialRefused) require.NoError(t, err)
require.Equal(t, Result{}, res) require.Equal(t, Result{AllAddrsRefused: true}, res)
}) })
t.Run("relay addrs", func(t *testing.T) { t.Run("relay addrs", func(t *testing.T) {
@@ -89,8 +90,8 @@ func TestServerInvalidAddrsRejected(t *testing.T) {
res, err := c.GetReachability(context.Background(), newTestRequests( 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)) []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.NoError(t, err)
require.Equal(t, Result{}, res) require.Equal(t, Result{AllAddrsRefused: true}, res)
}) })
t.Run("no addr", func(t *testing.T) { t.Run("no addr", func(t *testing.T) {
@@ -113,8 +114,8 @@ func TestServerInvalidAddrsRejected(t *testing.T) {
idAndWait(t, c, an) idAndWait(t, c, an)
res, err := c.GetReachability(context.Background(), newTestRequests(addrs, true)) res, err := c.GetReachability(context.Background(), newTestRequests(addrs, true))
require.ErrorIs(t, err, ErrDialRefused) require.NoError(t, err)
require.Equal(t, Result{}, res) require.Equal(t, Result{AllAddrsRefused: true}, res)
}) })
t.Run("msg too large", func(t *testing.T) { t.Run("msg too large", func(t *testing.T) {
@@ -135,7 +136,6 @@ func TestServerInvalidAddrsRejected(t *testing.T) {
require.ErrorIs(t, err, network.ErrReset) require.ErrorIs(t, err, network.ErrReset)
require.Equal(t, Result{}, res) require.Equal(t, Result{}, res)
}) })
} }
func TestServerDataRequest(t *testing.T) { func TestServerDataRequest(t *testing.T) {
@@ -178,8 +178,8 @@ func TestServerDataRequest(t *testing.T) {
require.Equal(t, Result{ require.Equal(t, Result{
Addr: quicAddr, Addr: quicAddr,
Idx: 0,
Reachability: network.ReachabilityPublic, Reachability: network.ReachabilityPublic,
Status: pb.DialStatus_OK,
}, res) }, res)
// Small messages should be rejected for dial data // Small messages should be rejected for dial data
@@ -191,14 +191,11 @@ func TestServerDataRequest(t *testing.T) {
func TestServerMaxConcurrentRequestsPerPeer(t *testing.T) { func TestServerMaxConcurrentRequestsPerPeer(t *testing.T) {
const concurrentRequests = 5 const concurrentRequests = 5
// server will skip all tcp addresses stallChan := make(chan struct{})
dialer := bhost.NewBlankHost(swarmt.GenSwarm(t, swarmt.OptDisableTCP)) an := newAutoNAT(t, nil, allowPrivateAddrs, withDataRequestPolicy(
doneChan := make(chan struct{})
an := newAutoNAT(t, dialer, allowPrivateAddrs, withDataRequestPolicy(
// stall all allowed requests // stall all allowed requests
func(_, _ ma.Multiaddr) bool { func(_, _ ma.Multiaddr) bool {
<-doneChan <-stallChan
return true return true
}), }),
WithServerRateLimit(10, 10, 10, concurrentRequests), WithServerRateLimit(10, 10, 10, concurrentRequests),
@@ -207,16 +204,18 @@ func TestServerMaxConcurrentRequestsPerPeer(t *testing.T) {
defer an.Close() defer an.Close()
defer an.host.Close() defer an.host.Close()
c := newAutoNAT(t, nil, allowPrivateAddrs) // server will skip all tcp addresses
dialer := bhost.NewBlankHost(swarmt.GenSwarm(t, swarmt.OptDisableTCP))
c := newAutoNAT(t, dialer, allowPrivateAddrs)
defer c.Close() defer c.Close()
defer c.host.Close() defer c.host.Close()
idAndWait(t, c, an) idAndWait(t, c, an)
errChan := make(chan error) errChan := make(chan error)
const N = 10 const n = 10
// num concurrentRequests will stall and N will fail // num concurrentRequests will stall and n will fail
for i := 0; i < concurrentRequests+N; i++ { for i := 0; i < concurrentRequests+n; i++ {
go func() { go func() {
_, err := c.GetReachability(context.Background(), []Request{{Addr: c.host.Addrs()[0], SendDialData: false}}) _, err := c.GetReachability(context.Background(), []Request{{Addr: c.host.Addrs()[0], SendDialData: false}})
errChan <- err errChan <- err
@@ -224,17 +223,20 @@ func TestServerMaxConcurrentRequestsPerPeer(t *testing.T) {
} }
// check N failures // check N failures
for i := 0; i < N; i++ { for i := 0; i < n; i++ {
select { select {
case err := <-errChan: case err := <-errChan:
require.Error(t, err) require.Error(t, err)
if !strings.Contains(err.Error(), "stream reset") && !strings.Contains(err.Error(), "E_REQUEST_REJECTED") {
t.Fatalf("invalid error: %s expected: stream reset or E_REQUEST_REJECTED", err)
}
case <-time.After(10 * time.Second): case <-time.After(10 * time.Second):
t.Fatalf("expected %d errors: got: %d", N, i) t.Fatalf("expected %d errors: got: %d", n, i)
} }
} }
close(stallChan) // complete stalled requests
// check concurrentRequests failures, as we won't send dial data // check concurrentRequests failures, as we won't send dial data
close(doneChan)
for i := 0; i < concurrentRequests; i++ { for i := 0; i < concurrentRequests; i++ {
select { select {
case err := <-errChan: case err := <-errChan:
@@ -290,8 +292,8 @@ func TestServerDataRequestJitter(t *testing.T) {
require.Equal(t, Result{ require.Equal(t, Result{
Addr: quicAddr, Addr: quicAddr,
Idx: 0,
Reachability: network.ReachabilityPublic, Reachability: network.ReachabilityPublic,
Status: pb.DialStatus_OK,
}, res) }, res)
if took > 500*time.Millisecond { if took > 500*time.Millisecond {
return return
@@ -320,8 +322,8 @@ func TestServerDial(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, Result{ require.Equal(t, Result{
Addr: unreachableAddr, Addr: unreachableAddr,
Idx: 0,
Reachability: network.ReachabilityPrivate, Reachability: network.ReachabilityPrivate,
Status: pb.DialStatus_E_DIAL_ERROR,
}, res) }, res)
}) })
@@ -330,16 +332,16 @@ func TestServerDial(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, Result{ require.Equal(t, Result{
Addr: hostAddrs[0], Addr: hostAddrs[0],
Idx: 0,
Reachability: network.ReachabilityPublic, Reachability: network.ReachabilityPublic,
Status: pb.DialStatus_OK,
}, res) }, res)
for _, addr := range c.host.Addrs() { for _, addr := range c.host.Addrs() {
res, err := c.GetReachability(context.Background(), newTestRequests([]ma.Multiaddr{addr}, false)) res, err := c.GetReachability(context.Background(), newTestRequests([]ma.Multiaddr{addr}, false))
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, Result{ require.Equal(t, Result{
Addr: addr, Addr: addr,
Idx: 0,
Reachability: network.ReachabilityPublic, Reachability: network.ReachabilityPublic,
Status: pb.DialStatus_OK,
}, res) }, res)
} }
}) })
@@ -347,12 +349,8 @@ func TestServerDial(t *testing.T) {
t.Run("dialback error", func(t *testing.T) { t.Run("dialback error", func(t *testing.T) {
c.host.RemoveStreamHandler(DialBackProtocol) c.host.RemoveStreamHandler(DialBackProtocol)
res, err := c.GetReachability(context.Background(), newTestRequests(c.host.Addrs(), false)) res, err := c.GetReachability(context.Background(), newTestRequests(c.host.Addrs(), false))
require.NoError(t, err) require.ErrorContains(t, err, "dial-back stream error")
require.Equal(t, Result{ require.Equal(t, Result{}, res)
Addr: hostAddrs[0],
Reachability: network.ReachabilityUnknown,
Status: pb.DialStatus_E_DIAL_BACK_ERROR,
}, res)
}) })
} }
@@ -396,7 +394,6 @@ func TestRateLimiter(t *testing.T) {
cl.AdvanceBy(10 * time.Second) cl.AdvanceBy(10 * time.Second)
require.True(t, r.Accept("peer3")) require.True(t, r.Accept("peer3"))
} }
func TestRateLimiterConcurrentRequests(t *testing.T) { func TestRateLimiterConcurrentRequests(t *testing.T) {
@@ -558,22 +555,23 @@ func TestServerDataRequestWithAmplificationAttackPrevention(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, Result{ require.Equal(t, Result{
Addr: quicv4Addr, Addr: quicv4Addr,
Idx: 0,
Reachability: network.ReachabilityPublic, Reachability: network.ReachabilityPublic,
Status: pb.DialStatus_OK,
}, res) }, res)
// ipv6 address should require dial data // ipv6 address should require dial data
_, err = c.GetReachability(context.Background(), []Request{{Addr: quicv6Addr, SendDialData: false}}) _, err = c.GetReachability(context.Background(), []Request{{Addr: quicv6Addr, SendDialData: false}})
require.Error(t, err) require.Error(t, err)
require.ErrorContains(t, err, "invalid dial data request: low priority addr") require.ErrorContains(t, err, "invalid dial data request")
require.ErrorContains(t, err, "low priority addr")
// ipv6 address should work fine with dial data // ipv6 address should work fine with dial data
res, err = c.GetReachability(context.Background(), []Request{{Addr: quicv6Addr, SendDialData: true}}) res, err = c.GetReachability(context.Background(), []Request{{Addr: quicv6Addr, SendDialData: true}})
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, Result{ require.Equal(t, Result{
Addr: quicv6Addr, Addr: quicv6Addr,
Idx: 0,
Reachability: network.ReachabilityPublic, Reachability: network.ReachabilityPublic,
Status: pb.DialStatus_OK,
}, res) }, res)
} }

View File

@@ -20,9 +20,9 @@ import (
"github.com/libp2p/go-libp2p/core/protocol" "github.com/libp2p/go-libp2p/core/protocol"
"github.com/libp2p/go-libp2p/core/record" "github.com/libp2p/go-libp2p/core/record"
"github.com/libp2p/go-libp2p/p2p/host/eventbus" "github.com/libp2p/go-libp2p/p2p/host/eventbus"
"github.com/libp2p/go-libp2p/p2p/internal/rate"
useragent "github.com/libp2p/go-libp2p/p2p/protocol/identify/internal/user-agent" useragent "github.com/libp2p/go-libp2p/p2p/protocol/identify/internal/user-agent"
"github.com/libp2p/go-libp2p/p2p/protocol/identify/pb" "github.com/libp2p/go-libp2p/p2p/protocol/identify/pb"
"github.com/libp2p/go-libp2p/x/rate"
logging "github.com/ipfs/go-log/v2" logging "github.com/ipfs/go-log/v2"
"github.com/libp2p/go-msgio/pbio" "github.com/libp2p/go-msgio/pbio"

View File

@@ -3,6 +3,7 @@ package transport_integration
import ( import (
"context" "context"
"fmt" "fmt"
"regexp"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
@@ -56,11 +57,6 @@ func TestResourceManagerIsUsed(t *testing.T) {
expectedAddr = gomock.Any() expectedAddr = gomock.Any()
} }
expectFd := true
if strings.Contains(tc.Name, "QUIC") || strings.Contains(tc.Name, "WebTransport") || strings.Contains(tc.Name, "WebRTC") {
expectFd = false
}
peerScope := mocknetwork.NewMockPeerScope(ctrl) peerScope := mocknetwork.NewMockPeerScope(ctrl)
peerScope.EXPECT().ReserveMemory(gomock.Any(), gomock.Any()).AnyTimes().Do(func(amount int, _ uint8) { peerScope.EXPECT().ReserveMemory(gomock.Any(), gomock.Any()).AnyTimes().Do(func(amount int, _ uint8) {
reservedMemory.Add(int32(amount)) reservedMemory.Add(int32(amount))
@@ -93,10 +89,16 @@ func TestResourceManagerIsUsed(t *testing.T) {
connScope.EXPECT().ReserveMemory(gomock.Any(), gomock.Any()) connScope.EXPECT().ReserveMemory(gomock.Any(), gomock.Any())
} }
connScope.EXPECT().Done().MinTimes(1) connScope.EXPECT().Done().MinTimes(1)
// udp transports won't have FD
udpTransportRegex := regexp.MustCompile(`QUIC|WebTransport|WebRTC`)
expectFd := !udpTransportRegex.MatchString(tc.Name)
if !testDialer && (strings.Contains(tc.Name, "QUIC") || strings.Contains(tc.Name, "WebTransport")) {
rcmgr.EXPECT().VerifySourceAddress(gomock.Any()).Return(false)
}
rcmgr.EXPECT().OpenConnection(expectedDir, expectFd, expectedAddr).Return(connScope, nil)
var allStreamsDone sync.WaitGroup var allStreamsDone sync.WaitGroup
rcmgr.EXPECT().OpenConnection(expectedDir, expectFd, expectedAddr).Return(connScope, nil)
rcmgr.EXPECT().OpenStream(expectedPeer, gomock.Any()).AnyTimes().DoAndReturn(func(_ peer.ID, _ network.Direction) (network.StreamManagementScope, error) { rcmgr.EXPECT().OpenStream(expectedPeer, gomock.Any()).AnyTimes().DoAndReturn(func(_ peer.ID, _ network.Direction) (network.StreamManagementScope, error) {
allStreamsDone.Add(1) allStreamsDone.Add(1)
streamScope := mocknetwork.NewMockStreamManagementScope(ctrl) streamScope := mocknetwork.NewMockStreamManagementScope(ctrl)

View File

@@ -14,6 +14,7 @@ import (
"io" "io"
"math/big" "math/big"
"net" "net"
"regexp"
"runtime" "runtime"
"strings" "strings"
"sync" "sync"
@@ -38,6 +39,7 @@ import (
"github.com/libp2p/go-libp2p/p2p/net/swarm" "github.com/libp2p/go-libp2p/p2p/net/swarm"
"github.com/libp2p/go-libp2p/p2p/protocol/ping" "github.com/libp2p/go-libp2p/p2p/protocol/ping"
"github.com/libp2p/go-libp2p/p2p/security/noise" "github.com/libp2p/go-libp2p/p2p/security/noise"
"github.com/libp2p/go-libp2p/p2p/transport/quicreuse"
"github.com/libp2p/go-libp2p/p2p/transport/tcp" "github.com/libp2p/go-libp2p/p2p/transport/tcp"
libp2pwebrtc "github.com/libp2p/go-libp2p/p2p/transport/webrtc" libp2pwebrtc "github.com/libp2p/go-libp2p/p2p/transport/webrtc"
"github.com/libp2p/go-libp2p/p2p/transport/websocket" "github.com/libp2p/go-libp2p/p2p/transport/websocket"
@@ -275,6 +277,29 @@ var transportsToTest = []TransportTestCase{
return h return h
}, },
}, },
{
Name: "QUIC-CustomReuse",
HostGenerator: func(t *testing.T, opts TransportTestCaseOpts) host.Host {
libp2pOpts := transformOpts(opts)
if opts.NoListen {
libp2pOpts = append(libp2pOpts, libp2p.NoListenAddrs, libp2p.QUICReuse(quicreuse.NewConnManager))
} else {
qr := libp2p.QUICReuse(quicreuse.NewConnManager)
if !opts.NoRcmgr && opts.ResourceManager != nil {
qr = libp2p.QUICReuse(
quicreuse.NewConnManager,
quicreuse.VerifySourceAddress(opts.ResourceManager.VerifySourceAddress))
}
libp2pOpts = append(libp2pOpts,
qr,
libp2p.ListenAddrStrings("/ip4/127.0.0.1/udp/0/quic-v1"),
)
}
h, err := libp2p.New(libp2pOpts...)
require.NoError(t, err)
return h
},
},
{ {
Name: "WebTransport", Name: "WebTransport",
HostGenerator: func(t *testing.T, opts TransportTestCaseOpts) host.Host { HostGenerator: func(t *testing.T, opts TransportTestCaseOpts) host.Host {
@@ -289,6 +314,30 @@ var transportsToTest = []TransportTestCase{
return h return h
}, },
}, },
{
Name: "WebTransport-CustomReuse",
HostGenerator: func(t *testing.T, opts TransportTestCaseOpts) host.Host {
libp2pOpts := transformOpts(opts)
if opts.NoListen {
libp2pOpts = append(libp2pOpts, libp2p.NoListenAddrs, libp2p.QUICReuse(quicreuse.NewConnManager))
} else {
qr := libp2p.QUICReuse(quicreuse.NewConnManager)
if !opts.NoRcmgr && opts.ResourceManager != nil {
qr = libp2p.QUICReuse(
quicreuse.NewConnManager,
quicreuse.VerifySourceAddress(opts.ResourceManager.VerifySourceAddress),
)
}
libp2pOpts = append(libp2pOpts,
qr,
libp2p.ListenAddrStrings("/ip4/127.0.0.1/udp/0/quic-v1/webtransport"),
)
}
h, err := libp2p.New(libp2pOpts...)
require.NoError(t, err)
return h
},
},
{ {
Name: "WebRTC", Name: "WebRTC",
HostGenerator: func(t *testing.T, opts TransportTestCaseOpts) host.Host { HostGenerator: func(t *testing.T, opts TransportTestCaseOpts) host.Host {
@@ -844,17 +893,23 @@ func TestDiscoverPeerIDFromSecurityNegotiation(t *testing.T) {
// TestCloseConnWhenBlocked tests that the server closes the connection when the rcmgr blocks it. // TestCloseConnWhenBlocked tests that the server closes the connection when the rcmgr blocks it.
func TestCloseConnWhenBlocked(t *testing.T) { func TestCloseConnWhenBlocked(t *testing.T) {
for _, tc := range transportsToTest { for _, tc := range transportsToTest {
// WebRTC doesn't have a connection when rcmgr blocks it, so there's nothing to close.
if tc.Name == "WebRTC" { if tc.Name == "WebRTC" {
continue // WebRTC doesn't have a connection when we block so there's nothing to close continue
} }
t.Run(tc.Name, func(t *testing.T) { t.Run(tc.Name, func(t *testing.T) {
ctrl := gomock.NewController(t) ctrl := gomock.NewController(t)
defer ctrl.Finish() defer ctrl.Finish()
mockRcmgr := mocknetwork.NewMockResourceManager(ctrl) mockRcmgr := mocknetwork.NewMockResourceManager(ctrl)
mockRcmgr.EXPECT().OpenConnection(network.DirInbound, gomock.Any(), gomock.Any()).DoAndReturn(func(network.Direction, bool, ma.Multiaddr) (network.ConnManagementScope, error) { if matched, _ := regexp.MatchString(`^(QUIC|WebTransport)`, tc.Name); matched {
// Block the connection mockRcmgr.EXPECT().VerifySourceAddress(gomock.Any()).AnyTimes().Return(false)
return nil, fmt.Errorf("connections blocked") // If the initial TLS ClientHello is split into two quic-go might call the transport multiple times to open a
}) // connection. This will only be called multiple times if the connection is rejected. If were were to accept
// the connection, this would have been called only once.
mockRcmgr.EXPECT().OpenConnection(network.DirInbound, gomock.Any(), gomock.Any()).Return(nil, errors.New("connection blocked")).AnyTimes()
} else {
mockRcmgr.EXPECT().OpenConnection(network.DirInbound, gomock.Any(), gomock.Any()).Return(nil, errors.New("connection blocked"))
}
mockRcmgr.EXPECT().Close().AnyTimes() mockRcmgr.EXPECT().Close().AnyTimes()
server := tc.HostGenerator(t, TransportTestCaseOpts{ResourceManager: mockRcmgr}) server := tc.HostGenerator(t, TransportTestCaseOpts{ResourceManager: mockRcmgr})
@@ -958,6 +1013,10 @@ func TestErrorCodes(t *testing.T) {
} }
for _, tc := range transportsToTest { for _, tc := range transportsToTest {
if strings.HasPrefix(tc.Name, "WebTransport") {
t.Skipf("skipping: %s, not implemented", tc.Name)
continue
}
t.Run(tc.Name, func(t *testing.T) { t.Run(tc.Name, func(t *testing.T) {
server := tc.HostGenerator(t, TransportTestCaseOpts{}) server := tc.HostGenerator(t, TransportTestCaseOpts{})
client := tc.HostGenerator(t, TransportTestCaseOpts{NoListen: true}) client := tc.HostGenerator(t, TransportTestCaseOpts{NoListen: true})
@@ -993,10 +1052,6 @@ func TestErrorCodes(t *testing.T) {
} }
t.Run("StreamResetWithError", func(t *testing.T) { t.Run("StreamResetWithError", func(t *testing.T) {
if tc.Name == "WebTransport" {
t.Skipf("skipping: %s, not implemented", tc.Name)
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
s, err := client.NewStream(ctx, server.ID(), "/test") s, err := client.NewStream(ctx, server.ID(), "/test")
@@ -1019,10 +1074,6 @@ func TestErrorCodes(t *testing.T) {
}) })
}) })
t.Run("StreamResetWithErrorByRemote", func(t *testing.T) { t.Run("StreamResetWithErrorByRemote", func(t *testing.T) {
if tc.Name == "WebTransport" {
t.Skipf("skipping: %s, not implemented", tc.Name)
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
s, err := client.NewStream(ctx, server.ID(), "/test") s, err := client.NewStream(ctx, server.ID(), "/test")
@@ -1046,7 +1097,7 @@ func TestErrorCodes(t *testing.T) {
}) })
t.Run("StreamResetByConnCloseWithError", func(t *testing.T) { t.Run("StreamResetByConnCloseWithError", func(t *testing.T) {
if tc.Name == "WebTransport" || tc.Name == "WebRTC" { if tc.Name == "WebRTC" {
t.Skipf("skipping: %s, not implemented", tc.Name) t.Skipf("skipping: %s, not implemented", tc.Name)
return return
} }
@@ -1074,7 +1125,7 @@ func TestErrorCodes(t *testing.T) {
}) })
t.Run("NewStreamErrorByConnCloseWithError", func(t *testing.T) { t.Run("NewStreamErrorByConnCloseWithError", func(t *testing.T) {
if tc.Name == "WebTransport" || tc.Name == "WebRTC" { if tc.Name == "WebRTC" {
t.Skipf("skipping: %s, not implemented", tc.Name) t.Skipf("skipping: %s, not implemented", tc.Name)
return return
} }

View File

@@ -580,7 +580,7 @@ func testStatelessReset(t *testing.T, tc *connTestCase) {
ln := runServer(t, serverTransport, "/ip4/127.0.0.1/udp/0/quic-v1") ln := runServer(t, serverTransport, "/ip4/127.0.0.1/udp/0/quic-v1")
var drop uint32 var drop uint32
dropCallback := func(quicproxy.Direction, []byte) bool { return atomic.LoadUint32(&drop) > 0 } dropCallback := func(quicproxy.Direction, net.Addr, net.Addr, []byte) bool { return atomic.LoadUint32(&drop) > 0 }
proxyConn, cleanup := newUDPConnLocalhost(t, 0) proxyConn, cleanup := newUDPConnLocalhost(t, 0)
proxy := quicproxy.Proxy{ proxy := quicproxy.Proxy{
Conn: proxyConn, Conn: proxyConn,

View File

@@ -88,12 +88,21 @@ func (l *listener) wrapConn(qconn quic.Connection) (*conn, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
connScope, err := network.UnwrapConnManagementScope(qconn.Context())
connScope, err := l.rcmgr.OpenConnection(network.DirInbound, false, remoteMultiaddr) if err != nil {
connScope = nil
// Don't error here.
// Setup scope if we don't have scope from quicreuse.
// This is better than failing so that users that don't use quicreuse.ConnContext option with the resource
// manager work correctly.
}
if connScope == nil {
connScope, err = l.rcmgr.OpenConnection(network.DirInbound, false, remoteMultiaddr)
if err != nil { if err != nil {
log.Debugw("resource manager blocked incoming connection", "addr", qconn.RemoteAddr(), "error", err) log.Debugw("resource manager blocked incoming connection", "addr", qconn.RemoteAddr(), "error", err)
return nil, err return nil, err
} }
}
c, err := l.wrapConnWithScope(qconn, connScope, remoteMultiaddr) c, err := l.wrapConnWithScope(qconn, connScope, remoteMultiaddr)
if err != nil { if err != nil {
connScope.Done() connScope.Done()

View File

@@ -20,6 +20,7 @@ import (
"github.com/quic-go/quic-go" "github.com/quic-go/quic-go"
quiclogging "github.com/quic-go/quic-go/logging" quiclogging "github.com/quic-go/quic-go/logging"
quicmetrics "github.com/quic-go/quic-go/metrics" quicmetrics "github.com/quic-go/quic-go/metrics"
"golang.org/x/time/rate"
) )
type QUICListener interface { type QUICListener interface {
@@ -38,7 +39,7 @@ type QUICTransport interface {
io.Closer io.Closer
} }
// ConnManager implements using the same listen address for both QUIC & WebTransport, reusing // ConnManager enables QUIC and WebTransport transports to listen on the same port, reusing
// listen addresses for dialing, and provides a PacketConn for sharing the listen address // listen addresses for dialing, and provides a PacketConn for sharing the listen address
// with other protocols like WebRTC. // with other protocols like WebRTC.
// Reusing the listen address for dialing helps with address discovery and hole punching. For details // Reusing the listen address for dialing helps with address discovery and hole punching. For details
@@ -64,6 +65,9 @@ type ConnManager struct {
srk quic.StatelessResetKey srk quic.StatelessResetKey
tokenKey quic.TokenGeneratorKey tokenKey quic.TokenGeneratorKey
connContext connContextFunc
verifySourceAddress func(addr net.Addr) bool
} }
type quicListenerEntry struct { type quicListenerEntry struct {
@@ -80,6 +84,11 @@ func defaultSourceIPSelectorFn() (SourceIPSelector, error) {
return &netrouteSourceIPSelector{routes: r}, err return &netrouteSourceIPSelector{routes: r}, err
} }
const (
unverifiedAddressNewConnectionRPS = 1000
unverifiedAddressNewConnectionBurst = 1000
)
// NewConnManager returns a new ConnManager // NewConnManager returns a new ConnManager
func NewConnManager(statelessResetKey quic.StatelessResetKey, tokenKey quic.TokenGeneratorKey, opts ...Option) (*ConnManager, error) { func NewConnManager(statelessResetKey quic.StatelessResetKey, tokenKey quic.TokenGeneratorKey, opts ...Option) (*ConnManager, error) {
cm := &ConnManager{ cm := &ConnManager{
@@ -103,9 +112,24 @@ func NewConnManager(statelessResetKey quic.StatelessResetKey, tokenKey quic.Toke
cm.clientConfig = quicConf cm.clientConfig = quicConf
cm.serverConfig = serverConfig cm.serverConfig = serverConfig
// Verify source addresses when under high load.
// This is ensures that the number of spoofed/unverified addresses that are passed to downstream rate limiters
// are limited, which enables IP address based rate limiting.
sourceAddrRateLimiter := rate.NewLimiter(unverifiedAddressNewConnectionRPS, unverifiedAddressNewConnectionBurst)
vsa := cm.verifySourceAddress
cm.verifySourceAddress = func(addr net.Addr) bool {
if sourceAddrRateLimiter.Allow() {
if vsa != nil {
return vsa(addr)
}
return false
}
return true
}
if cm.enableReuseport { if cm.enableReuseport {
cm.reuseUDP4 = newReuse(&statelessResetKey, &tokenKey, cm.listenUDP, cm.sourceIPSelectorFn) cm.reuseUDP4 = newReuse(&statelessResetKey, &tokenKey, cm.listenUDP, cm.sourceIPSelectorFn, cm.connContext, cm.verifySourceAddress)
cm.reuseUDP6 = newReuse(&statelessResetKey, &tokenKey, cm.listenUDP, cm.sourceIPSelectorFn) cm.reuseUDP6 = newReuse(&statelessResetKey, &tokenKey, cm.listenUDP, cm.sourceIPSelectorFn, cm.connContext, cm.verifySourceAddress)
} }
return cm, nil return cm, nil
} }
@@ -290,16 +314,7 @@ func (c *ConnManager) transportForListen(association any, network string, laddr
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &singleOwnerTransport{ return c.newSingleOwnerTransport(conn), nil
packetConn: conn,
Transport: &wrappedQUICTransport{
&quic.Transport{
Conn: conn,
StatelessResetKey: &c.srk,
TokenGeneratorKey: &c.tokenKey,
},
},
}, nil
} }
type associationKey struct{} type associationKey struct{}
@@ -378,11 +393,24 @@ func (c *ConnManager) TransportWithAssociationForDial(association any, network s
laddr = &net.UDPAddr{IP: net.IPv6zero, Port: 0} laddr = &net.UDPAddr{IP: net.IPv6zero, Port: 0}
} }
conn, err := c.listenUDP(network, laddr) conn, err := c.listenUDP(network, laddr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &singleOwnerTransport{Transport: &wrappedQUICTransport{&quic.Transport{Conn: conn, StatelessResetKey: &c.srk}}, packetConn: conn}, nil return c.newSingleOwnerTransport(conn), nil
}
func (c *ConnManager) newSingleOwnerTransport(conn net.PacketConn) *singleOwnerTransport {
return &singleOwnerTransport{
Transport: &wrappedQUICTransport{
Transport: newQUICTransport(
conn,
&c.tokenKey,
&c.srk,
c.connContext,
c.verifySourceAddress,
),
},
packetConn: conn}
} }
// Protocols returns the supported QUIC protocols. The only supported protocol at the moment is /quic-v1. // Protocols returns the supported QUIC protocols. The only supported protocol at the moment is /quic-v1.
@@ -414,3 +442,19 @@ var _ QUICTransport = (*wrappedQUICTransport)(nil)
func (t *wrappedQUICTransport) Listen(tlsConf *tls.Config, conf *quic.Config) (QUICListener, error) { func (t *wrappedQUICTransport) Listen(tlsConf *tls.Config, conf *quic.Config) (QUICListener, error) {
return t.Transport.Listen(tlsConf, conf) return t.Transport.Listen(tlsConf, conf)
} }
func newQUICTransport(
conn net.PacketConn,
tokenGeneratorKey *quic.TokenGeneratorKey,
statelessResetKey *quic.StatelessResetKey,
connContext connContextFunc,
verifySourceAddress func(addr net.Addr) bool,
) *quic.Transport {
return &quic.Transport{
Conn: conn,
TokenGeneratorKey: tokenGeneratorKey,
StatelessResetKey: statelessResetKey,
ConnContext: connContext,
VerifySourceAddress: verifySourceAddress,
}
}

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"crypto/rand" "crypto/rand"
"crypto/tls" "crypto/tls"
"errors"
"fmt" "fmt"
"net" "net"
"runtime" "runtime"
@@ -63,7 +64,7 @@ func testListenOnSameProto(t *testing.T, enableReuseport bool) {
ln1, err := cm.ListenQUIC(ma.StringCast("/ip4/127.0.0.1/udp/0/quic-v1"), &tls.Config{NextProtos: []string{alpn}}, nil) ln1, err := cm.ListenQUIC(ma.StringCast("/ip4/127.0.0.1/udp/0/quic-v1"), &tls.Config{NextProtos: []string{alpn}}, nil)
require.NoError(t, err) require.NoError(t, err)
defer ln1.Close() defer func() { _ = ln1.Close() }()
addr := ma.StringCast(fmt.Sprintf("/ip4/127.0.0.1/udp/%d/quic-v1", ln1.Addr().(*net.UDPAddr).Port)) addr := ma.StringCast(fmt.Sprintf("/ip4/127.0.0.1/udp/%d/quic-v1", ln1.Addr().(*net.UDPAddr).Port))
_, err = cm.ListenQUIC(addr, &tls.Config{NextProtos: []string{alpn}}, nil) _, err = cm.ListenQUIC(addr, &tls.Config{NextProtos: []string{alpn}}, nil)
@@ -72,7 +73,7 @@ func testListenOnSameProto(t *testing.T, enableReuseport bool) {
// listening on a different address works // listening on a different address works
ln2, err := cm.ListenQUIC(ma.StringCast("/ip4/127.0.0.1/udp/0/quic-v1"), &tls.Config{NextProtos: []string{alpn}}, nil) ln2, err := cm.ListenQUIC(ma.StringCast("/ip4/127.0.0.1/udp/0/quic-v1"), &tls.Config{NextProtos: []string{alpn}}, nil)
require.NoError(t, err) require.NoError(t, err)
defer ln2.Close() defer func() { _ = ln2.Close() }()
} }
// The conn passed to quic-go should be a conn that quic-go can be // The conn passed to quic-go should be a conn that quic-go can be
@@ -206,7 +207,9 @@ func connectWithProtocol(t *testing.T, addr net.Addr, alpn string) (peer.ID, err
cconn, err := net.ListenUDP("udp4", nil) cconn, err := net.ListenUDP("udp4", nil)
tlsConf.NextProtos = []string{alpn} tlsConf.NextProtos = []string{alpn}
require.NoError(t, err) require.NoError(t, err)
c, err := quic.Dial(context.Background(), cconn, addr, tlsConf, nil) ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
c, err := quic.Dial(ctx, cconn, addr, tlsConf, nil)
cancel()
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -387,3 +390,102 @@ func TestAssociate(t *testing.T) {
) )
}) })
} }
func TestConnContext(t *testing.T) {
for _, reuse := range []bool{true, false} {
t.Run(fmt.Sprintf("reuseport:%t_error", reuse), func(t *testing.T) {
opts := []Option{
ConnContext(func(ctx context.Context, _ *quic.ClientInfo) (context.Context, error) {
return ctx, errors.New("test error")
})}
if !reuse {
opts = append(opts, DisableReuseport())
}
cm, err := NewConnManager(
quic.StatelessResetKey{},
quic.TokenGeneratorKey{},
opts...,
)
require.NoError(t, err)
defer func() { _ = cm.Close() }()
proto1 := "proto1"
_, proto1TLS := getTLSConfForProto(t, proto1)
ln1, err := cm.ListenQUIC(
ma.StringCast("/ip4/127.0.0.1/udp/0/quic-v1"),
proto1TLS,
nil,
)
require.NoError(t, err)
defer ln1.Close()
proto2 := "proto2"
_, proto2TLS := getTLSConfForProto(t, proto2)
ln2, err := cm.ListenQUIC(
ma.StringCast(fmt.Sprintf("/ip4/127.0.0.1/udp/%d/quic-v1", ln1.Addr().(*net.UDPAddr).Port)),
proto2TLS,
nil,
)
require.NoError(t, err)
defer ln2.Close()
_, err = connectWithProtocol(t, ln1.Addr(), proto1)
require.ErrorContains(t, err, "CONNECTION_REFUSED")
_, err = connectWithProtocol(t, ln1.Addr(), proto2)
require.ErrorContains(t, err, "CONNECTION_REFUSED")
})
t.Run(fmt.Sprintf("reuseport:%t_success", reuse), func(t *testing.T) {
type ctxKey struct{}
opts := []Option{
ConnContext(func(ctx context.Context, _ *quic.ClientInfo) (context.Context, error) {
return context.WithValue(ctx, ctxKey{}, "success"), nil
})}
if !reuse {
opts = append(opts, DisableReuseport())
}
cm, err := NewConnManager(
quic.StatelessResetKey{},
quic.TokenGeneratorKey{},
opts...,
)
require.NoError(t, err)
defer func() { _ = cm.Close() }()
proto1 := "proto1"
_, proto1TLS := getTLSConfForProto(t, proto1)
ln1, err := cm.ListenQUIC(
ma.StringCast("/ip4/127.0.0.1/udp/0/quic-v1"),
proto1TLS,
nil,
)
require.NoError(t, err)
defer ln1.Close()
clientKey, _, err := crypto.GenerateEd25519Key(rand.Reader)
require.NoError(t, err)
clientIdentity, err := libp2ptls.NewIdentity(clientKey)
require.NoError(t, err)
tlsConf, peerChan := clientIdentity.ConfigForPeer("")
cconn, err := net.ListenUDP("udp4", nil)
tlsConf.NextProtos = []string{proto1}
require.NoError(t, err)
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
conn, err := quic.Dial(ctx, cconn, ln1.Addr(), tlsConf, nil)
cancel()
require.NoError(t, err)
defer conn.CloseWithError(0, "")
require.Equal(t, proto1, conn.ConnectionState().TLS.NegotiatedProtocol)
_, err = peer.IDFromPublicKey(<-peerChan)
require.NoError(t, err)
acceptCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
c, err := ln1.Accept(acceptCtx)
cancel()
require.NoError(t, err)
defer c.CloseWithError(0, "")
require.Equal(t, "success", c.Context().Value(ctxKey{}))
})
}
}

View File

@@ -1,9 +1,12 @@
package quicreuse package quicreuse
import ( import (
"context"
"errors"
"net" "net"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/quic-go/quic-go"
) )
type Option func(*ConnManager) error type Option func(*ConnManager) error
@@ -31,6 +34,27 @@ func DisableReuseport() Option {
} }
} }
// ConnContext sets the context for all connections accepted by listeners. This doesn't affect the
// context for dialed connections. To reject a connection, return a non nil error.
func ConnContext(f func(ctx context.Context, clientInfo *quic.ClientInfo) (context.Context, error)) Option {
return func(m *ConnManager) error {
if m.connContext != nil {
return errors.New("cannot set ConnContext more than once")
}
m.connContext = f
return nil
}
}
// VerifySourceAddress returns whether to verify the source address for incoming connection requests.
// For more details see: `quic.Transport.VerifySourceAddress`
func VerifySourceAddress(f func(addr net.Addr) bool) Option {
return func(m *ConnManager) error {
m.verifySourceAddress = f
return nil
}
}
// EnableMetrics enables Prometheus metrics collection. If reg is nil, // EnableMetrics enables Prometheus metrics collection. If reg is nil,
// prometheus.DefaultRegisterer will be used as the registerer. // prometheus.DefaultRegisterer will be used as the registerer.
func EnableMetrics(reg prometheus.Registerer) Option { func EnableMetrics(reg prometheus.Registerer) Option {

View File

@@ -91,6 +91,8 @@ type refcountedTransport struct {
assocations map[any]struct{} assocations map[any]struct{}
} }
type connContextFunc = func(context.Context, *quic.ClientInfo) (context.Context, error)
// associate an arbitrary value with this transport. // associate an arbitrary value with this transport.
// This lets us "tag" the refcountedTransport when listening so we can use it // This lets us "tag" the refcountedTransport when listening so we can use it
// later for dialing. Necessary for holepunching and learning about our own // later for dialing. Necessary for holepunching and learning about our own
@@ -183,9 +185,12 @@ type reuse struct {
statelessResetKey *quic.StatelessResetKey statelessResetKey *quic.StatelessResetKey
tokenGeneratorKey *quic.TokenGeneratorKey tokenGeneratorKey *quic.TokenGeneratorKey
connContext connContextFunc
verifySourceAddress func(addr net.Addr) bool
} }
func newReuse(srk *quic.StatelessResetKey, tokenKey *quic.TokenGeneratorKey, listenUDP listenUDP, sourceIPSelectorFn func() (SourceIPSelector, error)) *reuse { func newReuse(srk *quic.StatelessResetKey, tokenKey *quic.TokenGeneratorKey, listenUDP listenUDP, sourceIPSelectorFn func() (SourceIPSelector, error),
connContext connContextFunc, verifySourceAddress func(addr net.Addr) bool) *reuse {
r := &reuse{ r := &reuse{
unicast: make(map[string]map[int]*refcountedTransport), unicast: make(map[string]map[int]*refcountedTransport),
globalListeners: make(map[int]*refcountedTransport), globalListeners: make(map[int]*refcountedTransport),
@@ -196,6 +201,8 @@ func newReuse(srk *quic.StatelessResetKey, tokenKey *quic.TokenGeneratorKey, lis
sourceIPSelectorFn: sourceIPSelectorFn, sourceIPSelectorFn: sourceIPSelectorFn,
statelessResetKey: srk, statelessResetKey: srk,
tokenGeneratorKey: tokenKey, tokenGeneratorKey: tokenKey,
connContext: connContext,
verifySourceAddress: verifySourceAddress,
} }
go r.gc() go r.gc()
return r return r
@@ -341,16 +348,7 @@ func (r *reuse) transportForDialLocked(association any, network string, source *
if err != nil { if err != nil {
return nil, err return nil, err
} }
tr := &refcountedTransport{ tr := r.newTransport(conn)
QUICTransport: &wrappedQUICTransport{
Transport: &quic.Transport{
Conn: conn,
StatelessResetKey: r.statelessResetKey,
TokenGeneratorKey: r.tokenGeneratorKey,
},
},
packetConn: conn,
}
r.globalDialers[conn.LocalAddr().(*net.UDPAddr).Port] = tr r.globalDialers[conn.LocalAddr().(*net.UDPAddr).Port] = tr
return tr, nil return tr, nil
} }
@@ -434,18 +432,10 @@ func (r *reuse) TransportForListen(network string, laddr *net.UDPAddr) (*refcoun
if err != nil { if err != nil {
return nil, err return nil, err
} }
localAddr := conn.LocalAddr().(*net.UDPAddr) tr := r.newTransport(conn)
tr := &refcountedTransport{
QUICTransport: &wrappedQUICTransport{
Transport: &quic.Transport{
Conn: conn,
StatelessResetKey: r.statelessResetKey,
},
},
packetConn: conn,
}
tr.IncreaseCount() tr.IncreaseCount()
localAddr := conn.LocalAddr().(*net.UDPAddr)
// Deal with listen on a global address // Deal with listen on a global address
if localAddr.IP.IsUnspecified() { if localAddr.IP.IsUnspecified() {
// The kernel already checked that the laddr is not already listen // The kernel already checked that the laddr is not already listen
@@ -468,6 +458,21 @@ func (r *reuse) TransportForListen(network string, laddr *net.UDPAddr) (*refcoun
return tr, nil return tr, nil
} }
func (r *reuse) newTransport(conn net.PacketConn) *refcountedTransport {
return &refcountedTransport{
QUICTransport: &wrappedQUICTransport{
Transport: newQUICTransport(
conn,
r.tokenGeneratorKey,
r.statelessResetKey,
r.connContext,
r.verifySourceAddress,
),
},
packetConn: conn,
}
}
func (r *reuse) Close() error { func (r *reuse) Close() error {
close(r.closeChan) close(r.closeChan)
<-r.gcStopChan <-r.gcStopChan

View File

@@ -61,7 +61,7 @@ func cleanup(t *testing.T, reuse *reuse) {
} }
func TestReuseListenOnAllIPv4(t *testing.T) { func TestReuseListenOnAllIPv4(t *testing.T) {
reuse := newReuse(nil, nil, defaultListenUDP, defaultSourceIPSelectorFn) reuse := newReuse(nil, nil, defaultListenUDP, defaultSourceIPSelectorFn, nil, nil)
require.Eventually(t, isGarbageCollectorRunning, 500*time.Millisecond, 50*time.Millisecond, "expected garbage collector to be running") require.Eventually(t, isGarbageCollectorRunning, 500*time.Millisecond, 50*time.Millisecond, "expected garbage collector to be running")
cleanup(t, reuse) cleanup(t, reuse)
@@ -73,7 +73,7 @@ func TestReuseListenOnAllIPv4(t *testing.T) {
} }
func TestReuseListenOnAllIPv6(t *testing.T) { func TestReuseListenOnAllIPv6(t *testing.T) {
reuse := newReuse(nil, nil, defaultListenUDP, defaultSourceIPSelectorFn) reuse := newReuse(nil, nil, defaultListenUDP, defaultSourceIPSelectorFn, nil, nil)
require.Eventually(t, isGarbageCollectorRunning, 500*time.Millisecond, 50*time.Millisecond, "expected garbage collector to be running") require.Eventually(t, isGarbageCollectorRunning, 500*time.Millisecond, 50*time.Millisecond, "expected garbage collector to be running")
cleanup(t, reuse) cleanup(t, reuse)
@@ -86,7 +86,7 @@ func TestReuseListenOnAllIPv6(t *testing.T) {
} }
func TestReuseCreateNewGlobalConnOnDial(t *testing.T) { func TestReuseCreateNewGlobalConnOnDial(t *testing.T) {
reuse := newReuse(nil, nil, defaultListenUDP, defaultSourceIPSelectorFn) reuse := newReuse(nil, nil, defaultListenUDP, defaultSourceIPSelectorFn, nil, nil)
cleanup(t, reuse) cleanup(t, reuse)
addr, err := net.ResolveUDPAddr("udp4", "1.1.1.1:1234") addr, err := net.ResolveUDPAddr("udp4", "1.1.1.1:1234")
@@ -100,7 +100,7 @@ func TestReuseCreateNewGlobalConnOnDial(t *testing.T) {
} }
func TestReuseConnectionWhenDialing(t *testing.T) { func TestReuseConnectionWhenDialing(t *testing.T) {
reuse := newReuse(nil, nil, defaultListenUDP, defaultSourceIPSelectorFn) reuse := newReuse(nil, nil, defaultListenUDP, defaultSourceIPSelectorFn, nil, nil)
cleanup(t, reuse) cleanup(t, reuse)
addr, err := net.ResolveUDPAddr("udp4", "0.0.0.0:0") addr, err := net.ResolveUDPAddr("udp4", "0.0.0.0:0")
@@ -117,7 +117,7 @@ func TestReuseConnectionWhenDialing(t *testing.T) {
} }
func TestReuseConnectionWhenListening(t *testing.T) { func TestReuseConnectionWhenListening(t *testing.T) {
reuse := newReuse(nil, nil, defaultListenUDP, defaultSourceIPSelectorFn) reuse := newReuse(nil, nil, defaultListenUDP, defaultSourceIPSelectorFn, nil, nil)
cleanup(t, reuse) cleanup(t, reuse)
raddr, err := net.ResolveUDPAddr("udp4", "1.1.1.1:1234") raddr, err := net.ResolveUDPAddr("udp4", "1.1.1.1:1234")
@@ -132,7 +132,7 @@ func TestReuseConnectionWhenListening(t *testing.T) {
} }
func TestReuseConnectionWhenDialBeforeListen(t *testing.T) { func TestReuseConnectionWhenDialBeforeListen(t *testing.T) {
reuse := newReuse(nil, nil, defaultListenUDP, defaultSourceIPSelectorFn) reuse := newReuse(nil, nil, defaultListenUDP, defaultSourceIPSelectorFn, nil, nil)
cleanup(t, reuse) cleanup(t, reuse)
// dial any address // dial any address
@@ -166,7 +166,7 @@ func TestReuseListenOnSpecificInterface(t *testing.T) {
if platformHasRoutingTables() { if platformHasRoutingTables() {
t.Skip("this test only works on platforms that support routing tables") t.Skip("this test only works on platforms that support routing tables")
} }
reuse := newReuse(nil, nil, defaultListenUDP, defaultSourceIPSelectorFn) reuse := newReuse(nil, nil, defaultListenUDP, defaultSourceIPSelectorFn, nil, nil)
cleanup(t, reuse) cleanup(t, reuse)
router, err := netroute.New() router, err := netroute.New()
@@ -203,7 +203,7 @@ func TestReuseGarbageCollect(t *testing.T) {
maxUnusedDuration = 10 * maxUnusedDuration maxUnusedDuration = 10 * maxUnusedDuration
} }
reuse := newReuse(nil, nil, defaultListenUDP, defaultSourceIPSelectorFn) reuse := newReuse(nil, nil, defaultListenUDP, defaultSourceIPSelectorFn, nil, nil)
cleanup(t, reuse) cleanup(t, reuse)
numGlobals := func() int { numGlobals := func() int {

View File

@@ -799,7 +799,7 @@ func TestConnectionTimeoutOnListener(t *testing.T) {
proxy := quicproxy.Proxy{ proxy := quicproxy.Proxy{
Conn: newUDPConnLocalhost(t), Conn: newUDPConnLocalhost(t),
ServerAddr: ln.Addr().(*net.UDPAddr), ServerAddr: ln.Addr().(*net.UDPAddr),
DropPacket: func(quicproxy.Direction, []byte) bool { return drop.Load() }, DropPacket: func(_ quicproxy.Direction, _, _ net.Addr, _ []byte) bool { return drop.Load() },
} }
require.NoError(t, proxy.Start()) require.NoError(t, proxy.Start())
defer proxy.Close() defer proxy.Close()

View File

@@ -72,7 +72,7 @@ func (c *conn) allowWindowIncrease(size uint64) bool {
// garbage collection to properly work in this package. // garbage collection to properly work in this package.
func (c *conn) Close() error { func (c *conn) Close() error {
defer c.scope.Done() defer c.scope.Done()
c.transport.removeConn(c.session) c.transport.removeConn(c.qconn)
err := c.session.CloseWithError(0, "") err := c.session.CloseWithError(0, "")
_ = c.qconn.CloseWithError(1, "") _ = c.qconn.CloseWithError(1, "")
return err return err

View File

@@ -149,13 +149,22 @@ func (l *listener) httpHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusForbidden) w.WriteHeader(http.StatusForbidden)
return return
} }
connScope, err := network.UnwrapConnManagementScope(r.Context())
connScope, err := l.transport.rcmgr.OpenConnection(network.DirInbound, false, remoteMultiaddr) if err != nil {
connScope = nil
// Don't error here.
// Setup scope if we don't have scope from quicreuse.
// This is better than failing so that users that don't use quicreuse.ConnContext option with the resource
// manager still work correctly.
}
if connScope == nil {
connScope, err = l.transport.rcmgr.OpenConnection(network.DirInbound, false, remoteMultiaddr)
if err != nil { if err != nil {
log.Debugw("resource manager blocked incoming connection", "addr", r.RemoteAddr, "error", err) log.Debugw("resource manager blocked incoming connection", "addr", r.RemoteAddr, "error", err)
w.WriteHeader(http.StatusServiceUnavailable) w.WriteHeader(http.StatusServiceUnavailable)
return return
} }
}
err = l.httpHandlerWithConnScope(w, r, connScope) err = l.httpHandlerWithConnScope(w, r, connScope)
if err != nil { if err != nil {
connScope.Done() connScope.Done()
@@ -212,7 +221,7 @@ func (l *listener) httpHandlerWithConnScope(w http.ResponseWriter, r *http.Reque
} }
conn := newConn(l.transport, sess, sconn, connScope, qconn) conn := newConn(l.transport, sess, sconn, connScope, qconn)
l.transport.addConn(sess, conn) l.transport.addConn(qconn, conn)
select { select {
case l.queue <- conn: case l.queue <- conn:
default: default:

View File

@@ -86,7 +86,7 @@ type transport struct {
noise *noise.Transport noise *noise.Transport
connMx sync.Mutex connMx sync.Mutex
conns map[quic.ConnectionTracingID]*conn // using quic-go's ConnectionTracingKey as map key conns map[quic.Connection]*conn // quic connection -> *conn
handshakeTimeout time.Duration handshakeTimeout time.Duration
} }
@@ -113,7 +113,7 @@ func New(key ic.PrivKey, psk pnet.PSK, connManager *quicreuse.ConnManager, gater
gater: gater, gater: gater,
clock: clock.New(), clock: clock.New(),
connManager: connManager, connManager: connManager,
conns: map[quic.ConnectionTracingID]*conn{}, conns: map[quic.Connection]*conn{},
handshakeTimeout: handshakeTimeout, handshakeTimeout: handshakeTimeout,
} }
for _, opt := range opts { for _, opt := range opts {
@@ -184,7 +184,7 @@ func (t *transport) dialWithScope(ctx context.Context, raddr ma.Multiaddr, p pee
return nil, fmt.Errorf("secured connection gated") return nil, fmt.Errorf("secured connection gated")
} }
conn := newConn(t, sess, sconn, scope, qconn) conn := newConn(t, sess, sconn, scope, qconn)
t.addConn(sess, conn) t.addConn(qconn, conn)
return conn, nil return conn, nil
} }
@@ -361,22 +361,22 @@ func (t *transport) allowWindowIncrease(conn quic.Connection, size uint64) bool
t.connMx.Lock() t.connMx.Lock()
defer t.connMx.Unlock() defer t.connMx.Unlock()
c, ok := t.conns[conn.Context().Value(quic.ConnectionTracingKey).(quic.ConnectionTracingID)] c, ok := t.conns[conn]
if !ok { if !ok {
return false return false
} }
return c.allowWindowIncrease(size) return c.allowWindowIncrease(size)
} }
func (t *transport) addConn(sess *webtransport.Session, c *conn) { func (t *transport) addConn(conn quic.Connection, c *conn) {
t.connMx.Lock() t.connMx.Lock()
t.conns[sess.Context().Value(quic.ConnectionTracingKey).(quic.ConnectionTracingID)] = c t.conns[conn] = c
t.connMx.Unlock() t.connMx.Unlock()
} }
func (t *transport) removeConn(sess *webtransport.Session) { func (t *transport) removeConn(conn quic.Connection) {
t.connMx.Lock() t.connMx.Lock()
delete(t.conns, sess.Context().Value(quic.ConnectionTracingKey).(quic.ConnectionTracingID)) delete(t.conns, conn)
t.connMx.Unlock() t.connMx.Unlock()
} }

View File

@@ -584,7 +584,7 @@ func TestFlowControlWindowIncrease(t *testing.T) {
proxy := quicproxy.Proxy{ proxy := quicproxy.Proxy{
Conn: newUDPConnLocalhost(t), Conn: newUDPConnLocalhost(t),
ServerAddr: ln.Addr().(*net.UDPAddr), ServerAddr: ln.Addr().(*net.UDPAddr),
DelayPacket: func(quicproxy.Direction, []byte) time.Duration { return rtt / 2 }, DelayPacket: func(quicproxy.Direction, net.Addr, net.Addr, []byte) time.Duration { return rtt / 2 },
} }
require.NoError(t, proxy.Start()) require.NoError(t, proxy.Start())
defer proxy.Close() defer proxy.Close()

View File

@@ -19,7 +19,7 @@ require (
github.com/francoispqt/gojay v1.2.13 // indirect github.com/francoispqt/gojay v1.2.13 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/google/gopacket v1.1.19 // indirect github.com/google/gopacket v1.1.19 // indirect
github.com/google/pprof v0.0.0-20250208200701-d0013a598941 // indirect github.com/google/pprof v0.0.0-20250501235452-c0086092b71a // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect github.com/gorilla/websocket v1.5.3 // indirect
github.com/huin/goupnp v1.3.0 // indirect github.com/huin/goupnp v1.3.0 // indirect
@@ -54,7 +54,7 @@ require (
github.com/multiformats/go-multistream v0.6.0 // indirect github.com/multiformats/go-multistream v0.6.0 // indirect
github.com/multiformats/go-varint v0.0.7 // indirect github.com/multiformats/go-varint v0.0.7 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/onsi/ginkgo/v2 v2.22.2 // indirect github.com/onsi/ginkgo/v2 v2.23.4 // indirect
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
github.com/pion/datachannel v1.5.10 // indirect github.com/pion/datachannel v1.5.10 // indirect
github.com/pion/dtls/v2 v2.2.12 // indirect github.com/pion/dtls/v2 v2.2.12 // indirect
@@ -80,24 +80,25 @@ require (
github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect
github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.50.0 // indirect github.com/quic-go/quic-go v0.52.0 // indirect
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 // indirect github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/wlynxg/anet v0.0.5 // indirect github.com/wlynxg/anet v0.0.5 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/dig v1.18.0 // indirect go.uber.org/dig v1.18.0 // indirect
go.uber.org/fx v1.23.0 // indirect go.uber.org/fx v1.23.0 // indirect
go.uber.org/mock v0.5.0 // indirect go.uber.org/mock v0.5.2 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.35.0 // indirect golang.org/x/crypto v0.37.0 // indirect
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect
golang.org/x/mod v0.23.0 // indirect golang.org/x/mod v0.24.0 // indirect
golang.org/x/net v0.35.0 // indirect golang.org/x/net v0.39.0 // indirect
golang.org/x/sync v0.11.0 // indirect golang.org/x/sync v0.14.0 // indirect
golang.org/x/sys v0.30.0 // indirect golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.22.0 // indirect golang.org/x/text v0.24.0 // indirect
golang.org/x/time v0.11.0 // indirect golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.30.0 // indirect golang.org/x/tools v0.32.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect google.golang.org/protobuf v1.36.5 // indirect
lukechampine.com/blake3 v1.4.0 // indirect lukechampine.com/blake3 v1.4.0 // indirect
) )

View File

@@ -59,16 +59,16 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20250208200701-d0013a598941 h1:43XjGa6toxLpeksjcxs1jIoIyr+vUfOqY2c6HB4bpoc= github.com/google/pprof v0.0.0-20250501235452-c0086092b71a h1:rDA3FfmxwXR+BVKKdz55WwMJ1pD2hJQNW31d+l3mPk4=
github.com/google/pprof v0.0.0-20250208200701-d0013a598941/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/pprof v0.0.0-20250501235452-c0086092b71a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
@@ -179,10 +179,10 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
@@ -231,6 +231,8 @@ github.com/pion/webrtc/v4 v4.0.14/go.mod h1:R3+qTnQTS03UzwDarYecgioNf7DYgTsldxnC
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA= github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA=
github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
@@ -245,8 +247,8 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.50.0 h1:3H/ld1pa3CYhkcc20TPIyG1bNsdhn9qZBGN3b9/UyUo= github.com/quic-go/quic-go v0.52.0 h1:/SlHrCRElyaU6MaEPKqKr9z83sBg2v4FLLvWM+Z47pA=
github.com/quic-go/quic-go v0.50.0/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E= github.com/quic-go/quic-go v0.52.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ=
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 h1:4WFk6u3sOT6pLa1kQ50ZVdm8BQFgJNA117cepZxtLIg= github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 h1:4WFk6u3sOT6pLa1kQ50ZVdm8BQFgJNA117cepZxtLIg=
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66/go.mod h1:Vp72IJajgeOL6ddqrAhmp7IM9zbTcgkQxD/YdxrVwMw= github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66/go.mod h1:Vp72IJajgeOL6ddqrAhmp7IM9zbTcgkQxD/YdxrVwMw=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
@@ -301,6 +303,8 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw= go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw=
go.uber.org/dig v1.18.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= go.uber.org/dig v1.18.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
go.uber.org/fx v1.23.0 h1:lIr/gYWQGfTwGcSXWXu4vP5Ws6iqnNEIY+F/aFzCKTg= go.uber.org/fx v1.23.0 h1:lIr/gYWQGfTwGcSXWXu4vP5Ws6iqnNEIY+F/aFzCKTg=
@@ -308,8 +312,8 @@ go.uber.org/fx v1.23.0/go.mod h1:o/D9n+2mLP6v1EG+qsdT1O8wKopYAsqZasju97SDFCU=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
@@ -329,8 +333,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4= golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4=
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
@@ -343,8 +347,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -365,8 +369,8 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -380,8 +384,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -402,8 +406,8 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -419,8 +423,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
@@ -436,8 +440,8 @@ golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapK
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -9,7 +9,6 @@ import (
"time" "time"
"github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/network"
ma "github.com/multiformats/go-multiaddr"
manet "github.com/multiformats/go-multiaddr/net" manet "github.com/multiformats/go-multiaddr/net"
"golang.org/x/time/rate" "golang.org/x/time/rate"
) )
@@ -62,6 +61,8 @@ func (r *Limiter) init() {
} else { } else {
r.globalBucket = rate.NewLimiter(rate.Limit(r.GlobalLimit.RPS), r.GlobalLimit.Burst) r.globalBucket = rate.NewLimiter(rate.Limit(r.GlobalLimit.RPS), r.GlobalLimit.Burst)
} }
// clone the slice in case it's shared with other limiters
r.NetworkPrefixLimits = slices.Clone(r.NetworkPrefixLimits)
// sort such that the widest prefix (smallest bit count) is last. // sort such that the widest prefix (smallest bit count) is last.
slices.SortFunc(r.NetworkPrefixLimits, func(a, b PrefixLimit) int { return b.Prefix.Bits() - a.Prefix.Bits() }) slices.SortFunc(r.NetworkPrefixLimits, func(a, b PrefixLimit) int { return b.Prefix.Bits() - a.Prefix.Bits() })
r.networkPrefixBuckets = make([]*rate.Limiter, 0, len(r.NetworkPrefixLimits)) r.networkPrefixBuckets = make([]*rate.Limiter, 0, len(r.NetworkPrefixLimits))
@@ -79,7 +80,16 @@ func (r *Limiter) init() {
func (r *Limiter) Limit(f func(s network.Stream)) func(s network.Stream) { func (r *Limiter) Limit(f func(s network.Stream)) func(s network.Stream) {
r.init() r.init()
return func(s network.Stream) { return func(s network.Stream) {
if !r.allow(s.Conn().RemoteMultiaddr()) { addr := s.Conn().RemoteMultiaddr()
ip, err := manet.ToIP(addr)
if err != nil {
ip = nil
}
ipAddr, ok := netip.AddrFromSlice(ip)
if !ok {
ipAddr = netip.Addr{}
}
if !r.Allow(ipAddr) {
_ = s.ResetWithError(network.StreamRateLimited) _ = s.ResetWithError(network.StreamRateLimited)
return return
} }
@@ -87,7 +97,8 @@ func (r *Limiter) Limit(f func(s network.Stream)) func(s network.Stream) {
} }
} }
func (r *Limiter) allow(addr ma.Multiaddr) bool { // Allow returns true if requests for `ipAddr` are within specified rate limits
func (r *Limiter) Allow(ipAddr netip.Addr) bool {
r.init() r.init()
// Check buckets from the most specific to the least. // Check buckets from the most specific to the least.
// //
@@ -97,14 +108,6 @@ func (r *Limiter) allow(addr ma.Multiaddr) bool {
// bucket before the specific bucket, and the specific bucket rejected the // bucket before the specific bucket, and the specific bucket rejected the
// request, there's no way to return the token to the global bucket. So all // request, there's no way to return the token to the global bucket. So all
// rejected requests from the specific bucket would take up tokens from the global bucket. // rejected requests from the specific bucket would take up tokens from the global bucket.
ip, err := manet.ToIP(addr)
if err != nil {
return r.globalBucket.Allow()
}
ipAddr, ok := netip.AddrFromSlice(ip)
if !ok {
return r.globalBucket.Allow()
}
// prefixs have been sorted from most to least specific so rejected requests for more // prefixs have been sorted from most to least specific so rejected requests for more
// specific prefixes don't take up tokens from the less specific prefixes. // specific prefixes don't take up tokens from the less specific prefixes.

View File

@@ -6,7 +6,6 @@ import (
"testing" "testing"
"time" "time"
ma "github.com/multiformats/go-multiaddr"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"golang.org/x/time/rate" "golang.org/x/time/rate"
) )
@@ -25,19 +24,19 @@ func getSleepDurationAndRequestCount(rps float64) (time.Duration, int) {
return sleepDuration, requestCount return sleepDuration, requestCount
} }
func assertLimiter(t *testing.T, rl *Limiter, addr ma.Multiaddr, allowed, errorMargin int) { func assertLimiter(t *testing.T, rl *Limiter, ipAddr netip.Addr, allowed, errorMargin int) {
t.Helper() t.Helper()
for i := 0; i < allowed; i++ { for i := 0; i < allowed; i++ {
require.True(t, rl.allow(addr)) require.True(t, rl.Allow(ipAddr))
} }
for i := 0; i < errorMargin; i++ { for i := 0; i < errorMargin; i++ {
rl.allow(addr) rl.Allow(ipAddr)
} }
require.False(t, rl.allow(addr)) require.False(t, rl.Allow(ipAddr))
} }
func TestLimiterGlobal(t *testing.T) { func TestLimiterGlobal(t *testing.T) {
addr := ma.StringCast("/ip4/127.0.0.1/udp/123/quic-v1") addr := netip.MustParseAddr("127.0.0.1")
limits := []Limit{ limits := []Limit{
{RPS: 0.0, Burst: 1}, {RPS: 0.0, Burst: 1},
{RPS: 0.8, Burst: 1}, {RPS: 0.8, Burst: 1},
@@ -53,7 +52,7 @@ func TestLimiterGlobal(t *testing.T) {
if limit.RPS == 0 { if limit.RPS == 0 {
// 0 implies no rate limiting, any large number would do // 0 implies no rate limiting, any large number would do
for i := 0; i < 1000; i++ { for i := 0; i < 1000; i++ {
require.True(t, rl.allow(addr)) require.True(t, rl.Allow(addr))
} }
return return
} }
@@ -66,8 +65,8 @@ func TestLimiterGlobal(t *testing.T) {
} }
func TestLimiterNetworkPrefix(t *testing.T) { func TestLimiterNetworkPrefix(t *testing.T) {
local := ma.StringCast("/ip4/127.0.0.1/udp/123/quic-v1") local := netip.MustParseAddr("127.0.0.1")
public := ma.StringCast("/ip4/1.1.1.1/udp/123/quic-v1") public := netip.MustParseAddr("1.1.1.1")
rl := &Limiter{ rl := &Limiter{
NetworkPrefixLimits: []PrefixLimit{ NetworkPrefixLimits: []PrefixLimit{
{Prefix: netip.MustParsePrefix("127.0.0.0/24"), Limit: Limit{}}, {Prefix: netip.MustParsePrefix("127.0.0.0/24"), Limit: Limit{}},
@@ -76,22 +75,22 @@ func TestLimiterNetworkPrefix(t *testing.T) {
} }
// element within prefix is allowed even over the limit // element within prefix is allowed even over the limit
for range rl.GlobalLimit.Burst + 100 { for range rl.GlobalLimit.Burst + 100 {
require.True(t, rl.allow(local)) require.True(t, rl.Allow(local))
} }
// rate limit public ips // rate limit public ips
assertLimiter(t, rl, public, rl.GlobalLimit.Burst, int(rl.GlobalLimit.RPS*rateLimitErrorTolerance)) assertLimiter(t, rl, public, rl.GlobalLimit.Burst, int(rl.GlobalLimit.RPS*rateLimitErrorTolerance))
// public ip rejected // public ip rejected
require.False(t, rl.allow(public)) require.False(t, rl.Allow(public))
// local ip accepted // local ip accepted
for range 100 { for range 100 {
require.True(t, rl.allow(local)) require.True(t, rl.Allow(local))
} }
} }
func TestLimiterNetworkPrefixWidth(t *testing.T) { func TestLimiterNetworkPrefixWidth(t *testing.T) {
a1 := ma.StringCast("/ip4/1.1.1.1/udp/123/quic-v1") a1 := netip.MustParseAddr("1.1.1.1")
a2 := ma.StringCast("/ip4/1.1.0.1/udp/123/quic-v1") a2 := netip.MustParseAddr("1.1.0.1")
wideLimit := 20 wideLimit := 20
narrowLimit := 10 narrowLimit := 10
@@ -102,13 +101,13 @@ func TestLimiterNetworkPrefixWidth(t *testing.T) {
}, },
} }
for range 2 * wideLimit { for range 2 * wideLimit {
rl.allow(a1) rl.Allow(a1)
} }
// a1 rejected // a1 rejected
require.False(t, rl.allow(a1)) require.False(t, rl.Allow(a1))
// a2 accepted // a2 accepted
for range wideLimit - narrowLimit { for range wideLimit - narrowLimit {
require.True(t, rl.allow(a2)) require.True(t, rl.Allow(a2))
} }
} }