diff --git a/.github/workflows/release-check.yml b/.github/workflows/release-check.yml index 0b5ff6070f..4d37f9b4a3 100644 --- a/.github/workflows/release-check.yml +++ b/.github/workflows/release-check.yml @@ -16,4 +16,4 @@ concurrency: jobs: release-check: - uses: ipdxco/unified-github-workflows/.github/workflows/release-check.yml@v1.0 + uses: marcopolo/unified-github-workflows/.github/workflows/release-check.yml@e66cb9667a2e1148efda4591e29c56258eaf385b diff --git a/FUNDING.json b/FUNDING.json new file mode 100644 index 0000000000..5952e90cb0 --- /dev/null +++ b/FUNDING.json @@ -0,0 +1,10 @@ +{ + "opRetro": { + "projectId": "0xc71faa1bcb4ceb785816c3f22823377e9e5e7c48649badd9f0a0ce491f20d4b3" + }, + "drips": { + "filecoin": { + "ownedBy": "0x53DCAf729e11022D5b8949946f6601211C662B38" + } + } + } diff --git a/config/config.go b/config/config.go index d0a71664f7..f3ea35855a 100644 --- a/config/config.go +++ b/config/config.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "net" + "slices" "time" "github.com/libp2p/go-libp2p/core/connmgr" @@ -35,12 +36,13 @@ import ( circuitv2 "github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/client" relayv2 "github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/relay" "github.com/libp2p/go-libp2p/p2p/protocol/holepunch" + "github.com/libp2p/go-libp2p/p2p/protocol/identify" "github.com/libp2p/go-libp2p/p2p/transport/quicreuse" + "github.com/libp2p/go-libp2p/p2p/transport/tcpreuse" libp2pwebrtc "github.com/libp2p/go-libp2p/p2p/transport/webrtc" "github.com/prometheus/client_golang/prometheus" ma "github.com/multiformats/go-multiaddr" - madns "github.com/multiformats/go-multiaddr-dns" manet "github.com/multiformats/go-multiaddr/net" "github.com/quic-go/quic-go" "go.uber.org/fx" @@ -114,7 +116,7 @@ type Config struct { Peerstore peerstore.Peerstore Reporter metrics.Reporter - MultiaddrResolver *madns.Resolver + MultiaddrResolver network.MultiaddrDNSResolver DisablePing bool @@ -142,6 +144,10 @@ type Config struct { CustomUDPBlackHoleSuccessCounter bool IPv6BlackHoleSuccessCounter *swarm.BlackHoleSuccessCounter CustomIPv6BlackHoleSuccessCounter bool + + UserFxOptions []fx.Option + + ShareTCPListener bool } func (cfg *Config) makeSwarm(eventBus event.Bus, enableMetrics bool) (*swarm.Swarm, error) { @@ -286,7 +292,12 @@ func (cfg *Config) addTransports() ([]fx.Option, error) { fx.Provide(func() connmgr.ConnectionGater { return cfg.ConnectionGater }), fx.Provide(func() pnet.PSK { return cfg.PSK }), fx.Provide(func() network.ResourceManager { return cfg.ResourceManager }), - fx.Provide(func() *madns.Resolver { return cfg.MultiaddrResolver }), + fx.Provide(func(gater connmgr.ConnectionGater, rcmgr network.ResourceManager) *tcpreuse.ConnMgr { + if !cfg.ShareTCPListener { + return nil + } + return tcpreuse.NewConnMgr(tcpreuse.EnvReuseportVal, gater, rcmgr) + }), fx.Provide(func(cm *quicreuse.ConnManager, sw *swarm.Swarm) libp2pwebrtc.ListenUDPFn { hasQuicAddrPortFor := func(network string, laddr *net.UDPAddr) bool { quicAddrPorts := map[string]struct{}{} @@ -432,16 +443,6 @@ func (cfg *Config) newBasicHost(swrm *swarm.Swarm, eventBus event.Bus) (*bhost.B if err != nil { return nil, err } - if cfg.Relay { - // If we've enabled the relay, we should filter out relay - // addresses by default. - // - // TODO: We shouldn't be doing this here. - originalAddrFactory := h.AddrsFactory - h.AddrsFactory = func(addrs []ma.Multiaddr) []ma.Multiaddr { - return originalAddrFactory(autorelay.Filter(addrs)) - } - } return h, nil } @@ -493,6 +494,9 @@ func (cfg *Config) NewNode() (host.Host, error) { return sw, nil }), fx.Provide(cfg.newBasicHost), + fx.Provide(func(bh *bhost.BasicHost) identify.IDService { + return bh.IDService() + }), fx.Provide(func(bh *bhost.BasicHost) host.Host { return bh }), @@ -514,17 +518,8 @@ func (cfg *Config) NewNode() (host.Host, error) { ) } - // originalAddrFactory is the AddrFactory before it's modified by autorelay - // we need this for checking reachability via autonat - originalAddrFactory := func(addrs []ma.Multiaddr) []ma.Multiaddr { - return addrs - } - // enable autorelay fxopts = append(fxopts, - fx.Invoke(func(h *bhost.BasicHost) { - originalAddrFactory = h.AddrsFactory - }), fx.Invoke(func(h *bhost.BasicHost, lifecycle fx.Lifecycle) error { if cfg.EnableAutoRelay { if !cfg.DisableMetrics { @@ -556,12 +551,14 @@ func (cfg *Config) NewNode() (host.Host, error) { fxopts = append(fxopts, fx.Invoke(func(bho *routed.RoutedHost) { rh = bho })) } + fxopts = append(fxopts, cfg.UserFxOptions...) + app := fx.New(fxopts...) if err := app.Start(context.Background()); err != nil { return nil, err } - if err := cfg.addAutoNAT(bh, originalAddrFactory); err != nil { + if err := cfg.addAutoNAT(bh); err != nil { app.Stop(context.Background()) if cfg.Routing != nil { rh.Close() @@ -577,11 +574,20 @@ func (cfg *Config) NewNode() (host.Host, error) { return &closableBasicHost{App: app, BasicHost: bh}, nil } -func (cfg *Config) addAutoNAT(h *bhost.BasicHost, addrF AddrsFactory) error { +func (cfg *Config) addAutoNAT(h *bhost.BasicHost) error { + // Only use public addresses for autonat + addrFunc := func() []ma.Multiaddr { + return slices.DeleteFunc(h.AllAddrs(), func(a ma.Multiaddr) bool { return !manet.IsPublicAddr(a) }) + } + if cfg.AddrsFactory != nil { + addrFunc = func() []ma.Multiaddr { + return slices.DeleteFunc( + slices.Clone(cfg.AddrsFactory(h.AllAddrs())), + func(a ma.Multiaddr) bool { return !manet.IsPublicAddr(a) }) + } + } autonatOpts := []autonat.Option{ - autonat.UsingAddresses(func() []ma.Multiaddr { - return addrF(h.AllAddrs()) - }), + autonat.UsingAddresses(addrFunc), } if !cfg.DisableMetrics { autonatOpts = append(autonatOpts, autonat.WithMetricsTracer( @@ -664,7 +670,7 @@ func (cfg *Config) addAutoNAT(h *bhost.BasicHost, addrF AddrsFactory) error { autonat, err := autonat.New(h, autonatOpts...) if err != nil { - return fmt.Errorf("cannot enable autorelay; autonat failed to start: %v", err) + return fmt.Errorf("autonat init failed: %w", err) } h.SetAutoNat(autonat) return nil diff --git a/core/connmgr/manager.go b/core/connmgr/manager.go index d756e4399f..c4d796b39a 100644 --- a/core/connmgr/manager.go +++ b/core/connmgr/manager.go @@ -34,7 +34,7 @@ type ConnManager interface { // TagPeer tags a peer with a string, associating a weight with the tag. TagPeer(peer.ID, string, int) - // Untag removes the tagged value from the peer. + // UntagPeer removes the tagged value from the peer. UntagPeer(p peer.ID, tag string) // UpsertTag updates an existing tag or inserts a new one. diff --git a/core/crypto/pb/crypto.pb.go b/core/crypto/pb/crypto.pb.go index 4c83183e58..d960889763 100644 --- a/core/crypto/pb/crypto.pb.go +++ b/core/crypto/pb/crypto.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.34.2 -// protoc v5.27.3 +// protoc-gen-go v1.35.1 +// protoc v5.28.2 // source: core/crypto/pb/crypto.proto package pb @@ -93,11 +93,9 @@ type PublicKey struct { func (x *PublicKey) Reset() { *x = PublicKey{} - if protoimpl.UnsafeEnabled { - mi := &file_core_crypto_pb_crypto_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_core_crypto_pb_crypto_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *PublicKey) String() string { @@ -108,7 +106,7 @@ func (*PublicKey) ProtoMessage() {} func (x *PublicKey) ProtoReflect() protoreflect.Message { mi := &file_core_crypto_pb_crypto_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -148,11 +146,9 @@ type PrivateKey struct { func (x *PrivateKey) Reset() { *x = PrivateKey{} - if protoimpl.UnsafeEnabled { - mi := &file_core_crypto_pb_crypto_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_core_crypto_pb_crypto_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *PrivateKey) String() string { @@ -163,7 +159,7 @@ func (*PrivateKey) ProtoMessage() {} func (x *PrivateKey) ProtoReflect() protoreflect.Message { mi := &file_core_crypto_pb_crypto_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -250,32 +246,6 @@ func file_core_crypto_pb_crypto_proto_init() { if File_core_crypto_pb_crypto_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_core_crypto_pb_crypto_proto_msgTypes[0].Exporter = func(v any, i int) any { - switch v := v.(*PublicKey); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_core_crypto_pb_crypto_proto_msgTypes[1].Exporter = func(v any, i int) any { - switch v := v.(*PrivateKey); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ diff --git a/core/host/host.go b/core/host/host.go index 7990f7f456..ca0a7e00d9 100644 --- a/core/host/host.go +++ b/core/host/host.go @@ -28,10 +28,10 @@ type Host interface { // Peerstore returns the Host's repository of Peer Addresses and Keys. Peerstore() peerstore.Peerstore - // Returns the listen addresses of the Host + // Addrs returns the listen addresses of the Host Addrs() []ma.Multiaddr - // Networks returns the Network interface of the Host + // Network returns the Network interface of the Host Network() network.Network // Mux returns the Mux multiplexing incoming streams to protocol handlers @@ -41,7 +41,7 @@ type Host interface { // given peer.ID. Connect will absorb the addresses in pi into its internal // peerstore. If there is not an active connection, Connect will issue a // h.Network.Dial, and block until a connection is open, or an error is - // returned. // TODO: Relay + NAT. + // returned. Connect(ctx context.Context, pi peer.AddrInfo) error // SetStreamHandler sets the protocol handler on the Host's Mux. diff --git a/core/network/mocks/mock_conn_management_scope.go b/core/network/mocks/mock_conn_management_scope.go index d6538e17a3..649da0aa0f 100644 --- a/core/network/mocks/mock_conn_management_scope.go +++ b/core/network/mocks/mock_conn_management_scope.go @@ -21,6 +21,7 @@ import ( type MockConnManagementScope struct { ctrl *gomock.Controller recorder *MockConnManagementScopeMockRecorder + isgomock struct{} } // MockConnManagementScopeMockRecorder is the mock recorder for MockConnManagementScope. @@ -82,29 +83,29 @@ func (mr *MockConnManagementScopeMockRecorder) PeerScope() *gomock.Call { } // ReleaseMemory mocks base method. -func (m *MockConnManagementScope) ReleaseMemory(arg0 int) { +func (m *MockConnManagementScope) ReleaseMemory(size int) { m.ctrl.T.Helper() - m.ctrl.Call(m, "ReleaseMemory", arg0) + m.ctrl.Call(m, "ReleaseMemory", size) } // ReleaseMemory indicates an expected call of ReleaseMemory. -func (mr *MockConnManagementScopeMockRecorder) ReleaseMemory(arg0 any) *gomock.Call { +func (mr *MockConnManagementScopeMockRecorder) ReleaseMemory(size any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReleaseMemory", reflect.TypeOf((*MockConnManagementScope)(nil).ReleaseMemory), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReleaseMemory", reflect.TypeOf((*MockConnManagementScope)(nil).ReleaseMemory), size) } // ReserveMemory mocks base method. -func (m *MockConnManagementScope) ReserveMemory(arg0 int, arg1 byte) error { +func (m *MockConnManagementScope) ReserveMemory(size int, prio uint8) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ReserveMemory", arg0, arg1) + ret := m.ctrl.Call(m, "ReserveMemory", size, prio) ret0, _ := ret[0].(error) return ret0 } // ReserveMemory indicates an expected call of ReserveMemory. -func (mr *MockConnManagementScopeMockRecorder) ReserveMemory(arg0, arg1 any) *gomock.Call { +func (mr *MockConnManagementScopeMockRecorder) ReserveMemory(size, prio any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReserveMemory", reflect.TypeOf((*MockConnManagementScope)(nil).ReserveMemory), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReserveMemory", reflect.TypeOf((*MockConnManagementScope)(nil).ReserveMemory), size, prio) } // SetPeer mocks base method. diff --git a/core/network/mocks/mock_peer_scope.go b/core/network/mocks/mock_peer_scope.go index 325a894c2c..e5d0e70c1f 100644 --- a/core/network/mocks/mock_peer_scope.go +++ b/core/network/mocks/mock_peer_scope.go @@ -21,6 +21,7 @@ import ( type MockPeerScope struct { ctrl *gomock.Controller recorder *MockPeerScopeMockRecorder + isgomock struct{} } // MockPeerScopeMockRecorder is the mock recorder for MockPeerScope. @@ -70,29 +71,29 @@ func (mr *MockPeerScopeMockRecorder) Peer() *gomock.Call { } // ReleaseMemory mocks base method. -func (m *MockPeerScope) ReleaseMemory(arg0 int) { +func (m *MockPeerScope) ReleaseMemory(size int) { m.ctrl.T.Helper() - m.ctrl.Call(m, "ReleaseMemory", arg0) + m.ctrl.Call(m, "ReleaseMemory", size) } // ReleaseMemory indicates an expected call of ReleaseMemory. -func (mr *MockPeerScopeMockRecorder) ReleaseMemory(arg0 any) *gomock.Call { +func (mr *MockPeerScopeMockRecorder) ReleaseMemory(size any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReleaseMemory", reflect.TypeOf((*MockPeerScope)(nil).ReleaseMemory), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReleaseMemory", reflect.TypeOf((*MockPeerScope)(nil).ReleaseMemory), size) } // ReserveMemory mocks base method. -func (m *MockPeerScope) ReserveMemory(arg0 int, arg1 byte) error { +func (m *MockPeerScope) ReserveMemory(size int, prio uint8) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ReserveMemory", arg0, arg1) + ret := m.ctrl.Call(m, "ReserveMemory", size, prio) ret0, _ := ret[0].(error) return ret0 } // ReserveMemory indicates an expected call of ReserveMemory. -func (mr *MockPeerScopeMockRecorder) ReserveMemory(arg0, arg1 any) *gomock.Call { +func (mr *MockPeerScopeMockRecorder) ReserveMemory(size, prio any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReserveMemory", reflect.TypeOf((*MockPeerScope)(nil).ReserveMemory), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReserveMemory", reflect.TypeOf((*MockPeerScope)(nil).ReserveMemory), size, prio) } // Stat mocks base method. diff --git a/core/network/mocks/mock_protocol_scope.go b/core/network/mocks/mock_protocol_scope.go index 80d29676b9..237c3daf0a 100644 --- a/core/network/mocks/mock_protocol_scope.go +++ b/core/network/mocks/mock_protocol_scope.go @@ -21,6 +21,7 @@ import ( type MockProtocolScope struct { ctrl *gomock.Controller recorder *MockProtocolScopeMockRecorder + isgomock struct{} } // MockProtocolScopeMockRecorder is the mock recorder for MockProtocolScope. @@ -70,29 +71,29 @@ func (mr *MockProtocolScopeMockRecorder) Protocol() *gomock.Call { } // ReleaseMemory mocks base method. -func (m *MockProtocolScope) ReleaseMemory(arg0 int) { +func (m *MockProtocolScope) ReleaseMemory(size int) { m.ctrl.T.Helper() - m.ctrl.Call(m, "ReleaseMemory", arg0) + m.ctrl.Call(m, "ReleaseMemory", size) } // ReleaseMemory indicates an expected call of ReleaseMemory. -func (mr *MockProtocolScopeMockRecorder) ReleaseMemory(arg0 any) *gomock.Call { +func (mr *MockProtocolScopeMockRecorder) ReleaseMemory(size any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReleaseMemory", reflect.TypeOf((*MockProtocolScope)(nil).ReleaseMemory), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReleaseMemory", reflect.TypeOf((*MockProtocolScope)(nil).ReleaseMemory), size) } // ReserveMemory mocks base method. -func (m *MockProtocolScope) ReserveMemory(arg0 int, arg1 byte) error { +func (m *MockProtocolScope) ReserveMemory(size int, prio uint8) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ReserveMemory", arg0, arg1) + ret := m.ctrl.Call(m, "ReserveMemory", size, prio) ret0, _ := ret[0].(error) return ret0 } // ReserveMemory indicates an expected call of ReserveMemory. -func (mr *MockProtocolScopeMockRecorder) ReserveMemory(arg0, arg1 any) *gomock.Call { +func (mr *MockProtocolScopeMockRecorder) ReserveMemory(size, prio any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReserveMemory", reflect.TypeOf((*MockProtocolScope)(nil).ReserveMemory), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReserveMemory", reflect.TypeOf((*MockProtocolScope)(nil).ReserveMemory), size, prio) } // Stat mocks base method. diff --git a/core/network/mocks/mock_resource_manager.go b/core/network/mocks/mock_resource_manager.go index 98e186e1ff..b31ddb13d1 100644 --- a/core/network/mocks/mock_resource_manager.go +++ b/core/network/mocks/mock_resource_manager.go @@ -23,6 +23,7 @@ import ( type MockResourceManager struct { ctrl *gomock.Controller recorder *MockResourceManagerMockRecorder + isgomock struct{} } // MockResourceManagerMockRecorder is the mock recorder for MockResourceManager. @@ -57,33 +58,33 @@ func (mr *MockResourceManagerMockRecorder) Close() *gomock.Call { } // OpenConnection mocks base method. -func (m *MockResourceManager) OpenConnection(arg0 network.Direction, arg1 bool, arg2 multiaddr.Multiaddr) (network.ConnManagementScope, error) { +func (m *MockResourceManager) OpenConnection(dir network.Direction, usefd bool, endpoint multiaddr.Multiaddr) (network.ConnManagementScope, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "OpenConnection", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "OpenConnection", dir, usefd, endpoint) ret0, _ := ret[0].(network.ConnManagementScope) ret1, _ := ret[1].(error) return ret0, ret1 } // OpenConnection indicates an expected call of OpenConnection. -func (mr *MockResourceManagerMockRecorder) OpenConnection(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockResourceManagerMockRecorder) OpenConnection(dir, usefd, endpoint any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenConnection", reflect.TypeOf((*MockResourceManager)(nil).OpenConnection), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenConnection", reflect.TypeOf((*MockResourceManager)(nil).OpenConnection), dir, usefd, endpoint) } // OpenStream mocks base method. -func (m *MockResourceManager) OpenStream(arg0 peer.ID, arg1 network.Direction) (network.StreamManagementScope, error) { +func (m *MockResourceManager) OpenStream(p peer.ID, dir network.Direction) (network.StreamManagementScope, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "OpenStream", arg0, arg1) + ret := m.ctrl.Call(m, "OpenStream", p, dir) ret0, _ := ret[0].(network.StreamManagementScope) ret1, _ := ret[1].(error) return ret0, ret1 } // OpenStream indicates an expected call of OpenStream. -func (mr *MockResourceManagerMockRecorder) OpenStream(arg0, arg1 any) *gomock.Call { +func (mr *MockResourceManagerMockRecorder) OpenStream(p, dir any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenStream", reflect.TypeOf((*MockResourceManager)(nil).OpenStream), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenStream", reflect.TypeOf((*MockResourceManager)(nil).OpenStream), p, dir) } // ViewPeer mocks base method. diff --git a/core/network/mocks/mock_resource_scope_span.go b/core/network/mocks/mock_resource_scope_span.go index 5123ff8d75..0a1db10953 100644 --- a/core/network/mocks/mock_resource_scope_span.go +++ b/core/network/mocks/mock_resource_scope_span.go @@ -20,6 +20,7 @@ import ( type MockResourceScopeSpan struct { ctrl *gomock.Controller recorder *MockResourceScopeSpanMockRecorder + isgomock struct{} } // MockResourceScopeSpanMockRecorder is the mock recorder for MockResourceScopeSpan. @@ -67,29 +68,29 @@ func (mr *MockResourceScopeSpanMockRecorder) Done() *gomock.Call { } // ReleaseMemory mocks base method. -func (m *MockResourceScopeSpan) ReleaseMemory(arg0 int) { +func (m *MockResourceScopeSpan) ReleaseMemory(size int) { m.ctrl.T.Helper() - m.ctrl.Call(m, "ReleaseMemory", arg0) + m.ctrl.Call(m, "ReleaseMemory", size) } // ReleaseMemory indicates an expected call of ReleaseMemory. -func (mr *MockResourceScopeSpanMockRecorder) ReleaseMemory(arg0 any) *gomock.Call { +func (mr *MockResourceScopeSpanMockRecorder) ReleaseMemory(size any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReleaseMemory", reflect.TypeOf((*MockResourceScopeSpan)(nil).ReleaseMemory), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReleaseMemory", reflect.TypeOf((*MockResourceScopeSpan)(nil).ReleaseMemory), size) } // ReserveMemory mocks base method. -func (m *MockResourceScopeSpan) ReserveMemory(arg0 int, arg1 byte) error { +func (m *MockResourceScopeSpan) ReserveMemory(size int, prio uint8) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ReserveMemory", arg0, arg1) + ret := m.ctrl.Call(m, "ReserveMemory", size, prio) ret0, _ := ret[0].(error) return ret0 } // ReserveMemory indicates an expected call of ReserveMemory. -func (mr *MockResourceScopeSpanMockRecorder) ReserveMemory(arg0, arg1 any) *gomock.Call { +func (mr *MockResourceScopeSpanMockRecorder) ReserveMemory(size, prio any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReserveMemory", reflect.TypeOf((*MockResourceScopeSpan)(nil).ReserveMemory), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReserveMemory", reflect.TypeOf((*MockResourceScopeSpan)(nil).ReserveMemory), size, prio) } // Stat mocks base method. diff --git a/core/network/mocks/mock_stream_management_scope.go b/core/network/mocks/mock_stream_management_scope.go index 472eb7acb8..9336cac2f4 100644 --- a/core/network/mocks/mock_stream_management_scope.go +++ b/core/network/mocks/mock_stream_management_scope.go @@ -21,6 +21,7 @@ import ( type MockStreamManagementScope struct { ctrl *gomock.Controller recorder *MockStreamManagementScopeMockRecorder + isgomock struct{} } // MockStreamManagementScopeMockRecorder is the mock recorder for MockStreamManagementScope. @@ -96,29 +97,29 @@ func (mr *MockStreamManagementScopeMockRecorder) ProtocolScope() *gomock.Call { } // ReleaseMemory mocks base method. -func (m *MockStreamManagementScope) ReleaseMemory(arg0 int) { +func (m *MockStreamManagementScope) ReleaseMemory(size int) { m.ctrl.T.Helper() - m.ctrl.Call(m, "ReleaseMemory", arg0) + m.ctrl.Call(m, "ReleaseMemory", size) } // ReleaseMemory indicates an expected call of ReleaseMemory. -func (mr *MockStreamManagementScopeMockRecorder) ReleaseMemory(arg0 any) *gomock.Call { +func (mr *MockStreamManagementScopeMockRecorder) ReleaseMemory(size any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReleaseMemory", reflect.TypeOf((*MockStreamManagementScope)(nil).ReleaseMemory), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReleaseMemory", reflect.TypeOf((*MockStreamManagementScope)(nil).ReleaseMemory), size) } // ReserveMemory mocks base method. -func (m *MockStreamManagementScope) ReserveMemory(arg0 int, arg1 byte) error { +func (m *MockStreamManagementScope) ReserveMemory(size int, prio uint8) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ReserveMemory", arg0, arg1) + ret := m.ctrl.Call(m, "ReserveMemory", size, prio) ret0, _ := ret[0].(error) return ret0 } // ReserveMemory indicates an expected call of ReserveMemory. -func (mr *MockStreamManagementScopeMockRecorder) ReserveMemory(arg0, arg1 any) *gomock.Call { +func (mr *MockStreamManagementScopeMockRecorder) ReserveMemory(size, prio any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReserveMemory", reflect.TypeOf((*MockStreamManagementScope)(nil).ReserveMemory), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReserveMemory", reflect.TypeOf((*MockStreamManagementScope)(nil).ReserveMemory), size, prio) } // ServiceScope mocks base method. @@ -136,31 +137,31 @@ func (mr *MockStreamManagementScopeMockRecorder) ServiceScope() *gomock.Call { } // SetProtocol mocks base method. -func (m *MockStreamManagementScope) SetProtocol(arg0 protocol.ID) error { +func (m *MockStreamManagementScope) SetProtocol(proto protocol.ID) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetProtocol", arg0) + ret := m.ctrl.Call(m, "SetProtocol", proto) ret0, _ := ret[0].(error) return ret0 } // SetProtocol indicates an expected call of SetProtocol. -func (mr *MockStreamManagementScopeMockRecorder) SetProtocol(arg0 any) *gomock.Call { +func (mr *MockStreamManagementScopeMockRecorder) SetProtocol(proto any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetProtocol", reflect.TypeOf((*MockStreamManagementScope)(nil).SetProtocol), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetProtocol", reflect.TypeOf((*MockStreamManagementScope)(nil).SetProtocol), proto) } // SetService mocks base method. -func (m *MockStreamManagementScope) SetService(arg0 string) error { +func (m *MockStreamManagementScope) SetService(srv string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetService", arg0) + ret := m.ctrl.Call(m, "SetService", srv) ret0, _ := ret[0].(error) return ret0 } // SetService indicates an expected call of SetService. -func (mr *MockStreamManagementScopeMockRecorder) SetService(arg0 any) *gomock.Call { +func (mr *MockStreamManagementScopeMockRecorder) SetService(srv any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetService", reflect.TypeOf((*MockStreamManagementScope)(nil).SetService), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetService", reflect.TypeOf((*MockStreamManagementScope)(nil).SetService), srv) } // Stat mocks base method. diff --git a/core/network/network.go b/core/network/network.go index d2e2bc818d..7c0ad949e2 100644 --- a/core/network/network.go +++ b/core/network/network.go @@ -161,6 +161,14 @@ type Network interface { ResourceManager() ResourceManager } +type MultiaddrDNSResolver interface { + // ResolveDNSAddr resolves the first /dnsaddr component in a multiaddr. + // Recurisvely resolves DNSADDRs up to the recursion limit + ResolveDNSAddr(ctx context.Context, expectedPeerID peer.ID, maddr ma.Multiaddr, recursionLimit, outputLimit int) ([]ma.Multiaddr, error) + // ResolveDNSComponent resolves the first /{dns,dns4,dns6} component in a multiaddr. + ResolveDNSComponent(ctx context.Context, maddr ma.Multiaddr, outputLimit int) ([]ma.Multiaddr, error) +} + // Dialer represents a service that can dial out to peers // (this is usually just a Network, but other services may not need the whole // stack, and thus it becomes easier to mock) diff --git a/core/peer/addrinfo.go b/core/peer/addrinfo.go index de7dd4d989..397de274c9 100644 --- a/core/peer/addrinfo.go +++ b/core/peer/addrinfo.go @@ -61,6 +61,24 @@ func SplitAddr(m ma.Multiaddr) (transport ma.Multiaddr, id ID) { return transport, id } +// IDFromP2PAddr extracts the peer ID from a p2p Multiaddr +func IDFromP2PAddr(m ma.Multiaddr) (ID, error) { + if m == nil { + return "", ErrInvalidAddr + } + var lastComponent ma.Component + ma.ForEach(m, func(c ma.Component) bool { + lastComponent = c + return true + }) + if lastComponent.Protocol().Code != ma.P_P2P { + return "", ErrInvalidAddr + } + + id := ID(lastComponent.RawValue()) // already validated by the multiaddr library. + return id, nil +} + // AddrInfoFromString builds an AddrInfo from the string representation of a Multiaddr func AddrInfoFromString(s string) (*AddrInfo, error) { a, err := ma.NewMultiaddr(s) diff --git a/core/peer/addrinfo_test.go b/core/peer/addrinfo_test.go index 3fd1556e6a..614747fa69 100644 --- a/core/peer/addrinfo_test.go +++ b/core/peer/addrinfo_test.go @@ -4,6 +4,7 @@ import ( "testing" . "github.com/libp2p/go-libp2p/core/peer" + "github.com/stretchr/testify/require" ma "github.com/multiformats/go-multiaddr" ) @@ -50,6 +51,19 @@ func TestSplitAddr(t *testing.T) { } } +func TestIDFromP2PAddr(t *testing.T) { + id, err := IDFromP2PAddr(maddrFull) + require.NoError(t, err) + require.Equal(t, testID, id) + + id, err = IDFromP2PAddr(maddrPeer) + require.NoError(t, err) + require.Equal(t, testID, id) + + _, err = IDFromP2PAddr(maddrTpt) + require.ErrorIs(t, err, ErrInvalidAddr) +} + func TestAddrInfoFromP2pAddr(t *testing.T) { ai, err := AddrInfoFromP2pAddr(maddrFull) if err != nil { diff --git a/core/peer/pb/peer_record.pb.go b/core/peer/pb/peer_record.pb.go index ef9e2aea11..8249ba4009 100644 --- a/core/peer/pb/peer_record.pb.go +++ b/core/peer/pb/peer_record.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.34.2 -// protoc v5.27.3 +// protoc-gen-go v1.35.1 +// protoc v5.28.2 // source: core/peer/pb/peer_record.proto package pb @@ -26,7 +26,7 @@ const ( // // PeerRecords are designed to be serialized to bytes and placed inside of // SignedEnvelopes before sharing with other peers. -// See https://github.com/libp2p/go-libp2p/core/record/pb/envelope.proto for +// See https://github.com/libp2p/go-libp2p/blob/master/core/record/pb/envelope.proto for // the SignedEnvelope definition. type PeerRecord struct { state protoimpl.MessageState @@ -43,11 +43,9 @@ type PeerRecord struct { func (x *PeerRecord) Reset() { *x = PeerRecord{} - if protoimpl.UnsafeEnabled { - mi := &file_core_peer_pb_peer_record_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_core_peer_pb_peer_record_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *PeerRecord) String() string { @@ -58,7 +56,7 @@ func (*PeerRecord) ProtoMessage() {} func (x *PeerRecord) ProtoReflect() protoreflect.Message { mi := &file_core_peer_pb_peer_record_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -106,11 +104,9 @@ type PeerRecord_AddressInfo struct { func (x *PeerRecord_AddressInfo) Reset() { *x = PeerRecord_AddressInfo{} - if protoimpl.UnsafeEnabled { - mi := &file_core_peer_pb_peer_record_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_core_peer_pb_peer_record_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *PeerRecord_AddressInfo) String() string { @@ -121,7 +117,7 @@ func (*PeerRecord_AddressInfo) ProtoMessage() {} func (x *PeerRecord_AddressInfo) ProtoReflect() protoreflect.Message { mi := &file_core_peer_pb_peer_record_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -196,32 +192,6 @@ func file_core_peer_pb_peer_record_proto_init() { if File_core_peer_pb_peer_record_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_core_peer_pb_peer_record_proto_msgTypes[0].Exporter = func(v any, i int) any { - switch v := v.(*PeerRecord); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_core_peer_pb_peer_record_proto_msgTypes[1].Exporter = func(v any, i int) any { - switch v := v.(*PeerRecord_AddressInfo); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ diff --git a/core/peer/pb/peer_record.proto b/core/peer/pb/peer_record.proto index 3a97e1a410..c5022f49ed 100644 --- a/core/peer/pb/peer_record.proto +++ b/core/peer/pb/peer_record.proto @@ -10,7 +10,7 @@ option go_package = "github.com/libp2p/go-libp2p/core/peer/pb"; // // PeerRecords are designed to be serialized to bytes and placed inside of // SignedEnvelopes before sharing with other peers. -// See https://github.com/libp2p/go-libp2p/core/record/pb/envelope.proto for +// See https://github.com/libp2p/go-libp2p/blob/master/core/record/pb/envelope.proto for // the SignedEnvelope definition. message PeerRecord { diff --git a/core/peerstore/peerstore.go b/core/peerstore/peerstore.go index 10469e72cb..6366026c9d 100644 --- a/core/peerstore/peerstore.go +++ b/core/peerstore/peerstore.go @@ -31,6 +31,7 @@ var ( RecentlyConnectedAddrTTL = time.Minute * 15 // OwnObservedAddrTTL is used for our own external addresses observed by peers. + // // Deprecated: observed addresses are maintained till we disconnect from the peer which provided it OwnObservedAddrTTL = time.Minute * 30 ) @@ -65,6 +66,10 @@ type Peerstore interface { // Peers returns all the peer IDs stored across all inner stores. Peers() peer.IDSlice + + // RemovePeer removes all the peer related information except its addresses. To remove the + // addresses use `AddrBook.ClearAddrs` or set the address ttls to 0. + RemovePeer(peer.ID) } // PeerMetadata can handle values of any type. Serializing values is @@ -134,12 +139,13 @@ type AddrBook interface { // } type CertifiedAddrBook interface { // ConsumePeerRecord stores a signed peer record and the contained addresses for - // for ttl duration. + // ttl duration. // The addresses contained in the signed peer record will expire after ttl. If any // address is already present in the peer store, it'll expire at max of existing ttl and // provided ttl. // The signed peer record itself will be expired when all the addresses associated with the peer, // self-certified or not, are removed from the AddrBook. + // // To delete the signed peer record, use `AddrBook.UpdateAddrs`,`AddrBook.SetAddrs`, or // `AddrBook.ClearAddrs` with ttl 0. // Note: Future calls to ConsumePeerRecord will not expire self-certified addresses from the diff --git a/core/record/pb/envelope.pb.go b/core/record/pb/envelope.pb.go index 9be1a5fff1..8da0e0eea5 100644 --- a/core/record/pb/envelope.pb.go +++ b/core/record/pb/envelope.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.34.2 -// protoc v5.27.3 +// protoc-gen-go v1.35.1 +// protoc v5.28.2 // source: core/record/pb/envelope.proto package pb @@ -49,11 +49,9 @@ type Envelope struct { func (x *Envelope) Reset() { *x = Envelope{} - if protoimpl.UnsafeEnabled { - mi := &file_core_record_pb_envelope_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_core_record_pb_envelope_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Envelope) String() string { @@ -64,7 +62,7 @@ func (*Envelope) ProtoMessage() {} func (x *Envelope) ProtoReflect() protoreflect.Message { mi := &file_core_record_pb_envelope_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -161,20 +159,6 @@ func file_core_record_pb_envelope_proto_init() { if File_core_record_pb_envelope_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_core_record_pb_envelope_proto_msgTypes[0].Exporter = func(v any, i int) any { - switch v := v.(*Envelope); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ diff --git a/core/routing/routing.go b/core/routing/routing.go index b995b052b9..bb8de71541 100644 --- a/core/routing/routing.go +++ b/core/routing/routing.go @@ -18,17 +18,18 @@ var ErrNotFound = errors.New("routing: not found") // type/operation. var ErrNotSupported = errors.New("routing: operation or key not supported") -// ContentRouting is a value provider layer of indirection. It is used to find -// information about who has what content. -// -// Content is identified by CID (content identifier), which encodes a hash -// of the identified content in a future-proof manner. -type ContentRouting interface { +// ContentProviding is able to announce where to find content on the Routing +// system. +type ContentProviding interface { // Provide adds the given cid to the content routing system. If 'true' is // passed, it also announces it, otherwise it is just kept in the local // accounting of which objects are being provided. Provide(context.Context, cid.Cid, bool) error +} +// ContentDiscovery is able to retrieve providers for a given CID using +// the Routing system. +type ContentDiscovery interface { // Search for peers who are able to provide a given key // // When count is 0, this method will return an unbounded number of @@ -36,6 +37,16 @@ type ContentRouting interface { FindProvidersAsync(context.Context, cid.Cid, int) <-chan peer.AddrInfo } +// ContentRouting is a value provider layer of indirection. It is used to find +// information about who has what content. +// +// Content is identified by CID (content identifier), which encodes a hash +// of the identified content in a future-proof manner. +type ContentRouting interface { + ContentProviding + ContentDiscovery +} + // PeerRouting is a way to find address information about certain peers. // This can be implemented by a simple lookup table, a tracking server, // or even a DHT. diff --git a/core/sec/insecure/pb/plaintext.pb.go b/core/sec/insecure/pb/plaintext.pb.go index 4484464146..64ab191b99 100644 --- a/core/sec/insecure/pb/plaintext.pb.go +++ b/core/sec/insecure/pb/plaintext.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.34.2 -// protoc v5.27.3 +// protoc-gen-go v1.35.1 +// protoc v5.28.2 // source: core/sec/insecure/pb/plaintext.proto package pb @@ -32,11 +32,9 @@ type Exchange struct { func (x *Exchange) Reset() { *x = Exchange{} - if protoimpl.UnsafeEnabled { - mi := &file_core_sec_insecure_pb_plaintext_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_core_sec_insecure_pb_plaintext_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Exchange) String() string { @@ -47,7 +45,7 @@ func (*Exchange) ProtoMessage() {} func (x *Exchange) ProtoReflect() protoreflect.Message { mi := &file_core_sec_insecure_pb_plaintext_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -125,20 +123,6 @@ func file_core_sec_insecure_pb_plaintext_proto_init() { if File_core_sec_insecure_pb_plaintext_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_core_sec_insecure_pb_plaintext_proto_msgTypes[0].Exporter = func(v any, i int) any { - switch v := v.(*Exchange); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ diff --git a/core/transport/transport.go b/core/transport/transport.go index d56a3cff06..908fc5b3b5 100644 --- a/core/transport/transport.go +++ b/core/transport/transport.go @@ -50,6 +50,10 @@ type CapableConn interface { // shutdown. NOTE: `Dial` and `Listen` may be called after or concurrently with // `Close`. // +// In addition to the Transport interface, transports may implement +// Resolver or SkipResolver interface. When wrapping/embedding a transport, you should +// ensure that the Resolver/SkipResolver interface is handled correctly. +// // For a conceptual overview, see https://docs.libp2p.io/concepts/transport/ type Transport interface { // Dial dials a remote peer. It should try to reuse local listener @@ -85,6 +89,14 @@ type Resolver interface { Resolve(ctx context.Context, maddr ma.Multiaddr) ([]ma.Multiaddr, error) } +// SkipResolver can be optionally implemented by transports that don't want to +// resolve or transform the multiaddr. Useful for transports that indirectly +// wrap other transports (e.g. p2p-circuit). This lets the inner transport +// specify how a multiaddr is resolved later. +type SkipResolver interface { + SkipResolve(ctx context.Context, maddr ma.Multiaddr) bool +} + // Listener is an interface closely resembling the net.Listener interface. The // only real difference is that Accept() returns Conn's of the type in this // package, and also exposes a Multiaddr method as opposed to a regular Addr diff --git a/dashboards/prometheus.yml b/dashboards/prometheus.yml index f091718860..89534d55dd 100644 --- a/dashboards/prometheus.yml +++ b/dashboards/prometheus.yml @@ -6,7 +6,7 @@ alerting: alertmanagers: - scheme: http timeout: 10s - api_version: v1 + api_version: v2 static_configs: - targets: [] scrape_configs: diff --git a/dashboards/swarm/swarm.json b/dashboards/swarm/swarm.json index d206de29c4..e8536ea8e6 100644 --- a/dashboards/swarm/swarm.json +++ b/dashboards/swarm/swarm.json @@ -2598,6 +2598,128 @@ ], "title": "Dial Success Rates", "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "log": 2, + "type": "log" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 67 + }, + "id": 50, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "histogram_quantile(0.5, sum(rate(libp2p_swarm_dial_latency_seconds_bucket{outcome=\"success\", instance=~\"$instance\"}[$__rate_interval])) by (le))", + "instant": false, + "legendFormat": "50th percentile", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.9, sum(rate(libp2p_swarm_dial_latency_seconds_bucket{outcome=\"success\", instance=~\"$instance\"}[$__rate_interval])) by (le))", + "hide": false, + "legendFormat": "90th percentile", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.95, sum(rate(libp2p_swarm_dial_latency_seconds_bucket{outcome=\"success\", instance=~\"$instance\"}[$__rate_interval])) by (le))", + "hide": false, + "legendFormat": "95th percentile", + "range": true, + "refId": "C" + } + ], + "title": "Peer Dial Latency (Seconds)", + "type": "timeseries" }, { "datasource": { diff --git a/defaults.go b/defaults.go index daf8c461c6..31de6f025d 100644 --- a/defaults.go +++ b/defaults.go @@ -21,7 +21,6 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/multiformats/go-multiaddr" - madns "github.com/multiformats/go-multiaddr-dns" ) // DefaultSecurity is the default security option. @@ -128,11 +127,6 @@ var DefaultConnectionManager = func(cfg *Config) error { return cfg.Apply(ConnectionManager(mgr)) } -// DefaultMultiaddrResolver creates a default connection manager -var DefaultMultiaddrResolver = func(cfg *Config) error { - return cfg.Apply(MultiaddrResolver(madns.DefaultResolver)) -} - // DefaultPrometheusRegisterer configures libp2p to use the default registerer var DefaultPrometheusRegisterer = func(cfg *Config) error { return cfg.Apply(PrometheusRegisterer(prometheus.DefaultRegisterer)) @@ -198,10 +192,6 @@ var defaults = []struct { fallback: func(cfg *Config) bool { return cfg.ConnManager == nil }, opt: DefaultConnectionManager, }, - { - fallback: func(cfg *Config) bool { return cfg.MultiaddrResolver == nil }, - opt: DefaultMultiaddrResolver, - }, { fallback: func(cfg *Config) bool { return !cfg.DisableMetrics && cfg.PrometheusRegisterer == nil }, opt: DefaultPrometheusRegisterer, diff --git a/funding.json b/funding.json deleted file mode 100644 index f32c67b079..0000000000 --- a/funding.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "opRetro": { - "projectId": "0xc71faa1bcb4ceb785816c3f22823377e9e5e7c48649badd9f0a0ce491f20d4b3" - } - } \ No newline at end of file diff --git a/fx_options_test.go b/fx_options_test.go new file mode 100644 index 0000000000..48ac79b53d --- /dev/null +++ b/fx_options_test.go @@ -0,0 +1,60 @@ +package libp2p + +import ( + "testing" + + "github.com/libp2p/go-libp2p/core/event" + "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/p2p/protocol/identify" + "github.com/stretchr/testify/require" + "go.uber.org/fx" +) + +func TestGetPeerID(t *testing.T) { + var id peer.ID + host, err := New( + WithFxOption(fx.Populate(&id)), + ) + require.NoError(t, err) + defer host.Close() + + require.Equal(t, host.ID(), id) + +} + +func TestGetEventBus(t *testing.T) { + var eb event.Bus + host, err := New( + NoTransports, + WithFxOption(fx.Populate(&eb)), + ) + require.NoError(t, err) + defer host.Close() + + require.NotNil(t, eb) +} + +func TestGetHost(t *testing.T) { + var h host.Host + host, err := New( + NoTransports, + WithFxOption(fx.Populate(&h)), + ) + require.NoError(t, err) + defer host.Close() + + require.NotNil(t, h) +} + +func TestGetIDService(t *testing.T) { + var id identify.IDService + host, err := New( + NoTransports, + WithFxOption(fx.Populate(&id)), + ) + require.NoError(t, err) + defer host.Close() + + require.NotNil(t, id) +} diff --git a/go.mod b/go.mod index ec6d7346c7..62ef58c06c 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/libp2p/go-libp2p -go 1.22 +go 1.22.0 + +toolchain go1.22.1 retract v0.26.1 // Tag was applied incorrectly due to a bug in the release workflow. @@ -21,9 +23,9 @@ require ( github.com/ipfs/go-ds-leveldb v0.5.0 github.com/ipfs/go-log/v2 v2.5.1 github.com/jbenet/go-temp-err-catcher v0.1.0 - github.com/klauspost/compress v1.17.9 + github.com/klauspost/compress v1.17.11 github.com/libp2p/go-buffer-pool v0.1.0 - github.com/libp2p/go-flow-metrics v0.1.0 + github.com/libp2p/go-flow-metrics v0.2.0 github.com/libp2p/go-libp2p-asn-util v0.4.1 github.com/libp2p/go-libp2p-testing v0.12.0 github.com/libp2p/go-msgio v0.3.0 @@ -37,36 +39,36 @@ require ( github.com/mr-tron/base58 v1.2.0 github.com/multiformats/go-base32 v0.1.0 github.com/multiformats/go-multiaddr v0.13.0 - github.com/multiformats/go-multiaddr-dns v0.3.1 + github.com/multiformats/go-multiaddr-dns v0.4.1 github.com/multiformats/go-multiaddr-fmt v0.1.0 github.com/multiformats/go-multibase v0.2.0 github.com/multiformats/go-multicodec v0.9.0 github.com/multiformats/go-multihash v0.2.3 - github.com/multiformats/go-multistream v0.5.0 + github.com/multiformats/go-multistream v0.6.0 github.com/multiformats/go-varint v0.0.7 github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 - github.com/pion/datachannel v1.5.8 - github.com/pion/ice/v2 v2.3.34 + github.com/pion/datachannel v1.5.9 + github.com/pion/ice/v2 v2.3.36 github.com/pion/logging v0.2.2 - github.com/pion/sctp v1.8.20 + github.com/pion/sctp v1.8.33 github.com/pion/stun v0.6.1 - github.com/pion/webrtc/v3 v3.3.0 - github.com/prometheus/client_golang v1.19.1 + github.com/pion/webrtc/v3 v3.3.4 + github.com/prometheus/client_golang v1.20.5 github.com/prometheus/client_model v0.6.1 - github.com/quic-go/quic-go v0.45.2 - github.com/quic-go/webtransport-go v0.8.0 + github.com/quic-go/quic-go v0.48.2 + github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 github.com/raulk/go-watchdog v1.3.0 github.com/stretchr/testify v1.9.0 - go.uber.org/fx v1.22.1 + go.uber.org/fx v1.23.0 go.uber.org/goleak v1.3.0 - go.uber.org/mock v0.4.0 + go.uber.org/mock v0.5.0 go.uber.org/zap v1.27.0 - golang.org/x/crypto v0.25.0 - golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 - golang.org/x/sync v0.7.0 - golang.org/x/sys v0.22.0 - golang.org/x/tools v0.23.0 - google.golang.org/protobuf v1.34.2 + golang.org/x/crypto v0.28.0 + golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c + golang.org/x/sync v0.8.0 + golang.org/x/sys v0.26.0 + golang.org/x/tools v0.26.0 + google.golang.org/protobuf v1.35.1 ) require ( @@ -88,7 +90,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect - github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect + github.com/google/pprof v0.0.0-20241017200806-017d972448fc // indirect github.com/google/uuid v1.6.0 // indirect github.com/huin/goupnp v1.3.0 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect @@ -96,37 +98,37 @@ require ( github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/koron/go-ssdp v0.0.4 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/miekg/dns v1.1.61 // indirect + github.com/miekg/dns v1.1.62 // indirect github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect github.com/minio/sha256-simd v1.0.1 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/nxadm/tail v1.4.11 // indirect - github.com/onsi/ginkgo/v2 v2.19.1 // indirect + github.com/onsi/ginkgo/v2 v2.20.2 // indirect github.com/opencontainers/runtime-spec v1.2.0 // indirect github.com/pion/dtls/v2 v2.2.12 // indirect - github.com/pion/interceptor v0.1.29 // indirect + github.com/pion/interceptor v0.1.37 // indirect github.com/pion/mdns v0.0.12 // indirect github.com/pion/randutil v0.1.0 // indirect github.com/pion/rtcp v1.2.14 // indirect - github.com/pion/rtp v1.8.8 // indirect + github.com/pion/rtp v1.8.9 // indirect github.com/pion/sdp/v3 v3.0.9 // indirect github.com/pion/srtp/v2 v2.0.20 // indirect github.com/pion/transport/v2 v2.2.10 // indirect github.com/pion/turn/v2 v2.1.6 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/common v0.60.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect - github.com/quic-go/qpack v0.4.0 // indirect + github.com/quic-go/qpack v0.5.1 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/syndtr/goleveldb v1.0.0 // indirect - github.com/wlynxg/anet v0.0.3 // indirect - go.uber.org/dig v1.17.1 // indirect + github.com/wlynxg/anet v0.0.5 // indirect + go.uber.org/dig v1.18.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/mod v0.19.0 // indirect - golang.org/x/net v0.27.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/mod v0.21.0 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/text v0.19.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.3.0 // indirect ) diff --git a/go.sum b/go.sum index 634d497c4b..047083b063 100644 --- a/go.sum +++ b/go.sum @@ -111,8 +111,8 @@ github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF 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/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k= -github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +github.com/google/pprof v0.0.0-20241017200806-017d972448fc h1:NGyrhhFhwvRAZg02jnYVg3GBQy0qGBKmFQJwaPmpmxs= +github.com/google/pprof v0.0.0-20241017200806-017d972448fc/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -162,8 +162,8 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= -github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0= @@ -180,8 +180,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= -github.com/libp2p/go-flow-metrics v0.1.0 h1:0iPhMI8PskQwzh57jB9WxIuIOQ0r+15PChFGkx3Q3WM= -github.com/libp2p/go-flow-metrics v0.1.0/go.mod h1:4Xi8MX8wj5aWNDAZttg6UPmc0ZrnFNsMtpsYUClFtro= +github.com/libp2p/go-flow-metrics v0.2.0 h1:EIZzjmeOE6c8Dav0sNv35vhZxATIXWZg6j/C08XmmDw= +github.com/libp2p/go-flow-metrics v0.2.0/go.mod h1:st3qqfu8+pMfh+9Mzqb2GTiwrAGjIPszEjZmtksN8Jc= github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= @@ -208,10 +208,9 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= -github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= -github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs= -github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ= +github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= +github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8= github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms= github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc= @@ -234,11 +233,10 @@ github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYg github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo= -github.com/multiformats/go-multiaddr v0.2.0/go.mod h1:0nO36NvPpyV4QzvTLi/lafl2y95ncPj0vFwVF6k6wJ4= github.com/multiformats/go-multiaddr v0.13.0 h1:BCBzs61E3AGHcYYTv8dqRH43ZfyrqM8RXVPT8t13tLQ= github.com/multiformats/go-multiaddr v0.13.0/go.mod h1:sBXrNzucqkFJhvKOiwwLyqamGa/P5EIXNPLovyhQCII= -github.com/multiformats/go-multiaddr-dns v0.3.1 h1:QgQgR+LQVt3NPTjbrLLpsaT2ufAA2y0Mkk+QRVJbW3A= -github.com/multiformats/go-multiaddr-dns v0.3.1/go.mod h1:G/245BRQ6FJGmryJCrOuTdB37AMA5AMOVuO6NY3JwTk= +github.com/multiformats/go-multiaddr-dns v0.4.1 h1:whi/uCLbDS3mSEUMb1MsoT4uzUeZB0N32yzufqS0i5M= +github.com/multiformats/go-multiaddr-dns v0.4.1/go.mod h1:7hfthtB4E4pQwirrz+J0CcDUfbWzTqEzVyYKKIKpgkc= github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= @@ -248,9 +246,8 @@ github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI1 github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= -github.com/multiformats/go-multistream v0.5.0 h1:5htLSLl7lvJk3xx3qT/8Zm9J4K8vEOf/QGkvOGQAyiE= -github.com/multiformats/go-multistream v0.5.0/go.mod h1:n6tMZiwiP2wUsR8DgfDWw1dydlEqV3l6N3/GBsX6ILA= -github.com/multiformats/go-varint v0.0.1/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= +github.com/multiformats/go-multistream v0.6.0 h1:ZaHKbsL404720283o4c/IHQXiS6gb8qAN5EIJ4PN5EA= +github.com/multiformats/go-multistream v0.6.0/go.mod h1:MOyoG5otO24cHIg8kf9QW2/NozURlkP/rvi2FQJyCPg= github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= @@ -263,11 +260,11 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 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/v2 v2.19.1 h1:QXgq3Z8Crl5EL1WBAC98A5sEBHARrAJNzAmMxzLcRF0= -github.com/onsi/ginkgo/v2 v2.19.1/go.mod h1:O3DtEWQkPa/F7fBMgmZQKKsluAy8pd3rEQdrjkPb9zA= +github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= +github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.34.0 h1:eSSPsPNp6ZpsG8X1OVmOTxig+CblTc4AxpPBykhe2Os= -github.com/onsi/gomega v1.34.0/go.mod h1:MIKI8c+f+QLWk+hxbePD4i0LMJSExPaZOVfkoex4cAo= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= @@ -275,15 +272,15 @@ github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTm 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/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pion/datachannel v1.5.8 h1:ph1P1NsGkazkjrvyMfhRBUAWMxugJjq2HfQifaOoSNo= -github.com/pion/datachannel v1.5.8/go.mod h1:PgmdpoaNBLX9HNzNClmdki4DYW5JtI7Yibu8QzbL3tI= +github.com/pion/datachannel v1.5.9 h1:LpIWAOYPyDrXtU+BW7X0Yt/vGtYxtXQ8ql7dFfYUVZA= +github.com/pion/datachannel v1.5.9/go.mod h1:kDUuk4CU4Uxp82NH4LQZbISULkX/HtzKa4P7ldf9izE= github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= -github.com/pion/ice/v2 v2.3.34 h1:Ic1ppYCj4tUOcPAp76U6F3fVrlSw8A9JtRXLqw6BbUM= -github.com/pion/ice/v2 v2.3.34/go.mod h1:mBF7lnigdqgtB+YHkaY/Y6s6tsyRyo4u4rPGRuOjUBQ= -github.com/pion/interceptor v0.1.29 h1:39fsnlP1U8gw2JzOFWdfCU82vHvhW9o0rZnZF56wF+M= -github.com/pion/interceptor v0.1.29/go.mod h1:ri+LGNjRUc5xUNtDEPzfdkmSqISixVTBF/z/Zms/6T4= +github.com/pion/ice/v2 v2.3.36 h1:SopeXiVbbcooUg2EIR8sq4b13RQ8gzrkkldOVg+bBsc= +github.com/pion/ice/v2 v2.3.36/go.mod h1:mBF7lnigdqgtB+YHkaY/Y6s6tsyRyo4u4rPGRuOjUBQ= +github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI= +github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y= github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8= @@ -294,10 +291,10 @@ github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9 github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE= github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= -github.com/pion/rtp v1.8.8 h1:EtYFHI0rpUEjT/RMnGfb1vdJhbYmPG77szD72uUnSxs= -github.com/pion/rtp v1.8.8/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= -github.com/pion/sctp v1.8.20 h1:sOc3lkV/tQaP57ZUEXIMdM2V92IIB2ia5v/ygnBxaEg= -github.com/pion/sctp v1.8.20/go.mod h1:oTxw8i5m+WbDHZJL/xUpe6CPIn1Y0GIKKwTLF4h53H8= +github.com/pion/rtp v1.8.9 h1:E2HX740TZKaqdcPmf4pw6ZZuG8u5RlMMt+l3dxeu6Wk= +github.com/pion/rtp v1.8.9/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= +github.com/pion/sctp v1.8.33 h1:dSE4wX6uTJBcNm8+YlMg7lw1wqyKHggsP5uKbdj+NZw= +github.com/pion/sctp v1.8.33/go.mod h1:beTnqSzewI53KWoG3nqB282oDMGrhNxBdb+JZnkCwRM= github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY= github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M= github.com/pion/srtp/v2 v2.0.20 h1:HNNny4s+OUmG280ETrCdgFndp4ufx3/uy85EawYEhTk= @@ -310,36 +307,36 @@ github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLh github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= -github.com/pion/transport/v3 v3.0.6 h1:k1mQU06bmmX143qSWgXFqSH1KUJceQvIUuVH/K5ELWw= -github.com/pion/transport/v3 v3.0.6/go.mod h1:HvJr2N/JwNJAfipsRleqwFoR3t/pWyHeZUs89v3+t5s= +github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= +github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= github.com/pion/turn/v2 v2.1.6 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc= github.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= -github.com/pion/webrtc/v3 v3.3.0 h1:Rf4u6n6U5t5sUxhYPQk/samzU/oDv7jk6BA5hyO2F9I= -github.com/pion/webrtc/v3 v3.3.0/go.mod h1:hVmrDJvwhEertRWObeb1xzulzHGeVUoPlWvxdGzcfU0= +github.com/pion/webrtc/v3 v3.3.4 h1:v2heQVnXTSqNRXcaFQVOhIOYkLMxOu1iJG8uy1djvkk= +github.com/pion/webrtc/v3 v3.3.4/go.mod h1:liNa+E1iwyzyXqNUwvoMRNQ10x8h8FOeJKL8RkIbamE= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= -github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= -github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/common v0.60.0 h1:+V9PAREWNvJMAuJ1x1BaWl9dewMW4YrHZQbx0sJNllA= +github.com/prometheus/common v0.60.0/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= -github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= -github.com/quic-go/quic-go v0.45.2 h1:DfqBmqjb4ExSdxRIb/+qXhPC+7k6+DUNZha4oeiC9fY= -github.com/quic-go/quic-go v0.45.2/go.mod h1:1dLehS7TIR64+vxGR70GDcatWTOtMX2PUtnKsjbTurI= -github.com/quic-go/webtransport-go v0.8.0 h1:HxSrwun11U+LlmwpgM1kEqIqH90IT4N8auv/cD7QFJg= -github.com/quic-go/webtransport-go v0.8.0/go.mod h1:N99tjprW432Ut5ONql/aUhSLT0YVSlwHohQsuac9WaM= +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/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE= +github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs= +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/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk= github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -404,8 +401,9 @@ github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljT github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= -github.com/wlynxg/anet v0.0.3 h1:PvR53psxFXstc12jelG6f1Lv4MWqE0tI76/hHGjh9rg= github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -414,15 +412,15 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc= -go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= -go.uber.org/fx v1.22.1 h1:nvvln7mwyT5s1q201YE29V/BFrGor6vMiDNpU/78Mys= -go.uber.org/fx v1.22.1/go.mod h1:HT2M7d7RHo+ebKGh9NRcrsrHHfpZ60nW3QRubMRfv48= +go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw= +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/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.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= -go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -448,11 +446,11 @@ 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.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.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -465,8 +463,8 @@ golang.org/x/mod v0.3.0/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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= -golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -490,8 +488,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.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= 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-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -507,8 +505,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/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-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.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/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= @@ -540,8 +538,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.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.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -558,8 +556,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.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.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 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.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= @@ -581,8 +579,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f 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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= -golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= 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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -605,8 +603,8 @@ google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/libp2p_test.go b/libp2p_test.go index a5803add4d..3de82946d8 100644 --- a/libp2p_test.go +++ b/libp2p_test.go @@ -2,10 +2,16 @@ package libp2p import ( "context" + "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" "errors" "fmt" "io" + "math/big" "net" "net/netip" "regexp" @@ -26,11 +32,12 @@ import ( "github.com/libp2p/go-libp2p/p2p/net/swarm" "github.com/libp2p/go-libp2p/p2p/protocol/ping" "github.com/libp2p/go-libp2p/p2p/security/noise" - tls "github.com/libp2p/go-libp2p/p2p/security/tls" + sectls "github.com/libp2p/go-libp2p/p2p/security/tls" quic "github.com/libp2p/go-libp2p/p2p/transport/quic" "github.com/libp2p/go-libp2p/p2p/transport/quicreuse" "github.com/libp2p/go-libp2p/p2p/transport/tcp" libp2pwebrtc "github.com/libp2p/go-libp2p/p2p/transport/webrtc" + "github.com/libp2p/go-libp2p/p2p/transport/websocket" webtransport "github.com/libp2p/go-libp2p/p2p/transport/webtransport" "go.uber.org/goleak" @@ -52,7 +59,7 @@ func TestTransportConstructor(t *testing.T) { _ connmgr.ConnectionGater, upgrader transport.Upgrader, ) transport.Transport { - tpt, err := tcp.NewTCPTransport(upgrader, nil) + tpt, err := tcp.NewTCPTransport(upgrader, nil, nil) require.NoError(t, err) return tpt } @@ -256,7 +263,7 @@ func TestSecurityConstructor(t *testing.T) { h, err := New( Transport(tcp.NewTCPTransport), Security("/noisy", noise.New), - Security("/tls", tls.New), + Security("/tls", sectls.New), DefaultListenAddrs, DisableRelay(), ) @@ -655,3 +662,116 @@ func TestUseCorrectTransportForDialOut(t *testing.T) { } } } + +func TestCircuitBehindWSS(t *testing.T) { + relayTLSConf := getTLSConf(t, net.IPv4(127, 0, 0, 1), time.Now(), time.Now().Add(time.Hour)) + serverNameChan := make(chan string, 2) // Channel that returns what server names the client hello specified + relayTLSConf.GetConfigForClient = func(chi *tls.ClientHelloInfo) (*tls.Config, error) { + serverNameChan <- chi.ServerName + return relayTLSConf, nil + } + + relay, err := New( + EnableRelayService(), + ForceReachabilityPublic(), + Transport(websocket.New, websocket.WithTLSConfig(relayTLSConf)), + ListenAddrStrings("/ip4/127.0.0.1/tcp/0/wss"), + ) + require.NoError(t, err) + defer relay.Close() + + relayAddrPort, _ := relay.Addrs()[0].ValueForProtocol(ma.P_TCP) + relayAddrWithSNIString := fmt.Sprintf( + "/dns4/localhost/tcp/%s/wss", relayAddrPort, + ) + relayAddrWithSNI := []ma.Multiaddr{ma.StringCast(relayAddrWithSNIString)} + + h, err := New( + NoListenAddrs, + EnableRelay(), + Transport(websocket.New, websocket.WithTLSClientConfig(&tls.Config{InsecureSkipVerify: true})), + ForceReachabilityPrivate()) + require.NoError(t, err) + defer h.Close() + + peerBehindRelay, err := New( + NoListenAddrs, + Transport(websocket.New, websocket.WithTLSClientConfig(&tls.Config{InsecureSkipVerify: true})), + EnableRelay(), + EnableAutoRelayWithStaticRelays([]peer.AddrInfo{{ID: relay.ID(), Addrs: relayAddrWithSNI}}), + ForceReachabilityPrivate()) + require.NoError(t, err) + defer peerBehindRelay.Close() + + require.Equal(t, + "localhost", + <-serverNameChan, // The server connects to the relay + ) + + // Connect to the peer behind the relay + h.Connect(context.Background(), peer.AddrInfo{ + ID: peerBehindRelay.ID(), + Addrs: []ma.Multiaddr{ma.StringCast( + fmt.Sprintf("%s/p2p/%s/p2p-circuit", relayAddrWithSNIString, relay.ID()), + )}, + }) + require.NoError(t, err) + + require.Equal(t, + "localhost", + <-serverNameChan, // The client connects to the relay and sends the SNI + ) +} + +// getTLSConf is a helper to generate a self-signed TLS config +func getTLSConf(t *testing.T, ip net.IP, start, end time.Time) *tls.Config { + t.Helper() + certTempl := &x509.Certificate{ + SerialNumber: big.NewInt(1234), + Subject: pkix.Name{Organization: []string{"websocket"}}, + NotBefore: start, + NotAfter: end, + IsCA: true, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + IPAddresses: []net.IP{ip}, + } + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + caBytes, err := x509.CreateCertificate(rand.Reader, certTempl, certTempl, &priv.PublicKey, priv) + require.NoError(t, err) + cert, err := x509.ParseCertificate(caBytes) + require.NoError(t, err) + return &tls.Config{ + Certificates: []tls.Certificate{{ + Certificate: [][]byte{cert.Raw}, + PrivateKey: priv, + Leaf: cert, + }}, + } +} + +func TestSharedTCPAddr(t *testing.T) { + h, err := New( + ShareTCPListener(), + Transport(tcp.NewTCPTransport), + Transport(websocket.New), + ListenAddrStrings("/ip4/0.0.0.0/tcp/8888"), + ListenAddrStrings("/ip4/0.0.0.0/tcp/8888/ws"), + ) + require.NoError(t, err) + sawTCP := false + sawWS := false + for _, addr := range h.Addrs() { + if strings.HasSuffix(addr.String(), "/tcp/8888") { + sawTCP = true + } + if strings.HasSuffix(addr.String(), "/tcp/8888/ws") { + sawWS = true + } + } + require.True(t, sawTCP) + require.True(t, sawWS) + h.Close() +} diff --git a/options.go b/options.go index 1a8bc5dd55..0329b7e60b 100644 --- a/options.go +++ b/options.go @@ -31,7 +31,6 @@ import ( "github.com/prometheus/client_golang/prometheus" ma "github.com/multiformats/go-multiaddr" - madns "github.com/multiformats/go-multiaddr-dns" "go.uber.org/fx" ) @@ -495,7 +494,7 @@ func UserAgent(userAgent string) Option { } // MultiaddrResolver sets the libp2p dns resolver -func MultiaddrResolver(rslv *madns.Resolver) Option { +func MultiaddrResolver(rslv network.MultiaddrDNSResolver) Option { return func(cfg *Config) error { cfg.MultiaddrResolver = rslv return nil @@ -635,3 +634,24 @@ func IPv6BlackHoleSuccessCounter(f *swarm.BlackHoleSuccessCounter) Option { return nil } } + +// WithFxOption adds a user provided fx.Option to the libp2p constructor. +// Experimental: This option is subject to change or removal. +func WithFxOption(opts ...fx.Option) Option { + return func(cfg *Config) error { + cfg.UserFxOptions = append(cfg.UserFxOptions, opts...) + return nil + } +} + +// ShareTCPListener shares the same listen address between TCP and Websocket +// transports. This lets both transports use the same TCP port. +// +// Currently this behavior is Opt-in. In a future release this will be the +// default, and this option will be removed. +func ShareTCPListener() Option { + return func(cfg *Config) error { + cfg.ShareTCPListener = true + return nil + } +} diff --git a/p2p/host/autonat/autonat.go b/p2p/host/autonat/autonat.go index 479f31ecfb..6d3c9e242f 100644 --- a/p2p/host/autonat/autonat.go +++ b/p2p/host/autonat/autonat.go @@ -3,6 +3,7 @@ package autonat import ( "context" "math/rand" + "slices" "sync/atomic" "time" @@ -33,6 +34,8 @@ type AmbientAutoNAT struct { inboundConn chan network.Conn dialResponses chan error + // Used when testing the autonat service + observations chan network.Reachability // status is an autoNATResult reflecting current status. status atomic.Pointer[network.Reachability] // Reflects the confidence on of the NATStatus being private, as a single @@ -40,11 +43,12 @@ type AmbientAutoNAT struct { // If it is <3, then multiple autoNAT peers may be contacted for dialback // If only a single autoNAT peer is known, then the confidence increases // for each failure until it reaches 3. - confidence int - lastInbound time.Time - lastProbeTry time.Time - lastProbe time.Time - recentProbes map[peer.ID]time.Time + confidence int + lastInbound time.Time + lastProbe time.Time + recentProbes map[peer.ID]time.Time + pendingProbes int + ourAddrs map[string]struct{} service *autoNATService @@ -70,7 +74,11 @@ func New(h host.Host, options ...Option) (AutoNAT, error) { return nil, err } if conf.addressFunc == nil { - conf.addressFunc = h.Addrs + if aa, ok := h.(interface{ AllAddrs() []ma.Multiaddr }); ok { + conf.addressFunc = aa.AllAddrs + } else { + conf.addressFunc = h.Addrs + } } for _, o := range options { @@ -108,10 +116,12 @@ func New(h host.Host, options ...Option) (AutoNAT, error) { config: conf, inboundConn: make(chan network.Conn, 5), dialResponses: make(chan error, 1), + observations: make(chan network.Reachability, 1), emitReachabilityChanged: emitReachabilityChanged, service: service, recentProbes: make(map[peer.ID]time.Time), + ourAddrs: make(map[string]struct{}), } reachability := network.ReachabilityUnknown as.status.Store(&reachability) @@ -125,7 +135,6 @@ func New(h host.Host, options ...Option) (AutoNAT, error) { } as.subscriber = subscriber - h.Network().Notify(as) go as.background() return as, nil @@ -165,117 +174,126 @@ func (as *AmbientAutoNAT) background() { defer as.subscriber.Close() defer as.emitReachabilityChanged.Close() + // Fallback timer to update address in case EvtLocalAddressesUpdated is not emitted. + // TODO: The event not emitting properly is a bug. This is a workaround. + addrChangeTicker := time.NewTicker(30 * time.Minute) + defer addrChangeTicker.Stop() + timer := time.NewTimer(delay) defer timer.Stop() timerRunning := true - retryProbe := false + forceProbe := false for { select { - // new inbound connection. case conn := <-as.inboundConn: localAddrs := as.host.Addrs() if manet.IsPublicAddr(conn.RemoteMultiaddr()) && !ipInList(conn.RemoteMultiaddr(), localAddrs) { as.lastInbound = time.Now() } - + case <-addrChangeTicker.C: + // schedule a new probe if addresses have changed case e := <-subChan: switch e := e.(type) { - case event.EvtLocalAddressesUpdated: - // On local address update, reduce confidence from maximum so that we schedule - // the next probe sooner - if as.confidence == maxConfidence { - as.confidence-- - } case event.EvtPeerIdentificationCompleted: - if s, err := as.host.Peerstore().SupportsProtocols(e.Peer, AutoNATProto); err == nil && len(s) > 0 { - currentStatus := *as.status.Load() - if currentStatus == network.ReachabilityUnknown { - as.tryProbe(e.Peer) - } + if proto, err := as.host.Peerstore().SupportsProtocols(e.Peer, AutoNATProto); err == nil && len(proto) > 0 { + forceProbe = true } + case event.EvtLocalAddressesUpdated: + // schedule a new probe if addresses have changed default: log.Errorf("unknown event type: %T", e) } - - // probe finished. + case obs := <-as.observations: + as.recordObservation(obs) + continue case err, ok := <-as.dialResponses: if !ok { return } + as.pendingProbes-- if IsDialRefused(err) { - retryProbe = true + forceProbe = true } else { as.handleDialResponse(err) } case <-timer.C: + timerRunning = false + forceProbe = false + // Update the last probe time. We use it to ensure + // that we don't spam the peerstore. + as.lastProbe = time.Now() peer := as.getPeerToProbe() as.tryProbe(peer) - timerRunning = false - retryProbe = false case <-as.ctx.Done(): return } + // On address update, reduce confidence from maximum so that we schedule + // the next probe sooner + hasNewAddr := as.checkAddrs() + if hasNewAddr && as.confidence == maxConfidence { + as.confidence-- + } - // Drain the timer channel if it hasn't fired in preparation for Resetting it. if timerRunning && !timer.Stop() { <-timer.C } - timer.Reset(as.scheduleProbe(retryProbe)) + timer.Reset(as.scheduleProbe(forceProbe)) timerRunning = true } } -func (as *AmbientAutoNAT) cleanupRecentProbes() { - fixedNow := time.Now() - for k, v := range as.recentProbes { - if fixedNow.Sub(v) > as.throttlePeerPeriod { - delete(as.recentProbes, k) +func (as *AmbientAutoNAT) checkAddrs() (hasNewAddr bool) { + currentAddrs := as.addressFunc() + hasNewAddr = slices.ContainsFunc(currentAddrs, func(a ma.Multiaddr) bool { + _, ok := as.ourAddrs[string(a.Bytes())] + return !ok + }) + clear(as.ourAddrs) + for _, a := range currentAddrs { + if !manet.IsPublicAddr(a) { + continue } + as.ourAddrs[string(a.Bytes())] = struct{}{} } + return hasNewAddr } // scheduleProbe calculates when the next probe should be scheduled for. -func (as *AmbientAutoNAT) scheduleProbe(retryProbe bool) time.Duration { - // Our baseline is a probe every 'AutoNATRefreshInterval' - // This is modulated by: - // * if we are in an unknown state, have low confidence, or we want to retry because a probe was refused that - // should drop to 'AutoNATRetryInterval' - // * recent inbound connections (implying continued connectivity) should decrease the retry when public - // * recent inbound connections when not public mean we should try more actively to see if we're public. - fixedNow := time.Now() +func (as *AmbientAutoNAT) scheduleProbe(forceProbe bool) time.Duration { + now := time.Now() currentStatus := *as.status.Load() - - nextProbe := fixedNow - // Don't look for peers in the peer store more than once per second. - if !as.lastProbeTry.IsZero() { - backoff := as.lastProbeTry.Add(time.Second) - if backoff.After(nextProbe) { - nextProbe = backoff - } + nextProbeAfter := as.config.refreshInterval + receivedInbound := as.lastInbound.After(as.lastProbe) + switch { + case forceProbe && currentStatus == network.ReachabilityUnknown: + // retry very quicky if forceProbe is true *and* we don't know our reachability + // limit all peers fetch from peerstore to 1 per second. + nextProbeAfter = 2 * time.Second + nextProbeAfter = 2 * time.Second + case currentStatus == network.ReachabilityUnknown, + as.confidence < maxConfidence, + currentStatus != network.ReachabilityPublic && receivedInbound: + // Retry quickly in case: + // 1. Our reachability is Unknown + // 2. We don't have enough confidence in our reachability. + // 3. We're private but we received an inbound connection. + nextProbeAfter = as.config.retryInterval + case currentStatus == network.ReachabilityPublic && receivedInbound: + // We are public and we received an inbound connection recently, + // wait a little longer + nextProbeAfter *= 2 + nextProbeAfter = min(nextProbeAfter, maxRefreshInterval) } - if !as.lastProbe.IsZero() { - untilNext := as.config.refreshInterval - if retryProbe { - untilNext = as.config.retryInterval - } else if currentStatus == network.ReachabilityUnknown { - untilNext = as.config.retryInterval - } else if as.confidence < maxConfidence { - untilNext = as.config.retryInterval - } else if currentStatus == network.ReachabilityPublic && as.lastInbound.After(as.lastProbe) { - untilNext *= 2 - } else if currentStatus != network.ReachabilityPublic && as.lastInbound.After(as.lastProbe) { - untilNext /= 5 - } - - if as.lastProbe.Add(untilNext).After(nextProbe) { - nextProbe = as.lastProbe.Add(untilNext) - } + nextProbeTime := as.lastProbe.Add(nextProbeAfter) + if nextProbeTime.Before(now) { + nextProbeTime = now } if as.metricsTracer != nil { - as.metricsTracer.NextProbeTime(nextProbe) + as.metricsTracer.NextProbeTime(nextProbeTime) } - return nextProbe.Sub(fixedNow) + + return nextProbeTime.Sub(now) } // handleDialResponse updates the current status based on dial response. @@ -354,28 +372,14 @@ func (as *AmbientAutoNAT) recordObservation(observation network.Reachability) { } } -func (as *AmbientAutoNAT) tryProbe(p peer.ID) bool { - as.lastProbeTry = time.Now() - if p.Validate() != nil { - return false - } - - if lastTime, ok := as.recentProbes[p]; ok { - if time.Since(lastTime) < as.throttlePeerPeriod { - return false - } +func (as *AmbientAutoNAT) tryProbe(p peer.ID) { + if p == "" || as.pendingProbes > 5 { + return } - as.cleanupRecentProbes() - info := as.host.Peerstore().PeerInfo(p) - - if !as.config.dialPolicy.skipPeer(info.Addrs) { - as.recentProbes[p] = time.Now() - as.lastProbe = time.Now() - go as.probe(&info) - return true - } - return false + as.recentProbes[p] = time.Now() + as.pendingProbes++ + go as.probe(&info) } func (as *AmbientAutoNAT) probe(pi *peer.AddrInfo) { @@ -399,7 +403,19 @@ func (as *AmbientAutoNAT) getPeerToProbe() peer.ID { return "" } - candidates := make([]peer.ID, 0, len(peers)) + // clean old probes + fixedNow := time.Now() + for k, v := range as.recentProbes { + if fixedNow.Sub(v) > as.throttlePeerPeriod { + delete(as.recentProbes, k) + } + } + + // Shuffle peers + for n := len(peers); n > 0; n-- { + randIndex := rand.Intn(n) + peers[n-1], peers[randIndex] = peers[randIndex], peers[n-1] + } for _, p := range peers { info := as.host.Peerstore().PeerInfo(p) @@ -408,24 +424,13 @@ func (as *AmbientAutoNAT) getPeerToProbe() peer.ID { continue } - // Exclude peers in backoff. - if lastTime, ok := as.recentProbes[p]; ok { - if time.Since(lastTime) < as.throttlePeerPeriod { - continue - } - } - if as.config.dialPolicy.skipPeer(info.Addrs) { continue } - candidates = append(candidates, p) - } - - if len(candidates) == 0 { - return "" + return p } - return candidates[rand.Intn(len(candidates))] + return "" } func (as *AmbientAutoNAT) Close() error { diff --git a/p2p/host/autonat/autonat_test.go b/p2p/host/autonat/autonat_test.go index d8cfcc51e4..6a5768cd5a 100644 --- a/p2p/host/autonat/autonat_test.go +++ b/p2p/host/autonat/autonat_test.go @@ -15,6 +15,7 @@ import ( "github.com/libp2p/go-msgio/pbio" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -223,7 +224,7 @@ func TestAutoNATIncomingEvents(t *testing.T) { require.Eventually(t, func() bool { return an.Status() != network.ReachabilityUnknown - }, 500*time.Millisecond, 10*time.Millisecond, "Expected probe due to identification of autonat service") + }, 5*time.Second, 100*time.Millisecond, "Expected probe due to identification of autonat service") } func TestAutoNATDialRefused(t *testing.T) { @@ -258,6 +259,10 @@ func TestAutoNATDialRefused(t *testing.T) { close(done) } +func recordObservation(an *AmbientAutoNAT, status network.Reachability) { + an.observations <- status +} + func TestAutoNATObservationRecording(t *testing.T) { hs := makeAutoNATServicePublic(t) defer hs.Close() @@ -271,39 +276,34 @@ func TestAutoNATObservationRecording(t *testing.T) { t.Fatalf("failed to subscribe to event EvtLocalRoutabilityPublic, err=%s", err) } - an.recordObservation(network.ReachabilityPublic) - if an.Status() != network.ReachabilityPublic { - t.Fatalf("failed to transition to public.") + expectStatus := func(expected network.Reachability, msg string, args ...any) { + require.EventuallyWithTf(t, func(collect *assert.CollectT) { + assert.Equal(collect, expected, an.Status()) + }, 2*time.Second, 100*time.Millisecond, msg, args...) } + recordObservation(an, network.ReachabilityPublic) + expectStatus(network.ReachabilityPublic, "failed to transition to public.") expectEvent(t, s, network.ReachabilityPublic, 3*time.Second) // a single recording should have confidence still at 0, and transition to private quickly. - an.recordObservation(network.ReachabilityPrivate) - if an.Status() != network.ReachabilityPrivate { - t.Fatalf("failed to transition to private.") - } + recordObservation(an, network.ReachabilityPrivate) + expectStatus(network.ReachabilityPrivate, "failed to transition to private.") expectEvent(t, s, network.ReachabilityPrivate, 3*time.Second) // stronger public confidence should be harder to undo. - an.recordObservation(network.ReachabilityPublic) - an.recordObservation(network.ReachabilityPublic) - if an.Status() != network.ReachabilityPublic { - t.Fatalf("failed to transition to public.") - } + recordObservation(an, network.ReachabilityPublic) + recordObservation(an, network.ReachabilityPublic) + expectStatus(network.ReachabilityPublic, "failed to transition to public.") expectEvent(t, s, network.ReachabilityPublic, 3*time.Second) - an.recordObservation(network.ReachabilityPrivate) - if an.Status() != network.ReachabilityPublic { - t.Fatalf("too-extreme private transition.") - } + recordObservation(an, network.ReachabilityPrivate) + expectStatus(network.ReachabilityPublic, "too-extreme private transition.") // Don't emit events if reachability hasn't changed - an.recordObservation(network.ReachabilityPublic) - if an.Status() != network.ReachabilityPublic { - t.Fatalf("reachability should stay public") - } + recordObservation(an, network.ReachabilityPublic) + expectStatus(network.ReachabilityPublic, "reachability should stay public") select { case <-s.Out(): t.Fatal("received event without state transition") diff --git a/p2p/host/autonat/client.go b/p2p/host/autonat/client.go index fa0e03bc51..7f419a72ff 100644 --- a/p2p/host/autonat/client.go +++ b/p2p/host/autonat/client.go @@ -53,7 +53,14 @@ func (c *client) DialBack(ctx context.Context, p peer.ID) error { } defer s.Scope().ReleaseMemory(maxMsgSize) - s.SetDeadline(time.Now().Add(streamTimeout)) + deadline := time.Now().Add(streamTimeout) + if ctxDeadline, ok := ctx.Deadline(); ok { + if ctxDeadline.Before(deadline) { + deadline = ctxDeadline + } + } + + s.SetDeadline(deadline) // Might as well just reset the stream. Once we get to this point, we // don't care about being nice. defer s.Close() diff --git a/p2p/host/autonat/options.go b/p2p/host/autonat/options.go index dec62c5f1d..b378da348d 100644 --- a/p2p/host/autonat/options.go +++ b/p2p/host/autonat/options.go @@ -51,6 +51,8 @@ var defaults = func(c *config) error { return nil } +const maxRefreshInterval = 24 * time.Hour + // EnableService specifies that AutoNAT should be allowed to run a NAT service to help // other peers determine their own NAT status. The provided Network should not be the // default network/dialer of the host passed to `New`, as the NAT system will need to diff --git a/p2p/host/autonat/pb/autonat.pb.go b/p2p/host/autonat/pb/autonat.pb.go index 26944fcbcc..46eed5986b 100644 --- a/p2p/host/autonat/pb/autonat.pb.go +++ b/p2p/host/autonat/pb/autonat.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.34.2 -// protoc v5.27.3 +// protoc-gen-go v1.35.1 +// protoc v5.28.2 // source: p2p/host/autonat/pb/autonat.proto package pb @@ -153,11 +153,9 @@ type Message struct { func (x *Message) Reset() { *x = Message{} - if protoimpl.UnsafeEnabled { - mi := &file_p2p_host_autonat_pb_autonat_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_p2p_host_autonat_pb_autonat_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Message) String() string { @@ -168,7 +166,7 @@ func (*Message) ProtoMessage() {} func (x *Message) ProtoReflect() protoreflect.Message { mi := &file_p2p_host_autonat_pb_autonat_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -215,11 +213,9 @@ type Message_PeerInfo struct { func (x *Message_PeerInfo) Reset() { *x = Message_PeerInfo{} - if protoimpl.UnsafeEnabled { - mi := &file_p2p_host_autonat_pb_autonat_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_p2p_host_autonat_pb_autonat_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Message_PeerInfo) String() string { @@ -230,7 +226,7 @@ func (*Message_PeerInfo) ProtoMessage() {} func (x *Message_PeerInfo) ProtoReflect() protoreflect.Message { mi := &file_p2p_host_autonat_pb_autonat_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -269,11 +265,9 @@ type Message_Dial struct { func (x *Message_Dial) Reset() { *x = Message_Dial{} - if protoimpl.UnsafeEnabled { - mi := &file_p2p_host_autonat_pb_autonat_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_p2p_host_autonat_pb_autonat_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Message_Dial) String() string { @@ -284,7 +278,7 @@ func (*Message_Dial) ProtoMessage() {} func (x *Message_Dial) ProtoReflect() protoreflect.Message { mi := &file_p2p_host_autonat_pb_autonat_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -318,11 +312,9 @@ type Message_DialResponse struct { func (x *Message_DialResponse) Reset() { *x = Message_DialResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_p2p_host_autonat_pb_autonat_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_p2p_host_autonat_pb_autonat_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Message_DialResponse) String() string { @@ -333,7 +325,7 @@ func (*Message_DialResponse) ProtoMessage() {} func (x *Message_DialResponse) ProtoReflect() protoreflect.Message { mi := &file_p2p_host_autonat_pb_autonat_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -456,56 +448,6 @@ func file_p2p_host_autonat_pb_autonat_proto_init() { if File_p2p_host_autonat_pb_autonat_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_p2p_host_autonat_pb_autonat_proto_msgTypes[0].Exporter = func(v any, i int) any { - switch v := v.(*Message); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_p2p_host_autonat_pb_autonat_proto_msgTypes[1].Exporter = func(v any, i int) any { - switch v := v.(*Message_PeerInfo); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_p2p_host_autonat_pb_autonat_proto_msgTypes[2].Exporter = func(v any, i int) any { - switch v := v.(*Message_Dial); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_p2p_host_autonat_pb_autonat_proto_msgTypes[3].Exporter = func(v any, i int) any { - switch v := v.(*Message_DialResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ diff --git a/p2p/host/autorelay/autorelay.go b/p2p/host/autorelay/autorelay.go index 5900798533..b31302098d 100644 --- a/p2p/host/autorelay/autorelay.go +++ b/p2p/host/autorelay/autorelay.go @@ -29,8 +29,7 @@ type AutoRelay struct { relayFinder *relayFinder - host host.Host - addrsF basic.AddrsFactory + host host.Host metricsTracer MetricsTracer } @@ -38,7 +37,6 @@ type AutoRelay struct { func NewAutoRelay(bhost *basic.BasicHost, opts ...Option) (*AutoRelay, error) { r := &AutoRelay{ host: bhost, - addrsF: bhost.AddrsFactory, status: network.ReachabilityUnknown, } conf := defaultConfig @@ -51,7 +49,22 @@ func NewAutoRelay(bhost *basic.BasicHost, opts ...Option) (*AutoRelay, error) { r.conf = &conf r.relayFinder = newRelayFinder(bhost, conf.peerSource, &conf) r.metricsTracer = &wrappedMetricsTracer{conf.metricsTracer} - bhost.AddrsFactory = r.hostAddrs + + // Update the host address factory to use autorelay addresses if we're private + // + // TODO: Don't update host address factory. Instead send our relay addresses on the eventbus. + // The host can decide how to handle those. + addrF := bhost.AddrsFactory + bhost.AddrsFactory = func(addrs []ma.Multiaddr) []ma.Multiaddr { + addrs = addrF(addrs) + r.mx.Lock() + defer r.mx.Unlock() + + if r.status != network.ReachabilityPrivate { + return addrs + } + return r.relayFinder.relayAddrs(addrs) + } return r, nil } @@ -103,20 +116,6 @@ func (r *AutoRelay) background() { } } -func (r *AutoRelay) hostAddrs(addrs []ma.Multiaddr) []ma.Multiaddr { - return r.relayAddrs(r.addrsF(addrs)) -} - -func (r *AutoRelay) relayAddrs(addrs []ma.Multiaddr) []ma.Multiaddr { - r.mx.Lock() - defer r.mx.Unlock() - - if r.status != network.ReachabilityPrivate { - return addrs - } - return r.relayFinder.relayAddrs(addrs) -} - func (r *AutoRelay) Close() error { r.ctxCancel() err := r.relayFinder.Stop() diff --git a/p2p/host/autorelay/relay.go b/p2p/host/autorelay/relay.go index db0d97ec01..2ae5bf240c 100644 --- a/p2p/host/autorelay/relay.go +++ b/p2p/host/autorelay/relay.go @@ -5,6 +5,8 @@ import ( ) // Filter filters out all relay addresses. +// +// Deprecated: It is trivial for a user to implement this if they need this. func Filter(addrs []ma.Multiaddr) []ma.Multiaddr { raddrs := make([]ma.Multiaddr, 0, len(addrs)) for _, addr := range addrs { diff --git a/p2p/host/basic/basic_host.go b/p2p/host/basic/basic_host.go index b5d252e9d2..a85d1978d7 100644 --- a/p2p/host/basic/basic_host.go +++ b/p2p/host/basic/basic_host.go @@ -21,6 +21,7 @@ import ( "github.com/libp2p/go-libp2p/core/record" "github.com/libp2p/go-libp2p/core/transport" "github.com/libp2p/go-libp2p/p2p/host/autonat" + "github.com/libp2p/go-libp2p/p2p/host/basic/internal/backoff" "github.com/libp2p/go-libp2p/p2p/host/eventbus" "github.com/libp2p/go-libp2p/p2p/host/pstoremanager" "github.com/libp2p/go-libp2p/p2p/host/relaysvc" @@ -37,7 +38,6 @@ import ( logging "github.com/ipfs/go-log/v2" ma "github.com/multiformats/go-multiaddr" - madns "github.com/multiformats/go-multiaddr-dns" manet "github.com/multiformats/go-multiaddr/net" msmux "github.com/multiformats/go-multistream" ) @@ -81,7 +81,6 @@ type BasicHost struct { hps *holepunch.Service pings *ping.PingService natmgr NATManager - maResolver *madns.Resolver cmgr connmgr.ConnManager eventbus event.Bus relayManager *relaysvc.RelayManager @@ -98,6 +97,8 @@ type BasicHost struct { addrChangeChan chan struct{} addrMu sync.RWMutex + updateLocalIPv4Backoff backoff.ExpBackoff + updateLocalIPv6Backoff backoff.ExpBackoff filteredInterfaceAddrs []ma.Multiaddr allInterfaceAddrs []ma.Multiaddr @@ -121,19 +122,16 @@ type HostOpts struct { // MultistreamMuxer is essential for the *BasicHost and will use a sensible default value if omitted. MultistreamMuxer *msmux.MultistreamMuxer[protocol.ID] - // NegotiationTimeout determines the read and write timeouts on streams. - // If 0 or omitted, it will use DefaultNegotiationTimeout. - // If below 0, timeouts on streams will be deactivated. + // NegotiationTimeout determines the read and write timeouts when negotiating + // protocols for streams. If 0 or omitted, it will use + // DefaultNegotiationTimeout. If below 0, timeouts on streams will be + // deactivated. NegotiationTimeout time.Duration // AddrsFactory holds a function which can be used to override or filter the result of Addrs. // If omitted, there's no override or filtering, and the results of Addrs and AllAddrs are the same. AddrsFactory AddrsFactory - // MultiaddrResolves holds the go-multiaddr-dns.Resolver used for resolving - // /dns4, /dns6, and /dnsaddr addresses before trying to connect to a peer. - MultiaddrResolver *madns.Resolver - // NATManager takes care of setting NAT port mappings, and discovering external addresses. // If omitted, this will simply be disabled. NATManager func(network.Network) NATManager @@ -194,7 +192,6 @@ func NewHost(n network.Network, opts *HostOpts) (*BasicHost, error) { mux: msmux.NewMultistreamMuxer[protocol.ID](), negtimeout: DefaultNegotiationTimeout, AddrsFactory: DefaultAddrsFactory, - maResolver: madns.DefaultResolver, eventbus: opts.EventBus, addrChangeChan: make(chan struct{}, 1), ctx: hostCtx, @@ -271,7 +268,16 @@ func NewHost(n network.Network, opts *HostOpts) (*BasicHost, error) { opts.HolePunchingOptions = append(hpOpts, opts.HolePunchingOptions...) } - h.hps, err = holepunch.NewService(h, h.ids, opts.HolePunchingOptions...) + h.hps, err = holepunch.NewService(h, h.ids, func() []ma.Multiaddr { + addrs := h.AllAddrs() + if opts.AddrsFactory != nil { + addrs = slices.Clone(opts.AddrsFactory(addrs)) + } + // AllAddrs may ignore observed addresses in favour of NAT mappings. Use both for hole punching. + addrs = append(addrs, h.ids.OwnObservedAddrs()...) + addrs = ma.Unique(addrs) + return slices.DeleteFunc(addrs, func(a ma.Multiaddr) bool { return !manet.IsPublicAddr(a) }) + }, opts.HolePunchingOptions...) if err != nil { return nil, fmt.Errorf("failed to create hole punch service: %w", err) } @@ -284,29 +290,11 @@ func NewHost(n network.Network, opts *HostOpts) (*BasicHost, error) { if opts.AddrsFactory != nil { h.AddrsFactory = opts.AddrsFactory } - // This is a terrible hack. - // We want to use this AddrsFactory for autonat. Wrapping AddrsFactory here ensures - // that autonat receives addresses with the correct certhashes. - // - // This logic cannot be in Addrs method as autonat cannot use the Addrs method directly. - // The autorelay package updates AddrsFactory to only provide p2p-circuit addresses when - // reachability is Private. - // - // Wrapping it here allows us to provide the wrapped AddrsFactory to autonat before - // autorelay updates it. - addrFactory := h.AddrsFactory - h.AddrsFactory = func(addrs []ma.Multiaddr) []ma.Multiaddr { - return h.addCertHashes(addrFactory(addrs)) - } if opts.NATManager != nil { h.natmgr = opts.NATManager(n) } - if opts.MultiaddrResolver != nil { - h.maResolver = opts.MultiaddrResolver - } - if opts.ConnManager == nil { h.cmgr = &connmgr.NullConnMgr{} } else { @@ -367,18 +355,32 @@ func (h *BasicHost) updateLocalIpAddr() { if r, err := netroute.New(); err != nil { log.Debugw("failed to build Router for kernel's routing table", "error", err) } else { - if _, _, localIPv4, err := r.Route(net.IPv4zero); err != nil { + + var localIPv4 net.IP + var ran bool + err, ran = h.updateLocalIPv4Backoff.Run(func() error { + _, _, localIPv4, err = r.Route(net.IPv4zero) + return err + }) + + if ran && err != nil { log.Debugw("failed to fetch local IPv4 address", "error", err) - } else if localIPv4.IsGlobalUnicast() { + } else if ran && localIPv4.IsGlobalUnicast() { maddr, err := manet.FromIP(localIPv4) if err == nil { h.filteredInterfaceAddrs = append(h.filteredInterfaceAddrs, maddr) } } - if _, _, localIPv6, err := r.Route(net.IPv6unspecified); err != nil { + var localIPv6 net.IP + err, ran = h.updateLocalIPv6Backoff.Run(func() error { + _, _, localIPv6, err = r.Route(net.IPv6unspecified) + return err + }) + + if ran && err != nil { log.Debugw("failed to fetch local IPv6 address", "error", err) - } else if localIPv6.IsGlobalUnicast() { + } else if ran && localIPv6.IsGlobalUnicast() { maddr, err := manet.FromIP(localIPv6) if err == nil { h.filteredInterfaceAddrs = append(h.filteredInterfaceAddrs, maddr) @@ -500,6 +502,7 @@ func (h *BasicHost) makeUpdatedAddrEvent(prev, current []ma.Multiaddr) *event.Ev return nil } prevmap := make(map[string]ma.Multiaddr, len(prev)) + currmap := make(map[string]ma.Multiaddr, len(current)) evt := &event.EvtLocalAddressesUpdated{Diffs: true} addrsAdded := false @@ -507,6 +510,9 @@ func (h *BasicHost) makeUpdatedAddrEvent(prev, current []ma.Multiaddr) *event.Ev prevmap[string(addr.Bytes())] = addr } for _, addr := range current { + currmap[string(addr.Bytes())] = addr + } + for _, addr := range currmap { _, ok := prevmap[string(addr.Bytes())] updated := event.UpdatedAddress{Address: addr} if ok { @@ -684,6 +690,14 @@ func (h *BasicHost) RemoveStreamHandler(pid protocol.ID) { // to create one. If ProtocolID is "", writes no header. // (Thread-safe) func (h *BasicHost) NewStream(ctx context.Context, p peer.ID, pids ...protocol.ID) (str network.Stream, strErr error) { + if _, ok := ctx.Deadline(); !ok { + if h.negtimeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, h.negtimeout) + defer cancel() + } + } + // If the caller wants to prevent the host from dialing, it should use the NoDial option. if nodial, _ := network.GetNoDial(ctx); !nodial { err := h.Connect(ctx, peer.AddrInfo{ID: p}) @@ -822,16 +836,15 @@ func (h *BasicHost) ConnManager() connmgr.ConnManager { return h.cmgr } -// Addrs returns listening addresses that are safe to announce to the network. -// The output is the same as AllAddrs, but processed by AddrsFactory. +// Addrs returns listening addresses. The output is the same as AllAddrs, but +// processed by AddrsFactory. +// When used with AutoRelay, and if the host is not publicly reachable, +// this will only have host's private, relay, and no public addresses. func (h *BasicHost) Addrs() []ma.Multiaddr { - // We don't need to append certhashes here, the user provided addrsFactory was - // wrapped with addCertHashes in the constructor. - addrs := h.AddrsFactory(h.AllAddrs()) // Make a copy. Consumers can modify the slice elements - res := make([]ma.Multiaddr, len(addrs)) - copy(res, addrs) - return res + addrs := slices.Clone(h.AddrsFactory(h.AllAddrs())) + // Add certhashes for the addresses provided by the user via address factory. + return h.addCertHashes(ma.Unique(addrs)) } // NormalizeMultiaddr returns a multiaddr suitable for equality checks. @@ -851,9 +864,9 @@ func (h *BasicHost) NormalizeMultiaddr(addr ma.Multiaddr) ma.Multiaddr { return addr } +var p2pCircuitAddr = ma.StringCast("/p2p-circuit") + // AllAddrs returns all the addresses the host is listening on except circuit addresses. -// The output has webtransport addresses inferred from quic addresses. -// All the addresses have the correct func (h *BasicHost) AllAddrs() []ma.Multiaddr { listenAddrs := h.Network().ListenAddresses() if len(listenAddrs) == 0 { @@ -867,7 +880,7 @@ func (h *BasicHost) AllAddrs() []ma.Multiaddr { // Iterate over all _unresolved_ listen addresses, resolving our primary // interface only to avoid advertising too many addresses. - var finalAddrs []ma.Multiaddr + finalAddrs := make([]ma.Multiaddr, 0, 8) if resolved, err := manet.ResolveUnspecifiedAddresses(listenAddrs, filteredIfaceAddrs); err != nil { // This can happen if we're listening on no addrs, or listening // on IPv6 addrs, but only have IPv4 interface addrs. @@ -946,6 +959,16 @@ func (h *BasicHost) AllAddrs() []ma.Multiaddr { finalAddrs = append(finalAddrs, observedAddrs...) } finalAddrs = ma.Unique(finalAddrs) + // Remove /p2p-circuit addresses from the list. + // The p2p-circuit tranport listener reports its address as just /p2p-circuit + // This is useless for dialing. Users need to manage their circuit addresses themselves, + // or use AutoRelay. + finalAddrs = slices.DeleteFunc(finalAddrs, func(a ma.Multiaddr) bool { + return a.Equal(p2pCircuitAddr) + }) + // Add certhashes for /webrtc-direct, /webtransport, etc addresses discovered + // using identify. + finalAddrs = h.addCertHashes(finalAddrs) return finalAddrs } diff --git a/p2p/host/basic/basic_host_test.go b/p2p/host/basic/basic_host_test.go index 9014e1b4df..2a7a772976 100644 --- a/p2p/host/basic/basic_host_test.go +++ b/p2p/host/basic/basic_host_test.go @@ -2,6 +2,7 @@ package basichost import ( "context" + "encoding/binary" "fmt" "io" "reflect" @@ -10,6 +11,7 @@ import ( "testing" "time" + "github.com/libp2p/go-libp2p-testing/race" "github.com/libp2p/go-libp2p/core/event" "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/network" @@ -248,6 +250,63 @@ func TestAllAddrs(t *testing.T) { require.True(t, ma.Contains(h.AllAddrs(), firstAddr), "should still contain the original addr") } +func TestAllAddrsUnique(t *testing.T) { + if race.WithRace() { + t.Skip("updates addrChangeTickrInterval which might be racy") + } + oldInterval := addrChangeTickrInterval + addrChangeTickrInterval = 100 * time.Millisecond + defer func() { + addrChangeTickrInterval = oldInterval + }() + sendNewAddrs := make(chan struct{}) + opts := HostOpts{ + AddrsFactory: func(addrs []ma.Multiaddr) []ma.Multiaddr { + select { + case <-sendNewAddrs: + return []ma.Multiaddr{ + ma.StringCast("/ip4/1.2.3.4/tcp/1"), + ma.StringCast("/ip4/1.2.3.4/tcp/1"), + ma.StringCast("/ip4/1.2.3.4/tcp/1"), + ma.StringCast("/ip4/1.2.3.4/udp/1/quic-v1"), + ma.StringCast("/ip4/1.2.3.4/udp/1/quic-v1"), + } + default: + return nil + } + }, + } + // no listen addrs + h, err := NewHost(swarmt.GenSwarm(t, swarmt.OptDialOnly), &opts) + require.NoError(t, err) + defer h.Close() + h.Start() + + sub, err := h.EventBus().Subscribe(&event.EvtLocalAddressesUpdated{}) + require.NoError(t, err) + out := make(chan int) + done := make(chan struct{}) + go func() { + cnt := 0 + for { + select { + case <-sub.Out(): + cnt++ + case <-done: + out <- cnt + return + } + } + }() + close(sendNewAddrs) + require.Len(t, h.Addrs(), 2) + require.ElementsMatch(t, []ma.Multiaddr{ma.StringCast("/ip4/1.2.3.4/tcp/1"), ma.StringCast("/ip4/1.2.3.4/udp/1/quic-v1")}, h.Addrs()) + time.Sleep(2*addrChangeTickrInterval + 1*time.Second) // the background loop runs every 5 seconds. Wait for 2x that time. + close(done) + cnt := <-out + require.Equal(t, 1, cnt) +} + // getHostPair gets a new pair of hosts. // The first host initiates the connection to the second host. func getHostPair(t *testing.T) (host.Host, host.Host) { @@ -883,3 +942,56 @@ func TestTrimHostAddrList(t *testing.T) { }) } } + +func TestHostTimeoutNewStream(t *testing.T) { + h1, err := NewHost(swarmt.GenSwarm(t), nil) + require.NoError(t, err) + h1.Start() + defer h1.Close() + + const proto = "/testing" + h2 := swarmt.GenSwarm(t) + + h2.SetStreamHandler(func(s network.Stream) { + // First message is multistream header. Just echo it + msHeader := []byte("\x19/multistream/1.0.0\n") + _, err := s.Read(msHeader) + assert.NoError(t, err) + _, err = s.Write(msHeader) + assert.NoError(t, err) + + buf := make([]byte, 1024) + n, err := s.Read(buf) + assert.NoError(t, err) + + msgLen, varintN := binary.Uvarint(buf[:n]) + buf = buf[varintN:] + proto := buf[:int(msgLen)] + if string(proto) == "/ipfs/id/1.0.0\n" { + // Signal we don't support identify + na := []byte("na\n") + n := binary.PutUvarint(buf, uint64(len(na))) + copy(buf[n:], na) + + _, err = s.Write(buf[:int(n)+len(na)]) + assert.NoError(t, err) + } else { + // Stall + time.Sleep(5 * time.Second) + } + t.Log("Resetting") + s.Reset() + }) + + err = h1.Connect(context.Background(), peer.AddrInfo{ + ID: h2.LocalPeer(), + Addrs: h2.ListenAddresses(), + }) + require.NoError(t, err) + + // No context passed in, fallback to negtimeout + h1.negtimeout = time.Second + _, err = h1.NewStream(context.Background(), h2.LocalPeer(), proto) + require.Error(t, err) + require.ErrorContains(t, err, "context deadline exceeded") +} diff --git a/p2p/host/basic/internal/backoff/backoff.go b/p2p/host/basic/internal/backoff/backoff.go new file mode 100644 index 0000000000..3d1fd23778 --- /dev/null +++ b/p2p/host/basic/internal/backoff/backoff.go @@ -0,0 +1,52 @@ +package backoff + +import ( + "time" +) + +var since = time.Since + +const defaultDelay = 100 * time.Millisecond +const defaultMaxDelay = 1 * time.Minute + +type ExpBackoff struct { + Delay time.Duration + MaxDelay time.Duration + + failures int + lastRun time.Time +} + +func (b *ExpBackoff) init() { + if b.Delay == 0 { + b.Delay = defaultDelay + } + if b.MaxDelay == 0 { + b.MaxDelay = defaultMaxDelay + } +} + +func (b *ExpBackoff) calcDelay() time.Duration { + delay := b.Delay * time.Duration(1<<(b.failures-1)) + delay = min(delay, b.MaxDelay) + return delay +} + +func (b *ExpBackoff) Run(f func() error) (err error, ran bool) { + b.init() + + if b.failures != 0 { + if since(b.lastRun) < b.calcDelay() { + return nil, false + } + } + + b.lastRun = time.Now() + err = f() + if err == nil { + b.failures = 0 + } else { + b.failures++ + } + return err, true +} diff --git a/p2p/host/basic/internal/backoff/backoff_test.go b/p2p/host/basic/internal/backoff/backoff_test.go new file mode 100644 index 0000000000..6396b4e649 --- /dev/null +++ b/p2p/host/basic/internal/backoff/backoff_test.go @@ -0,0 +1,59 @@ +package backoff + +import ( + "errors" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestBackoff(t *testing.T) { + origSince := since + defer func() { since = origSince }() + + var timeSince time.Duration + since = func(time.Time) time.Duration { + return timeSince + } + + var maybeErr error + b := &ExpBackoff{} + f := func() error { return maybeErr } + + err, ran := b.Run(f) + require.True(t, ran) + require.NoError(t, err) + + maybeErr = errors.New("some error") + err, ran = b.Run(f) + require.True(t, ran) + require.Error(t, err) + + // Rerun again + _, ran = b.Run(f) + require.False(t, ran) + + timeSince = 100*time.Millisecond + 1 + err, ran = b.Run(f) + require.True(t, ran) + require.Error(t, err) + + timeSince = 100*time.Millisecond + 1 + _, ran = b.Run(f) + require.False(t, ran) + + timeSince = 200*time.Millisecond + 1 + err, ran = b.Run(f) + require.True(t, ran) + require.Error(t, err) + + for timeSince < defaultMaxDelay*4 { + timeSince *= 2 + err, ran = b.Run(f) + require.True(t, ran) + require.Error(t, err) + } + + require.Equal(t, defaultMaxDelay, b.calcDelay()) +} diff --git a/p2p/host/basic/mock_nat_test.go b/p2p/host/basic/mock_nat_test.go index e601e7109d..924e52c566 100644 --- a/p2p/host/basic/mock_nat_test.go +++ b/p2p/host/basic/mock_nat_test.go @@ -21,6 +21,7 @@ import ( type MockNAT struct { ctrl *gomock.Controller recorder *MockNATMockRecorder + isgomock struct{} } // MockNATMockRecorder is the mock recorder for MockNAT. @@ -41,17 +42,17 @@ func (m *MockNAT) EXPECT() *MockNATMockRecorder { } // AddMapping mocks base method. -func (m *MockNAT) AddMapping(arg0 context.Context, arg1 string, arg2 int) error { +func (m *MockNAT) AddMapping(ctx context.Context, protocol string, port int) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddMapping", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "AddMapping", ctx, protocol, port) ret0, _ := ret[0].(error) return ret0 } // AddMapping indicates an expected call of AddMapping. -func (mr *MockNATMockRecorder) AddMapping(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockNATMockRecorder) AddMapping(ctx, protocol, port any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddMapping", reflect.TypeOf((*MockNAT)(nil).AddMapping), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddMapping", reflect.TypeOf((*MockNAT)(nil).AddMapping), ctx, protocol, port) } // Close mocks base method. @@ -69,30 +70,30 @@ func (mr *MockNATMockRecorder) Close() *gomock.Call { } // GetMapping mocks base method. -func (m *MockNAT) GetMapping(arg0 string, arg1 int) (netip.AddrPort, bool) { +func (m *MockNAT) GetMapping(protocol string, port int) (netip.AddrPort, bool) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetMapping", arg0, arg1) + ret := m.ctrl.Call(m, "GetMapping", protocol, port) ret0, _ := ret[0].(netip.AddrPort) ret1, _ := ret[1].(bool) return ret0, ret1 } // GetMapping indicates an expected call of GetMapping. -func (mr *MockNATMockRecorder) GetMapping(arg0, arg1 any) *gomock.Call { +func (mr *MockNATMockRecorder) GetMapping(protocol, port any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMapping", reflect.TypeOf((*MockNAT)(nil).GetMapping), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMapping", reflect.TypeOf((*MockNAT)(nil).GetMapping), protocol, port) } // RemoveMapping mocks base method. -func (m *MockNAT) RemoveMapping(arg0 context.Context, arg1 string, arg2 int) error { +func (m *MockNAT) RemoveMapping(ctx context.Context, protocol string, port int) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RemoveMapping", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "RemoveMapping", ctx, protocol, port) ret0, _ := ret[0].(error) return ret0 } // RemoveMapping indicates an expected call of RemoveMapping. -func (mr *MockNATMockRecorder) RemoveMapping(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockNATMockRecorder) RemoveMapping(ctx, protocol, port any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveMapping", reflect.TypeOf((*MockNAT)(nil).RemoveMapping), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveMapping", reflect.TypeOf((*MockNAT)(nil).RemoveMapping), ctx, protocol, port) } diff --git a/p2p/host/eventbus/basic.go b/p2p/host/eventbus/basic.go index 42365a7916..af6a74bd02 100644 --- a/p2p/host/eventbus/basic.go +++ b/p2p/host/eventbus/basic.go @@ -6,10 +6,20 @@ import ( "reflect" "sync" "sync/atomic" + "time" + logging "github.com/ipfs/go-log/v2" "github.com/libp2p/go-libp2p/core/event" ) +type logInterface interface { + Errorf(string, ...interface{}) +} + +var log logInterface = logging.Logger("eventbus") + +const slowConsumerWarningTimeout = time.Second + // ///////////////////// // BUS @@ -116,6 +126,7 @@ type wildcardSub struct { w *wildcardNode metricsTracer MetricsTracer name string + closeOnce sync.Once } func (w *wildcardSub) Out() <-chan interface{} { @@ -123,10 +134,13 @@ func (w *wildcardSub) Out() <-chan interface{} { } func (w *wildcardSub) Close() error { - w.w.removeSink(w.ch) - if w.metricsTracer != nil { - w.metricsTracer.RemoveSubscriber(reflect.TypeOf(event.WildcardSubscription)) - } + w.closeOnce.Do(func() { + w.w.removeSink(w.ch) + if w.metricsTracer != nil { + w.metricsTracer.RemoveSubscriber(reflect.TypeOf(event.WildcardSubscription)) + } + }) + return nil } @@ -145,6 +159,7 @@ type sub struct { dropper func(reflect.Type) metricsTracer MetricsTracer name string + closeOnce sync.Once } func (s *sub) Name() string { @@ -162,31 +177,32 @@ func (s *sub) Close() error { for range s.ch { } }() - - for _, n := range s.nodes { - n.lk.Lock() - - for i := 0; i < len(n.sinks); i++ { - if n.sinks[i].ch == s.ch { - n.sinks[i], n.sinks[len(n.sinks)-1] = n.sinks[len(n.sinks)-1], nil - n.sinks = n.sinks[:len(n.sinks)-1] - - if s.metricsTracer != nil { - s.metricsTracer.RemoveSubscriber(n.typ) + s.closeOnce.Do(func() { + for _, n := range s.nodes { + n.lk.Lock() + + for i := 0; i < len(n.sinks); i++ { + if n.sinks[i].ch == s.ch { + n.sinks[i], n.sinks[len(n.sinks)-1] = n.sinks[len(n.sinks)-1], nil + n.sinks = n.sinks[:len(n.sinks)-1] + + if s.metricsTracer != nil { + s.metricsTracer.RemoveSubscriber(n.typ) + } + break } - break } - } - tryDrop := len(n.sinks) == 0 && n.nEmitters.Load() == 0 + tryDrop := len(n.sinks) == 0 && n.nEmitters.Load() == 0 - n.lk.Unlock() + n.lk.Unlock() - if tryDrop { - s.dropper(n.typ) + if tryDrop { + s.dropper(n.typ) + } } - } - close(s.ch) + close(s.ch) + }) return nil } @@ -322,6 +338,8 @@ type wildcardNode struct { nSinks atomic.Int32 sinks []*namedSink metricsTracer MetricsTracer + + slowConsumerTimer *time.Timer } func (n *wildcardNode) addSink(sink *namedSink) { @@ -336,6 +354,12 @@ func (n *wildcardNode) addSink(sink *namedSink) { } func (n *wildcardNode) removeSink(ch chan interface{}) { + go func() { + // drain the event channel, will return when closed and drained. + // this is necessary to unblock publishes to this channel. + for range ch { + } + }() n.nSinks.Add(-1) // ok to do outside the lock n.Lock() for i := 0; i < len(n.sinks); i++ { @@ -348,6 +372,8 @@ func (n *wildcardNode) removeSink(ch chan interface{}) { n.Unlock() } +var wildcardType = reflect.TypeOf(event.WildcardSubscription) + func (n *wildcardNode) emit(evt interface{}) { if n.nSinks.Load() == 0 { return @@ -360,7 +386,16 @@ func (n *wildcardNode) emit(evt interface{}) { // record channel full events before blocking sendSubscriberMetrics(n.metricsTracer, sink) - sink.ch <- evt + select { + case sink.ch <- evt: + default: + slowConsumerTimer := emitAndLogError(n.slowConsumerTimer, wildcardType, evt, sink) + defer func() { + n.Lock() + n.slowConsumerTimer = slowConsumerTimer + n.Unlock() + }() + } } n.RUnlock() } @@ -379,6 +414,8 @@ type node struct { sinks []*namedSink metricsTracer MetricsTracer + + slowConsumerTimer *time.Timer } func newNode(typ reflect.Type, metricsTracer MetricsTracer) *node { @@ -404,11 +441,37 @@ func (n *node) emit(evt interface{}) { // Sending metrics before sending on channel allows us to // record channel full events before blocking sendSubscriberMetrics(n.metricsTracer, sink) - sink.ch <- evt + select { + case sink.ch <- evt: + default: + n.slowConsumerTimer = emitAndLogError(n.slowConsumerTimer, n.typ, evt, sink) + } } n.lk.Unlock() } +func emitAndLogError(timer *time.Timer, typ reflect.Type, evt interface{}, sink *namedSink) *time.Timer { + // Slow consumer. Log a warning if stalled for the timeout + if timer == nil { + timer = time.NewTimer(slowConsumerWarningTimeout) + } else { + timer.Reset(slowConsumerWarningTimeout) + } + + select { + case sink.ch <- evt: + if !timer.Stop() { + <-timer.C + } + case <-timer.C: + log.Errorf("subscriber named \"%s\" is a slow consumer of %s. This can lead to libp2p stalling and hard to debug issues.", sink.name, typ) + // Continue to stall since there's nothing else we can do. + sink.ch <- evt + } + + return timer +} + func sendSubscriberMetrics(metricsTracer MetricsTracer, sink *namedSink) { if metricsTracer != nil { metricsTracer.SubscriberQueueLength(sink.name, len(sink.ch)+1) diff --git a/p2p/host/eventbus/basic_test.go b/p2p/host/eventbus/basic_test.go index 57362ce9b7..530a46ff19 100644 --- a/p2p/host/eventbus/basic_test.go +++ b/p2p/host/eventbus/basic_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "reflect" + "strings" "sync" "sync/atomic" "testing" @@ -13,6 +14,7 @@ import ( "github.com/libp2p/go-libp2p-testing/race" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -131,6 +133,85 @@ func TestEmitNoSubNoBlock(t *testing.T) { em.Emit(EventA{}) } +type mockLogger struct { + mu sync.Mutex + logs []string +} + +func (m *mockLogger) Errorf(format string, args ...interface{}) { + m.mu.Lock() + defer m.mu.Unlock() + m.logs = append(m.logs, fmt.Sprintf(format, args...)) +} + +func (m *mockLogger) Logs() []string { + m.mu.Lock() + defer m.mu.Unlock() + return m.logs +} + +func (m *mockLogger) Clear() { + m.mu.Lock() + defer m.mu.Unlock() + m.logs = nil +} + +func TestEmitLogsErrorOnStall(t *testing.T) { + oldLogger := log + defer func() { + log = oldLogger + }() + ml := &mockLogger{} + log = ml + + bus1 := NewBus() + bus2 := NewBus() + + eventSub, err := bus1.Subscribe(new(EventA)) + if err != nil { + t.Fatal(err) + } + + wildcardSub, err := bus2.Subscribe(event.WildcardSubscription) + if err != nil { + t.Fatal(err) + } + + testCases := []event.Subscription{eventSub, wildcardSub} + eventBuses := []event.Bus{bus1, bus2} + + for i, sub := range testCases { + bus := eventBuses[i] + em, err := bus.Emitter(new(EventA)) + if err != nil { + t.Fatal(err) + } + defer em.Close() + + go func() { + for i := 0; i < subSettingsDefault.buffer+2; i++ { + em.Emit(EventA{}) + } + }() + + require.EventuallyWithT(t, func(collect *assert.CollectT) { + logs := ml.Logs() + found := false + for _, log := range logs { + if strings.Contains(log, "slow consumer") { + found = true + break + } + } + assert.True(collect, found, "expected to find slow consumer log") + }, 3*time.Second, 500*time.Millisecond) + ml.Clear() + + // Close the subscriber so the worker can finish. + sub.Close() + } +} + func TestEmitOnClosed(t *testing.T) { bus := NewBus() @@ -313,10 +394,13 @@ func TestManyWildcardSubscriptions(t *testing.T) { require.NoError(t, em1.Emit(EventA{})) require.NoError(t, em2.Emit(EventB(1))) - // the first five still have 2 events, while the other five have 4 events. - for _, s := range subs[:5] { - require.Len(t, s.Out(), 2) - } + // the first five 0 events because it was closed. The other five + // have 4 events. + require.EventuallyWithT(t, func(t *assert.CollectT) { + for _, s := range subs[:5] { + require.Len(t, s.Out(), 0, "expected closed subscription to have flushed events") + } + }, 2*time.Second, 100*time.Millisecond) for _, s := range subs[5:] { require.Len(t, s.Out(), 4) @@ -326,6 +410,10 @@ func TestManyWildcardSubscriptions(t *testing.T) { for _, s := range subs { require.NoError(t, s.Close()) } + + for _, s := range subs { + require.Zero(t, s.(*wildcardSub).w.nSinks.Load()) + } } func TestWildcardValidations(t *testing.T) { @@ -481,6 +569,17 @@ func TestSubFailFully(t *testing.T) { } } +func TestSubCloseMultiple(t *testing.T) { + bus := NewBus() + + sub, err := bus.Subscribe([]interface{}{new(EventB)}) + require.NoError(t, err) + err = sub.Close() + require.NoError(t, err) + err = sub.Close() + require.NoError(t, err) +} + func testMany(t testing.TB, subs, emits, msgs int, stateful bool) { if race.WithRace() && subs+emits > 5000 { t.SkipNow() diff --git a/p2p/host/peerstore/pstoreds/pb/pstore.pb.go b/p2p/host/peerstore/pstoreds/pb/pstore.pb.go index 5ac20406cc..5130bb3865 100644 --- a/p2p/host/peerstore/pstoreds/pb/pstore.pb.go +++ b/p2p/host/peerstore/pstoreds/pb/pstore.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.34.2 -// protoc v5.27.3 +// protoc-gen-go v1.35.1 +// protoc v5.28.2 // source: p2p/host/peerstore/pstoreds/pb/pstore.proto package pb @@ -36,11 +36,9 @@ type AddrBookRecord struct { func (x *AddrBookRecord) Reset() { *x = AddrBookRecord{} - if protoimpl.UnsafeEnabled { - mi := &file_p2p_host_peerstore_pstoreds_pb_pstore_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_p2p_host_peerstore_pstoreds_pb_pstore_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *AddrBookRecord) String() string { @@ -51,7 +49,7 @@ func (*AddrBookRecord) ProtoMessage() {} func (x *AddrBookRecord) ProtoReflect() protoreflect.Message { mi := &file_p2p_host_peerstore_pstoreds_pb_pstore_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -102,11 +100,9 @@ type AddrBookRecord_AddrEntry struct { func (x *AddrBookRecord_AddrEntry) Reset() { *x = AddrBookRecord_AddrEntry{} - if protoimpl.UnsafeEnabled { - mi := &file_p2p_host_peerstore_pstoreds_pb_pstore_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_p2p_host_peerstore_pstoreds_pb_pstore_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *AddrBookRecord_AddrEntry) String() string { @@ -117,7 +113,7 @@ func (*AddrBookRecord_AddrEntry) ProtoMessage() {} func (x *AddrBookRecord_AddrEntry) ProtoReflect() protoreflect.Message { mi := &file_p2p_host_peerstore_pstoreds_pb_pstore_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -168,11 +164,9 @@ type AddrBookRecord_CertifiedRecord struct { func (x *AddrBookRecord_CertifiedRecord) Reset() { *x = AddrBookRecord_CertifiedRecord{} - if protoimpl.UnsafeEnabled { - mi := &file_p2p_host_peerstore_pstoreds_pb_pstore_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_p2p_host_peerstore_pstoreds_pb_pstore_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *AddrBookRecord_CertifiedRecord) String() string { @@ -183,7 +177,7 @@ func (*AddrBookRecord_CertifiedRecord) ProtoMessage() {} func (x *AddrBookRecord_CertifiedRecord) ProtoReflect() protoreflect.Message { mi := &file_p2p_host_peerstore_pstoreds_pb_pstore_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -278,44 +272,6 @@ func file_p2p_host_peerstore_pstoreds_pb_pstore_proto_init() { if File_p2p_host_peerstore_pstoreds_pb_pstore_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_p2p_host_peerstore_pstoreds_pb_pstore_proto_msgTypes[0].Exporter = func(v any, i int) any { - switch v := v.(*AddrBookRecord); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_p2p_host_peerstore_pstoreds_pb_pstore_proto_msgTypes[1].Exporter = func(v any, i int) any { - switch v := v.(*AddrBookRecord_AddrEntry); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_p2p_host_peerstore_pstoreds_pb_pstore_proto_msgTypes[2].Exporter = func(v any, i int) any { - switch v := v.(*AddrBookRecord_CertifiedRecord); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ diff --git a/p2p/host/peerstore/pstoremem/addr_book.go b/p2p/host/peerstore/pstoremem/addr_book.go index 89b87bdb47..2520f6ef22 100644 --- a/p2p/host/peerstore/pstoremem/addr_book.go +++ b/p2p/host/peerstore/pstoremem/addr_book.go @@ -3,13 +3,14 @@ package pstoremem import ( "container/heap" "context" + "errors" "fmt" "sort" "sync" "time" "github.com/libp2p/go-libp2p/core/peer" - pstore "github.com/libp2p/go-libp2p/core/peerstore" + "github.com/libp2p/go-libp2p/core/peerstore" "github.com/libp2p/go-libp2p/core/record" logging "github.com/ipfs/go-log/v2" @@ -23,7 +24,7 @@ type expiringAddr struct { TTL time.Duration Expiry time.Time Peer peer.ID - // to sort by expiry time + // to sort by expiry time, -1 means it's not in the heap heapIndex int } @@ -31,6 +32,32 @@ func (e *expiringAddr) ExpiredBy(t time.Time) bool { return !t.Before(e.Expiry) } +func (e *expiringAddr) IsConnected() bool { + return ttlIsConnected(e.TTL) +} + +// ttlIsConnected returns true if the TTL is at least as long as the connected +// TTL. +func ttlIsConnected(ttl time.Duration) bool { + return ttl >= peerstore.ConnectedAddrTTL +} + +var expiringAddrPool = sync.Pool{New: func() any { return &expiringAddr{} }} + +func getExpiringAddrs() *expiringAddr { + a := expiringAddrPool.Get().(*expiringAddr) + a.heapIndex = -1 + return a +} + +func putExpiringAddrs(ea *expiringAddr) { + if ea == nil { + return + } + *ea = expiringAddr{} + expiringAddrPool.Put(ea) +} + type peerRecordState struct { Envelope *record.Envelope Seq uint64 @@ -40,7 +67,9 @@ type peerRecordState struct { var _ heap.Interface = &peerAddrs{} type peerAddrs struct { - Addrs map[peer.ID]map[string]*expiringAddr // peer.ID -> addr.Bytes() -> *expiringAddr + Addrs map[peer.ID]map[string]*expiringAddr // peer.ID -> addr.Bytes() -> *expiringAddr + // expiringHeap only stores non-connected addresses. Since connected address + // basically have an infinite TTL expiringHeap []*expiringAddr } @@ -61,10 +90,6 @@ func (pa *peerAddrs) Swap(i, j int) { } func (pa *peerAddrs) Push(x any) { a := x.(*expiringAddr) - if _, ok := pa.Addrs[a.Peer]; !ok { - pa.Addrs[a.Peer] = make(map[string]*expiringAddr) - } - pa.Addrs[a.Peer][string(a.Addr.Bytes())] = a a.heapIndex = len(pa.expiringHeap) pa.expiringHeap = append(pa.expiringHeap, a) } @@ -72,34 +97,24 @@ func (pa *peerAddrs) Pop() any { a := pa.expiringHeap[len(pa.expiringHeap)-1] a.heapIndex = -1 pa.expiringHeap = pa.expiringHeap[0 : len(pa.expiringHeap)-1] - - if m, ok := pa.Addrs[a.Peer]; ok { - delete(m, string(a.Addr.Bytes())) - if len(m) == 0 { - delete(pa.Addrs, a.Peer) - } - } return a } -func (pa *peerAddrs) Fix(a *expiringAddr) { - heap.Fix(pa, a.heapIndex) -} - func (pa *peerAddrs) Delete(a *expiringAddr) { - heap.Remove(pa, a.heapIndex) - a.heapIndex = -1 - if m, ok := pa.Addrs[a.Peer]; ok { - delete(m, string(a.Addr.Bytes())) - if len(m) == 0 { + if ea, ok := pa.Addrs[a.Peer][string(a.Addr.Bytes())]; ok { + if ea.heapIndex != -1 { + heap.Remove(pa, a.heapIndex) + } + delete(pa.Addrs[a.Peer], string(a.Addr.Bytes())) + if len(pa.Addrs[a.Peer]) == 0 { delete(pa.Addrs, a.Peer) } } } -func (pa *peerAddrs) FindAddr(p peer.ID, addrBytes ma.Multiaddr) (*expiringAddr, bool) { +func (pa *peerAddrs) FindAddr(p peer.ID, addr ma.Multiaddr) (*expiringAddr, bool) { if m, ok := pa.Addrs[p]; ok { - v, ok := m[string(addrBytes.Bytes())] + v, ok := m[string(addr.Bytes())] return v, ok } return nil, false @@ -115,12 +130,44 @@ func (pa *peerAddrs) NextExpiry() time.Time { func (pa *peerAddrs) PopIfExpired(now time.Time) (*expiringAddr, bool) { // Use `!Before` instead of `After` to ensure that we expire *at* now, and not *just after now*. if len(pa.expiringHeap) > 0 && !now.Before(pa.NextExpiry()) { - a := heap.Pop(pa) - return a.(*expiringAddr), true + ea := heap.Pop(pa).(*expiringAddr) + delete(pa.Addrs[ea.Peer], string(ea.Addr.Bytes())) + if len(pa.Addrs[ea.Peer]) == 0 { + delete(pa.Addrs, ea.Peer) + } + return ea, true } return nil, false } +func (pa *peerAddrs) Update(a *expiringAddr) { + if a.heapIndex == -1 { + return + } + if a.IsConnected() { + heap.Remove(pa, a.heapIndex) + } else { + heap.Fix(pa, a.heapIndex) + } +} + +func (pa *peerAddrs) Insert(a *expiringAddr) { + a.heapIndex = -1 + if _, ok := pa.Addrs[a.Peer]; !ok { + pa.Addrs[a.Peer] = make(map[string]*expiringAddr) + } + pa.Addrs[a.Peer][string(a.Addr.Bytes())] = a + // don't add connected addr to heap. + if a.IsConnected() { + return + } + heap.Push(pa, a) +} + +func (pa *peerAddrs) NumUnconnectedAddrs() int { + return len(pa.expiringHeap) +} + type clock interface { Now() time.Time } @@ -131,12 +178,18 @@ func (rc realclock) Now() time.Time { return time.Now() } +const ( + defaultMaxSignedPeerRecords = 100_000 + defaultMaxUnconnectedAddrs = 1_000_000 +) + // memoryAddrBook manages addresses. type memoryAddrBook struct { - mu sync.RWMutex - // TODO bound the number of not connected addresses we store. - addrs peerAddrs - signedPeerRecords map[peer.ID]*peerRecordState + mu sync.RWMutex + addrs peerAddrs + signedPeerRecords map[peer.ID]*peerRecordState + maxUnconnectedAddrs int + maxSignedPeerRecords int refCount sync.WaitGroup cancel func() @@ -145,19 +198,25 @@ type memoryAddrBook struct { clock clock } -var _ pstore.AddrBook = (*memoryAddrBook)(nil) -var _ pstore.CertifiedAddrBook = (*memoryAddrBook)(nil) +var _ peerstore.AddrBook = (*memoryAddrBook)(nil) +var _ peerstore.CertifiedAddrBook = (*memoryAddrBook)(nil) -func NewAddrBook() *memoryAddrBook { +func NewAddrBook(opts ...AddrBookOption) *memoryAddrBook { ctx, cancel := context.WithCancel(context.Background()) ab := &memoryAddrBook{ - addrs: newPeerAddrs(), - signedPeerRecords: make(map[peer.ID]*peerRecordState), - subManager: NewAddrSubManager(), - cancel: cancel, - clock: realclock{}, + addrs: newPeerAddrs(), + signedPeerRecords: make(map[peer.ID]*peerRecordState), + subManager: NewAddrSubManager(), + cancel: cancel, + clock: realclock{}, + maxUnconnectedAddrs: defaultMaxUnconnectedAddrs, + maxSignedPeerRecords: defaultMaxSignedPeerRecords, + } + for _, opt := range opts { + opt(ab) } + ab.refCount.Add(1) go ab.background(ctx) return ab @@ -172,6 +231,23 @@ func WithClock(clock clock) AddrBookOption { } } +// WithMaxAddresses sets the maximum number of unconnected addresses to store. +// The maximum number of connected addresses is bounded by the connection +// limits in the Connection Manager and Resource Manager. +func WithMaxAddresses(n int) AddrBookOption { + return func(b *memoryAddrBook) error { + b.maxUnconnectedAddrs = n + return nil + } +} + +func WithMaxSignedPeerRecords(n int) AddrBookOption { + return func(b *memoryAddrBook) error { + b.maxSignedPeerRecords = n + return nil + } +} + // background periodically schedules a gc func (mab *memoryAddrBook) background(ctx context.Context) { defer mab.refCount.Done() @@ -204,6 +280,7 @@ func (mab *memoryAddrBook) gc() { if !ok { return } + putExpiringAddrs(ea) mab.maybeDeleteSignedPeerRecordUnlocked(ea.Peer) } } @@ -252,6 +329,10 @@ func (mab *memoryAddrBook) ConsumePeerRecord(recordEnvelope *record.Envelope, tt if found && lastState.Seq > rec.Seq { return false, nil } + // check if we are over the max signed peer record limit + if !found && len(mab.signedPeerRecords) >= mab.maxSignedPeerRecords { + return false, errors.New("too many signed peer records") + } mab.signedPeerRecords[rec.PeerID] = &peerRecordState{ Envelope: recordEnvelope, Seq: rec.Seq, @@ -281,6 +362,11 @@ func (mab *memoryAddrBook) addAddrsUnlocked(p peer.ID, addrs []ma.Multiaddr, ttl return } + // we are over limit, drop these addrs. + if !ttlIsConnected(ttl) && mab.addrs.NumUnconnectedAddrs() >= mab.maxUnconnectedAddrs { + return + } + exp := mab.clock.Now().Add(ttl) for _, addr := range addrs { // Remove suffix of /p2p/peer-id from address @@ -296,8 +382,9 @@ func (mab *memoryAddrBook) addAddrsUnlocked(p peer.ID, addrs []ma.Multiaddr, ttl a, found := mab.addrs.FindAddr(p, addr) if !found { // not found, announce it. - entry := &expiringAddr{Addr: addr, Expiry: exp, TTL: ttl, Peer: p} - heap.Push(&mab.addrs, entry) + entry := getExpiringAddrs() + *entry = expiringAddr{Addr: addr, Expiry: exp, TTL: ttl, Peer: p} + mab.addrs.Insert(entry) mab.subManager.BroadcastAddr(p, addr) } else { // update ttl & exp to whichever is greater between new and existing entry @@ -311,7 +398,7 @@ func (mab *memoryAddrBook) addAddrsUnlocked(p peer.ID, addrs []ma.Multiaddr, ttl a.Expiry = exp } if changed { - mab.addrs.Fix(a) + mab.addrs.Update(a) } } } @@ -344,17 +431,28 @@ func (mab *memoryAddrBook) SetAddrs(p peer.ID, addrs []ma.Multiaddr, ttl time.Du if a, found := mab.addrs.FindAddr(p, addr); found { if ttl > 0 { - a.Addr = addr - a.Expiry = exp - a.TTL = ttl - mab.addrs.Fix(a) - mab.subManager.BroadcastAddr(p, addr) + if a.IsConnected() && !ttlIsConnected(ttl) && mab.addrs.NumUnconnectedAddrs() >= mab.maxUnconnectedAddrs { + mab.addrs.Delete(a) + putExpiringAddrs(a) + } else { + a.Addr = addr + a.Expiry = exp + a.TTL = ttl + mab.addrs.Update(a) + mab.subManager.BroadcastAddr(p, addr) + } } else { mab.addrs.Delete(a) + putExpiringAddrs(a) } } else { if ttl > 0 { - heap.Push(&mab.addrs, &expiringAddr{Addr: addr, Expiry: exp, TTL: ttl, Peer: p}) + if !ttlIsConnected(ttl) && mab.addrs.NumUnconnectedAddrs() >= mab.maxUnconnectedAddrs { + continue + } + entry := getExpiringAddrs() + *entry = expiringAddr{Addr: addr, Expiry: exp, TTL: ttl, Peer: p} + mab.addrs.Insert(entry) mab.subManager.BroadcastAddr(p, addr) } } @@ -374,10 +472,17 @@ func (mab *memoryAddrBook) UpdateAddrs(p peer.ID, oldTTL time.Duration, newTTL t if oldTTL == a.TTL { if newTTL == 0 { mab.addrs.Delete(a) + putExpiringAddrs(a) } else { - a.TTL = newTTL - a.Expiry = exp - mab.addrs.Fix(a) + // We are over limit, drop these addresses. + if ttlIsConnected(oldTTL) && !ttlIsConnected(newTTL) && mab.addrs.NumUnconnectedAddrs() >= mab.maxUnconnectedAddrs { + mab.addrs.Delete(a) + putExpiringAddrs(a) + } else { + a.TTL = newTTL + a.Expiry = exp + mab.addrs.Update(a) + } } } } @@ -436,6 +541,7 @@ func (mab *memoryAddrBook) ClearAddrs(p peer.ID) { delete(mab.signedPeerRecords, p) for _, a := range mab.addrs.Addrs[p] { mab.addrs.Delete(a) + putExpiringAddrs(a) } } diff --git a/p2p/host/peerstore/pstoremem/addr_book_test.go b/p2p/host/peerstore/pstoremem/addr_book_test.go index 963c4552cf..e8ba89ff93 100644 --- a/p2p/host/peerstore/pstoremem/addr_book_test.go +++ b/p2p/host/peerstore/pstoremem/addr_book_test.go @@ -22,8 +22,8 @@ func TestPeerAddrsNextExpiry(t *testing.T) { // t1 is before t2 t1 := time.Time{}.Add(1 * time.Second) t2 := time.Time{}.Add(2 * time.Second) - heap.Push(pa, &expiringAddr{Addr: a1, Expiry: t1, TTL: 10 * time.Second, Peer: "p1"}) - heap.Push(pa, &expiringAddr{Addr: a2, Expiry: t2, TTL: 10 * time.Second, Peer: "p2"}) + paa.Insert(&expiringAddr{Addr: a1, Expiry: t1, TTL: 10 * time.Second, Peer: "p1"}) + paa.Insert(&expiringAddr{Addr: a2, Expiry: t2, TTL: 10 * time.Second, Peer: "p2"}) if pa.NextExpiry() != t1 { t.Fatal("expiry should be set to t1, got", pa.NextExpiry()) @@ -49,7 +49,7 @@ func TestPeerAddrsHeapProperty(t *testing.T) { const N = 10000 expiringAddrs := peerAddrsInput(N) for i := 0; i < N; i++ { - heap.Push(pa, expiringAddrs[i]) + paa.Insert(expiringAddrs[i]) } for i := 0; i < N; i++ { @@ -70,7 +70,7 @@ func TestPeerAddrsHeapPropertyDeletions(t *testing.T) { const N = 10000 expiringAddrs := peerAddrsInput(N) for i := 0; i < N; i++ { - heap.Push(pa, expiringAddrs[i]) + paa.Insert(expiringAddrs[i]) } // delete every 3rd element @@ -108,7 +108,7 @@ func TestPeerAddrsHeapPropertyUpdates(t *testing.T) { var endElements []ma.Multiaddr for i := 0; i < N; i += 3 { expiringAddrs[i].Expiry = time.Time{}.Add(1000_000 * time.Second) - pa.Fix(expiringAddrs[i]) + pa.Update(expiringAddrs[i]) endElements = append(endElements, expiringAddrs[i].Addr) } @@ -148,7 +148,7 @@ func TestPeerAddrsExpiry(t *testing.T) { expiringAddrs[i].Expiry = time.Time{}.Add(time.Duration(1+rand.Intn(N)) * time.Second) } for i := 0; i < N; i++ { - heap.Push(pa, expiringAddrs[i]) + pa.Insert(expiringAddrs[i]) } expiry := time.Time{}.Add(time.Duration(1+rand.Intn(N)) * time.Second) @@ -174,6 +174,18 @@ func TestPeerAddrsExpiry(t *testing.T) { } } +func TestPeerLimits(t *testing.T) { + ab := NewAddrBook() + defer ab.Close() + ab.maxUnconnectedAddrs = 1024 + + peers := peerAddrsInput(2048) + for _, p := range peers { + ab.AddAddr(p.Peer, p.Addr, p.TTL) + } + require.Equal(t, 1024, ab.addrs.NumUnconnectedAddrs()) +} + func BenchmarkPeerAddrs(b *testing.B) { sizes := [...]int{1, 10, 100, 1000, 10_000, 100_000, 1000_000} for _, sz := range sizes { @@ -184,7 +196,7 @@ func BenchmarkPeerAddrs(b *testing.B) { pa := &paa expiringAddrs := peerAddrsInput(sz) for i := 0; i < sz; i++ { - heap.Push(pa, expiringAddrs[i]) + pa.Insert(expiringAddrs[i]) } b.StartTimer() for { diff --git a/p2p/host/pstoremanager/mock_peerstore_test.go b/p2p/host/pstoremanager/mock_peerstore_test.go index b704cb1fd1..2196dccbef 100644 --- a/p2p/host/pstoremanager/mock_peerstore_test.go +++ b/p2p/host/pstoremanager/mock_peerstore_test.go @@ -25,6 +25,7 @@ import ( type MockPeerstore struct { ctrl *gomock.Controller recorder *MockPeerstoreMockRecorder + isgomock struct{} } // MockPeerstoreMockRecorder is the mock recorder for MockPeerstore. @@ -45,27 +46,27 @@ func (m *MockPeerstore) EXPECT() *MockPeerstoreMockRecorder { } // AddAddr mocks base method. -func (m *MockPeerstore) AddAddr(arg0 peer.ID, arg1 multiaddr.Multiaddr, arg2 time.Duration) { +func (m *MockPeerstore) AddAddr(p peer.ID, addr multiaddr.Multiaddr, ttl time.Duration) { m.ctrl.T.Helper() - m.ctrl.Call(m, "AddAddr", arg0, arg1, arg2) + m.ctrl.Call(m, "AddAddr", p, addr, ttl) } // AddAddr indicates an expected call of AddAddr. -func (mr *MockPeerstoreMockRecorder) AddAddr(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockPeerstoreMockRecorder) AddAddr(p, addr, ttl any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddAddr", reflect.TypeOf((*MockPeerstore)(nil).AddAddr), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddAddr", reflect.TypeOf((*MockPeerstore)(nil).AddAddr), p, addr, ttl) } // AddAddrs mocks base method. -func (m *MockPeerstore) AddAddrs(arg0 peer.ID, arg1 []multiaddr.Multiaddr, arg2 time.Duration) { +func (m *MockPeerstore) AddAddrs(p peer.ID, addrs []multiaddr.Multiaddr, ttl time.Duration) { m.ctrl.T.Helper() - m.ctrl.Call(m, "AddAddrs", arg0, arg1, arg2) + m.ctrl.Call(m, "AddAddrs", p, addrs, ttl) } // AddAddrs indicates an expected call of AddAddrs. -func (mr *MockPeerstoreMockRecorder) AddAddrs(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockPeerstoreMockRecorder) AddAddrs(p, addrs, ttl any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddAddrs", reflect.TypeOf((*MockPeerstore)(nil).AddAddrs), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddAddrs", reflect.TypeOf((*MockPeerstore)(nil).AddAddrs), p, addrs, ttl) } // AddPrivKey mocks base method. @@ -130,29 +131,29 @@ func (mr *MockPeerstoreMockRecorder) AddrStream(arg0, arg1 any) *gomock.Call { } // Addrs mocks base method. -func (m *MockPeerstore) Addrs(arg0 peer.ID) []multiaddr.Multiaddr { +func (m *MockPeerstore) Addrs(p peer.ID) []multiaddr.Multiaddr { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Addrs", arg0) + ret := m.ctrl.Call(m, "Addrs", p) ret0, _ := ret[0].([]multiaddr.Multiaddr) return ret0 } // Addrs indicates an expected call of Addrs. -func (mr *MockPeerstoreMockRecorder) Addrs(arg0 any) *gomock.Call { +func (mr *MockPeerstoreMockRecorder) Addrs(p any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Addrs", reflect.TypeOf((*MockPeerstore)(nil).Addrs), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Addrs", reflect.TypeOf((*MockPeerstore)(nil).Addrs), p) } // ClearAddrs mocks base method. -func (m *MockPeerstore) ClearAddrs(arg0 peer.ID) { +func (m *MockPeerstore) ClearAddrs(p peer.ID) { m.ctrl.T.Helper() - m.ctrl.Call(m, "ClearAddrs", arg0) + m.ctrl.Call(m, "ClearAddrs", p) } // ClearAddrs indicates an expected call of ClearAddrs. -func (mr *MockPeerstoreMockRecorder) ClearAddrs(arg0 any) *gomock.Call { +func (mr *MockPeerstoreMockRecorder) ClearAddrs(p any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClearAddrs", reflect.TypeOf((*MockPeerstore)(nil).ClearAddrs), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClearAddrs", reflect.TypeOf((*MockPeerstore)(nil).ClearAddrs), p) } // Close mocks base method. @@ -190,18 +191,18 @@ func (mr *MockPeerstoreMockRecorder) FirstSupportedProtocol(arg0 any, arg1 ...an } // Get mocks base method. -func (m *MockPeerstore) Get(arg0 peer.ID, arg1 string) (any, error) { +func (m *MockPeerstore) Get(p peer.ID, key string) (any, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Get", arg0, arg1) + ret := m.ctrl.Call(m, "Get", p, key) ret0, _ := ret[0].(any) ret1, _ := ret[1].(error) return ret0, ret1 } // Get indicates an expected call of Get. -func (mr *MockPeerstoreMockRecorder) Get(arg0, arg1 any) *gomock.Call { +func (mr *MockPeerstoreMockRecorder) Get(p, key any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockPeerstore)(nil).Get), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockPeerstore)(nil).Get), p, key) } // GetProtocols mocks base method. @@ -318,17 +319,17 @@ func (mr *MockPeerstoreMockRecorder) PubKey(arg0 any) *gomock.Call { } // Put mocks base method. -func (m *MockPeerstore) Put(arg0 peer.ID, arg1 string, arg2 any) error { +func (m *MockPeerstore) Put(p peer.ID, key string, val any) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Put", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "Put", p, key, val) ret0, _ := ret[0].(error) return ret0 } // Put indicates an expected call of Put. -func (mr *MockPeerstoreMockRecorder) Put(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockPeerstoreMockRecorder) Put(p, key, val any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Put", reflect.TypeOf((*MockPeerstore)(nil).Put), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Put", reflect.TypeOf((*MockPeerstore)(nil).Put), p, key, val) } // RecordLatency mocks base method. @@ -375,27 +376,27 @@ func (mr *MockPeerstoreMockRecorder) RemoveProtocols(arg0 any, arg1 ...any) *gom } // SetAddr mocks base method. -func (m *MockPeerstore) SetAddr(arg0 peer.ID, arg1 multiaddr.Multiaddr, arg2 time.Duration) { +func (m *MockPeerstore) SetAddr(p peer.ID, addr multiaddr.Multiaddr, ttl time.Duration) { m.ctrl.T.Helper() - m.ctrl.Call(m, "SetAddr", arg0, arg1, arg2) + m.ctrl.Call(m, "SetAddr", p, addr, ttl) } // SetAddr indicates an expected call of SetAddr. -func (mr *MockPeerstoreMockRecorder) SetAddr(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockPeerstoreMockRecorder) SetAddr(p, addr, ttl any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAddr", reflect.TypeOf((*MockPeerstore)(nil).SetAddr), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAddr", reflect.TypeOf((*MockPeerstore)(nil).SetAddr), p, addr, ttl) } // SetAddrs mocks base method. -func (m *MockPeerstore) SetAddrs(arg0 peer.ID, arg1 []multiaddr.Multiaddr, arg2 time.Duration) { +func (m *MockPeerstore) SetAddrs(p peer.ID, addrs []multiaddr.Multiaddr, ttl time.Duration) { m.ctrl.T.Helper() - m.ctrl.Call(m, "SetAddrs", arg0, arg1, arg2) + m.ctrl.Call(m, "SetAddrs", p, addrs, ttl) } // SetAddrs indicates an expected call of SetAddrs. -func (mr *MockPeerstoreMockRecorder) SetAddrs(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockPeerstoreMockRecorder) SetAddrs(p, addrs, ttl any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAddrs", reflect.TypeOf((*MockPeerstore)(nil).SetAddrs), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAddrs", reflect.TypeOf((*MockPeerstore)(nil).SetAddrs), p, addrs, ttl) } // SetProtocols mocks base method. @@ -438,13 +439,13 @@ func (mr *MockPeerstoreMockRecorder) SupportsProtocols(arg0 any, arg1 ...any) *g } // UpdateAddrs mocks base method. -func (m *MockPeerstore) UpdateAddrs(arg0 peer.ID, arg1, arg2 time.Duration) { +func (m *MockPeerstore) UpdateAddrs(p peer.ID, oldTTL, newTTL time.Duration) { m.ctrl.T.Helper() - m.ctrl.Call(m, "UpdateAddrs", arg0, arg1, arg2) + m.ctrl.Call(m, "UpdateAddrs", p, oldTTL, newTTL) } // UpdateAddrs indicates an expected call of UpdateAddrs. -func (mr *MockPeerstoreMockRecorder) UpdateAddrs(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockPeerstoreMockRecorder) UpdateAddrs(p, oldTTL, newTTL any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAddrs", reflect.TypeOf((*MockPeerstore)(nil).UpdateAddrs), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAddrs", reflect.TypeOf((*MockPeerstore)(nil).UpdateAddrs), p, oldTTL, newTTL) } diff --git a/p2p/http/auth/auth.go b/p2p/http/auth/auth.go new file mode 100644 index 0000000000..692464fa22 --- /dev/null +++ b/p2p/http/auth/auth.go @@ -0,0 +1,11 @@ +package httppeeridauth + +import ( + logging "github.com/ipfs/go-log/v2" + "github.com/libp2p/go-libp2p/p2p/http/auth/internal/handshake" +) + +const PeerIDAuthScheme = handshake.PeerIDAuthScheme +const ProtocolID = "/http-peer-id-auth/1.0.0" + +var log = logging.Logger("http-peer-id-auth") diff --git a/p2p/http/auth/auth_test.go b/p2p/http/auth/auth_test.go new file mode 100644 index 0000000000..d080b19511 --- /dev/null +++ b/p2p/http/auth/auth_test.go @@ -0,0 +1,243 @@ +package httppeeridauth + +import ( + "bytes" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "crypto/tls" + "hash" + "io" + "net/http" + "net/http/httptest" + "strings" + "sync" + "testing" + "time" + + logging "github.com/ipfs/go-log/v2" + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestMutualAuth tests that we can do a mutually authenticated round trip +func TestMutualAuth(t *testing.T) { + logging.SetLogLevel("httppeeridauth", "DEBUG") + + zeroBytes := make([]byte, 64) + serverKey, _, err := crypto.GenerateEd25519Key(bytes.NewReader(zeroBytes)) + require.NoError(t, err) + + type clientTestCase struct { + name string + clientKeyGen func(t *testing.T) crypto.PrivKey + } + + clientTestCases := []clientTestCase{ + { + name: "ED25519", + clientKeyGen: func(t *testing.T) crypto.PrivKey { + t.Helper() + clientKey, _, err := crypto.GenerateEd25519Key(rand.Reader) + require.NoError(t, err) + return clientKey + }, + }, + { + name: "RSA", + clientKeyGen: func(t *testing.T) crypto.PrivKey { + t.Helper() + clientKey, _, err := crypto.GenerateRSAKeyPair(2048, rand.Reader) + require.NoError(t, err) + return clientKey + }, + }, + } + + type serverTestCase struct { + name string + serverGen func(t *testing.T) (*httptest.Server, *ServerPeerIDAuth) + } + + serverTestCases := []serverTestCase{ + { + name: "no TLS", + serverGen: func(t *testing.T) (*httptest.Server, *ServerPeerIDAuth) { + t.Helper() + auth := ServerPeerIDAuth{ + PrivKey: serverKey, + ValidHostnameFn: func(s string) bool { + return s == "example.com" + }, + TokenTTL: time.Hour, + NoTLS: true, + } + + ts := httptest.NewServer(&auth) + t.Cleanup(ts.Close) + return ts, &auth + }, + }, + { + name: "TLS", + serverGen: func(t *testing.T) (*httptest.Server, *ServerPeerIDAuth) { + t.Helper() + auth := ServerPeerIDAuth{ + PrivKey: serverKey, + ValidHostnameFn: func(s string) bool { + return s == "example.com" + }, + TokenTTL: time.Hour, + } + + ts := httptest.NewTLSServer(&auth) + t.Cleanup(ts.Close) + return ts, &auth + }, + }, + } + + for _, ctc := range clientTestCases { + for _, stc := range serverTestCases { + t.Run(ctc.name+"+"+stc.name, func(t *testing.T) { + ts, server := stc.serverGen(t) + client := ts.Client() + roundTripper := instrumentedRoundTripper{client.Transport, 0} + client.Transport = &roundTripper + requestsSent := func() int { + defer func() { roundTripper.timesRoundtripped = 0 }() + return roundTripper.timesRoundtripped + } + + tlsClientConfig := roundTripper.TLSClientConfig() + if tlsClientConfig != nil { + // If we're using TLS, we need to set the SNI so that the + // server can verify the request Host matches it. + tlsClientConfig.ServerName = "example.com" + } + clientKey := ctc.clientKeyGen(t) + clientAuth := ClientPeerIDAuth{PrivKey: clientKey} + + expectedServerID, err := peer.IDFromPrivateKey(serverKey) + require.NoError(t, err) + + req, err := http.NewRequest("POST", ts.URL, nil) + require.NoError(t, err) + req.Host = "example.com" + serverID, resp, err := clientAuth.AuthenticatedDo(client, req) + require.NoError(t, err) + require.Equal(t, expectedServerID, serverID) + require.NotZero(t, clientAuth.tm.tokenMap["example.com"]) + require.Equal(t, http.StatusOK, resp.StatusCode) + require.Equal(t, 2, requestsSent()) + + // Once more with the auth token + req, err = http.NewRequest("POST", ts.URL, nil) + require.NoError(t, err) + req.Host = "example.com" + serverID, resp, err = clientAuth.AuthenticatedDo(client, req) + require.NotEmpty(t, req.Header.Get("Authorization")) + require.NoError(t, err) + require.Equal(t, expectedServerID, serverID) + require.NotZero(t, clientAuth.tm.tokenMap["example.com"]) + require.Equal(t, http.StatusOK, resp.StatusCode) + require.Equal(t, 1, requestsSent(), "should only call newRequest once since we have a token") + + t.Run("Tokens Expired", func(t *testing.T) { + // Clear the auth token on the server side + server.TokenTTL = 1 // Small TTL + time.Sleep(100 * time.Millisecond) + resetServerTokenTTL := sync.OnceFunc(func() { + server.TokenTTL = time.Hour + }) + + req, err := http.NewRequest("POST", ts.URL, nil) + require.NoError(t, err) + req.Host = "example.com" + req.GetBody = func() (io.ReadCloser, error) { + resetServerTokenTTL() + return nil, nil + } + serverID, resp, err = clientAuth.AuthenticatedDo(client, req) + require.NoError(t, err) + require.NotEmpty(t, req.Header.Get("Authorization")) + require.Equal(t, http.StatusOK, resp.StatusCode) + require.Equal(t, expectedServerID, serverID) + require.NotZero(t, clientAuth.tm.tokenMap["example.com"]) + require.Equal(t, 3, requestsSent(), "should call newRequest 3x since our token expired") + }) + + t.Run("Tokens Invalidated", func(t *testing.T) { + // Clear the auth token on the server side + server.Hmac = func() hash.Hash { + key := make([]byte, 32) + _, err := rand.Read(key) + if err != nil { + panic(err) + } + return hmac.New(sha256.New, key) + }() + + req, err := http.NewRequest("POST", ts.URL, nil) + req.GetBody = func() (io.ReadCloser, error) { + return nil, nil + } + require.NoError(t, err) + req.Host = "example.com" + serverID, resp, err = clientAuth.AuthenticatedDo(client, req) + require.NoError(t, err) + require.NotEmpty(t, req.Header.Get("Authorization")) + require.Equal(t, http.StatusOK, resp.StatusCode) + require.Equal(t, expectedServerID, serverID) + require.NotZero(t, clientAuth.tm.tokenMap["example.com"]) + require.Equal(t, 3, requestsSent(), "should call have sent 3 reqs since our token expired") + }) + + }) + } + } +} + +func TestBodyNotSentDuringRedirect(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + b, err := io.ReadAll(r.Body) + assert.NoError(t, err) + assert.Empty(t, string(b)) + if r.URL.Path != "/redirected" { + w.Header().Set("Location", "/redirected") + w.WriteHeader(http.StatusTemporaryRedirect) + return + } + })) + t.Cleanup(ts.Close) + client := ts.Client() + clientKey, _, _ := crypto.GenerateEd25519Key(rand.Reader) + clientAuth := ClientPeerIDAuth{PrivKey: clientKey} + + req, err := + http.NewRequest( + "POST", + ts.URL, + strings.NewReader("Only for authenticated servers"), + ) + req.Host = "example.com" + require.NoError(t, err) + _, _, err = clientAuth.AuthenticatedDo(client, req) + require.ErrorContains(t, err, "signature not set") // server doesn't actually handshake +} + +type instrumentedRoundTripper struct { + http.RoundTripper + timesRoundtripped int +} + +func (irt *instrumentedRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + irt.timesRoundtripped++ + return irt.RoundTripper.RoundTrip(req) +} + +func (irt *instrumentedRoundTripper) TLSClientConfig() *tls.Config { + return irt.RoundTripper.(*http.Transport).TLSClientConfig +} diff --git a/p2p/http/auth/client.go b/p2p/http/auth/client.go new file mode 100644 index 0000000000..a6bdece61a --- /dev/null +++ b/p2p/http/auth/client.go @@ -0,0 +1,194 @@ +package httppeeridauth + +import ( + "errors" + "fmt" + "io" + "net/http" + "sync" + "time" + + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/p2p/http/auth/internal/handshake" +) + +type ClientPeerIDAuth struct { + PrivKey crypto.PrivKey + TokenTTL time.Duration + + tm tokenMap +} + +// AuthenticatedDo is like http.Client.Do, but it does the libp2p peer ID auth +// handshake if needed. +// +// It is recommended to pass in an http.Request with `GetBody` set, so that this +// method can retry sending the request in case a previously used token has +// expired. +func (a *ClientPeerIDAuth) AuthenticatedDo(client *http.Client, req *http.Request) (peer.ID, *http.Response, error) { + hostname := req.Host + ti, hasToken := a.tm.get(hostname, a.TokenTTL) + handshake := handshake.PeerIDAuthHandshakeClient{ + Hostname: hostname, + PrivKey: a.PrivKey, + } + + if hasToken { + // We have a token. Attempt to use that, but fallback to server initiated challenge if it fails. + peer, resp, err := a.doWithToken(client, req, ti) + switch { + case err == nil: + return peer, resp, nil + case errors.Is(err, errTokenRejected): + // Token was rejected, we need to re-authenticate + break + default: + return "", nil, err + } + + // Token didn't work, we need to re-authenticate. + // Run the server-initiated handshake + req = req.Clone(req.Context()) + req.Body, err = req.GetBody() + if err != nil { + return "", nil, err + } + + handshake.ParseHeader(resp.Header) + } else { + // We didn't have a handshake token, so we initiate the handshake. + // If our token was rejected, the server initiates the handshake. + handshake.SetInitiateChallenge() + } + + serverPeerID, resp, err := a.runHandshake(client, req, clearBody(req), &handshake) + if err != nil { + return "", nil, fmt.Errorf("failed to run handshake: %w", err) + } + a.tm.set(hostname, tokenInfo{ + token: handshake.BearerToken(), + insertedAt: time.Now(), + peerID: serverPeerID, + }) + return serverPeerID, resp, nil +} + +func (a *ClientPeerIDAuth) runHandshake(client *http.Client, req *http.Request, b bodyMeta, hs *handshake.PeerIDAuthHandshakeClient) (peer.ID, *http.Response, error) { + maxSteps := 5 // Avoid infinite loops in case of buggy handshake. Shouldn't happen. + var resp *http.Response + + err := hs.Run() + if err != nil { + return "", nil, err + } + + sentBody := false + for !hs.HandshakeDone() || !sentBody { + req = req.Clone(req.Context()) + hs.AddHeader(req.Header) + if hs.ServerAuthenticated() { + sentBody = true + b.setBody(req) + } + + resp, err = client.Do(req) + if err != nil { + return "", nil, err + } + + hs.ParseHeader(resp.Header) + err = hs.Run() + if err != nil { + resp.Body.Close() + return "", nil, err + } + + if maxSteps--; maxSteps == 0 { + return "", nil, errors.New("handshake took too many steps") + } + } + + p, err := hs.PeerID() + if err != nil { + resp.Body.Close() + return "", nil, err + } + return p, resp, nil +} + +var errTokenRejected = errors.New("token rejected") + +func (a *ClientPeerIDAuth) doWithToken(client *http.Client, req *http.Request, ti tokenInfo) (peer.ID, *http.Response, error) { + // Try to make the request with the token + req.Header.Set("Authorization", ti.token) + resp, err := client.Do(req) + if err != nil { + return "", nil, err + } + if resp.StatusCode != http.StatusUnauthorized { + // our token is still valid + return ti.peerID, resp, nil + } + if req.GetBody == nil { + // We can't retry this request even if we wanted to. + // Return the response and an error + return "", resp, errors.New("expired token. Couldn't run handshake because req.GetBody is nil") + } + resp.Body.Close() + + return "", resp, errTokenRejected +} + +type bodyMeta struct { + body io.ReadCloser + contentLength int64 + getBody func() (io.ReadCloser, error) +} + +func clearBody(req *http.Request) bodyMeta { + defer func() { + req.Body = nil + req.ContentLength = 0 + req.GetBody = nil + }() + return bodyMeta{body: req.Body, contentLength: req.ContentLength, getBody: req.GetBody} +} + +func (b *bodyMeta) setBody(req *http.Request) { + req.Body = b.body + req.ContentLength = b.contentLength + req.GetBody = b.getBody +} + +type tokenInfo struct { + token string + insertedAt time.Time + peerID peer.ID +} + +type tokenMap struct { + tokenMapMu sync.Mutex + tokenMap map[string]tokenInfo +} + +func (tm *tokenMap) get(hostname string, ttl time.Duration) (tokenInfo, bool) { + tm.tokenMapMu.Lock() + defer tm.tokenMapMu.Unlock() + + ti, ok := tm.tokenMap[hostname] + if ok && ttl != 0 && time.Since(ti.insertedAt) > ttl { + delete(tm.tokenMap, hostname) + return tokenInfo{}, false + } + return ti, ok +} + +func (tm *tokenMap) set(hostname string, ti tokenInfo) { + tm.tokenMapMu.Lock() + defer tm.tokenMapMu.Unlock() + if tm.tokenMap == nil { + tm.tokenMap = make(map[string]tokenInfo) + } + tm.tokenMap[hostname] = ti +} diff --git a/p2p/http/auth/internal/handshake/alloc_test.go b/p2p/http/auth/internal/handshake/alloc_test.go new file mode 100644 index 0000000000..333bad4f0d --- /dev/null +++ b/p2p/http/auth/internal/handshake/alloc_test.go @@ -0,0 +1,20 @@ +//go:build nocover + +package handshake + +import "testing" + +func TestParsePeerIDAuthSchemeParamsNoAllocNoCover(t *testing.T) { + str := []byte(`libp2p-PeerID peer-id="", sig="", public-key="", bearer=""`) + + allocs := testing.AllocsPerRun(1000, func() { + p := params{} + err := p.parsePeerIDAuthSchemeParams(str) + if err != nil { + t.Fatal(err) + } + }) + if allocs > 0 { + t.Fatalf("alloc test failed expected 0 received %0.2f", allocs) + } +} diff --git a/p2p/http/auth/internal/handshake/client.go b/p2p/http/auth/internal/handshake/client.go new file mode 100644 index 0000000000..f8d39e9c14 --- /dev/null +++ b/p2p/http/auth/internal/handshake/client.go @@ -0,0 +1,247 @@ +package handshake + +import ( + "encoding/base64" + "errors" + "fmt" + "io" + "net/http" + + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peer" +) + +type peerIDAuthClientState int + +const ( + peerIDAuthClientStateSignChallenge peerIDAuthClientState = iota + peerIDAuthClientStateVerifyChallenge + peerIDAuthClientStateDone // We have the bearer token, and there's nothing left to do + + // Client initiated handshake + peerIDAuthClientInitiateChallenge + peerIDAuthClientStateVerifyAndSignChallenge + peerIDAuthClientStateWaitingForBearer +) + +type PeerIDAuthHandshakeClient struct { + Hostname string + PrivKey crypto.PrivKey + + serverPeerID peer.ID + serverPubKey crypto.PubKey + state peerIDAuthClientState + p params + hb headerBuilder + challengeServer []byte + buf [128]byte +} + +var errMissingChallenge = errors.New("missing challenge") + +func (h *PeerIDAuthHandshakeClient) SetInitiateChallenge() { + h.state = peerIDAuthClientInitiateChallenge +} + +func (h *PeerIDAuthHandshakeClient) ParseHeader(header http.Header) error { + if h.state == peerIDAuthClientStateDone || h.state == peerIDAuthClientInitiateChallenge { + return nil + } + h.p = params{} + + var headerVal []byte + switch h.state { + case peerIDAuthClientStateSignChallenge, peerIDAuthClientStateVerifyAndSignChallenge: + headerVal = []byte(header.Get("WWW-Authenticate")) + case peerIDAuthClientStateVerifyChallenge, peerIDAuthClientStateWaitingForBearer: + headerVal = []byte(header.Get("Authentication-Info")) + } + + if len(headerVal) == 0 { + return errMissingChallenge + } + + err := h.p.parsePeerIDAuthSchemeParams(headerVal) + if err != nil { + return err + } + + if h.serverPubKey == nil && len(h.p.publicKeyB64) > 0 { + serverPubKeyBytes, err := base64.URLEncoding.AppendDecode(nil, h.p.publicKeyB64) + if err != nil { + return err + } + h.serverPubKey, err = crypto.UnmarshalPublicKey(serverPubKeyBytes) + if err != nil { + return err + } + h.serverPeerID, err = peer.IDFromPublicKey(h.serverPubKey) + if err != nil { + return err + } + } + + return err +} + +func (h *PeerIDAuthHandshakeClient) Run() error { + if h.state == peerIDAuthClientStateDone { + return nil + } + + h.hb.clear() + clientPubKeyBytes, err := crypto.MarshalPublicKey(h.PrivKey.GetPublic()) + if err != nil { + return err + } + switch h.state { + case peerIDAuthClientInitiateChallenge: + h.hb.writeScheme(PeerIDAuthScheme) + h.addChallengeServerParam() + h.hb.writeParamB64(nil, "public-key", clientPubKeyBytes) + h.state = peerIDAuthClientStateVerifyAndSignChallenge + return nil + case peerIDAuthClientStateVerifyAndSignChallenge: + if len(h.p.sigB64) == 0 && len(h.p.challengeClient) != 0 { + // The server refused a client initiated handshake, so we need run the server initiated handshake + h.state = peerIDAuthClientStateSignChallenge + return h.Run() + } + if err := h.verifySig(clientPubKeyBytes); err != nil { + return err + } + + h.hb.writeScheme(PeerIDAuthScheme) + h.hb.writeParam("opaque", h.p.opaqueB64) + h.addSigParam() + h.state = peerIDAuthClientStateWaitingForBearer + return nil + + case peerIDAuthClientStateWaitingForBearer: + h.hb.writeScheme(PeerIDAuthScheme) + h.hb.writeParam("bearer", h.p.bearerTokenB64) + h.state = peerIDAuthClientStateDone + return nil + + case peerIDAuthClientStateSignChallenge: + if len(h.p.challengeClient) < challengeLen { + return errors.New("challenge too short") + } + + h.hb.writeScheme(PeerIDAuthScheme) + h.hb.writeParamB64(nil, "public-key", clientPubKeyBytes) + if err := h.addChallengeServerParam(); err != nil { + return err + } + if err := h.addSigParam(); err != nil { + return err + } + h.hb.writeParam("opaque", h.p.opaqueB64) + + h.state = peerIDAuthClientStateVerifyChallenge + return nil + case peerIDAuthClientStateVerifyChallenge: + if err := h.verifySig(clientPubKeyBytes); err != nil { + return err + } + + h.hb.writeScheme(PeerIDAuthScheme) + h.hb.writeParam("bearer", h.p.bearerTokenB64) + h.state = peerIDAuthClientStateDone + + return nil + } + + return errors.New("unhandled state") +} + +func (h *PeerIDAuthHandshakeClient) addChallengeServerParam() error { + _, err := io.ReadFull(randReader, h.buf[:challengeLen]) + if err != nil { + return err + } + h.challengeServer = base64.URLEncoding.AppendEncode(nil, h.buf[:challengeLen]) + clear(h.buf[:challengeLen]) + h.hb.writeParam("challenge-server", h.challengeServer) + return nil +} + +func (h *PeerIDAuthHandshakeClient) verifySig(clientPubKeyBytes []byte) error { + if len(h.p.sigB64) == 0 { + return errors.New("signature not set") + } + sig, err := base64.URLEncoding.AppendDecode(nil, h.p.sigB64) + if err != nil { + return fmt.Errorf("failed to decode signature: %w", err) + } + err = verifySig(h.serverPubKey, PeerIDAuthScheme, []sigParam{ + {"challenge-server", h.challengeServer}, + {"client-public-key", clientPubKeyBytes}, + {"hostname", []byte(h.Hostname)}, + }, sig) + return err +} + +func (h *PeerIDAuthHandshakeClient) addSigParam() error { + if h.serverPubKey == nil { + return errors.New("server public key not set") + } + serverPubKeyBytes, err := crypto.MarshalPublicKey(h.serverPubKey) + if err != nil { + return err + } + clientSig, err := sign(h.PrivKey, PeerIDAuthScheme, []sigParam{ + {"challenge-client", h.p.challengeClient}, + {"server-public-key", serverPubKeyBytes}, + {"hostname", []byte(h.Hostname)}, + }) + if err != nil { + return fmt.Errorf("failed to sign challenge: %w", err) + } + h.hb.writeParamB64(nil, "sig", clientSig) + return nil + +} + +// PeerID returns the peer ID of the authenticated client. +func (h *PeerIDAuthHandshakeClient) PeerID() (peer.ID, error) { + switch h.state { + case peerIDAuthClientStateDone: + case peerIDAuthClientStateWaitingForBearer: + default: + return "", errors.New("server not authenticated yet") + } + + if h.serverPeerID == "" { + return "", errors.New("peer ID not set") + } + return h.serverPeerID, nil +} + +func (h *PeerIDAuthHandshakeClient) AddHeader(hdr http.Header) { + hdr.Set("Authorization", h.hb.b.String()) +} + +// BearerToken returns the server given bearer token for the client. Set this on +// the Authorization header in the client's request. +func (h *PeerIDAuthHandshakeClient) BearerToken() string { + if h.state != peerIDAuthClientStateDone { + return "" + } + return h.hb.b.String() +} + +func (h *PeerIDAuthHandshakeClient) ServerAuthenticated() bool { + switch h.state { + case peerIDAuthClientStateDone: + case peerIDAuthClientStateWaitingForBearer: + default: + return false + } + + return h.serverPeerID != "" +} + +func (h *PeerIDAuthHandshakeClient) HandshakeDone() bool { + return h.state == peerIDAuthClientStateDone +} diff --git a/p2p/http/auth/internal/handshake/handshake.go b/p2p/http/auth/internal/handshake/handshake.go new file mode 100644 index 0000000000..1c237ae3a3 --- /dev/null +++ b/p2p/http/auth/internal/handshake/handshake.go @@ -0,0 +1,218 @@ +package handshake + +import ( + "bufio" + "bytes" + "crypto/rand" + "encoding/base64" + "encoding/binary" + "errors" + "fmt" + "slices" + "strings" + "time" + + "github.com/libp2p/go-libp2p/core/crypto" + + pool "github.com/libp2p/go-buffer-pool" +) + +const PeerIDAuthScheme = "libp2p-PeerID" +const challengeLen = 32 +const maxHeaderSize = 2048 + +var peerIDAuthSchemeBytes = []byte(PeerIDAuthScheme) + +var errTooBig = errors.New("header value too big") +var errInvalid = errors.New("invalid header value") +var errNotRan = errors.New("not ran. call Run() first") + +var randReader = rand.Reader // A var so it can be changed in tests +var nowFn = time.Now // A var so it can be changed in tests + +// params represent params passed in via headers. All []byte fields to avoid allocations. +type params struct { + bearerTokenB64 []byte + challengeClient []byte + challengeServer []byte + opaqueB64 []byte + publicKeyB64 []byte + sigB64 []byte +} + +// parsePeerIDAuthSchemeParams parses the parameters of the PeerID auth scheme +// from the header string. zero alloc. +func (p *params) parsePeerIDAuthSchemeParams(headerVal []byte) error { + if len(headerVal) > maxHeaderSize { + return errTooBig + } + startIdx := bytes.Index(headerVal, peerIDAuthSchemeBytes) + if startIdx == -1 { + return nil + } + + headerVal = headerVal[startIdx+len(PeerIDAuthScheme):] + advance, token, err := splitAuthHeaderParams(headerVal, true) + for ; err == nil; advance, token, err = splitAuthHeaderParams(headerVal, true) { + headerVal = headerVal[advance:] + bs := token + splitAt := bytes.Index(bs, []byte("=")) + if splitAt == -1 { + return errInvalid + } + kB := bs[:splitAt] + v := bs[splitAt+1:] + if len(v) < 2 || v[0] != '"' || v[len(v)-1] != '"' { + return errInvalid + } + v = v[1 : len(v)-1] // drop quotes + switch string(kB) { + case "bearer": + p.bearerTokenB64 = v + case "challenge-client": + p.challengeClient = v + case "challenge-server": + p.challengeServer = v + case "opaque": + p.opaqueB64 = v + case "public-key": + p.publicKeyB64 = v + case "sig": + p.sigB64 = v + } + } + if err == bufio.ErrFinalToken { + err = nil + } + return err +} + +func splitAuthHeaderParams(data []byte, atEOF bool) (advance int, token []byte, err error) { + if len(data) == 0 && atEOF { + return 0, nil, bufio.ErrFinalToken + } + + start := 0 + for start < len(data) && (data[start] == ' ' || data[start] == ',') { + // Ignore leading spaces and commas + start++ + } + if start == len(data) { + return len(data), nil, nil + } + end := start + 1 + for end < len(data) && data[end] != ' ' && data[end] != ',' { + // Consume until we hit a space or comma + end++ + } + token = data[start:end] + if !bytes.ContainsAny(token, "=") { + // This isn't a param. It's likely the next scheme. We're done + return len(data), nil, bufio.ErrFinalToken + } + + return end, token, nil +} + +type headerBuilder struct { + b strings.Builder + pastFirstField bool +} + +func (h *headerBuilder) clear() { + h.b.Reset() + h.pastFirstField = false +} + +func (h *headerBuilder) writeScheme(scheme string) { + h.b.WriteString(scheme) + h.b.WriteByte(' ') +} + +func (h *headerBuilder) maybeAddComma() { + if !h.pastFirstField { + h.pastFirstField = true + return + } + h.b.WriteString(", ") +} + +// writeParam writes a key value pair to the header. It first b64 encodes the +// value. It uses buf as scratch space. +func (h *headerBuilder) writeParamB64(buf []byte, key string, val []byte) { + if buf == nil { + buf = make([]byte, base64.URLEncoding.EncodedLen(len(val))) + } + encodedVal := base64.URLEncoding.AppendEncode(buf[:0], val) + h.writeParam(key, encodedVal) +} + +// writeParam writes a key value pair to the header. It writes the val as-is. +func (h *headerBuilder) writeParam(key string, val []byte) { + if len(val) == 0 { + return + } + h.maybeAddComma() + + h.b.Grow(len(key) + len(`="`) + len(val) + 1) + // Not doing fmt.Fprintf here to avoid one allocation + h.b.WriteString(key) + h.b.WriteString(`="`) + h.b.Write(val) + h.b.WriteByte('"') +} + +type sigParam struct { + k string + v []byte +} + +func verifySig(publicKey crypto.PubKey, prefix string, signedParts []sigParam, sig []byte) error { + if publicKey == nil { + return fmt.Errorf("no public key to verify signature") + } + + b := pool.Get(4096) + defer pool.Put(b) + buf, err := genDataToSign(b[:0], prefix, signedParts) + if err != nil { + return fmt.Errorf("failed to generate signed data: %w", err) + } + ok, err := publicKey.Verify(buf, sig) + if err != nil { + return err + } + if !ok { + return fmt.Errorf("signature verification failed") + } + + return nil +} + +func sign(privKey crypto.PrivKey, prefix string, partsToSign []sigParam) ([]byte, error) { + if privKey == nil { + return nil, fmt.Errorf("no private key available to sign") + } + b := pool.Get(4096) + defer pool.Put(b) + buf, err := genDataToSign(b[:0], prefix, partsToSign) + if err != nil { + return nil, fmt.Errorf("failed to generate data to sign: %w", err) + } + return privKey.Sign(buf) +} + +func genDataToSign(buf []byte, prefix string, parts []sigParam) ([]byte, error) { + // Sort the parts in lexicographic order + slices.SortFunc(parts, func(a, b sigParam) int { + return strings.Compare(a.k, b.k) + }) + buf = append(buf, prefix...) + for _, p := range parts { + buf = binary.AppendUvarint(buf, uint64(len(p.k)+1+len(p.v))) // +1 for '=' + buf = append(buf, p.k...) + buf = append(buf, '=') + buf = append(buf, p.v...) + } + return buf, nil +} diff --git a/p2p/http/auth/internal/handshake/handshake_test.go b/p2p/http/auth/internal/handshake/handshake_test.go new file mode 100644 index 0000000000..0579b95163 --- /dev/null +++ b/p2p/http/auth/internal/handshake/handshake_test.go @@ -0,0 +1,652 @@ +package handshake + +import ( + "bytes" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "net/http" + "net/url" + "testing" + "time" + + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/stretchr/testify/require" +) + +func TestHandshake(t *testing.T) { + for _, clientInitiated := range []bool{true, false} { + t.Run(fmt.Sprintf("clientInitiated=%t", clientInitiated), func(t *testing.T) { + hostname := "example.com" + serverPriv, _, _ := crypto.GenerateEd25519Key(rand.Reader) + clientPriv, _, _ := crypto.GenerateEd25519Key(rand.Reader) + + serverHandshake := PeerIDAuthHandshakeServer{ + Hostname: hostname, + PrivKey: serverPriv, + TokenTTL: time.Hour, + Hmac: hmac.New(sha256.New, make([]byte, 32)), + } + + clientHandshake := PeerIDAuthHandshakeClient{ + Hostname: hostname, + PrivKey: clientPriv, + } + if clientInitiated { + clientHandshake.state = peerIDAuthClientInitiateChallenge + } + + headers := make(http.Header) + + // Start the handshake + if !clientInitiated { + require.NoError(t, serverHandshake.ParseHeaderVal(nil)) + require.NoError(t, serverHandshake.Run()) + serverHandshake.SetHeader(headers) + } + + // Server Inititated: Client receives the challenge and signs it. Also sends the challenge server + // Client Inititated: Client forms the challenge and sends it + require.NoError(t, clientHandshake.ParseHeader(headers)) + clear(headers) + require.NoError(t, clientHandshake.Run()) + clientHandshake.AddHeader(headers) + + // Server Inititated: Server receives the sig and verifies it. Also signs the challenge-server (client authenticated) + // Client Inititated: Server receives the challenge and signs it. Also sends the challenge-client + serverHandshake.Reset() + require.NoError(t, serverHandshake.ParseHeaderVal([]byte(headers.Get("Authorization")))) + clear(headers) + require.NoError(t, serverHandshake.Run()) + serverHandshake.SetHeader(headers) + + // Server Inititated: Client verifies sig and sets the bearer token for future requests (server authenticated) + // Client Inititated: Client verifies sig, and signs challenge. Sends it along with any application data (server authenticated) + require.NoError(t, clientHandshake.ParseHeader(headers)) + clear(headers) + require.NoError(t, clientHandshake.Run()) + clientHandshake.AddHeader(headers) + + // Server Inititated: Server verifies the bearer token + // Client Inititated: Server verifies the sig, sets the bearer token (client authenticated) + // and processes any application data + serverHandshake.Reset() + require.NoError(t, serverHandshake.ParseHeaderVal([]byte(headers.Get("Authorization")))) + clear(headers) + require.NoError(t, serverHandshake.Run()) + serverHandshake.SetHeader(headers) + + expectedClientPeerID, _ := peer.IDFromPrivateKey(clientPriv) + expectedServerPeerID, _ := peer.IDFromPrivateKey(serverPriv) + clientPeerID, err := serverHandshake.PeerID() + require.NoError(t, err) + require.Equal(t, expectedClientPeerID, clientPeerID) + + serverPeerID, err := clientHandshake.PeerID() + require.NoError(t, err) + require.Equal(t, expectedServerPeerID, serverPeerID) + }) + } +} + +func TestServerRefusesClientInitiatedHandshake(t *testing.T) { + hostname := "example.com" + serverPriv, _, _ := crypto.GenerateEd25519Key(rand.Reader) + clientPriv, _, _ := crypto.GenerateEd25519Key(rand.Reader) + + serverHandshake := PeerIDAuthHandshakeServer{ + Hostname: hostname, + PrivKey: serverPriv, + TokenTTL: time.Hour, + Hmac: hmac.New(sha256.New, make([]byte, 32)), + } + + clientHandshake := PeerIDAuthHandshakeClient{ + Hostname: hostname, + PrivKey: clientPriv, + } + clientHandshake.SetInitiateChallenge() + + headers := make(http.Header) + // Client initiates the handshake + require.NoError(t, clientHandshake.Run()) + clientHandshake.AddHeader(headers) + + // Server receives the challenge-server, but chooses to reject it (simulating this by not passing the challenge) + serverHandshake.Reset() + require.NoError(t, serverHandshake.ParseHeaderVal(nil)) + clear(headers) + require.NoError(t, serverHandshake.Run()) + serverHandshake.SetHeader(headers) + + // Client now runs the server-initiated handshake. Signs challenge-client; sends challenge-server + require.NoError(t, clientHandshake.ParseHeader(headers)) + clear(headers) + require.NoError(t, clientHandshake.Run()) + clientHandshake.AddHeader(headers) + + // Server verifies the challenge-client and signs the challenge-server + serverHandshake.Reset() + require.NoError(t, serverHandshake.ParseHeaderVal([]byte(headers.Get("Authorization")))) + clear(headers) + require.NoError(t, serverHandshake.Run()) + serverHandshake.SetHeader(headers) + + // Client verifies the challenge-server and sets the bearer token + require.NoError(t, clientHandshake.ParseHeader(headers)) + clear(headers) + require.NoError(t, clientHandshake.Run()) + clientHandshake.AddHeader(headers) + + expectedClientPeerID, _ := peer.IDFromPrivateKey(clientPriv) + expectedServerPeerID, _ := peer.IDFromPrivateKey(serverPriv) + clientPeerID, err := serverHandshake.PeerID() + require.NoError(t, err) + require.Equal(t, expectedClientPeerID, clientPeerID) + + serverPeerID, err := clientHandshake.PeerID() + require.NoError(t, err) + require.True(t, clientHandshake.HandshakeDone()) + require.Equal(t, expectedServerPeerID, serverPeerID) +} + +func BenchmarkServerHandshake(b *testing.B) { + clientHeader1 := make(http.Header) + clientHeader2 := make(http.Header) + headers := make(http.Header) + + hostname := "example.com" + serverPriv, _, _ := crypto.GenerateEd25519Key(rand.Reader) + clientPriv, _, _ := crypto.GenerateEd25519Key(rand.Reader) + + serverHandshake := PeerIDAuthHandshakeServer{ + Hostname: hostname, + PrivKey: serverPriv, + TokenTTL: time.Hour, + Hmac: hmac.New(sha256.New, make([]byte, 32)), + } + + clientHandshake := PeerIDAuthHandshakeClient{ + Hostname: hostname, + PrivKey: clientPriv, + } + require.NoError(b, serverHandshake.ParseHeaderVal(nil)) + require.NoError(b, serverHandshake.Run()) + serverHandshake.SetHeader(headers) + + // Client receives the challenge and signs it. Also sends the challenge server + require.NoError(b, clientHandshake.ParseHeader(headers)) + clear(headers) + require.NoError(b, clientHandshake.Run()) + clientHandshake.AddHeader(clientHeader1) + + // Server receives the sig and verifies it. Also signs the challenge server + serverHandshake.Reset() + require.NoError(b, serverHandshake.ParseHeaderVal([]byte(clientHeader1.Get("Authorization")))) + clear(headers) + require.NoError(b, serverHandshake.Run()) + serverHandshake.SetHeader(headers) + + // Client verifies sig and sets the bearer token for future requests + require.NoError(b, clientHandshake.ParseHeader(headers)) + clear(headers) + require.NoError(b, clientHandshake.Run()) + clientHandshake.AddHeader(clientHeader2) + + // Server verifies the bearer token + serverHandshake.Reset() + require.NoError(b, serverHandshake.ParseHeaderVal([]byte(clientHeader2.Get("Authorization")))) + clear(headers) + require.NoError(b, serverHandshake.Run()) + serverHandshake.SetHeader(headers) + + initialClientAuth := []byte(clientHeader1.Get("Authorization")) + bearerClientAuth := []byte(clientHeader2.Get("Authorization")) + _ = initialClientAuth + _ = bearerClientAuth + + b.ResetTimer() + for i := 0; i < b.N; i++ { + serverHandshake.Reset() + serverHandshake.ParseHeaderVal(nil) + serverHandshake.Run() + + serverHandshake.Reset() + serverHandshake.ParseHeaderVal(initialClientAuth) + serverHandshake.Run() + + serverHandshake.Reset() + serverHandshake.ParseHeaderVal(bearerClientAuth) + serverHandshake.Run() + } + +} + +func TestParsePeerIDAuthSchemeParams(t *testing.T) { + str := `libp2p-PeerID sig="", public-key="", bearer=""` + p := params{} + expectedParam := params{ + sigB64: []byte(``), + publicKeyB64: []byte(``), + bearerTokenB64: []byte(``), + } + err := p.parsePeerIDAuthSchemeParams([]byte(str)) + require.NoError(t, err) + require.Equal(t, expectedParam, p) +} + +func BenchmarkParsePeerIDAuthSchemeParams(b *testing.B) { + str := []byte(`libp2p-PeerID peer-id="", sig="", public-key="", bearer=""`) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + p := params{} + err := p.parsePeerIDAuthSchemeParams(str) + if err != nil { + b.Fatal(err) + } + } +} + +func TestHeaderBuilder(t *testing.T) { + hb := headerBuilder{} + hb.writeScheme(PeerIDAuthScheme) + hb.writeParam("peer-id", []byte("foo")) + hb.writeParam("challenge-client", []byte("something-else")) + hb.writeParam("hostname", []byte("example.com")) + + expected := `libp2p-PeerID peer-id="foo", challenge-client="something-else", hostname="example.com"` + require.Equal(t, expected, hb.b.String()) +} + +func BenchmarkHeaderBuilder(b *testing.B) { + h := headerBuilder{} + scratch := make([]byte, 256) + scratch = scratch[:0] + + b.ResetTimer() + for i := 0; i < b.N; i++ { + h.b.Grow(256) + h.writeParamB64(scratch, "foo", []byte("bar")) + h.clear() + } +} + +// Test Vectors +var zeroBytes = make([]byte, 64) +var zeroKey, _, _ = crypto.GenerateEd25519Key(bytes.NewReader(zeroBytes)) + +// Peer ID derived from the zero key +var zeroID, _ = peer.IDFromPublicKey(zeroKey.GetPublic()) + +func TestOpaqueStateRoundTrip(t *testing.T) { + zeroBytes := [32]byte{} + + // To drop the monotonic clock reading + timeAfterUnmarshal := time.Now() + b, err := json.Marshal(timeAfterUnmarshal) + require.NoError(t, err) + require.NoError(t, json.Unmarshal(b, &timeAfterUnmarshal)) + hmac := hmac.New(sha256.New, zeroBytes[:]) + + o := opaqueState{ + ChallengeClient: "foo-bar", + CreatedTime: timeAfterUnmarshal, + IsToken: true, + PeerID: zeroID, + Hostname: "example.com", + } + + hmac.Reset() + b, err = o.Marshal(hmac, nil) + require.NoError(t, err) + + o2 := opaqueState{} + + hmac.Reset() + err = o2.Unmarshal(hmac, b) + require.NoError(t, err) + require.EqualValues(t, o, o2) +} + +func FuzzServerHandshakeNoPanic(f *testing.F) { + zeroBytes := [32]byte{} + hmac := hmac.New(sha256.New, zeroBytes[:]) + + f.Fuzz(func(t *testing.T, data []byte) { + hmac.Reset() + h := PeerIDAuthHandshakeServer{ + Hostname: "example.com", + PrivKey: zeroKey, + Hmac: hmac, + } + err := h.ParseHeaderVal(data) + if err != nil { + return + } + err = h.Run() + if err != nil { + return + } + h.PeerID() + }) +} + +func BenchmarkOpaqueStateWrite(b *testing.B) { + zeroBytes := [32]byte{} + hmac := hmac.New(sha256.New, zeroBytes[:]) + o := opaqueState{ + ChallengeClient: "foo-bar", + CreatedTime: time.Now(), + } + d := make([]byte, 512) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + hmac.Reset() + _, err := o.Marshal(hmac, d[:0]) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkOpaqueStateRead(b *testing.B) { + zeroBytes := [32]byte{} + hmac := hmac.New(sha256.New, zeroBytes[:]) + o := opaqueState{ + ChallengeClient: "foo-bar", + CreatedTime: time.Now(), + } + d := make([]byte, 256) + d, err := o.Marshal(hmac, d[:0]) + require.NoError(b, err) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + hmac.Reset() + err := o.Unmarshal(hmac, d) + if err != nil { + b.Fatal(err) + } + } +} + +func FuzzParsePeerIDAuthSchemeParamsNoPanic(f *testing.F) { + p := params{} + // Just check that we don't panic + f.Fuzz(func(t *testing.T, data []byte) { + p.parsePeerIDAuthSchemeParams(data) + }) +} + +type specsExampleParameters struct { + hostname string + serverPriv crypto.PrivKey + serverHmacKey [32]byte + clientPriv crypto.PrivKey +} + +func TestSpecsExample(t *testing.T) { + originalRandReader := randReader + originalNowFn := nowFn + randReader = bytes.NewReader(append( + bytes.Repeat([]byte{0x11}, 32), + bytes.Repeat([]byte{0x33}, 32)..., + )) + nowFn = func() time.Time { + return time.Unix(0, 0) + } + defer func() { + randReader = originalRandReader + nowFn = originalNowFn + }() + + parameters := specsExampleParameters{ + hostname: "example.com", + } + serverPrivBytes, err := hex.AppendDecode(nil, []byte("0801124001010101010101010101010101010101010101010101010101010101010101018a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c")) + require.NoError(t, err) + clientPrivBytes, err := hex.AppendDecode(nil, []byte("0801124002020202020202020202020202020202020202020202020202020202020202028139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b394")) + require.NoError(t, err) + + parameters.serverPriv, err = crypto.UnmarshalPrivateKey(serverPrivBytes) + require.NoError(t, err) + + parameters.clientPriv, err = crypto.UnmarshalPrivateKey(clientPrivBytes) + require.NoError(t, err) + + serverHandshake := PeerIDAuthHandshakeServer{ + Hostname: parameters.hostname, + PrivKey: parameters.serverPriv, + TokenTTL: time.Hour, + Hmac: hmac.New(sha256.New, parameters.serverHmacKey[:]), + } + + clientHandshake := PeerIDAuthHandshakeClient{ + Hostname: parameters.hostname, + PrivKey: parameters.clientPriv, + } + + headers := make(http.Header) + + // Start the handshake + require.NoError(t, serverHandshake.ParseHeaderVal(nil)) + require.NoError(t, serverHandshake.Run()) + serverHandshake.SetHeader(headers) + initialWWWAuthenticate := headers.Get("WWW-Authenticate") + + // Client receives the challenge and signs it. Also sends the challenge server + require.NoError(t, clientHandshake.ParseHeader(headers)) + clear(headers) + require.NoError(t, clientHandshake.Run()) + clientHandshake.AddHeader(headers) + clientAuthentication := headers.Get("Authorization") + + // Server receives the sig and verifies it. Also signs the challenge server + serverHandshake.Reset() + require.NoError(t, serverHandshake.ParseHeaderVal([]byte(headers.Get("Authorization")))) + clear(headers) + require.NoError(t, serverHandshake.Run()) + serverHandshake.SetHeader(headers) + serverAuthentication := headers.Get("Authentication-Info") + + // Client verifies sig and sets the bearer token for future requests + require.NoError(t, clientHandshake.ParseHeader(headers)) + clear(headers) + require.NoError(t, clientHandshake.Run()) + clientHandshake.AddHeader(headers) + clientBearerToken := headers.Get("Authorization") + + params := params{} + params.parsePeerIDAuthSchemeParams([]byte(initialWWWAuthenticate)) + challengeClient := params.challengeClient + params.parsePeerIDAuthSchemeParams([]byte(clientAuthentication)) + challengeServer := params.challengeServer + + fmt.Println("### Parameters") + fmt.Println("| Parameter | Value |") + fmt.Println("| --- | --- |") + fmt.Printf("| hostname | %s |\n", parameters.hostname) + fmt.Printf("| Server Private Key (pb encoded as hex) | %s |\n", hex.EncodeToString(serverPrivBytes)) + fmt.Printf("| Server HMAC Key (hex) | %s |\n", hex.EncodeToString(parameters.serverHmacKey[:])) + fmt.Printf("| Challenge Client | %s |\n", string(challengeClient)) + fmt.Printf("| Client Private Key (pb encoded as hex) | %s |\n", hex.EncodeToString(clientPrivBytes)) + fmt.Printf("| Challenge Server | %s |\n", string(challengeServer)) + fmt.Printf("| \"Now\" time | %s |\n", nowFn()) + fmt.Println() + fmt.Println("### Handshake Diagram") + + fmt.Println("```mermaid") + fmt.Printf(`sequenceDiagram +Client->>Server: Initial request +Server->>Client: WWW-Authenticate=%s +Client->>Server: Authorization=%s +Note left of Server: Server has authenticated Client +Server->>Client: Authentication-Info=%s +Note right of Client: Client has authenticated Server + +Note over Client: Future requests use the bearer token +Client->>Server: Authorization=%s +`, initialWWWAuthenticate, clientAuthentication, serverAuthentication, clientBearerToken) + fmt.Println("```") + +} + +func TestSpecsClientInitiatedExample(t *testing.T) { + originalRandReader := randReader + originalNowFn := nowFn + randReader = bytes.NewReader(append( + bytes.Repeat([]byte{0x33}, 32), + bytes.Repeat([]byte{0x11}, 32)..., + )) + nowFn = func() time.Time { + return time.Unix(0, 0) + } + defer func() { + randReader = originalRandReader + nowFn = originalNowFn + }() + + parameters := specsExampleParameters{ + hostname: "example.com", + } + serverPrivBytes, err := hex.AppendDecode(nil, []byte("0801124001010101010101010101010101010101010101010101010101010101010101018a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c")) + require.NoError(t, err) + clientPrivBytes, err := hex.AppendDecode(nil, []byte("0801124002020202020202020202020202020202020202020202020202020202020202028139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b394")) + require.NoError(t, err) + + parameters.serverPriv, err = crypto.UnmarshalPrivateKey(serverPrivBytes) + require.NoError(t, err) + + parameters.clientPriv, err = crypto.UnmarshalPrivateKey(clientPrivBytes) + require.NoError(t, err) + + serverHandshake := PeerIDAuthHandshakeServer{ + Hostname: parameters.hostname, + PrivKey: parameters.serverPriv, + TokenTTL: time.Hour, + Hmac: hmac.New(sha256.New, parameters.serverHmacKey[:]), + } + + clientHandshake := PeerIDAuthHandshakeClient{ + Hostname: parameters.hostname, + PrivKey: parameters.clientPriv, + } + + headers := make(http.Header) + + // Start the handshake + clientHandshake.SetInitiateChallenge() + require.NoError(t, clientHandshake.Run()) + clientHandshake.AddHeader(headers) + clientChallenge := headers.Get("Authorization") + + // Server receives the challenge and signs it. Also sends challenge-client + serverHandshake.Reset() + require.NoError(t, serverHandshake.ParseHeaderVal([]byte(headers.Get("Authorization")))) + clear(headers) + require.NoError(t, serverHandshake.Run()) + serverHandshake.SetHeader(headers) + serverAuthentication := headers.Get("WWW-Authenticate") + params := params{} + params.parsePeerIDAuthSchemeParams([]byte(serverAuthentication)) + challengeClient := params.challengeClient + + // Client verifies sig and signs the challenge-client + require.NoError(t, clientHandshake.ParseHeader(headers)) + clear(headers) + require.NoError(t, clientHandshake.Run()) + clientHandshake.AddHeader(headers) + clientAuthentication := headers.Get("Authorization") + + // Server verifies sig and sets the bearer token + serverHandshake.Reset() + require.NoError(t, serverHandshake.ParseHeaderVal([]byte(headers.Get("Authorization")))) + clear(headers) + require.NoError(t, serverHandshake.Run()) + serverHandshake.SetHeader(headers) + serverReplayWithBearer := headers.Get("Authentication-Info") + + params.parsePeerIDAuthSchemeParams([]byte(clientChallenge)) + challengeServer := params.challengeServer + + fmt.Println("### Parameters") + fmt.Println("| Parameter | Value |") + fmt.Println("| --- | --- |") + fmt.Printf("| hostname | %s |\n", parameters.hostname) + fmt.Printf("| Server Private Key (pb encoded as hex) | %s |\n", hex.EncodeToString(serverPrivBytes)) + fmt.Printf("| Server HMAC Key (hex) | %s |\n", hex.EncodeToString(parameters.serverHmacKey[:])) + fmt.Printf("| Challenge Client | %s |\n", string(challengeClient)) + fmt.Printf("| Client Private Key (pb encoded as hex) | %s |\n", hex.EncodeToString(clientPrivBytes)) + fmt.Printf("| Challenge Server | %s |\n", string(challengeServer)) + fmt.Printf("| \"Now\" time | %s |\n", nowFn()) + fmt.Println() + fmt.Println("### Handshake Diagram") + + fmt.Println("```mermaid") + fmt.Printf(`sequenceDiagram +Client->>Server: Authorization=%s +Server->>Client: WWW-Authenticate=%s +Note right of Client: Client has authenticated Server + +Client->>Server: Authorization=%s +Note left of Server: Server has authenticated Client +Server->>Client: Authentication-Info=%s +Note over Client: Future requests use the bearer token +`, clientChallenge, serverAuthentication, clientAuthentication, serverReplayWithBearer) + fmt.Println("```") + +} + +func TestSigningExample(t *testing.T) { + serverPrivBytes, err := hex.AppendDecode(nil, []byte("0801124001010101010101010101010101010101010101010101010101010101010101018a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c")) + require.NoError(t, err) + serverPriv, err := crypto.UnmarshalPrivateKey(serverPrivBytes) + require.NoError(t, err) + clientPrivBytes, err := hex.AppendDecode(nil, []byte("0801124002020202020202020202020202020202020202020202020202020202020202028139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b394")) + require.NoError(t, err) + clientPriv, err := crypto.UnmarshalPrivateKey(clientPrivBytes) + require.NoError(t, err) + clientPubKeyBytes, err := crypto.MarshalPublicKey(clientPriv.GetPublic()) + require.NoError(t, err) + + require.NoError(t, err) + challenge := "ERERERERERERERERERERERERERERERERERERERERERE=" + + hostname := "example.com" + dataToSign, err := genDataToSign(nil, PeerIDAuthScheme, []sigParam{ + {"challenge-server", []byte(challenge)}, + {"client-public-key", clientPubKeyBytes}, + {"hostname", []byte(hostname)}, + }) + require.NoError(t, err) + + sig, err := sign(serverPriv, PeerIDAuthScheme, []sigParam{ + {"challenge-server", []byte(challenge)}, + {"client-public-key", clientPubKeyBytes}, + {"hostname", []byte(hostname)}, + }) + require.NoError(t, err) + + fmt.Println("### Signing Example") + + fmt.Println("| Parameter | Value |") + fmt.Println("| --- | --- |") + fmt.Printf("| hostname | %s |\n", hostname) + fmt.Printf("| Server Private Key (pb encoded as hex) | %s |\n", hex.EncodeToString(serverPrivBytes)) + fmt.Printf("| challenge-server | %s |\n", string(challenge)) + fmt.Printf("| Client Public Key (pb encoded as hex) | %s |\n", hex.EncodeToString(clientPubKeyBytes)) + fmt.Printf("| data to sign ([percent encoded](https://datatracker.ietf.org/doc/html/rfc3986#section-2.1)) | %s |\n", url.PathEscape(string(dataToSign))) + fmt.Printf("| data to sign (hex encoded) | %s |\n", hex.EncodeToString(dataToSign)) + fmt.Printf("| signature (base64 encoded) | %s |\n", base64.URLEncoding.EncodeToString(sig)) + fmt.Println() + + fmt.Println("Note that the `=` after the libp2p-PeerID scheme is actually the varint length of the challenge-server parameter.") + +} diff --git a/p2p/http/auth/internal/handshake/server.go b/p2p/http/auth/internal/handshake/server.go new file mode 100644 index 0000000000..6b84038d93 --- /dev/null +++ b/p2p/http/auth/internal/handshake/server.go @@ -0,0 +1,373 @@ +package handshake + +import ( + "crypto/hmac" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "hash" + "io" + "net/http" + "time" + + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peer" +) + +var ( + ErrExpiredChallenge = errors.New("challenge expired") + ErrExpiredToken = errors.New("token expired") + ErrInvalidHMAC = errors.New("invalid HMAC") +) + +const challengeTTL = 5 * time.Minute + +type peerIDAuthServerState int + +const ( + // Server initiated + peerIDAuthServerStateChallengeClient peerIDAuthServerState = iota + peerIDAuthServerStateVerifyChallenge + peerIDAuthServerStateVerifyBearer + + // Client initiated + peerIDAuthServerStateSignChallenge +) + +type opaqueState struct { + IsToken bool `json:"is-token,omitempty"` + ClientPublicKey []byte `json:"client-public-key,omitempty"` + PeerID peer.ID `json:"peer-id,omitempty"` + ChallengeClient string `json:"challenge-client,omitempty"` + Hostname string `json:"hostname"` + CreatedTime time.Time `json:"created-time"` +} + +// Marshal serializes the state by appending it to the byte slice. +func (o *opaqueState) Marshal(hmac hash.Hash, b []byte) ([]byte, error) { + hmac.Reset() + fieldsMarshalled, err := json.Marshal(o) + if err != nil { + return b, err + } + _, err = hmac.Write(fieldsMarshalled) + if err != nil { + return b, err + } + b = hmac.Sum(b) + b = append(b, fieldsMarshalled...) + return b, nil +} + +func (o *opaqueState) Unmarshal(hmacImpl hash.Hash, d []byte) error { + hmacImpl.Reset() + if len(d) < hmacImpl.Size() { + return ErrInvalidHMAC + } + hmacVal := d[:hmacImpl.Size()] + fields := d[hmacImpl.Size():] + _, err := hmacImpl.Write(fields) + if err != nil { + return err + } + expectedHmac := hmacImpl.Sum(nil) + if !hmac.Equal(hmacVal, expectedHmac) { + return ErrInvalidHMAC + } + + err = json.Unmarshal(fields, &o) + if err != nil { + return err + } + return nil +} + +type PeerIDAuthHandshakeServer struct { + Hostname string + PrivKey crypto.PrivKey + TokenTTL time.Duration + // used to authenticate opaque blobs and tokens + Hmac hash.Hash + + ran bool + buf [1024]byte + + state peerIDAuthServerState + p params + hb headerBuilder + + opaque opaqueState +} + +var errInvalidHeader = errors.New("invalid header") + +func (h *PeerIDAuthHandshakeServer) Reset() { + h.Hmac.Reset() + h.ran = false + clear(h.buf[:]) + h.state = 0 + h.p = params{} + h.hb.clear() + h.opaque = opaqueState{} +} + +func (h *PeerIDAuthHandshakeServer) ParseHeaderVal(headerVal []byte) error { + if len(headerVal) == 0 { + // We are in the initial state. Nothing to parse. + return nil + } + err := h.p.parsePeerIDAuthSchemeParams(headerVal) + if err != nil { + return err + } + switch { + case h.p.sigB64 != nil && h.p.opaqueB64 != nil: + h.state = peerIDAuthServerStateVerifyChallenge + case h.p.bearerTokenB64 != nil: + h.state = peerIDAuthServerStateVerifyBearer + case h.p.challengeServer != nil && h.p.publicKeyB64 != nil: + h.state = peerIDAuthServerStateSignChallenge + default: + return errInvalidHeader + + } + return nil +} + +func (h *PeerIDAuthHandshakeServer) Run() error { + h.ran = true + switch h.state { + case peerIDAuthServerStateSignChallenge: + h.hb.writeScheme(PeerIDAuthScheme) + if err := h.addChallengeClientParam(); err != nil { + return err + } + if err := h.addPublicKeyParam(); err != nil { + return err + } + + publicKeyBytes, err := base64.URLEncoding.AppendDecode(nil, h.p.publicKeyB64) + if err != nil { + return err + } + h.opaque.ClientPublicKey = publicKeyBytes + if err := h.addServerSigParam(publicKeyBytes); err != nil { + return err + } + if err := h.addOpaqueParam(); err != nil { + return err + } + case peerIDAuthServerStateChallengeClient: + h.hb.writeScheme(PeerIDAuthScheme) + if err := h.addChallengeClientParam(); err != nil { + return err + } + if err := h.addPublicKeyParam(); err != nil { + return err + } + if err := h.addOpaqueParam(); err != nil { + return err + } + case peerIDAuthServerStateVerifyChallenge: + opaque, err := base64.URLEncoding.AppendDecode(h.buf[:0], h.p.opaqueB64) + if err != nil { + return err + } + err = h.opaque.Unmarshal(h.Hmac, opaque) + if err != nil { + return err + } + + if nowFn().After(h.opaque.CreatedTime.Add(challengeTTL)) { + return ErrExpiredChallenge + } + if h.opaque.IsToken { + return errors.New("expected challenge, got token") + } + + if h.Hostname != h.opaque.Hostname { + return errors.New("hostname in opaque mismatch") + } + + var publicKeyBytes []byte + clientInitiatedHandshake := h.opaque.ClientPublicKey != nil + + if clientInitiatedHandshake { + publicKeyBytes = h.opaque.ClientPublicKey + } else { + if len(h.p.publicKeyB64) == 0 { + return errors.New("missing public key") + } + var err error + publicKeyBytes, err = base64.URLEncoding.AppendDecode(nil, h.p.publicKeyB64) + if err != nil { + return err + } + } + pubKey, err := crypto.UnmarshalPublicKey(publicKeyBytes) + if err != nil { + return err + } + if err := h.verifySig(pubKey); err != nil { + return err + } + + peerID, err := peer.IDFromPublicKey(pubKey) + if err != nil { + return err + } + + // And create a bearer token for the client + h.opaque = opaqueState{ + IsToken: true, + PeerID: peerID, + Hostname: h.Hostname, + CreatedTime: nowFn(), + } + + h.hb.writeScheme(PeerIDAuthScheme) + + if !clientInitiatedHandshake { + if err := h.addServerSigParam(publicKeyBytes); err != nil { + return err + } + } + if err := h.addBearerParam(); err != nil { + return err + } + case peerIDAuthServerStateVerifyBearer: + bearerToken, err := base64.URLEncoding.AppendDecode(h.buf[:0], h.p.bearerTokenB64) + if err != nil { + return err + } + err = h.opaque.Unmarshal(h.Hmac, bearerToken) + if err != nil { + return err + } + + if !h.opaque.IsToken { + return errors.New("expected token, got challenge") + } + + if nowFn().After(h.opaque.CreatedTime.Add(h.TokenTTL)) { + return ErrExpiredToken + } + + return nil + default: + return errors.New("unhandled state") + } + + return nil +} + +func (h *PeerIDAuthHandshakeServer) addChallengeClientParam() error { + _, err := io.ReadFull(randReader, h.buf[:challengeLen]) + if err != nil { + return err + } + encodedChallenge := base64.URLEncoding.AppendEncode(h.buf[challengeLen:challengeLen], h.buf[:challengeLen]) + h.opaque.ChallengeClient = string(encodedChallenge) + h.opaque.Hostname = h.Hostname + h.opaque.CreatedTime = nowFn() + h.hb.writeParam("challenge-client", encodedChallenge) + return nil +} + +func (h *PeerIDAuthHandshakeServer) addOpaqueParam() error { + opaqueVal, err := h.opaque.Marshal(h.Hmac, h.buf[:0]) + if err != nil { + return err + } + h.hb.writeParamB64(h.buf[len(opaqueVal):], "opaque", opaqueVal) + return nil +} + +func (h *PeerIDAuthHandshakeServer) addServerSigParam(clientPublicKeyBytes []byte) error { + if len(h.p.challengeServer) < challengeLen { + return errors.New("challenge too short") + } + serverSig, err := sign(h.PrivKey, PeerIDAuthScheme, []sigParam{ + {"challenge-server", h.p.challengeServer}, + {"client-public-key", clientPublicKeyBytes}, + {"hostname", []byte(h.Hostname)}, + }) + if err != nil { + return fmt.Errorf("failed to sign challenge: %w", err) + } + h.hb.writeParamB64(h.buf[:], "sig", serverSig) + return nil +} + +func (h *PeerIDAuthHandshakeServer) addBearerParam() error { + bearerToken, err := h.opaque.Marshal(h.Hmac, h.buf[:0]) + if err != nil { + return err + } + h.hb.writeParamB64(h.buf[len(bearerToken):], "bearer", bearerToken) + return nil +} + +func (h *PeerIDAuthHandshakeServer) addPublicKeyParam() error { + serverPubKey := h.PrivKey.GetPublic() + pubKeyBytes, err := crypto.MarshalPublicKey(serverPubKey) + if err != nil { + return err + } + h.hb.writeParamB64(h.buf[:], "public-key", pubKeyBytes) + return nil +} + +func (h *PeerIDAuthHandshakeServer) verifySig(clientPubKey crypto.PubKey) error { + serverPubKey := h.PrivKey.GetPublic() + serverPubKeyBytes, err := crypto.MarshalPublicKey(serverPubKey) + if err != nil { + return err + } + sig, err := base64.URLEncoding.AppendDecode(h.buf[:0], h.p.sigB64) + if err != nil { + return fmt.Errorf("failed to decode signature: %w", err) + } + err = verifySig(clientPubKey, PeerIDAuthScheme, []sigParam{ + {k: "challenge-client", v: []byte(h.opaque.ChallengeClient)}, + {k: "server-public-key", v: serverPubKeyBytes}, + {k: "hostname", v: []byte(h.Hostname)}, + }, sig) + if err != nil { + return err + } + return nil +} + +// PeerID returns the peer ID of the authenticated client. +func (h *PeerIDAuthHandshakeServer) PeerID() (peer.ID, error) { + if !h.ran { + return "", errNotRan + } + switch h.state { + case peerIDAuthServerStateVerifyChallenge: + case peerIDAuthServerStateVerifyBearer: + default: + return "", errors.New("not in proper state") + } + if h.opaque.PeerID == "" { + return "", errors.New("peer ID not set") + } + return h.opaque.PeerID, nil +} + +func (h *PeerIDAuthHandshakeServer) SetHeader(hdr http.Header) { + if !h.ran { + return + } + defer h.hb.clear() + switch h.state { + case peerIDAuthServerStateChallengeClient, peerIDAuthServerStateSignChallenge: + hdr.Set("WWW-Authenticate", h.hb.b.String()) + case peerIDAuthServerStateVerifyChallenge: + hdr.Set("Authentication-Info", h.hb.b.String()) + case peerIDAuthServerStateVerifyBearer: + // For completeness. Nothing to do + } +} diff --git a/p2p/http/auth/server.go b/p2p/http/auth/server.go new file mode 100644 index 0000000000..3ee4f96dc8 --- /dev/null +++ b/p2p/http/auth/server.go @@ -0,0 +1,128 @@ +package httppeeridauth + +import ( + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "errors" + "hash" + "net/http" + "sync" + "time" + + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/p2p/http/auth/internal/handshake" +) + +type ServerPeerIDAuth struct { + PrivKey crypto.PrivKey + TokenTTL time.Duration + Next func(peer peer.ID, w http.ResponseWriter, r *http.Request) + // NoTLS is a flag that allows the server to accept requests without a TLS + // ServerName. Used when something else is terminating the TLS connection. + NoTLS bool + // Required when NoTLS is true. The server will only accept requests for + // which the Host header returns true. + ValidHostnameFn func(hostname string) bool + + Hmac hash.Hash + initHmac sync.Once +} + +// ServeHTTP implements the http.Handler interface for PeerIDAuth. It will +// attempt to authenticate the request using using the libp2p peer ID auth +// scheme. If a Next handler is set, it will be called on authenticated +// requests. +func (a *ServerPeerIDAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) { + a.initHmac.Do(func() { + if a.Hmac == nil { + key := make([]byte, 32) + _, err := rand.Read(key) + if err != nil { + panic(err) + } + a.Hmac = hmac.New(sha256.New, key) + } + }) + + hostname := r.Host + if a.NoTLS { + if a.ValidHostnameFn == nil { + log.Error("No ValidHostnameFn set. Required for NoTLS") + w.WriteHeader(http.StatusInternalServerError) + return + } + if !a.ValidHostnameFn(hostname) { + log.Debugf("Unauthorized request for host %s: hostname returned false for ValidHostnameFn", hostname) + w.WriteHeader(http.StatusBadRequest) + return + } + } else { + if r.TLS == nil { + log.Warn("No TLS connection, and NoTLS is false") + w.WriteHeader(http.StatusBadRequest) + return + } + if hostname != r.TLS.ServerName { + log.Debugf("Unauthorized request for host %s: hostname mismatch. Expected %s", hostname, r.TLS.ServerName) + w.WriteHeader(http.StatusBadRequest) + return + } + if a.ValidHostnameFn != nil && !a.ValidHostnameFn(hostname) { + log.Debugf("Unauthorized request for host %s: hostname returned false for ValidHostnameFn", hostname) + w.WriteHeader(http.StatusBadRequest) + return + } + } + + hs := handshake.PeerIDAuthHandshakeServer{ + Hostname: hostname, + PrivKey: a.PrivKey, + TokenTTL: a.TokenTTL, + Hmac: a.Hmac, + } + err := hs.ParseHeaderVal([]byte(r.Header.Get("Authorization"))) + if err != nil { + log.Debugf("Failed to parse header: %v", err) + w.WriteHeader(http.StatusBadRequest) + return + } + err = hs.Run() + if err != nil { + switch { + case errors.Is(err, handshake.ErrInvalidHMAC), + errors.Is(err, handshake.ErrExpiredChallenge), + errors.Is(err, handshake.ErrExpiredToken): + + hs := handshake.PeerIDAuthHandshakeServer{ + Hostname: hostname, + PrivKey: a.PrivKey, + TokenTTL: a.TokenTTL, + Hmac: a.Hmac, + } + hs.Run() + hs.SetHeader(w.Header()) + w.WriteHeader(http.StatusUnauthorized) + + return + } + + log.Debugf("Failed to run handshake: %v", err) + w.WriteHeader(http.StatusBadRequest) + return + } + hs.SetHeader(w.Header()) + + peer, err := hs.PeerID() + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + return + } + + if a.Next == nil { + w.WriteHeader(http.StatusOK) + return + } + a.Next(peer, w, r) +} diff --git a/p2p/http/example_test.go b/p2p/http/example_test.go index 7073b7f0e3..8e94f6e7e0 100644 --- a/p2p/http/example_test.go +++ b/p2p/http/example_test.go @@ -6,6 +6,7 @@ import ( "log" "net" "net/http" + "regexp" "strings" "github.com/libp2p/go-libp2p" @@ -125,18 +126,24 @@ func ExampleHost_overLibp2pStreams() { // Output: Hello HTTP } +var tcpPortRE = regexp.MustCompile(`/tcp/(\d+)`) + func ExampleHost_Serve() { server := libp2phttp.Host{ InsecureAllowHTTP: true, // For our example, we'll allow insecure HTTP - ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/50221/http")}, + ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/0/http")}, } go server.Serve() defer server.Close() - fmt.Println(server.Addrs()) + for _, a := range server.Addrs() { + s := a.String() + addrWithoutSpecificPort := tcpPortRE.ReplaceAllString(s, "/tcp/") + fmt.Println(addrWithoutSpecificPort) + } - // Output: [/ip4/127.0.0.1/tcp/50221/http] + // Output: /ip4/127.0.0.1/tcp//http } func ExampleHost_SetHTTPHandler() { diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index d2a544d224..4dad47dd43 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -359,6 +359,7 @@ func (h *Host) Serve() error { expectedErrCount := len(h.httpTransport.listeners) select { case <-h.httpTransport.closeListeners: + err = http.ErrServerClosed case err = <-errCh: expectedErrCount-- } diff --git a/p2p/http/libp2phttp_test.go b/p2p/http/libp2phttp_test.go index fa8687e308..ae3285b9a3 100644 --- a/p2p/http/libp2phttp_test.go +++ b/p2p/http/libp2phttp_test.go @@ -30,6 +30,7 @@ import ( httpping "github.com/libp2p/go-libp2p/p2p/http/ping" libp2pquic "github.com/libp2p/go-libp2p/p2p/transport/quic" ma "github.com/multiformats/go-multiaddr" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -996,3 +997,20 @@ func TestImpliedHostIsSet(t *testing.T) { } } + +func TestErrServerClosed(t *testing.T) { + server := libp2phttp.Host{ + InsecureAllowHTTP: true, + ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/0/http")}, + } + + done := make(chan struct{}) + go func() { + err := server.Serve() + assert.Equal(t, http.ErrServerClosed, err) + close(done) + }() + + server.Close() + <-done +} diff --git a/p2p/net/nat/mock_nat_test.go b/p2p/net/nat/mock_nat_test.go index 3e453f65fa..c8023344ea 100644 --- a/p2p/net/nat/mock_nat_test.go +++ b/p2p/net/nat/mock_nat_test.go @@ -22,6 +22,7 @@ import ( type MockNAT struct { ctrl *gomock.Controller recorder *MockNATMockRecorder + isgomock struct{} } // MockNATMockRecorder is the mock recorder for MockNAT. @@ -42,32 +43,32 @@ func (m *MockNAT) EXPECT() *MockNATMockRecorder { } // AddPortMapping mocks base method. -func (m *MockNAT) AddPortMapping(arg0 context.Context, arg1 string, arg2 int, arg3 string, arg4 time.Duration) (int, error) { +func (m *MockNAT) AddPortMapping(ctx context.Context, protocol string, internalPort int, description string, timeout time.Duration) (int, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddPortMapping", arg0, arg1, arg2, arg3, arg4) + ret := m.ctrl.Call(m, "AddPortMapping", ctx, protocol, internalPort, description, timeout) ret0, _ := ret[0].(int) ret1, _ := ret[1].(error) return ret0, ret1 } // AddPortMapping indicates an expected call of AddPortMapping. -func (mr *MockNATMockRecorder) AddPortMapping(arg0, arg1, arg2, arg3, arg4 any) *gomock.Call { +func (mr *MockNATMockRecorder) AddPortMapping(ctx, protocol, internalPort, description, timeout any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddPortMapping", reflect.TypeOf((*MockNAT)(nil).AddPortMapping), arg0, arg1, arg2, arg3, arg4) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddPortMapping", reflect.TypeOf((*MockNAT)(nil).AddPortMapping), ctx, protocol, internalPort, description, timeout) } // DeletePortMapping mocks base method. -func (m *MockNAT) DeletePortMapping(arg0 context.Context, arg1 string, arg2 int) error { +func (m *MockNAT) DeletePortMapping(ctx context.Context, protocol string, internalPort int) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeletePortMapping", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "DeletePortMapping", ctx, protocol, internalPort) ret0, _ := ret[0].(error) return ret0 } // DeletePortMapping indicates an expected call of DeletePortMapping. -func (mr *MockNATMockRecorder) DeletePortMapping(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockNATMockRecorder) DeletePortMapping(ctx, protocol, internalPort any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePortMapping", reflect.TypeOf((*MockNAT)(nil).DeletePortMapping), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePortMapping", reflect.TypeOf((*MockNAT)(nil).DeletePortMapping), ctx, protocol, internalPort) } // GetDeviceAddress mocks base method. diff --git a/p2p/net/pnet/psk_conn.go b/p2p/net/pnet/psk_conn.go index c600c8d093..b36d434904 100644 --- a/p2p/net/pnet/psk_conn.go +++ b/p2p/net/pnet/psk_conn.go @@ -3,6 +3,7 @@ package pnet import ( "crypto/cipher" "crypto/rand" + "fmt" "io" "net" @@ -33,7 +34,7 @@ func (c *pskConn) Read(out []byte) (int, error) { nonce := make([]byte, 24) _, err := io.ReadFull(c.Conn, nonce) if err != nil { - return 0, errShortNonce + return 0, fmt.Errorf("%w: %w", errShortNonce, err) } c.readS20 = salsa20.New(c.psk, nonce) } diff --git a/p2p/net/swarm/dial_test.go b/p2p/net/swarm/dial_test.go index 3b139ef389..feb9f49174 100644 --- a/p2p/net/swarm/dial_test.go +++ b/p2p/net/swarm/dial_test.go @@ -52,7 +52,7 @@ func TestBasicDialPeerWithResolver(t *testing.T) { resolver, err := madns.NewResolver(madns.WithDomainResolver("example.com", &mockResolver)) require.NoError(t, err) - swarms := makeSwarms(t, 2, swarmt.WithSwarmOpts(swarm.WithMultiaddrResolver(resolver))) + swarms := makeSwarms(t, 2, swarmt.WithSwarmOpts(swarm.WithMultiaddrResolver(swarm.ResolverFromMaDNS{resolver}))) defer closeSwarms(swarms) s1 := swarms[0] s2 := swarms[1] diff --git a/p2p/net/swarm/dial_worker.go b/p2p/net/swarm/dial_worker.go index 360a99e2ab..9d097f3c26 100644 --- a/p2p/net/swarm/dial_worker.go +++ b/p2p/net/swarm/dial_worker.go @@ -162,7 +162,7 @@ loop: case req, ok := <-w.reqch: if !ok { if w.s.metricsTracer != nil { - w.s.metricsTracer.DialCompleted(w.connected, totalDials) + w.s.metricsTracer.DialCompleted(w.connected, totalDials, time.Since(startTime)) } return } diff --git a/p2p/net/swarm/dial_worker_test.go b/p2p/net/swarm/dial_worker_test.go index ed4f00ff58..d264fd1230 100644 --- a/p2p/net/swarm/dial_worker_test.go +++ b/p2p/net/swarm/dial_worker_test.go @@ -84,7 +84,7 @@ func makeSwarmWithNoListenAddrs(t *testing.T, opts ...Option) *Swarm { upgrader := makeUpgrader(t, s) var tcpOpts []tcp.Option tcpOpts = append(tcpOpts, tcp.DisableReuseport()) - tcpTransport, err := tcp.NewTCPTransport(upgrader, nil, tcpOpts...) + tcpTransport, err := tcp.NewTCPTransport(upgrader, nil, nil, tcpOpts...) require.NoError(t, err) if err := s.AddTransport(tcpTransport); err != nil { t.Fatal(err) diff --git a/p2p/net/swarm/resolve_test.go b/p2p/net/swarm/resolve_test.go new file mode 100644 index 0000000000..1921e9433d --- /dev/null +++ b/p2p/net/swarm/resolve_test.go @@ -0,0 +1,120 @@ +package swarm + +import ( + "context" + "net" + "strconv" + "testing" + + "github.com/multiformats/go-multiaddr" + madns "github.com/multiformats/go-multiaddr-dns" + "github.com/stretchr/testify/require" +) + +func TestSwarmResolver(t *testing.T) { + mockResolver := madns.MockResolver{IP: make(map[string][]net.IPAddr)} + ipaddr, err := net.ResolveIPAddr("ip4", "127.0.0.1") + require.NoError(t, err) + mockResolver.IP["example.com"] = []net.IPAddr{*ipaddr} + mockResolver.TXT = map[string][]string{ + "_dnsaddr.example.com": {"dnsaddr=/ip4/127.0.0.1"}, + } + madnsResolver, err := madns.NewResolver(madns.WithDomainResolver("example.com", &mockResolver)) + require.NoError(t, err) + swarmResolver := ResolverFromMaDNS{madnsResolver} + + ctx := context.Background() + res, err := swarmResolver.ResolveDNSComponent(ctx, multiaddr.StringCast("/dns/example.com"), 10) + require.NoError(t, err) + require.Equal(t, 1, len(res)) + require.Equal(t, "/ip4/127.0.0.1", res[0].String()) + + res, err = swarmResolver.ResolveDNSAddr(ctx, "", multiaddr.StringCast("/dnsaddr/example.com"), 1, 10) + require.NoError(t, err) + require.Equal(t, 1, len(res)) + require.Equal(t, "/ip4/127.0.0.1", res[0].String()) + + t.Run("Test Limits", func(t *testing.T) { + var ipaddrs []net.IPAddr + var manyDNSAddrs []string + for i := 0; i < 255; i++ { + ip := "1.2.3." + strconv.Itoa(i) + ipaddrs = append(ipaddrs, net.IPAddr{IP: net.ParseIP(ip)}) + manyDNSAddrs = append(manyDNSAddrs, "dnsaddr=/ip4/"+ip) + } + + mockResolver.IP = map[string][]net.IPAddr{ + "example.com": ipaddrs, + } + mockResolver.TXT = map[string][]string{ + "_dnsaddr.example.com": manyDNSAddrs, + } + + res, err := swarmResolver.ResolveDNSComponent(ctx, multiaddr.StringCast("/dns/example.com"), 10) + require.NoError(t, err) + require.Equal(t, 10, len(res)) + for i := 0; i < 10; i++ { + require.Equal(t, "/ip4/1.2.3."+strconv.Itoa(i), res[i].String()) + } + + res, err = swarmResolver.ResolveDNSAddr(ctx, "", multiaddr.StringCast("/dnsaddr/example.com"), 1, 10) + require.NoError(t, err) + require.Equal(t, 10, len(res)) + for i := 0; i < 10; i++ { + require.Equal(t, "/ip4/1.2.3."+strconv.Itoa(i), res[i].String()) + } + }) + + t.Run("Test Recursive Limits", func(t *testing.T) { + recursiveDNSAddr := make(map[string][]string) + for i := 0; i < 255; i++ { + recursiveDNSAddr["_dnsaddr."+strconv.Itoa(i)+".example.com"] = []string{"dnsaddr=/dnsaddr/" + strconv.Itoa(i+1) + ".example.com"} + } + recursiveDNSAddr["_dnsaddr.255.example.com"] = []string{"dnsaddr=/ip4/127.0.0.1"} + mockResolver.TXT = recursiveDNSAddr + + res, err = swarmResolver.ResolveDNSAddr(ctx, "", multiaddr.StringCast("/dnsaddr/0.example.com"), 256, 10) + require.NoError(t, err) + require.Equal(t, 1, len(res)) + require.Equal(t, "/ip4/127.0.0.1", res[0].String()) + + res, err = swarmResolver.ResolveDNSAddr(ctx, "", multiaddr.StringCast("/dnsaddr/0.example.com"), 255, 10) + require.NoError(t, err) + require.Equal(t, 1, len(res)) + require.Equal(t, "/dnsaddr/255.example.com", res[0].String()) + }) + + t.Run("Test Resolve at output limit", func(t *testing.T) { + recursiveDNSAddr := make(map[string][]string) + recursiveDNSAddr["_dnsaddr.example.com"] = []string{ + "dnsaddr=/dnsaddr/0.example.com", + "dnsaddr=/dnsaddr/1.example.com", + "dnsaddr=/dnsaddr/2.example.com", + "dnsaddr=/dnsaddr/3.example.com", + "dnsaddr=/dnsaddr/4.example.com", + "dnsaddr=/dnsaddr/5.example.com", + "dnsaddr=/dnsaddr/6.example.com", + "dnsaddr=/dnsaddr/7.example.com", + "dnsaddr=/dnsaddr/8.example.com", + "dnsaddr=/dnsaddr/9.example.com", + } + recursiveDNSAddr["_dnsaddr.0.example.com"] = []string{"dnsaddr=/ip4/127.0.0.1"} + recursiveDNSAddr["_dnsaddr.1.example.com"] = []string{"dnsaddr=/ip4/127.0.0.1"} + recursiveDNSAddr["_dnsaddr.2.example.com"] = []string{"dnsaddr=/ip4/127.0.0.1"} + recursiveDNSAddr["_dnsaddr.3.example.com"] = []string{"dnsaddr=/ip4/127.0.0.1"} + recursiveDNSAddr["_dnsaddr.4.example.com"] = []string{"dnsaddr=/ip4/127.0.0.1"} + recursiveDNSAddr["_dnsaddr.5.example.com"] = []string{"dnsaddr=/ip4/127.0.0.1"} + recursiveDNSAddr["_dnsaddr.6.example.com"] = []string{"dnsaddr=/ip4/127.0.0.1"} + recursiveDNSAddr["_dnsaddr.7.example.com"] = []string{"dnsaddr=/ip4/127.0.0.1"} + recursiveDNSAddr["_dnsaddr.8.example.com"] = []string{"dnsaddr=/ip4/127.0.0.1"} + recursiveDNSAddr["_dnsaddr.9.example.com"] = []string{"dnsaddr=/ip4/127.0.0.1"} + mockResolver.TXT = recursiveDNSAddr + + res, err = swarmResolver.ResolveDNSAddr(ctx, "", multiaddr.StringCast("/dnsaddr/example.com"), 256, 10) + require.NoError(t, err) + require.Equal(t, 10, len(res)) + for _, r := range res { + require.Equal(t, "/ip4/127.0.0.1", r.String()) + } + }) +} diff --git a/p2p/net/swarm/swarm.go b/p2p/net/swarm/swarm.go index 0127555552..ef1fc2a2b3 100644 --- a/p2p/net/swarm/swarm.go +++ b/p2p/net/swarm/swarm.go @@ -10,6 +10,8 @@ import ( "sync/atomic" "time" + "slices" + "github.com/libp2p/go-libp2p/core/connmgr" "github.com/libp2p/go-libp2p/core/event" "github.com/libp2p/go-libp2p/core/metrics" @@ -17,7 +19,6 @@ import ( "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/peerstore" "github.com/libp2p/go-libp2p/core/transport" - "golang.org/x/exp/slices" logging "github.com/ipfs/go-log/v2" ma "github.com/multiformats/go-multiaddr" @@ -60,9 +61,9 @@ func WithConnectionGater(gater connmgr.ConnectionGater) Option { } // WithMultiaddrResolver sets a custom multiaddress resolver -func WithMultiaddrResolver(maResolver *madns.Resolver) Option { +func WithMultiaddrResolver(resolver network.MultiaddrDNSResolver) Option { return func(s *Swarm) error { - s.maResolver = maResolver + s.multiaddrResolver = resolver return nil } } @@ -196,7 +197,7 @@ type Swarm struct { m map[int]transport.Transport } - maResolver *madns.Resolver + multiaddrResolver network.MultiaddrDNSResolver // stream handlers streamh atomic.Pointer[network.StreamHandler] @@ -231,15 +232,15 @@ func NewSwarm(local peer.ID, peers peerstore.Peerstore, eventBus event.Bus, opts } ctx, cancel := context.WithCancel(context.Background()) s := &Swarm{ - local: local, - peers: peers, - emitter: emitter, - ctx: ctx, - ctxCancel: cancel, - dialTimeout: defaultDialTimeout, - dialTimeoutLocal: defaultDialTimeoutLocal, - maResolver: madns.DefaultResolver, - dialRanker: DefaultDialRanker, + local: local, + peers: peers, + emitter: emitter, + ctx: ctx, + ctxCancel: cancel, + dialTimeout: defaultDialTimeout, + dialTimeoutLocal: defaultDialTimeoutLocal, + multiaddrResolver: ResolverFromMaDNS{madns.DefaultResolver}, + dialRanker: DefaultDialRanker, // A black hole is a binary property. On a network if UDP dials are blocked or there is // no IPv6 connectivity, all dials will fail. So a low success rate of 5 out 100 dials @@ -624,7 +625,6 @@ func isBetterConn(a, b *Conn) bool { // bestConnToPeer returns the best connection to peer. func (s *Swarm) bestConnToPeer(p peer.ID) *Conn { - // TODO: Prefer some transports over others. // For now, prefers direct connections over Relayed connections. // For tie-breaking, select the newest non-closed connection with the most streams. @@ -813,8 +813,10 @@ func (s *Swarm) ResourceManager() network.ResourceManager { } // Swarm is a Network. -var _ network.Network = (*Swarm)(nil) -var _ transport.TransportNetwork = (*Swarm)(nil) +var ( + _ network.Network = (*Swarm)(nil) + _ transport.TransportNetwork = (*Swarm)(nil) +) type connWithMetrics struct { transport.CapableConn @@ -846,3 +848,103 @@ func (c connWithMetrics) Stat() network.ConnStats { } var _ network.ConnStat = connWithMetrics{} + +type ResolverFromMaDNS struct { + *madns.Resolver +} + +var _ network.MultiaddrDNSResolver = ResolverFromMaDNS{} + +func startsWithDNSADDR(m ma.Multiaddr) bool { + if m == nil { + return false + } + + startsWithDNSADDR := false + // Using ForEach to avoid allocating + ma.ForEach(m, func(c ma.Component) bool { + startsWithDNSADDR = c.Protocol().Code == ma.P_DNSADDR + return false + }) + return startsWithDNSADDR +} + +// ResolveDNSAddr implements MultiaddrDNSResolver +func (r ResolverFromMaDNS) ResolveDNSAddr(ctx context.Context, expectedPeerID peer.ID, maddr ma.Multiaddr, recursionLimit int, outputLimit int) ([]ma.Multiaddr, error) { + if outputLimit <= 0 { + return nil, nil + } + if recursionLimit <= 0 { + return []ma.Multiaddr{maddr}, nil + } + var resolved, toResolve []ma.Multiaddr + addrs, err := r.Resolve(ctx, maddr) + if err != nil { + return nil, err + } + if len(addrs) > outputLimit { + addrs = addrs[:outputLimit] + } + + for _, addr := range addrs { + if startsWithDNSADDR(addr) { + toResolve = append(toResolve, addr) + } else { + resolved = append(resolved, addr) + } + } + + for i, addr := range toResolve { + // Set the nextOutputLimit to: + // outputLimit + // - len(resolved) // What we already have resolved + // - (len(toResolve) - i) // How many addresses we have left to resolve + // + 1 // The current address we are resolving + // This assumes that each DNSADDR address will resolve to at least one multiaddr. + // This assumption lets us bound the space we reserve for resolving. + nextOutputLimit := outputLimit - len(resolved) - (len(toResolve) - i) + 1 + resolvedAddrs, err := r.ResolveDNSAddr(ctx, expectedPeerID, addr, recursionLimit-1, nextOutputLimit) + if err != nil { + log.Warnf("failed to resolve dnsaddr %v %s: ", addr, err) + // Dropping this address + continue + } + resolved = append(resolved, resolvedAddrs...) + } + + if len(resolved) > outputLimit { + resolved = resolved[:outputLimit] + } + + // If the address contains a peer id, make sure it matches our expectedPeerID + if expectedPeerID != "" { + removeMismatchPeerID := func(a ma.Multiaddr) bool { + id, err := peer.IDFromP2PAddr(a) + if err == peer.ErrInvalidAddr { + // This multiaddr didn't contain a peer id, assume it's for this peer. + // Handshake will fail later if it's not. + return false + } else if err != nil { + // This multiaddr is invalid, drop it. + return true + } + + return id != expectedPeerID + } + resolved = slices.DeleteFunc(resolved, removeMismatchPeerID) + } + + return resolved, nil +} + +// ResolveDNSComponent implements MultiaddrDNSResolver +func (r ResolverFromMaDNS) ResolveDNSComponent(ctx context.Context, maddr ma.Multiaddr, outputLimit int) ([]ma.Multiaddr, error) { + addrs, err := r.Resolve(ctx, maddr) + if err != nil { + return nil, err + } + if len(addrs) > outputLimit { + addrs = addrs[:outputLimit] + } + return addrs, nil +} diff --git a/p2p/net/swarm/swarm_addr_test.go b/p2p/net/swarm/swarm_addr_test.go index 435866e920..43e76716e5 100644 --- a/p2p/net/swarm/swarm_addr_test.go +++ b/p2p/net/swarm/swarm_addr_test.go @@ -79,7 +79,7 @@ func TestDialAddressSelection(t *testing.T) { s, err := swarm.NewSwarm("local", nil, eventbus.NewBus()) require.NoError(t, err) - tcpTr, err := tcp.NewTCPTransport(nil, nil) + tcpTr, err := tcp.NewTCPTransport(nil, nil, nil) require.NoError(t, err) require.NoError(t, s.AddTransport(tcpTr)) reuse, err := quicreuse.NewConnManager(quic.StatelessResetKey{}, quic.TokenGeneratorKey{}) diff --git a/p2p/net/swarm/swarm_dial.go b/p2p/net/swarm/swarm_dial.go index 446ece4504..35fc567fd8 100644 --- a/p2p/net/swarm/swarm_dial.go +++ b/p2p/net/swarm/swarm_dial.go @@ -16,14 +16,15 @@ import ( "github.com/libp2p/go-libp2p/core/transport" ma "github.com/multiformats/go-multiaddr" - madns "github.com/multiformats/go-multiaddr-dns" mafmt "github.com/multiformats/go-multiaddr-fmt" manet "github.com/multiformats/go-multiaddr/net" ) -// The maximum number of address resolution steps we'll perform for a single -// peer (for all addresses). -const maxAddressResolution = 32 +// The maximum number of addresses we'll return when resolving all of a peer's +// address +const maximumResolvedAddresses = 100 + +const maximumDNSADDRRecursion = 4 // Diagram of dial sync: // @@ -302,10 +303,7 @@ func (s *Swarm) addrsForDial(ctx context.Context, p peer.ID) (goodAddrs []ma.Mul } // Resolve dns or dnsaddrs - resolved, err := s.resolveAddrs(ctx, peer.AddrInfo{ID: p, Addrs: peerAddrs}) - if err != nil { - return nil, nil, err - } + resolved := s.resolveAddrs(ctx, peer.AddrInfo{ID: p, Addrs: peerAddrs}) goodAddrs = ma.Unique(resolved) goodAddrs, addrErrs = s.filterKnownUndialables(p, goodAddrs) @@ -322,84 +320,142 @@ func (s *Swarm) addrsForDial(ctx context.Context, p peer.ID) (goodAddrs []ma.Mul return goodAddrs, addrErrs, nil } -func (s *Swarm) resolveAddrs(ctx context.Context, pi peer.AddrInfo) ([]ma.Multiaddr, error) { - p2paddr, err := ma.NewMultiaddr("/" + ma.ProtocolWithCode(ma.P_P2P).Name + "/" + pi.ID.String()) - if err != nil { - return nil, err +func startsWithDNSComponent(m ma.Multiaddr) bool { + if m == nil { + return false } - - var resolveSteps int - // Recursively resolve all addrs. - // - // While the toResolve list is non-empty: - // * Pop an address off. - // * If the address is fully resolved, add it to the resolved list. - // * Otherwise, resolve it and add the results to the "to resolve" list. - toResolve := append([]ma.Multiaddr{}, pi.Addrs...) - resolved := make([]ma.Multiaddr, 0, len(pi.Addrs)) - for len(toResolve) > 0 { - // pop the last addr off. - addr := toResolve[len(toResolve)-1] - toResolve = toResolve[:len(toResolve)-1] - - // if it's resolved, add it to the resolved list. - if !madns.Matches(addr) { - resolved = append(resolved, addr) - continue + startsWithDNS := false + // Using ForEach to avoid allocating + ma.ForEach(m, func(c ma.Component) bool { + switch c.Protocol().Code { + case ma.P_DNS, ma.P_DNS4, ma.P_DNS6: + startsWithDNS = true } - resolveSteps++ - - // We've resolved too many addresses. We can keep all the fully - // resolved addresses but we'll need to skip the rest. - if resolveSteps >= maxAddressResolution { - log.Warnf( - "peer %s asked us to resolve too many addresses: %s/%s", - pi.ID, - resolveSteps, - maxAddressResolution, - ) - continue + return false + }) + return startsWithDNS +} + +func stripP2PComponent(addrs []ma.Multiaddr) []ma.Multiaddr { + for i, addr := range addrs { + if id, _ := peer.IDFromP2PAddr(addr); id != "" { + addrs[i], _ = ma.SplitLast(addr) } + } + return addrs +} - tpt := s.TransportForDialing(addr) - resolver, ok := tpt.(transport.Resolver) - if ok { - resolvedAddrs, err := resolver.Resolve(ctx, addr) - if err != nil { - log.Warnf("Failed to resolve multiaddr %s by transport %v: %v", addr, tpt, err) +type resolver struct { + canResolve func(ma.Multiaddr) bool + resolve func(ctx context.Context, maddr ma.Multiaddr, outputLimit int) ([]ma.Multiaddr, error) +} + +type resolveErr struct { + addr ma.Multiaddr + err error +} + +func chainResolvers(ctx context.Context, addrs []ma.Multiaddr, outputLimit int, resolvers []resolver) ([]ma.Multiaddr, []resolveErr) { + nextAddrs := make([]ma.Multiaddr, 0, len(addrs)) + errs := make([]resolveErr, 0) + for _, r := range resolvers { + for _, a := range addrs { + if !r.canResolve(a) { + nextAddrs = append(nextAddrs, a) continue } - var added bool - for _, a := range resolvedAddrs { - if !addr.Equal(a) { - toResolve = append(toResolve, a) - added = true - } + if len(nextAddrs) >= outputLimit { + nextAddrs = nextAddrs[:outputLimit] + break } - if added { + next, err := r.resolve(ctx, a, outputLimit-len(nextAddrs)) + if err != nil { + errs = append(errs, resolveErr{addr: a, err: err}) continue } + nextAddrs = append(nextAddrs, next...) } + addrs, nextAddrs = nextAddrs, addrs + nextAddrs = nextAddrs[:0] + } + return addrs, errs +} - // otherwise, resolve it - reqaddr := addr.Encapsulate(p2paddr) - resaddrs, err := s.maResolver.Resolve(ctx, reqaddr) - if err != nil { - log.Infof("error resolving %s: %s", reqaddr, err) - } +// resolveAddrs resolves DNS/DNSADDR components in the given peer's addresses. +// We want to resolve the DNS components to IP addresses becase we want the +// swarm to manage ranking and dialing multiple connections, and a single DNS +// address can resolve to multiple IP addresses. +func (s *Swarm) resolveAddrs(ctx context.Context, pi peer.AddrInfo) []ma.Multiaddr { + dnsAddrResolver := resolver{ + canResolve: startsWithDNSADDR, + resolve: func(ctx context.Context, maddr ma.Multiaddr, outputLimit int) ([]ma.Multiaddr, error) { + return s.multiaddrResolver.ResolveDNSAddr(ctx, pi.ID, maddr, maximumDNSADDRRecursion, outputLimit) + }, + } + + var skipped []ma.Multiaddr + skipResolver := resolver{ + canResolve: func(addr ma.Multiaddr) bool { + tpt := s.TransportForDialing(addr) + if tpt == nil { + return false + } + _, ok := tpt.(transport.SkipResolver) + return ok + + }, + resolve: func(ctx context.Context, addr ma.Multiaddr, outputLimit int) ([]ma.Multiaddr, error) { + tpt := s.TransportForDialing(addr) + resolver, ok := tpt.(transport.SkipResolver) + if !ok { + return []ma.Multiaddr{addr}, nil + } + if resolver.SkipResolve(ctx, addr) { + skipped = append(skipped, addr) + return nil, nil + } + return []ma.Multiaddr{addr}, nil + }, + } - // add the results to the toResolve list. - for _, res := range resaddrs { - pi, err := peer.AddrInfoFromP2pAddr(res) + tptResolver := resolver{ + canResolve: func(addr ma.Multiaddr) bool { + tpt := s.TransportForDialing(addr) + if tpt == nil { + return false + } + _, ok := tpt.(transport.Resolver) + return ok + }, + resolve: func(ctx context.Context, addr ma.Multiaddr, outputLimit int) ([]ma.Multiaddr, error) { + tpt := s.TransportForDialing(addr) + resolver, ok := tpt.(transport.Resolver) + if !ok { + return []ma.Multiaddr{addr}, nil + } + addrs, err := resolver.Resolve(ctx, addr) if err != nil { - log.Infof("error parsing %s: %s", res, err) + return nil, err } - toResolve = append(toResolve, pi.Addrs...) - } + if len(addrs) > outputLimit { + addrs = addrs[:outputLimit] + } + return addrs, nil + }, } - return resolved, nil + dnsResolver := resolver{ + canResolve: startsWithDNSComponent, + resolve: s.multiaddrResolver.ResolveDNSComponent, + } + addrs, errs := chainResolvers(ctx, pi.Addrs, maximumResolvedAddresses, []resolver{dnsAddrResolver, skipResolver, tptResolver, dnsResolver}) + for _, err := range errs { + log.Warnf("Failed to resolve addr %s: %v", err.addr, err.err) + } + // Add skipped addresses back to the resolved addresses + addrs = append(addrs, skipped...) + return stripP2PComponent(addrs) } func (s *Swarm) dialNextAddr(ctx context.Context, p peer.ID, addr ma.Multiaddr, resch chan transport.DialUpdate) error { @@ -571,7 +627,7 @@ func (s *Swarm) dialAddr(ctx context.Context, p peer.ID, addr ma.Multiaddr, updC // Trust the transport? Yeah... right. if connC.RemotePeer() != p { connC.Close() - err = fmt.Errorf("BUG in transport %T: tried to dial %s, dialed %s", p, connC.RemotePeer(), tpt) + err = fmt.Errorf("BUG in transport %T: tried to dial %s, dialed %s", tpt, p, connC.RemotePeer()) log.Error(err) return nil, err } diff --git a/p2p/net/swarm/swarm_dial_test.go b/p2p/net/swarm/swarm_dial_test.go index f4c33170a9..add6f5cbba 100644 --- a/p2p/net/swarm/swarm_dial_test.go +++ b/p2p/net/swarm/swarm_dial_test.go @@ -53,9 +53,9 @@ func TestAddrsForDial(t *testing.T) { ps.AddPrivKey(id, priv) t.Cleanup(func() { ps.Close() }) - tpt, err := websocket.New(nil, &network.NullResourceManager{}) + tpt, err := websocket.New(nil, &network.NullResourceManager{}, nil) require.NoError(t, err) - s, err := NewSwarm(id, ps, eventbus.NewBus(), WithMultiaddrResolver(resolver)) + s, err := NewSwarm(id, ps, eventbus.NewBus(), WithMultiaddrResolver(ResolverFromMaDNS{resolver})) require.NoError(t, err) defer s.Close() err = s.AddTransport(tpt) @@ -96,11 +96,11 @@ func TestDedupAddrsForDial(t *testing.T) { ps.AddPrivKey(id, priv) t.Cleanup(func() { ps.Close() }) - s, err := NewSwarm(id, ps, eventbus.NewBus(), WithMultiaddrResolver(resolver)) + s, err := NewSwarm(id, ps, eventbus.NewBus(), WithMultiaddrResolver(ResolverFromMaDNS{resolver})) require.NoError(t, err) defer s.Close() - tpt, err := tcp.NewTCPTransport(nil, &network.NullResourceManager{}) + tpt, err := tcp.NewTCPTransport(nil, &network.NullResourceManager{}, nil) require.NoError(t, err) err = s.AddTransport(tpt) require.NoError(t, err) @@ -127,14 +127,14 @@ func newTestSwarmWithResolver(t *testing.T, resolver *madns.Resolver) *Swarm { ps.AddPubKey(id, priv.GetPublic()) ps.AddPrivKey(id, priv) t.Cleanup(func() { ps.Close() }) - s, err := NewSwarm(id, ps, eventbus.NewBus(), WithMultiaddrResolver(resolver)) + s, err := NewSwarm(id, ps, eventbus.NewBus(), WithMultiaddrResolver(ResolverFromMaDNS{resolver})) require.NoError(t, err) t.Cleanup(func() { s.Close() }) // Add a tcp transport so that we know we can dial a tcp multiaddr and we don't filter it out. - tpt, err := tcp.NewTCPTransport(nil, &network.NullResourceManager{}) + tpt, err := tcp.NewTCPTransport(nil, &network.NullResourceManager{}, nil) require.NoError(t, err) err = s.AddTransport(tpt) require.NoError(t, err) @@ -151,7 +151,7 @@ func newTestSwarmWithResolver(t *testing.T, resolver *madns.Resolver) *Swarm { err = s.AddTransport(wtTpt) require.NoError(t, err) - wsTpt, err := websocket.New(nil, &network.NullResourceManager{}) + wsTpt, err := websocket.New(nil, &network.NullResourceManager{}, nil) require.NoError(t, err) err = s.AddTransport(wsTpt) require.NoError(t, err) @@ -398,3 +398,18 @@ func TestBlackHoledAddrBlocked(t *testing.T) { } require.ErrorIs(t, err, ErrDialRefusedBlackHole) } + +func TestSkipDialingManyDNS(t *testing.T) { + resolver, err := madns.NewResolver() + if err != nil { + t.Fatal(err) + } + s := newTestSwarmWithResolver(t, resolver) + defer s.Close() + id := test.RandPeerIDFatal(t) + addr := ma.StringCast("/dns/example.com/udp/1234/p2p-circuit/dns/example.com/p2p-circuit/dns/example.com") + + resolved := s.resolveAddrs(context.Background(), peer.AddrInfo{ID: id, Addrs: []ma.Multiaddr{addr}}) + require.NoError(t, err) + require.Less(t, len(resolved), 3) +} diff --git a/p2p/net/swarm/swarm_metrics.go b/p2p/net/swarm/swarm_metrics.go index bedc936d95..2413e9faed 100644 --- a/p2p/net/swarm/swarm_metrics.go +++ b/p2p/net/swarm/swarm_metrics.go @@ -77,6 +77,15 @@ var ( }, []string{"outcome", "num_dials"}, ) + dialLatency = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: metricNamespace, + Name: "dial_latency_seconds", + Help: "time taken to establish connection with the peer", + Buckets: []float64{0.001, 0.01, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.75, 1, 2}, + }, + []string{"outcome", "num_dials"}, + ) dialRankingDelay = prometheus.NewHistogram( prometheus.HistogramOpts{ Namespace: metricNamespace, @@ -118,6 +127,7 @@ var ( connHandshakeLatency, dialsPerPeer, dialRankingDelay, + dialLatency, blackHoleSuccessCounterSuccessFraction, blackHoleSuccessCounterState, blackHoleSuccessCounterNextRequestAllowedAfter, @@ -129,7 +139,7 @@ type MetricsTracer interface { ClosedConnection(network.Direction, time.Duration, network.ConnectionState, ma.Multiaddr) CompletedHandshake(time.Duration, network.ConnectionState, ma.Multiaddr) FailedDialing(ma.Multiaddr, error, error) - DialCompleted(success bool, totalDials int) + DialCompleted(success bool, totalDials int, latency time.Duration) DialRankingDelay(d time.Duration) UpdatedBlackHoleSuccessCounter(name string, state BlackHoleState, nextProbeAfter int, successFraction float64) } @@ -250,7 +260,7 @@ func (m *metricsTracer) FailedDialing(addr ma.Multiaddr, dialErr error, cause er dialError.WithLabelValues(*tags...).Inc() } -func (m *metricsTracer) DialCompleted(success bool, totalDials int) { +func (m *metricsTracer) DialCompleted(success bool, totalDials int, latency time.Duration) { tags := metricshelper.GetStringSlice() defer metricshelper.PutStringSlice(tags) if success { @@ -268,6 +278,7 @@ func (m *metricsTracer) DialCompleted(success bool, totalDials int) { } *tags = append(*tags, numDials) dialsPerPeer.WithLabelValues(*tags...).Inc() + dialLatency.WithLabelValues(*tags...).Observe(latency.Seconds()) } func (m *metricsTracer) DialRankingDelay(d time.Duration) { diff --git a/p2p/net/swarm/swarm_metrics_test.go b/p2p/net/swarm/swarm_metrics_test.go index ef136df28a..151765931b 100644 --- a/p2p/net/swarm/swarm_metrics_test.go +++ b/p2p/net/swarm/swarm_metrics_test.go @@ -92,7 +92,7 @@ func TestMetricsNoAllocNoCover(t *testing.T) { mt.CompletedHandshake(time.Duration(mrand.Intn(100))*time.Second, randItem(connections), randItem(addrs)) }, "FailedDialing": func() { mt.FailedDialing(randItem(addrs), randItem(errors), randItem(errors)) }, - "DialCompleted": func() { mt.DialCompleted(mrand.Intn(2) == 1, mrand.Intn(10)) }, + "DialCompleted": func() { mt.DialCompleted(mrand.Intn(2) == 1, mrand.Intn(10), time.Duration(mrand.Intn(1000_000_000))) }, "DialRankingDelay": func() { mt.DialRankingDelay(time.Duration(mrand.Intn(1e10))) }, "UpdatedBlackHoleSuccessCounter": func() { mt.UpdatedBlackHoleSuccessCounter( diff --git a/p2p/net/swarm/swarm_transport.go b/p2p/net/swarm/swarm_transport.go index 924f0384aa..3aaa335cbc 100644 --- a/p2p/net/swarm/swarm_transport.go +++ b/p2p/net/swarm/swarm_transport.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/transport" ma "github.com/multiformats/go-multiaddr" @@ -12,6 +13,9 @@ import ( // TransportForDialing retrieves the appropriate transport for dialing the given // multiaddr. func (s *Swarm) TransportForDialing(a ma.Multiaddr) transport.Transport { + if a == nil { + return nil + } protocols := a.Protocols() if len(protocols) == 0 { return nil @@ -30,6 +34,13 @@ func (s *Swarm) TransportForDialing(a ma.Multiaddr) transport.Transport { if isRelayAddr(a) { return s.transports.m[ma.P_CIRCUIT] } + if id, _ := peer.IDFromP2PAddr(a); id != "" { + // This addr has a p2p component. Drop it so we can check transport. + a, _ = ma.SplitLast(a) + if a == nil { + return nil + } + } for _, t := range s.transports.m { if t.CanDial(a) { return t diff --git a/p2p/net/swarm/testing/testing.go b/p2p/net/swarm/testing/testing.go index 2bbe8b27a5..773314a1b8 100644 --- a/p2p/net/swarm/testing/testing.go +++ b/p2p/net/swarm/testing/testing.go @@ -164,7 +164,7 @@ func GenSwarm(t testing.TB, opts ...Option) *swarm.Swarm { if cfg.disableReuseport { tcpOpts = append(tcpOpts, tcp.DisableReuseport()) } - tcpTransport, err := tcp.NewTCPTransport(upgrader, nil, tcpOpts...) + tcpTransport, err := tcp.NewTCPTransport(upgrader, nil, nil, tcpOpts...) require.NoError(t, err) if err := s.AddTransport(tcpTransport); err != nil { t.Fatal(err) diff --git a/p2p/net/upgrader/listener.go b/p2p/net/upgrader/listener.go index 8af2791b36..c2e81d2e93 100644 --- a/p2p/net/upgrader/listener.go +++ b/p2p/net/upgrader/listener.go @@ -84,23 +84,33 @@ func (l *listener) handleIncoming() { } catcher.Reset() - // gate the connection if applicable - if l.upgrader.connGater != nil && !l.upgrader.connGater.InterceptAccept(maconn) { - log.Debugf("gater blocked incoming connection on local addr %s from %s", - maconn.LocalMultiaddr(), maconn.RemoteMultiaddr()) - if err := maconn.Close(); err != nil { - log.Warnf("failed to close incoming connection rejected by gater: %s", err) - } - continue + // Check if we already have a connection scope. See the comment in tcpreuse/listener.go for an explanation. + var connScope network.ConnManagementScope + if sc, ok := maconn.(interface { + Scope() network.ConnManagementScope + }); ok { + connScope = sc.Scope() } + if connScope == nil { + // gate the connection if applicable + if l.upgrader.connGater != nil && !l.upgrader.connGater.InterceptAccept(maconn) { + log.Debugf("gater blocked incoming connection on local addr %s from %s", + maconn.LocalMultiaddr(), maconn.RemoteMultiaddr()) + if err := maconn.Close(); err != nil { + log.Warnf("failed to close incoming connection rejected by gater: %s", err) + } + continue + } - connScope, err := l.rcmgr.OpenConnection(network.DirInbound, true, maconn.RemoteMultiaddr()) - if err != nil { - log.Debugw("resource manager blocked accept of new connection", "error", err) - if err := maconn.Close(); err != nil { - log.Warnf("failed to incoming connection rejected by resource manager: %s", err) + var err error + connScope, err = l.rcmgr.OpenConnection(network.DirInbound, true, maconn.RemoteMultiaddr()) + if err != nil { + log.Debugw("resource manager blocked accept of new connection", "error", err) + if err := maconn.Close(); err != nil { + log.Warnf("failed to open incoming connection. Rejected by resource manager: %s", err) + } + continue } - continue } // The go routine below calls Release when the context is diff --git a/p2p/protocol/autonatv2/autonat.go b/p2p/protocol/autonatv2/autonat.go index 3dc3bc166a..de933d73c8 100644 --- a/p2p/protocol/autonatv2/autonat.go +++ b/p2p/protocol/autonatv2/autonat.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "slices" "sync" "time" @@ -16,7 +17,6 @@ import ( ma "github.com/multiformats/go-multiaddr" manet "github.com/multiformats/go-multiaddr/net" "golang.org/x/exp/rand" - "golang.org/x/exp/slices" ) const ( @@ -25,9 +25,9 @@ const ( DialProtocol = "/libp2p/autonat/2/dial-request" maxMsgSize = 8192 - streamTimeout = time.Minute + streamTimeout = 15 * time.Second dialBackStreamTimeout = 5 * time.Second - dialBackDialTimeout = 30 * time.Second + dialBackDialTimeout = 10 * time.Second dialBackMaxMsgSize = 1024 minHandshakeSizeBytes = 30_000 // for amplification attack prevention maxHandshakeSizeBytes = 100_000 diff --git a/p2p/protocol/autonatv2/autonat_test.go b/p2p/protocol/autonatv2/autonat_test.go index ee97ccc695..11c8f02195 100644 --- a/p2p/protocol/autonatv2/autonat_test.go +++ b/p2p/protocol/autonatv2/autonat_test.go @@ -657,5 +657,4 @@ func TestAreAddrsConsistency(t *testing.T) { } }) } - } diff --git a/p2p/protocol/autonatv2/client.go b/p2p/protocol/autonatv2/client.go index c1d82b7f3a..6ed7a6bb92 100644 --- a/p2p/protocol/autonatv2/client.go +++ b/p2p/protocol/autonatv2/client.go @@ -3,6 +3,8 @@ package autonatv2 import ( "context" "fmt" + "os" + "runtime/debug" "sync" "time" @@ -248,6 +250,13 @@ func newDialRequest(reqs []Request, nonce uint64) pb.Message { // handleDialBack receives the nonce on the dial-back stream func (ac *client) handleDialBack(s network.Stream) { + defer func() { + if rerr := recover(); rerr != nil { + fmt.Fprintf(os.Stderr, "caught panic: %s\n%s\n", rerr, debug.Stack()) + } + s.Reset() + }() + if err := s.Scope().SetService(ServiceName); err != nil { log.Debugf("failed to attach stream to service %s: %w", ServiceName, err) s.Reset() diff --git a/p2p/protocol/autonatv2/pb/autonatv2.pb.go b/p2p/protocol/autonatv2/pb/autonatv2.pb.go index 2138374c4a..3c1016fefd 100644 --- a/p2p/protocol/autonatv2/pb/autonatv2.pb.go +++ b/p2p/protocol/autonatv2/pb/autonatv2.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.34.2 -// protoc v5.27.3 +// protoc-gen-go v1.35.1 +// protoc v5.28.2 // source: p2p/protocol/autonatv2/pb/autonatv2.proto package pb @@ -183,11 +183,9 @@ type Message struct { func (x *Message) Reset() { *x = Message{} - if protoimpl.UnsafeEnabled { - mi := &file_p2p_protocol_autonatv2_pb_autonatv2_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_p2p_protocol_autonatv2_pb_autonatv2_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Message) String() string { @@ -198,7 +196,7 @@ func (*Message) ProtoMessage() {} func (x *Message) ProtoReflect() protoreflect.Message { mi := &file_p2p_protocol_autonatv2_pb_autonatv2_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -287,11 +285,9 @@ type DialRequest struct { func (x *DialRequest) Reset() { *x = DialRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_p2p_protocol_autonatv2_pb_autonatv2_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_p2p_protocol_autonatv2_pb_autonatv2_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DialRequest) String() string { @@ -302,7 +298,7 @@ func (*DialRequest) ProtoMessage() {} func (x *DialRequest) ProtoReflect() protoreflect.Message { mi := &file_p2p_protocol_autonatv2_pb_autonatv2_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -342,11 +338,9 @@ type DialDataRequest struct { func (x *DialDataRequest) Reset() { *x = DialDataRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_p2p_protocol_autonatv2_pb_autonatv2_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_p2p_protocol_autonatv2_pb_autonatv2_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DialDataRequest) String() string { @@ -357,7 +351,7 @@ func (*DialDataRequest) ProtoMessage() {} func (x *DialDataRequest) ProtoReflect() protoreflect.Message { mi := &file_p2p_protocol_autonatv2_pb_autonatv2_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -398,11 +392,9 @@ type DialResponse struct { func (x *DialResponse) Reset() { *x = DialResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_p2p_protocol_autonatv2_pb_autonatv2_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_p2p_protocol_autonatv2_pb_autonatv2_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DialResponse) String() string { @@ -413,7 +405,7 @@ func (*DialResponse) ProtoMessage() {} func (x *DialResponse) ProtoReflect() protoreflect.Message { mi := &file_p2p_protocol_autonatv2_pb_autonatv2_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -459,11 +451,9 @@ type DialDataResponse struct { func (x *DialDataResponse) Reset() { *x = DialDataResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_p2p_protocol_autonatv2_pb_autonatv2_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_p2p_protocol_autonatv2_pb_autonatv2_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DialDataResponse) String() string { @@ -474,7 +464,7 @@ func (*DialDataResponse) ProtoMessage() {} func (x *DialDataResponse) ProtoReflect() protoreflect.Message { mi := &file_p2p_protocol_autonatv2_pb_autonatv2_proto_msgTypes[4] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -506,11 +496,9 @@ type DialBack struct { func (x *DialBack) Reset() { *x = DialBack{} - if protoimpl.UnsafeEnabled { - mi := &file_p2p_protocol_autonatv2_pb_autonatv2_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_p2p_protocol_autonatv2_pb_autonatv2_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DialBack) String() string { @@ -521,7 +509,7 @@ func (*DialBack) ProtoMessage() {} func (x *DialBack) ProtoReflect() protoreflect.Message { mi := &file_p2p_protocol_autonatv2_pb_autonatv2_proto_msgTypes[5] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -553,11 +541,9 @@ type DialBackResponse struct { func (x *DialBackResponse) Reset() { *x = DialBackResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_p2p_protocol_autonatv2_pb_autonatv2_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_p2p_protocol_autonatv2_pb_autonatv2_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DialBackResponse) String() string { @@ -568,7 +554,7 @@ func (*DialBackResponse) ProtoMessage() {} func (x *DialBackResponse) ProtoReflect() protoreflect.Message { mi := &file_p2p_protocol_autonatv2_pb_autonatv2_proto_msgTypes[6] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -709,92 +695,6 @@ func file_p2p_protocol_autonatv2_pb_autonatv2_proto_init() { if File_p2p_protocol_autonatv2_pb_autonatv2_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_p2p_protocol_autonatv2_pb_autonatv2_proto_msgTypes[0].Exporter = func(v any, i int) any { - switch v := v.(*Message); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_p2p_protocol_autonatv2_pb_autonatv2_proto_msgTypes[1].Exporter = func(v any, i int) any { - switch v := v.(*DialRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_p2p_protocol_autonatv2_pb_autonatv2_proto_msgTypes[2].Exporter = func(v any, i int) any { - switch v := v.(*DialDataRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_p2p_protocol_autonatv2_pb_autonatv2_proto_msgTypes[3].Exporter = func(v any, i int) any { - switch v := v.(*DialResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_p2p_protocol_autonatv2_pb_autonatv2_proto_msgTypes[4].Exporter = func(v any, i int) any { - switch v := v.(*DialDataResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_p2p_protocol_autonatv2_pb_autonatv2_proto_msgTypes[5].Exporter = func(v any, i int) any { - switch v := v.(*DialBack); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_p2p_protocol_autonatv2_pb_autonatv2_proto_msgTypes[6].Exporter = func(v any, i int) any { - switch v := v.(*DialBackResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } file_p2p_protocol_autonatv2_pb_autonatv2_proto_msgTypes[0].OneofWrappers = []any{ (*Message_DialRequest)(nil), (*Message_DialResponse)(nil), diff --git a/p2p/protocol/autonatv2/server.go b/p2p/protocol/autonatv2/server.go index c5dd2dce3e..d28cfc9371 100644 --- a/p2p/protocol/autonatv2/server.go +++ b/p2p/protocol/autonatv2/server.go @@ -5,6 +5,8 @@ import ( "errors" "fmt" "io" + "os" + "runtime/debug" "sync" "time" @@ -88,6 +90,14 @@ func (as *server) Close() { // handleDialRequest is the dial-request protocol stream handler func (as *server) handleDialRequest(s network.Stream) { + defer func() { + if rerr := recover(); rerr != nil { + fmt.Fprintf(os.Stderr, "caught panic: %s\n%s\n", rerr, debug.Stack()) + s.Reset() + } + }() + + log.Debugf("received dial-request from: %s, addr: %s", s.Conn().RemotePeer(), s.Conn().RemoteMultiaddr()) evt := as.serveDialRequest(s) log.Debugf("completed dial-request from %s, response status: %s, dial status: %s, err: %s", s.Conn().RemotePeer(), evt.ResponseStatus, evt.DialStatus, evt.Error) diff --git a/p2p/protocol/circuitv2/client/transport.go b/p2p/protocol/circuitv2/client/transport.go index 2c9e49f509..553af77f0b 100644 --- a/p2p/protocol/circuitv2/client/transport.go +++ b/p2p/protocol/circuitv2/client/transport.go @@ -46,8 +46,20 @@ func AddTransport(h host.Host, upgrader transport.Upgrader) error { // Transport interface var _ transport.Transport = (*Client)(nil) + +// p2p-circuit implements the SkipResolver interface so that the underlying +// transport can do the address resolution later. If you wrap this transport, +// make sure you also implement SkipResolver as well. +var _ transport.SkipResolver = (*Client)(nil) var _ io.Closer = (*Client)(nil) +// SkipResolve returns true since we always defer to the inner transport for +// the actual connection. By skipping resolution here, we let the inner +// transport decide how to resolve the multiaddr +func (c *Client) SkipResolve(ctx context.Context, maddr ma.Multiaddr) bool { + return true +} + func (c *Client) Dial(ctx context.Context, a ma.Multiaddr, p peer.ID) (transport.CapableConn, error) { connScope, err := c.host.Network().ResourceManager().OpenConnection(network.DirOutbound, false, a) diff --git a/p2p/protocol/circuitv2/pb/circuit.pb.go b/p2p/protocol/circuitv2/pb/circuit.pb.go index f9cc203671..fda13bcbbc 100644 --- a/p2p/protocol/circuitv2/pb/circuit.pb.go +++ b/p2p/protocol/circuitv2/pb/circuit.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.34.2 -// protoc v5.27.3 +// protoc-gen-go v1.35.1 +// protoc v5.28.2 // source: p2p/protocol/circuitv2/pb/circuit.proto package pb @@ -199,11 +199,9 @@ type HopMessage struct { func (x *HopMessage) Reset() { *x = HopMessage{} - if protoimpl.UnsafeEnabled { - mi := &file_p2p_protocol_circuitv2_pb_circuit_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_p2p_protocol_circuitv2_pb_circuit_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *HopMessage) String() string { @@ -214,7 +212,7 @@ func (*HopMessage) ProtoMessage() {} func (x *HopMessage) ProtoReflect() protoreflect.Message { mi := &file_p2p_protocol_circuitv2_pb_circuit_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -279,11 +277,9 @@ type StopMessage struct { func (x *StopMessage) Reset() { *x = StopMessage{} - if protoimpl.UnsafeEnabled { - mi := &file_p2p_protocol_circuitv2_pb_circuit_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_p2p_protocol_circuitv2_pb_circuit_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *StopMessage) String() string { @@ -294,7 +290,7 @@ func (*StopMessage) ProtoMessage() {} func (x *StopMessage) ProtoReflect() protoreflect.Message { mi := &file_p2p_protocol_circuitv2_pb_circuit_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -350,11 +346,9 @@ type Peer struct { func (x *Peer) Reset() { *x = Peer{} - if protoimpl.UnsafeEnabled { - mi := &file_p2p_protocol_circuitv2_pb_circuit_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_p2p_protocol_circuitv2_pb_circuit_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Peer) String() string { @@ -365,7 +359,7 @@ func (*Peer) ProtoMessage() {} func (x *Peer) ProtoReflect() protoreflect.Message { mi := &file_p2p_protocol_circuitv2_pb_circuit_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -408,11 +402,9 @@ type Reservation struct { func (x *Reservation) Reset() { *x = Reservation{} - if protoimpl.UnsafeEnabled { - mi := &file_p2p_protocol_circuitv2_pb_circuit_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_p2p_protocol_circuitv2_pb_circuit_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Reservation) String() string { @@ -423,7 +415,7 @@ func (*Reservation) ProtoMessage() {} func (x *Reservation) ProtoReflect() protoreflect.Message { mi := &file_p2p_protocol_circuitv2_pb_circuit_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -470,11 +462,9 @@ type Limit struct { func (x *Limit) Reset() { *x = Limit{} - if protoimpl.UnsafeEnabled { - mi := &file_p2p_protocol_circuitv2_pb_circuit_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_p2p_protocol_circuitv2_pb_circuit_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Limit) String() string { @@ -485,7 +475,7 @@ func (*Limit) ProtoMessage() {} func (x *Limit) ProtoReflect() protoreflect.Message { mi := &file_p2p_protocol_circuitv2_pb_circuit_proto_msgTypes[4] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -643,68 +633,6 @@ func file_p2p_protocol_circuitv2_pb_circuit_proto_init() { if File_p2p_protocol_circuitv2_pb_circuit_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_p2p_protocol_circuitv2_pb_circuit_proto_msgTypes[0].Exporter = func(v any, i int) any { - switch v := v.(*HopMessage); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_p2p_protocol_circuitv2_pb_circuit_proto_msgTypes[1].Exporter = func(v any, i int) any { - switch v := v.(*StopMessage); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_p2p_protocol_circuitv2_pb_circuit_proto_msgTypes[2].Exporter = func(v any, i int) any { - switch v := v.(*Peer); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_p2p_protocol_circuitv2_pb_circuit_proto_msgTypes[3].Exporter = func(v any, i int) any { - switch v := v.(*Reservation); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_p2p_protocol_circuitv2_pb_circuit_proto_msgTypes[4].Exporter = func(v any, i int) any { - switch v := v.(*Limit); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } file_p2p_protocol_circuitv2_pb_circuit_proto_msgTypes[0].OneofWrappers = []any{} file_p2p_protocol_circuitv2_pb_circuit_proto_msgTypes[1].OneofWrappers = []any{} file_p2p_protocol_circuitv2_pb_circuit_proto_msgTypes[2].OneofWrappers = []any{} diff --git a/p2p/protocol/circuitv2/pb/voucher.pb.go b/p2p/protocol/circuitv2/pb/voucher.pb.go index 02d8f21222..5837d4de23 100644 --- a/p2p/protocol/circuitv2/pb/voucher.pb.go +++ b/p2p/protocol/circuitv2/pb/voucher.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.34.2 -// protoc v5.27.3 +// protoc-gen-go v1.35.1 +// protoc v5.28.2 // source: p2p/protocol/circuitv2/pb/voucher.proto package pb @@ -34,11 +34,9 @@ type ReservationVoucher struct { func (x *ReservationVoucher) Reset() { *x = ReservationVoucher{} - if protoimpl.UnsafeEnabled { - mi := &file_p2p_protocol_circuitv2_pb_voucher_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_p2p_protocol_circuitv2_pb_voucher_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ReservationVoucher) String() string { @@ -49,7 +47,7 @@ func (*ReservationVoucher) ProtoMessage() {} func (x *ReservationVoucher) ProtoReflect() protoreflect.Message { mi := &file_p2p_protocol_circuitv2_pb_voucher_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -136,20 +134,6 @@ func file_p2p_protocol_circuitv2_pb_voucher_proto_init() { if File_p2p_protocol_circuitv2_pb_voucher_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_p2p_protocol_circuitv2_pb_voucher_proto_msgTypes[0].Exporter = func(v any, i int) any { - switch v := v.(*ReservationVoucher); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } file_p2p_protocol_circuitv2_pb_voucher_proto_msgTypes[0].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ diff --git a/p2p/protocol/circuitv2/relay/constraints.go b/p2p/protocol/circuitv2/relay/constraints.go index 28698d9c06..4cce1f0176 100644 --- a/p2p/protocol/circuitv2/relay/constraints.go +++ b/p2p/protocol/circuitv2/relay/constraints.go @@ -2,6 +2,7 @@ package relay import ( "errors" + "slices" "sync" "time" @@ -12,24 +13,25 @@ import ( manet "github.com/multiformats/go-multiaddr/net" ) -var validity = 30 * time.Minute - var ( - errTooManyReservations = errors.New("too many reservations") - errTooManyReservationsForPeer = errors.New("too many reservations for peer") - errTooManyReservationsForIP = errors.New("too many peers for IP address") - errTooManyReservationsForASN = errors.New("too many peers for ASN") + errTooManyReservations = errors.New("too many reservations") + errTooManyReservationsForIP = errors.New("too many peers for IP address") + errTooManyReservationsForASN = errors.New("too many peers for ASN") ) +type peerWithExpiry struct { + Expiry time.Time + Peer peer.ID +} + // constraints implements various reservation constraints type constraints struct { rc *Resources mutex sync.Mutex - total []time.Time - peers map[peer.ID][]time.Time - ips map[string][]time.Time - asns map[uint32][]time.Time + total []peerWithExpiry + ips map[string][]peerWithExpiry + asns map[uint32][]peerWithExpiry } // newConstraints creates a new constraints object. @@ -37,21 +39,22 @@ type constraints struct { // is required. func newConstraints(rc *Resources) *constraints { return &constraints{ - rc: rc, - peers: make(map[peer.ID][]time.Time), - ips: make(map[string][]time.Time), - asns: make(map[uint32][]time.Time), + rc: rc, + ips: make(map[string][]peerWithExpiry), + asns: make(map[uint32][]peerWithExpiry), } } -// AddReservation adds a reservation for a given peer with a given multiaddr. -// If adding this reservation violates IP constraints, an error is returned. -func (c *constraints) AddReservation(p peer.ID, a ma.Multiaddr) error { +// Reserve adds a reservation for a given peer with a given multiaddr. +// If adding this reservation violates IP, ASN, or total reservation constraints, an error is returned. +func (c *constraints) Reserve(p peer.ID, a ma.Multiaddr, expiry time.Time) error { c.mutex.Lock() defer c.mutex.Unlock() now := time.Now() c.cleanup(now) + // To handle refreshes correctly, remove the existing reservation for the peer. + c.cleanupPeer(p) if len(c.total) >= c.rc.MaxReservations { return errTooManyReservations @@ -62,17 +65,12 @@ func (c *constraints) AddReservation(p peer.ID, a ma.Multiaddr) error { return errors.New("no IP address associated with peer") } - peerReservations := c.peers[p] - if len(peerReservations) >= c.rc.MaxReservationsPerPeer { - return errTooManyReservationsForPeer - } - ipReservations := c.ips[ip.String()] if len(ipReservations) >= c.rc.MaxReservationsPerIP { return errTooManyReservationsForIP } - var asnReservations []time.Time + var asnReservations []peerWithExpiry var asn uint32 if ip.To4() == nil { asn = asnutil.AsnForIPv6(ip) @@ -84,42 +82,52 @@ func (c *constraints) AddReservation(p peer.ID, a ma.Multiaddr) error { } } - expiry := now.Add(validity) - c.total = append(c.total, expiry) - - peerReservations = append(peerReservations, expiry) - c.peers[p] = peerReservations + c.total = append(c.total, peerWithExpiry{Expiry: expiry, Peer: p}) - ipReservations = append(ipReservations, expiry) + ipReservations = append(ipReservations, peerWithExpiry{Expiry: expiry, Peer: p}) c.ips[ip.String()] = ipReservations if asn != 0 { - asnReservations = append(asnReservations, expiry) + asnReservations = append(asnReservations, peerWithExpiry{Expiry: expiry, Peer: p}) c.asns[asn] = asnReservations } return nil } -func (c *constraints) cleanupList(l []time.Time, now time.Time) []time.Time { - var index int - for i, t := range l { - if t.After(now) { - break +func (c *constraints) cleanup(now time.Time) { + expireFunc := func(pe peerWithExpiry) bool { + return pe.Expiry.Before(now) + } + c.total = slices.DeleteFunc(c.total, expireFunc) + for k, ipReservations := range c.ips { + c.ips[k] = slices.DeleteFunc(ipReservations, expireFunc) + if len(c.ips[k]) == 0 { + delete(c.ips, k) + } + } + for k, asnReservations := range c.asns { + c.asns[k] = slices.DeleteFunc(asnReservations, expireFunc) + if len(c.asns[k]) == 0 { + delete(c.asns, k) } - index = i + 1 } - return l[index:] } -func (c *constraints) cleanup(now time.Time) { - c.total = c.cleanupList(c.total, now) - for k, peerReservations := range c.peers { - c.peers[k] = c.cleanupList(peerReservations, now) +func (c *constraints) cleanupPeer(p peer.ID) { + removeFunc := func(pe peerWithExpiry) bool { + return pe.Peer == p } + c.total = slices.DeleteFunc(c.total, removeFunc) for k, ipReservations := range c.ips { - c.ips[k] = c.cleanupList(ipReservations, now) + c.ips[k] = slices.DeleteFunc(ipReservations, removeFunc) + if len(c.ips[k]) == 0 { + delete(c.ips, k) + } } for k, asnReservations := range c.asns { - c.asns[k] = c.cleanupList(asnReservations, now) + c.asns[k] = slices.DeleteFunc(asnReservations, removeFunc) + if len(c.asns[k]) == 0 { + delete(c.asns, k) + } } } diff --git a/p2p/protocol/circuitv2/relay/constraints_test.go b/p2p/protocol/circuitv2/relay/constraints_test.go index 352fe22405..bced8e4097 100644 --- a/p2p/protocol/circuitv2/relay/constraints_test.go +++ b/p2p/protocol/circuitv2/relay/constraints_test.go @@ -34,35 +34,40 @@ func TestConstraints(t *testing.T) { } } const limit = 7 + expiry := time.Now().Add(30 * time.Minute) t.Run("total reservations", func(t *testing.T) { res := infResources() res.MaxReservations = limit c := newConstraints(res) for i := 0; i < limit; i++ { - if err := c.AddReservation(test.RandPeerIDFatal(t), randomIPv4Addr(t)); err != nil { + if err := c.Reserve(test.RandPeerIDFatal(t), randomIPv4Addr(t), expiry); err != nil { t.Fatal(err) } } - if err := c.AddReservation(test.RandPeerIDFatal(t), randomIPv4Addr(t)); err != errTooManyReservations { + if err := c.Reserve(test.RandPeerIDFatal(t), randomIPv4Addr(t), expiry); err != errTooManyReservations { t.Fatalf("expected to run into total reservation limit, got %v", err) } }) - t.Run("reservations per peer", func(t *testing.T) { + t.Run("updates reservations on the same peer", func(t *testing.T) { p := test.RandPeerIDFatal(t) + p2 := test.RandPeerIDFatal(t) res := infResources() - res.MaxReservationsPerPeer = limit + res.MaxReservationsPerIP = 1 c := newConstraints(res) - for i := 0; i < limit; i++ { - if err := c.AddReservation(p, randomIPv4Addr(t)); err != nil { - t.Fatal(err) - } + + ipAddr := randomIPv4Addr(t) + if err := c.Reserve(p, ipAddr, expiry); err != nil { + t.Fatal(err) } - if err := c.AddReservation(p, randomIPv4Addr(t)); err != errTooManyReservationsForPeer { - t.Fatalf("expected to run into total reservation limit, got %v", err) + if err := c.Reserve(p2, ipAddr, expiry); err != errTooManyReservationsForIP { + t.Fatalf("expected to run into IP reservation limit as this IP has already been reserved by a different peer, got %v", err) } - if err := c.AddReservation(test.RandPeerIDFatal(t), randomIPv4Addr(t)); err != nil { + if err := c.Reserve(p, randomIPv4Addr(t), expiry); err != nil { + t.Fatalf("expected to update existing reservation for peer, got %v", err) + } + if err := c.Reserve(p2, ipAddr, expiry); err != nil { t.Fatalf("expected reservation for different peer to be possible, got %v", err) } }) @@ -73,14 +78,14 @@ func TestConstraints(t *testing.T) { res.MaxReservationsPerIP = limit c := newConstraints(res) for i := 0; i < limit; i++ { - if err := c.AddReservation(test.RandPeerIDFatal(t), ip); err != nil { + if err := c.Reserve(test.RandPeerIDFatal(t), ip, expiry); err != nil { t.Fatal(err) } } - if err := c.AddReservation(test.RandPeerIDFatal(t), ip); err != errTooManyReservationsForIP { + if err := c.Reserve(test.RandPeerIDFatal(t), ip, expiry); err != errTooManyReservationsForIP { t.Fatalf("expected to run into total reservation limit, got %v", err) } - if err := c.AddReservation(test.RandPeerIDFatal(t), randomIPv4Addr(t)); err != nil { + if err := c.Reserve(test.RandPeerIDFatal(t), randomIPv4Addr(t), expiry); err != nil { t.Fatalf("expected reservation for different IP to be possible, got %v", err) } }) @@ -101,25 +106,23 @@ func TestConstraints(t *testing.T) { const ipv6Prefix = "2a03:2880:f003:c07:face:b00c::" for i := 0; i < limit; i++ { addr := getAddr(t, net.ParseIP(fmt.Sprintf("%s%d", ipv6Prefix, i+1))) - if err := c.AddReservation(test.RandPeerIDFatal(t), addr); err != nil { + if err := c.Reserve(test.RandPeerIDFatal(t), addr, expiry); err != nil { t.Fatal(err) } } - if err := c.AddReservation(test.RandPeerIDFatal(t), getAddr(t, net.ParseIP(fmt.Sprintf("%s%d", ipv6Prefix, 42)))); err != errTooManyReservationsForASN { + if err := c.Reserve(test.RandPeerIDFatal(t), getAddr(t, net.ParseIP(fmt.Sprintf("%s%d", ipv6Prefix, 42))), expiry); err != errTooManyReservationsForASN { t.Fatalf("expected to run into total reservation limit, got %v", err) } - if err := c.AddReservation(test.RandPeerIDFatal(t), randomIPv4Addr(t)); err != nil { + if err := c.Reserve(test.RandPeerIDFatal(t), randomIPv4Addr(t), expiry); err != nil { t.Fatalf("expected reservation for different IP to be possible, got %v", err) } }) } func TestConstraintsCleanup(t *testing.T) { - origValidity := validity - defer func() { validity = origValidity }() - validity = 500 * time.Millisecond - const limit = 7 + validity := 500 * time.Millisecond + expiry := time.Now().Add(validity) res := &Resources{ MaxReservations: limit, MaxReservationsPerPeer: math.MaxInt32, @@ -128,16 +131,16 @@ func TestConstraintsCleanup(t *testing.T) { } c := newConstraints(res) for i := 0; i < limit; i++ { - if err := c.AddReservation(test.RandPeerIDFatal(t), randomIPv4Addr(t)); err != nil { + if err := c.Reserve(test.RandPeerIDFatal(t), randomIPv4Addr(t), expiry); err != nil { t.Fatal(err) } } - if err := c.AddReservation(test.RandPeerIDFatal(t), randomIPv4Addr(t)); err != errTooManyReservations { + if err := c.Reserve(test.RandPeerIDFatal(t), randomIPv4Addr(t), expiry); err != errTooManyReservations { t.Fatalf("expected to run into total reservation limit, got %v", err) } time.Sleep(validity + time.Millisecond) - if err := c.AddReservation(test.RandPeerIDFatal(t), randomIPv4Addr(t)); err != nil { + if err := c.Reserve(test.RandPeerIDFatal(t), randomIPv4Addr(t), expiry); err != nil { t.Fatalf("expected old reservations to have been garbage collected, %v", err) } } diff --git a/p2p/protocol/circuitv2/relay/relay.go b/p2p/protocol/circuitv2/relay/relay.go index 1629a6d1a1..326d17781b 100644 --- a/p2p/protocol/circuitv2/relay/relay.go +++ b/p2p/protocol/circuitv2/relay/relay.go @@ -9,6 +9,7 @@ import ( "sync/atomic" "time" + "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/peer" @@ -117,7 +118,7 @@ func (r *Relay) Close() error { r.host.RemoveStreamHandler(proto.ProtoIDv2Hop) r.host.Network().StopNotify(r.notifiee) - r.scope.Done() + defer r.scope.Done() r.cancel() r.gc() if r.metricsTracer != nil { @@ -202,18 +203,16 @@ func (r *Relay) handleReserve(s network.Stream) pbv2.Status { return pbv2.Status_PERMISSION_DENIED } now := time.Now() + expire := now.Add(r.rc.ReservationTTL) _, exists := r.rsvp[p] - if !exists { - if err := r.constraints.AddReservation(p, a); err != nil { - r.mx.Unlock() - log.Debugf("refusing relay reservation for %s; IP constraint violation: %s", p, err) - r.handleError(s, pbv2.Status_RESERVATION_REFUSED) - return pbv2.Status_RESERVATION_REFUSED - } + if err := r.constraints.Reserve(p, a, expire); err != nil { + r.mx.Unlock() + log.Debugf("refusing relay reservation for %s; IP constraint violation: %s", p, err) + r.handleError(s, pbv2.Status_RESERVATION_REFUSED) + return pbv2.Status_RESERVATION_REFUSED } - expire := now.Add(r.rc.ReservationTTL) r.rsvp[p] = expire r.host.ConnManager().TagPeer(p, "relay-reservation", ReservationTagWeight) r.mx.Unlock() @@ -226,7 +225,13 @@ func (r *Relay) handleReserve(s network.Stream) pbv2.Status { // Delivery of the reservation might fail for a number of reasons. // For example, the stream might be reset or the connection might be closed before the reservation is received. // In that case, the reservation will just be garbage collected later. - if err := r.writeResponse(s, pbv2.Status_OK, r.makeReservationMsg(p, expire), r.makeLimitMsg(p)); err != nil { + rsvp := makeReservationMsg( + r.host.Peerstore().PrivKey(r.host.ID()), + r.host.ID(), + r.host.Addrs(), + p, + expire) + if err := r.writeResponse(s, pbv2.Status_OK, rsvp, r.makeLimitMsg(p)); err != nil { log.Debugf("error writing reservation response; retracting reservation for %s", p) s.Reset() return pbv2.Status_CONNECTION_FAILED @@ -310,7 +315,7 @@ func (r *Relay) handleConnect(s network.Stream, msg *pbv2.HopMessage) pbv2.Statu connStTime := time.Now() cleanup := func() { - span.Done() + defer span.Done() r.mx.Lock() r.rmConn(src) r.rmConn(dest.ID) @@ -569,31 +574,54 @@ func (r *Relay) writeResponse(s network.Stream, status pbv2.Status, rsvp *pbv2.R return wr.WriteMsg(&msg) } -func (r *Relay) makeReservationMsg(p peer.ID, expire time.Time) *pbv2.Reservation { +func makeReservationMsg( + signingKey crypto.PrivKey, + selfID peer.ID, + selfAddrs []ma.Multiaddr, + p peer.ID, + expire time.Time, +) *pbv2.Reservation { expireUnix := uint64(expire.Unix()) + rsvp := &pbv2.Reservation{Expire: &expireUnix} + + selfP2PAddr, err := ma.NewComponent("p2p", selfID.String()) + if err != nil { + log.Errorf("error creating p2p component: %s", err) + return rsvp + } + var addrBytes [][]byte - for _, addr := range r.host.Addrs() { + for _, addr := range selfAddrs { if !manet.IsPublicAddr(addr) { continue } - addr = addr.Encapsulate(r.selfAddr) + id, _ := peer.IDFromP2PAddr(addr) + switch { + case id == "": + // No ID, we'll add one to the address + addr = addr.Encapsulate(selfP2PAddr) + case id == selfID: + // This address already has our ID in it. + // Do nothing + case id != selfID: + // This address has a different ID in it. Skip it. + log.Warnf("skipping address %s: contains an unexpected ID", addr) + continue + } addrBytes = append(addrBytes, addr.Bytes()) } - rsvp := &pbv2.Reservation{ - Expire: &expireUnix, - Addrs: addrBytes, - } + rsvp.Addrs = addrBytes voucher := &proto.ReservationVoucher{ - Relay: r.host.ID(), + Relay: selfID, Peer: p, Expiration: expire, } - envelope, err := record.Seal(voucher, r.host.Peerstore().PrivKey(r.host.ID())) + envelope, err := record.Seal(voucher, signingKey) if err != nil { log.Errorf("error sealing voucher for %s: %s", p, err) return rsvp @@ -673,6 +701,7 @@ func (r *Relay) disconnected(n network.Network, c network.Conn) { if ok { delete(r.rsvp, p) } + r.constraints.cleanupPeer(p) r.mx.Unlock() if ok && r.metricsTracer != nil { diff --git a/p2p/protocol/circuitv2/relay/relay_priv_test.go b/p2p/protocol/circuitv2/relay/relay_priv_test.go new file mode 100644 index 0000000000..dd07e8e784 --- /dev/null +++ b/p2p/protocol/circuitv2/relay/relay_priv_test.go @@ -0,0 +1,53 @@ +package relay + +import ( + "crypto/rand" + "testing" + "time" + + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/stretchr/testify/require" + + ma "github.com/multiformats/go-multiaddr" +) + +func genKeyAndID(t *testing.T) (crypto.PrivKey, peer.ID) { + t.Helper() + key, _, err := crypto.GenerateEd25519Key(rand.Reader) + require.NoError(t, err) + id, err := peer.IDFromPrivateKey(key) + require.NoError(t, err) + return key, id +} + +// TestMakeReservationWithP2PAddrs ensures that our reservation message builder +// sanitizes the input addresses +func TestMakeReservationWithP2PAddrs(t *testing.T) { + selfKey, selfID := genKeyAndID(t) + _, otherID := genKeyAndID(t) + _, reserverID := genKeyAndID(t) + + addrs := []ma.Multiaddr{ + ma.StringCast("/ip4/1.2.3.4/tcp/1234"), // No p2p part + ma.StringCast("/ip4/1.2.3.4/tcp/1235/p2p/" + selfID.String()), // Already has p2p part + ma.StringCast("/ip4/1.2.3.4/tcp/1236/p2p/" + otherID.String()), // Some other peer (?? Not expected, but we could get anything in this func) + } + + rsvp := makeReservationMsg(selfKey, selfID, addrs, reserverID, time.Now().Add(time.Minute)) + require.NotNil(t, rsvp) + + expectedAddrs := []string{ + "/ip4/1.2.3.4/tcp/1234/p2p/" + selfID.String(), + "/ip4/1.2.3.4/tcp/1235/p2p/" + selfID.String(), + } + + var addrsFromRsvp []string + for _, addr := range rsvp.GetAddrs() { + a, err := ma.NewMultiaddrBytes(addr) + require.NoError(t, err) + addrsFromRsvp = append(addrsFromRsvp, a.String()) + } + + require.Equal(t, expectedAddrs, addrsFromRsvp) +} diff --git a/p2p/protocol/circuitv2/relay/relay_test.go b/p2p/protocol/circuitv2/relay/relay_test.go index e5d32b0c96..f6b63e32de 100644 --- a/p2p/protocol/circuitv2/relay/relay_test.go +++ b/p2p/protocol/circuitv2/relay/relay_test.go @@ -60,7 +60,7 @@ func getNetHosts(t *testing.T, ctx context.Context, n int) (hosts []host.Host, u upgrader := swarmt.GenUpgrader(t, netw, nil) upgraders = append(upgraders, upgrader) - tpt, err := tcp.NewTCPTransport(upgrader, nil) + tpt, err := tcp.NewTCPTransport(upgrader, nil, nil) if err != nil { t.Fatal(err) } diff --git a/p2p/protocol/circuitv2/relay/resources.go b/p2p/protocol/circuitv2/relay/resources.go index 0ae0169899..dc0207bca6 100644 --- a/p2p/protocol/circuitv2/relay/resources.go +++ b/p2p/protocol/circuitv2/relay/resources.go @@ -22,6 +22,8 @@ type Resources struct { // MaxReservationsPerPeer is the maximum number of reservations originating from the same // peer; default is 4. + // + // Deprecated: We only need 1 reservation per peer. MaxReservationsPerPeer int // MaxReservationsPerIP is the maximum number of reservations originating from the same // IP address; default is 8. @@ -51,7 +53,7 @@ func DefaultResources() Resources { MaxCircuits: 16, BufferSize: 2048, - MaxReservationsPerPeer: 4, + MaxReservationsPerPeer: 1, MaxReservationsPerIP: 8, MaxReservationsPerASN: 32, } diff --git a/p2p/protocol/holepunch/holepunch_test.go b/p2p/protocol/holepunch/holepunch_test.go index 23593c7970..00a76023ed 100644 --- a/p2p/protocol/holepunch/holepunch_test.go +++ b/p2p/protocol/holepunch/holepunch_test.go @@ -3,6 +3,7 @@ package holepunch_test import ( "context" "net" + "slices" "sync" "testing" "time" @@ -94,7 +95,6 @@ func TestNoHolePunchIfDirectConnExists(t *testing.T) { require.GreaterOrEqual(t, nc1, 1) nc2 := len(h2.Network().ConnsToPeer(h1.ID())) require.GreaterOrEqual(t, nc2, 1) - require.NoError(t, hps.DirectConnect(h2.ID())) require.Len(t, h1.Network().ConnsToPeer(h2.ID()), nc1) require.Len(t, h2.Network().ConnsToPeer(h1.ID()), nc2) @@ -473,8 +473,7 @@ func makeRelayedHosts(t *testing.T, h1opt, h2opt []holepunch.Option, addHolePunc hps = addHolePunchService(t, h2, h2opt...) } - // h1 has a relay addr - // h2 should connect to the relay addr + // h2 has a relay addr var raddr ma.Multiaddr for _, a := range h2.Addrs() { if _, err := a.ValueForProtocol(ma.P_CIRCUIT); err == nil { @@ -483,6 +482,7 @@ func makeRelayedHosts(t *testing.T, h1opt, h2opt []holepunch.Option, addHolePunc } } require.NotEmpty(t, raddr) + // h1 should connect to the relay addr require.NoError(t, h1.Connect(context.Background(), peer.AddrInfo{ ID: h2.ID(), Addrs: []ma.Multiaddr{raddr}, @@ -492,7 +492,11 @@ func makeRelayedHosts(t *testing.T, h1opt, h2opt []holepunch.Option, addHolePunc func addHolePunchService(t *testing.T, h host.Host, opts ...holepunch.Option) *holepunch.Service { t.Helper() - hps, err := holepunch.NewService(h, newMockIDService(t, h), opts...) + hps, err := holepunch.NewService(h, newMockIDService(t, h), func() []ma.Multiaddr { + addrs := h.Addrs() + addrs = slices.DeleteFunc(addrs, func(a ma.Multiaddr) bool { return !manet.IsPublicAddr(a) }) + return append(addrs, ma.StringCast("/ip4/1.2.3.4/tcp/1234")) + }, opts...) require.NoError(t, err) return hps } @@ -505,7 +509,6 @@ func mkHostWithHolePunchSvc(t *testing.T, opts ...holepunch.Option) (host.Host, libp2p.ResourceManager(&network.NullResourceManager{}), ) require.NoError(t, err) - hps, err := holepunch.NewService(h, newMockIDService(t, h), opts...) - require.NoError(t, err) + hps := addHolePunchService(t, h, opts...) return h, hps } diff --git a/p2p/protocol/holepunch/holepuncher.go b/p2p/protocol/holepunch/holepuncher.go index a30e653761..20d0558fc5 100644 --- a/p2p/protocol/holepunch/holepuncher.go +++ b/p2p/protocol/holepunch/holepuncher.go @@ -37,7 +37,8 @@ type holePuncher struct { host host.Host refCount sync.WaitGroup - ids identify.IDService + ids identify.IDService + listenAddrs func() []ma.Multiaddr // active hole punches for deduplicating activeMx sync.Mutex @@ -50,13 +51,14 @@ type holePuncher struct { filter AddrFilter } -func newHolePuncher(h host.Host, ids identify.IDService, tracer *tracer, filter AddrFilter) *holePuncher { +func newHolePuncher(h host.Host, ids identify.IDService, listenAddrs func() []ma.Multiaddr, tracer *tracer, filter AddrFilter) *holePuncher { hp := &holePuncher{ - host: h, - ids: ids, - active: make(map[peer.ID]struct{}), - tracer: tracer, - filter: filter, + host: h, + ids: ids, + active: make(map[peer.ID]struct{}), + tracer: tracer, + filter: filter, + listenAddrs: listenAddrs, } hp.ctx, hp.ctxCancel = context.WithCancel(context.Background()) h.Network().Notify((*netNotifiee)(hp)) @@ -102,16 +104,15 @@ func (hp *holePuncher) directConnect(rp peer.ID) error { if getDirectConnection(hp.host, rp) != nil { return nil } - // short-circuit hole punching if a direct dial works. // attempt a direct connection ONLY if we have a public address for the remote peer for _, a := range hp.host.Peerstore().Addrs(rp) { - if manet.IsPublicAddr(a) && !isRelayAddress(a) { + if !isRelayAddress(a) && manet.IsPublicAddr(a) { forceDirectConnCtx := network.WithForceDirectDial(hp.ctx, "hole-punching") dialCtx, cancel := context.WithTimeout(forceDirectConnCtx, dialTimeout) tstart := time.Now() - // This dials *all* public addresses from the peerstore. + // This dials *all* addresses, public and private, from the peerstore. err := hp.host.Connect(dialCtx, peer.AddrInfo{ID: rp}) dt := time.Since(tstart) cancel() @@ -206,7 +207,7 @@ func (hp *holePuncher) initiateHolePunchImpl(str network.Stream) ([]ma.Multiaddr str.SetDeadline(time.Now().Add(StreamTimeout)) // send a CONNECT and start RTT measurement. - obsAddrs := removeRelayAddrs(hp.ids.OwnObservedAddrs()) + obsAddrs := removeRelayAddrs(hp.listenAddrs()) if hp.filter != nil { obsAddrs = hp.filter.FilterLocal(str.Conn().RemotePeer(), obsAddrs) } diff --git a/p2p/protocol/holepunch/pb/holepunch.pb.go b/p2p/protocol/holepunch/pb/holepunch.pb.go index dde534a73b..c8ab6f16c4 100644 --- a/p2p/protocol/holepunch/pb/holepunch.pb.go +++ b/p2p/protocol/holepunch/pb/holepunch.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.34.2 -// protoc v5.27.3 +// protoc-gen-go v1.35.1 +// protoc v5.28.2 // source: p2p/protocol/holepunch/pb/holepunch.proto package pb @@ -88,11 +88,9 @@ type HolePunch struct { func (x *HolePunch) Reset() { *x = HolePunch{} - if protoimpl.UnsafeEnabled { - mi := &file_p2p_protocol_holepunch_pb_holepunch_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_p2p_protocol_holepunch_pb_holepunch_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *HolePunch) String() string { @@ -103,7 +101,7 @@ func (*HolePunch) ProtoMessage() {} func (x *HolePunch) ProtoReflect() protoreflect.Message { mi := &file_p2p_protocol_holepunch_pb_holepunch_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -184,20 +182,6 @@ func file_p2p_protocol_holepunch_pb_holepunch_proto_init() { if File_p2p_protocol_holepunch_pb_holepunch_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_p2p_protocol_holepunch_pb_holepunch_proto_msgTypes[0].Exporter = func(v any, i int) any { - switch v := v.(*HolePunch); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ diff --git a/p2p/protocol/holepunch/svc.go b/p2p/protocol/holepunch/svc.go index eb8ad9fd38..2e6fdd1a6a 100644 --- a/p2p/protocol/holepunch/svc.go +++ b/p2p/protocol/holepunch/svc.go @@ -8,18 +8,15 @@ import ( "time" logging "github.com/ipfs/go-log/v2" - "github.com/libp2p/go-libp2p/core/event" "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/protocol" - "github.com/libp2p/go-libp2p/p2p/host/eventbus" "github.com/libp2p/go-libp2p/p2p/protocol/holepunch/pb" "github.com/libp2p/go-libp2p/p2p/protocol/identify" "github.com/libp2p/go-msgio/pbio" ma "github.com/multiformats/go-multiaddr" - manet "github.com/multiformats/go-multiaddr/net" ) // Protocol is the libp2p protocol for Hole Punching. @@ -47,7 +44,13 @@ type Service struct { ctxCancel context.CancelFunc host host.Host - ids identify.IDService + // ids helps with connection reversal. We wait for identify to complete and attempt + // a direct connection to the peer if it's publicly reachable. + ids identify.IDService + // listenAddrs provides the addresses for the host to be used for hole punching. We use this + // and not host.Addrs because host.Addrs might remove public unreachable address and only advertise + // publicly reachable relay addresses. + listenAddrs func() []ma.Multiaddr holePuncherMx sync.Mutex holePuncher *holePuncher @@ -65,7 +68,9 @@ type Service struct { // no matter if they are behind a NAT / firewall or not. // The Service handles DCUtR streams (which are initiated from the node behind // a NAT / Firewall once we establish a connection to them through a relay. -func NewService(h host.Host, ids identify.IDService, opts ...Option) (*Service, error) { +// +// listenAddrs MUST only return public addresses. +func NewService(h host.Host, ids identify.IDService, listenAddrs func() []ma.Multiaddr, opts ...Option) (*Service, error) { if ids == nil { return nil, errors.New("identify service can't be nil") } @@ -76,6 +81,7 @@ func NewService(h host.Host, ids identify.IDService, opts ...Option) (*Service, ctxCancel: cancel, host: h, ids: ids, + listenAddrs: listenAddrs, hasPublicAddrsChan: make(chan struct{}), } @@ -88,18 +94,18 @@ func NewService(h host.Host, ids identify.IDService, opts ...Option) (*Service, s.tracer.Start() s.refCount.Add(1) - go s.watchForPublicAddr() + go s.waitForPublicAddr() return s, nil } -func (s *Service) watchForPublicAddr() { +func (s *Service) waitForPublicAddr() { defer s.refCount.Done() log.Debug("waiting until we have at least one public address", "peer", s.host.ID()) // TODO: We should have an event here that fires when identify discovers a new - // address (and when autonat confirms that address). + // address. // As we currently don't have an event like this, just check our observed addresses // regularly (exponential backoff starting at 250 ms, capped at 5s). duration := 250 * time.Millisecond @@ -107,7 +113,7 @@ func (s *Service) watchForPublicAddr() { t := time.NewTimer(duration) defer t.Stop() for { - if len(s.getPublicAddrs()) > 0 { + if len(s.listenAddrs()) > 0 { log.Debug("Host now has a public address. Starting holepunch protocol.") s.host.SetStreamHandler(Protocol, s.handleNewStream) break @@ -125,36 +131,20 @@ func (s *Service) watchForPublicAddr() { } } - // Only start the holePuncher if we're behind a NAT / firewall. - sub, err := s.host.EventBus().Subscribe(&event.EvtLocalReachabilityChanged{}, eventbus.Name("holepunch")) - if err != nil { - log.Debugf("failed to subscripe to Reachability event: %s", err) + s.holePuncherMx.Lock() + if s.ctx.Err() != nil { + // service is closed return } - defer sub.Close() - for { - select { - case <-s.ctx.Done(): - return - case e, ok := <-sub.Out(): - if !ok { - return - } - if e.(event.EvtLocalReachabilityChanged).Reachability != network.ReachabilityPrivate { - continue - } - s.holePuncherMx.Lock() - s.holePuncher = newHolePuncher(s.host, s.ids, s.tracer, s.filter) - s.holePuncherMx.Unlock() - close(s.hasPublicAddrsChan) - return - } - } + s.holePuncher = newHolePuncher(s.host, s.ids, s.listenAddrs, s.tracer, s.filter) + s.holePuncherMx.Unlock() + close(s.hasPublicAddrsChan) } // Close closes the Hole Punch Service. func (s *Service) Close() error { var err error + s.ctxCancel() s.holePuncherMx.Lock() if s.holePuncher != nil { err = s.holePuncher.Close() @@ -162,7 +152,6 @@ func (s *Service) Close() error { s.holePuncherMx.Unlock() s.tracer.Close() s.host.RemoveStreamHandler(Protocol) - s.ctxCancel() s.refCount.Wait() return err } @@ -172,7 +161,7 @@ func (s *Service) incomingHolePunch(str network.Stream) (rtt time.Duration, remo if !isRelayAddress(str.Conn().RemoteMultiaddr()) { return 0, nil, nil, fmt.Errorf("received hole punch stream: %s", str.Conn().RemoteMultiaddr()) } - ownAddrs = s.getPublicAddrs() + ownAddrs = s.listenAddrs() if s.filter != nil { ownAddrs = s.filter.FilterLocal(str.Conn().RemotePeer(), ownAddrs) } @@ -275,29 +264,6 @@ func (s *Service) handleNewStream(str network.Stream) { s.tracer.HolePunchFinished("receiver", 1, addrs, ownAddrs, getDirectConnection(s.host, rp)) } -// getPublicAddrs returns public observed and interface addresses -func (s *Service) getPublicAddrs() []ma.Multiaddr { - addrs := removeRelayAddrs(s.ids.OwnObservedAddrs()) - - interfaceListenAddrs, err := s.host.Network().InterfaceListenAddresses() - if err != nil { - log.Debugf("failed to get to get InterfaceListenAddresses: %s", err) - } else { - addrs = append(addrs, interfaceListenAddrs...) - } - - addrs = ma.Unique(addrs) - - publicAddrs := make([]ma.Multiaddr, 0, len(addrs)) - - for _, addr := range addrs { - if manet.IsPublicAddr(addr) { - publicAddrs = append(publicAddrs, addr) - } - } - return publicAddrs -} - // DirectConnect is only exposed for testing purposes. // TODO: find a solution for this. func (s *Service) DirectConnect(p peer.ID) error { diff --git a/p2p/protocol/holepunch/tracer.go b/p2p/protocol/holepunch/tracer.go index 82e0ebfc0f..3ba06f653d 100644 --- a/p2p/protocol/holepunch/tracer.go +++ b/p2p/protocol/holepunch/tracer.go @@ -20,13 +20,10 @@ const ( func WithTracer(et EventTracer) Option { return func(hps *Service) error { hps.tracer = &tracer{ - et: et, - mt: nil, - self: hps.host.ID(), - peers: make(map[peer.ID]struct { - counter int - last time.Time - }), + et: et, + mt: nil, + self: hps.host.ID(), + peers: make(map[peer.ID]peerInfo), } return nil } @@ -36,13 +33,10 @@ func WithTracer(et EventTracer) Option { func WithMetricsTracer(mt MetricsTracer) Option { return func(hps *Service) error { hps.tracer = &tracer{ - et: nil, - mt: mt, - self: hps.host.ID(), - peers: make(map[peer.ID]struct { - counter int - last time.Time - }), + et: nil, + mt: mt, + self: hps.host.ID(), + peers: make(map[peer.ID]peerInfo), } return nil } @@ -52,13 +46,10 @@ func WithMetricsTracer(mt MetricsTracer) Option { func WithMetricsAndEventTracer(mt MetricsTracer, et EventTracer) Option { return func(hps *Service) error { hps.tracer = &tracer{ - et: et, - mt: mt, - self: hps.host.ID(), - peers: make(map[peer.ID]struct { - counter int - last time.Time - }), + et: et, + mt: mt, + self: hps.host.ID(), + peers: make(map[peer.ID]peerInfo), } return nil } @@ -74,10 +65,12 @@ type tracer struct { ctxCancel context.CancelFunc mutex sync.Mutex - peers map[peer.ID]struct { - counter int - last time.Time - } + peers map[peer.ID]peerInfo +} + +type peerInfo struct { + counter int + last time.Time } type EventTracer interface { diff --git a/p2p/protocol/holepunch/util.go b/p2p/protocol/holepunch/util.go index 947b1ffd82..c0f34d0928 100644 --- a/p2p/protocol/holepunch/util.go +++ b/p2p/protocol/holepunch/util.go @@ -2,6 +2,7 @@ package holepunch import ( "context" + "slices" "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/network" @@ -11,13 +12,7 @@ import ( ) func removeRelayAddrs(addrs []ma.Multiaddr) []ma.Multiaddr { - result := make([]ma.Multiaddr, 0, len(addrs)) - for _, addr := range addrs { - if !isRelayAddress(addr) { - result = append(result, addr) - } - } - return result + return slices.DeleteFunc(addrs, isRelayAddress) } func isRelayAddress(a ma.Multiaddr) bool { diff --git a/p2p/protocol/identify/id.go b/p2p/protocol/identify/id.go index acaa40aca3..b6d5240ba6 100644 --- a/p2p/protocol/identify/id.go +++ b/p2p/protocol/identify/id.go @@ -6,11 +6,10 @@ import ( "errors" "fmt" "io" + "slices" "sync" "time" - "golang.org/x/exp/slices" - "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/event" "github.com/libp2p/go-libp2p/core/host" @@ -348,7 +347,8 @@ func (ids *idService) sendPushes(ctx context.Context) { defer func() { <-sem }() ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() - str, err := ids.Host.NewStream(ctx, c.RemotePeer(), IDPush) + + str, err := newStreamAndNegotiate(ctx, c, IDPush) if err != nil { // connection might have been closed recently return } @@ -438,25 +438,38 @@ func (ids *idService) IdentifyWait(c network.Conn) <-chan struct{} { return e.IdentifyWaitChan } -func (ids *idService) identifyConn(c network.Conn) error { - ctx, cancel := context.WithTimeout(context.Background(), Timeout) - defer cancel() +// newStreamAndNegotiate opens a new stream on the given connection and negotiates the given protocol. +func newStreamAndNegotiate(ctx context.Context, c network.Conn, proto protocol.ID) (network.Stream, error) { s, err := c.NewStream(network.WithAllowLimitedConn(ctx, "identify")) if err != nil { log.Debugw("error opening identify stream", "peer", c.RemotePeer(), "error", err) - return err + return nil, err + } + err = s.SetDeadline(time.Now().Add(Timeout)) + if err != nil { + return nil, err } - s.SetDeadline(time.Now().Add(Timeout)) - if err := s.SetProtocol(ID); err != nil { + if err := s.SetProtocol(proto); err != nil { log.Warnf("error setting identify protocol for stream: %s", err) - s.Reset() + _ = s.Reset() } // ok give the response to our handler. - if err := msmux.SelectProtoOrFail(ID, s); err != nil { + if err := msmux.SelectProtoOrFail(proto, s); err != nil { log.Infow("failed negotiate identify protocol with peer", "peer", c.RemotePeer(), "error", err) - s.Reset() + _ = s.Reset() + return nil, err + } + return s, nil +} + +func (ids *idService) identifyConn(c network.Conn) error { + ctx, cancel := context.WithTimeout(context.Background(), Timeout) + defer cancel() + s, err := newStreamAndNegotiate(network.WithAllowLimitedConn(ctx, "identify"), c, ID) + if err != nil { + log.Debugw("error opening identify stream", "peer", c.RemotePeer(), "error", err) return err } diff --git a/p2p/protocol/identify/obsaddr.go b/p2p/protocol/identify/obsaddr.go index ffe60345e1..06e54bf5fd 100644 --- a/p2p/protocol/identify/obsaddr.go +++ b/p2p/protocol/identify/obsaddr.go @@ -335,6 +335,11 @@ func (o *ObservedAddrManager) worker() { } } +func isRelayedAddress(a ma.Multiaddr) bool { + _, err := a.ValueForProtocol(ma.P_CIRCUIT) + return err == nil +} + func (o *ObservedAddrManager) shouldRecordObservation(conn connMultiaddrs, observed ma.Multiaddr) (shouldRecord bool, localTW thinWaist, observedTW thinWaist) { if conn == nil || observed == nil { return false, thinWaist{}, thinWaist{} @@ -350,6 +355,12 @@ func (o *ObservedAddrManager) shouldRecordObservation(conn connMultiaddrs, obser return false, thinWaist{}, thinWaist{} } + // Ignore p2p-circuit addresses. These are the observed address of the relay. + // Not useful for us. + if isRelayedAddress(observed) { + return false, thinWaist{}, thinWaist{} + } + // we should only use ObservedAddr when our connection's LocalAddr is one // of our ListenAddrs. If we Dial out using an ephemeral addr, knowing that // address's external mapping is not very useful because the port will not be @@ -410,7 +421,7 @@ func (o *ObservedAddrManager) maybeRecordObservation(conn connMultiaddrs, observ if !shouldRecord { return } - log.Debugw("added own observed listen addr", "observed", observed) + log.Debugw("added own observed listen addr", "conn", conn, "observed", observed) o.mu.Lock() defer o.mu.Unlock() diff --git a/p2p/protocol/identify/obsaddr_glass_test.go b/p2p/protocol/identify/obsaddr_glass_test.go index 31fd4f5726..3211aa5f54 100644 --- a/p2p/protocol/identify/obsaddr_glass_test.go +++ b/p2p/protocol/identify/obsaddr_glass_test.go @@ -53,6 +53,24 @@ func TestShouldRecordObservationWithWebTransport(t *testing.T) { require.True(t, shouldRecord) } +func TestShouldNotRecordObservationWithRelayedAddr(t *testing.T) { + listenAddr := ma.StringCast("/ip4/1.2.3.4/udp/8888/quic-v1/p2p-circuit") + ifaceAddr := ma.StringCast("/ip4/10.0.0.2/udp/9999/quic-v1") + listenAddrs := func() []ma.Multiaddr { return []ma.Multiaddr{listenAddr} } + ifaceListenAddrs := func() ([]ma.Multiaddr, error) { return []ma.Multiaddr{ifaceAddr}, nil } + addrs := func() []ma.Multiaddr { return []ma.Multiaddr{listenAddr} } + + c := &mockConn{ + local: listenAddr, + remote: ma.StringCast("/ip4/1.2.3.6/udp/1236/quic-v1/p2p-circuit"), + } + observedAddr := ma.StringCast("/ip4/1.2.3.4/udp/1231/quic-v1/p2p-circuit") + o, err := NewObservedAddrManager(listenAddrs, addrs, ifaceListenAddrs, normalize) + require.NoError(t, err) + shouldRecord, _, _ := o.shouldRecordObservation(c, observedAddr) + require.False(t, shouldRecord) +} + func TestShouldRecordObservationWithNAT64Addr(t *testing.T) { listenAddr1 := ma.StringCast("/ip4/0.0.0.0/tcp/1234") ifaceAddr1 := ma.StringCast("/ip4/10.0.0.2/tcp/4321") diff --git a/p2p/protocol/identify/pb/identify.pb.go b/p2p/protocol/identify/pb/identify.pb.go index a63ae000ba..66f4cb703a 100644 --- a/p2p/protocol/identify/pb/identify.pb.go +++ b/p2p/protocol/identify/pb/identify.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.34.2 -// protoc v5.27.3 +// protoc-gen-go v1.35.1 +// protoc v5.28.2 // source: p2p/protocol/identify/pb/identify.proto package pb @@ -52,11 +52,9 @@ type Identify struct { func (x *Identify) Reset() { *x = Identify{} - if protoimpl.UnsafeEnabled { - mi := &file_p2p_protocol_identify_pb_identify_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_p2p_protocol_identify_pb_identify_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Identify) String() string { @@ -67,7 +65,7 @@ func (*Identify) ProtoMessage() {} func (x *Identify) ProtoReflect() protoreflect.Message { mi := &file_p2p_protocol_identify_pb_identify_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -189,20 +187,6 @@ func file_p2p_protocol_identify_pb_identify_proto_init() { if File_p2p_protocol_identify_pb_identify_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_p2p_protocol_identify_pb_identify_proto_msgTypes[0].Exporter = func(v any, i int) any { - switch v := v.(*Identify); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ diff --git a/p2p/security/noise/pb/payload.pb.go b/p2p/security/noise/pb/payload.pb.go index 3572d12846..45a104e222 100644 --- a/p2p/security/noise/pb/payload.pb.go +++ b/p2p/security/noise/pb/payload.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.34.2 -// protoc v5.27.3 +// protoc-gen-go v1.35.1 +// protoc v5.28.2 // source: p2p/security/noise/pb/payload.proto package pb @@ -31,11 +31,9 @@ type NoiseExtensions struct { func (x *NoiseExtensions) Reset() { *x = NoiseExtensions{} - if protoimpl.UnsafeEnabled { - mi := &file_p2p_security_noise_pb_payload_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_p2p_security_noise_pb_payload_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *NoiseExtensions) String() string { @@ -46,7 +44,7 @@ func (*NoiseExtensions) ProtoMessage() {} func (x *NoiseExtensions) ProtoReflect() protoreflect.Message { mi := &file_p2p_security_noise_pb_payload_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -87,11 +85,9 @@ type NoiseHandshakePayload struct { func (x *NoiseHandshakePayload) Reset() { *x = NoiseHandshakePayload{} - if protoimpl.UnsafeEnabled { - mi := &file_p2p_security_noise_pb_payload_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_p2p_security_noise_pb_payload_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *NoiseHandshakePayload) String() string { @@ -102,7 +98,7 @@ func (*NoiseHandshakePayload) ProtoMessage() {} func (x *NoiseHandshakePayload) ProtoReflect() protoreflect.Message { mi := &file_p2p_security_noise_pb_payload_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -197,32 +193,6 @@ func file_p2p_security_noise_pb_payload_proto_init() { if File_p2p_security_noise_pb_payload_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_p2p_security_noise_pb_payload_proto_msgTypes[0].Exporter = func(v any, i int) any { - switch v := v.(*NoiseExtensions); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_p2p_security_noise_pb_payload_proto_msgTypes[1].Exporter = func(v any, i int) any { - switch v := v.(*NoiseHandshakePayload); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ diff --git a/p2p/test/transport/gating_test.go b/p2p/test/transport/gating_test.go index df53da6eeb..99ce67b521 100644 --- a/p2p/test/transport/gating_test.go +++ b/p2p/test/transport/gating_test.go @@ -2,6 +2,8 @@ package transport_integration import ( "context" + "encoding/binary" + "net/netip" "strings" "testing" "time" @@ -30,6 +32,23 @@ func stripCertHash(addr ma.Multiaddr) ma.Multiaddr { return addr } +func addrPort(addr ma.Multiaddr) netip.AddrPort { + a := netip.Addr{} + p := uint16(0) + ma.ForEach(addr, func(c ma.Component) bool { + if c.Protocol().Code == ma.P_IP4 || c.Protocol().Code == ma.P_IP6 { + a, _ = netip.AddrFromSlice(c.RawValue()) + return false + } + if c.Protocol().Code == ma.P_UDP || c.Protocol().Code == ma.P_TCP { + p = binary.BigEndian.Uint16(c.RawValue()) + return true + } + return false + }) + return netip.AddrPortFrom(a, p) +} + func TestInterceptPeerDial(t *testing.T) { if race.WithRace() { t.Skip("The upgrader spawns a new Go routine, which leads to race conditions when using GoMock.") @@ -173,10 +192,14 @@ func TestInterceptAccept(t *testing.T) { // remove the certhash component from WebTransport addresses require.Equal(t, stripCertHash(h2.Addrs()[0]), addrs.LocalMultiaddr()) }).AnyTimes() + } else if strings.Contains(tc.Name, "WebSocket-Shared") { + connGater.EXPECT().InterceptAccept(gomock.Any()).Do(func(addrs network.ConnMultiaddrs) { + require.Equal(t, addrPort(h2.Addrs()[0]), addrPort(addrs.LocalMultiaddr())) + }) } else { connGater.EXPECT().InterceptAccept(gomock.Any()).Do(func(addrs network.ConnMultiaddrs) { // remove the certhash component from WebTransport addresses - require.Equal(t, stripCertHash(h2.Addrs()[0]), addrs.LocalMultiaddr()) + require.Equal(t, stripCertHash(h2.Addrs()[0]), addrs.LocalMultiaddr(), "%s\n%s", h2.Addrs()[0], addrs.LocalMultiaddr()) }) } diff --git a/p2p/test/transport/mock_connection_gater_test.go b/p2p/test/transport/mock_connection_gater_test.go index d617f72fce..5e13863f54 100644 --- a/p2p/test/transport/mock_connection_gater_test.go +++ b/p2p/test/transport/mock_connection_gater_test.go @@ -23,6 +23,7 @@ import ( type MockConnectionGater struct { ctrl *gomock.Controller recorder *MockConnectionGaterMockRecorder + isgomock struct{} } // MockConnectionGaterMockRecorder is the mock recorder for MockConnectionGater. @@ -71,17 +72,17 @@ func (mr *MockConnectionGaterMockRecorder) InterceptAddrDial(arg0, arg1 any) *go } // InterceptPeerDial mocks base method. -func (m *MockConnectionGater) InterceptPeerDial(arg0 peer.ID) bool { +func (m *MockConnectionGater) InterceptPeerDial(p peer.ID) bool { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InterceptPeerDial", arg0) + ret := m.ctrl.Call(m, "InterceptPeerDial", p) ret0, _ := ret[0].(bool) return ret0 } // InterceptPeerDial indicates an expected call of InterceptPeerDial. -func (mr *MockConnectionGaterMockRecorder) InterceptPeerDial(arg0 any) *gomock.Call { +func (mr *MockConnectionGaterMockRecorder) InterceptPeerDial(p any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InterceptPeerDial", reflect.TypeOf((*MockConnectionGater)(nil).InterceptPeerDial), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InterceptPeerDial", reflect.TypeOf((*MockConnectionGater)(nil).InterceptPeerDial), p) } // InterceptSecured mocks base method. diff --git a/p2p/test/transport/transport_test.go b/p2p/test/transport/transport_test.go index 7cfab5f3ca..60f8ca0c06 100644 --- a/p2p/test/transport/transport_test.go +++ b/p2p/test/transport/transport_test.go @@ -99,6 +99,38 @@ var transportsToTest = []TransportTestCase{ return h }, }, + { + Name: "TCP-Shared / TLS / Yamux", + HostGenerator: func(t *testing.T, opts TransportTestCaseOpts) host.Host { + libp2pOpts := transformOpts(opts) + libp2pOpts = append(libp2pOpts, libp2p.ShareTCPListener()) + libp2pOpts = append(libp2pOpts, libp2p.Security(tls.ID, tls.New)) + libp2pOpts = append(libp2pOpts, libp2p.Muxer(yamux.ID, yamux.DefaultTransport)) + if opts.NoListen { + libp2pOpts = append(libp2pOpts, libp2p.NoListenAddrs) + } else { + libp2pOpts = append(libp2pOpts, libp2p.ListenAddrStrings("/ip4/127.0.0.1/tcp/0")) + } + h, err := libp2p.New(libp2pOpts...) + require.NoError(t, err) + return h + }, + }, + { + Name: "WebSocket-Shared", + HostGenerator: func(t *testing.T, opts TransportTestCaseOpts) host.Host { + libp2pOpts := transformOpts(opts) + libp2pOpts = append(libp2pOpts, libp2p.ShareTCPListener()) + if opts.NoListen { + libp2pOpts = append(libp2pOpts, libp2p.NoListenAddrs) + } else { + libp2pOpts = append(libp2pOpts, libp2p.ListenAddrStrings("/ip4/127.0.0.1/tcp/0/ws")) + } + h, err := libp2p.New(libp2pOpts...) + require.NoError(t, err) + return h + }, + }, { Name: "WebSocket", HostGenerator: func(t *testing.T, opts TransportTestCaseOpts) host.Host { diff --git a/p2p/transport/quic/mock_connection_gater_test.go b/p2p/transport/quic/mock_connection_gater_test.go index 8e23aad370..0dd3586bb6 100644 --- a/p2p/transport/quic/mock_connection_gater_test.go +++ b/p2p/transport/quic/mock_connection_gater_test.go @@ -23,6 +23,7 @@ import ( type MockConnectionGater struct { ctrl *gomock.Controller recorder *MockConnectionGaterMockRecorder + isgomock struct{} } // MockConnectionGaterMockRecorder is the mock recorder for MockConnectionGater. @@ -71,17 +72,17 @@ func (mr *MockConnectionGaterMockRecorder) InterceptAddrDial(arg0, arg1 any) *go } // InterceptPeerDial mocks base method. -func (m *MockConnectionGater) InterceptPeerDial(arg0 peer.ID) bool { +func (m *MockConnectionGater) InterceptPeerDial(p peer.ID) bool { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InterceptPeerDial", arg0) + ret := m.ctrl.Call(m, "InterceptPeerDial", p) ret0, _ := ret[0].(bool) return ret0 } // InterceptPeerDial indicates an expected call of InterceptPeerDial. -func (mr *MockConnectionGaterMockRecorder) InterceptPeerDial(arg0 any) *gomock.Call { +func (mr *MockConnectionGaterMockRecorder) InterceptPeerDial(p any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InterceptPeerDial", reflect.TypeOf((*MockConnectionGater)(nil).InterceptPeerDial), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InterceptPeerDial", reflect.TypeOf((*MockConnectionGater)(nil).InterceptPeerDial), p) } // InterceptSecured mocks base method. diff --git a/p2p/transport/tcp/metrics.go b/p2p/transport/tcp/metrics.go index 213ee2200a..50820d870c 100644 --- a/p2p/transport/tcp/metrics.go +++ b/p2p/transport/tcp/metrics.go @@ -24,7 +24,7 @@ var ( const collectFrequency = 10 * time.Second -var collector *aggregatingCollector +var defaultCollector *aggregatingCollector var initMetricsOnce sync.Once @@ -34,8 +34,8 @@ func initMetrics() { bytesSentDesc = prometheus.NewDesc("tcp_sent_bytes", "TCP bytes sent", nil, nil) bytesRcvdDesc = prometheus.NewDesc("tcp_rcvd_bytes", "TCP bytes received", nil, nil) - collector = newAggregatingCollector() - prometheus.MustRegister(collector) + defaultCollector = newAggregatingCollector() + prometheus.MustRegister(defaultCollector) const direction = "direction" @@ -196,7 +196,7 @@ func (c *aggregatingCollector) Collect(metrics chan<- prometheus.Metric) { func (c *aggregatingCollector) ClosedConn(conn *tracingConn, direction string) { c.mutex.Lock() - collector.removeConn(conn.id) + c.removeConn(conn.id) c.mutex.Unlock() closedConns.WithLabelValues(direction).Inc() } @@ -204,6 +204,8 @@ func (c *aggregatingCollector) ClosedConn(conn *tracingConn, direction string) { type tracingConn struct { id uint64 + collector *aggregatingCollector + startTime time.Time isClient bool @@ -213,7 +215,8 @@ type tracingConn struct { closeErr error } -func newTracingConn(c manet.Conn, isClient bool) (*tracingConn, error) { +// newTracingConn wraps a manet.Conn with a tracingConn. A nil collector will use the default collector. +func newTracingConn(c manet.Conn, collector *aggregatingCollector, isClient bool) (*tracingConn, error) { initMetricsOnce.Do(func() { initMetrics() }) conn, err := tcp.NewConn(c) if err != nil { @@ -224,8 +227,12 @@ func newTracingConn(c manet.Conn, isClient bool) (*tracingConn, error) { isClient: isClient, Conn: c, tcpConn: conn, + collector: collector, + } + if tc.collector == nil { + tc.collector = defaultCollector } - tc.id = collector.AddConn(tc) + tc.id = tc.collector.AddConn(tc) newConns.WithLabelValues(tc.getDirection()).Inc() return tc, nil } @@ -239,7 +246,7 @@ func (c *tracingConn) getDirection() string { func (c *tracingConn) Close() error { c.closeOnce.Do(func() { - collector.ClosedConn(c, c.getDirection()) + c.collector.ClosedConn(c, c.getDirection()) c.closeErr = c.Conn.Close() }) return c.closeErr @@ -258,10 +265,12 @@ func (c *tracingConn) getTCPInfo() (*tcpinfo.Info, error) { type tracingListener struct { manet.Listener + collector *aggregatingCollector } -func newTracingListener(l manet.Listener) *tracingListener { - return &tracingListener{Listener: l} +// newTracingListener wraps a manet.Listener with a tracingListener. A nil collector will use the default collector. +func newTracingListener(l manet.Listener, collector *aggregatingCollector) *tracingListener { + return &tracingListener{Listener: l, collector: collector} } func (l *tracingListener) Accept() (manet.Conn, error) { @@ -269,5 +278,5 @@ func (l *tracingListener) Accept() (manet.Conn, error) { if err != nil { return nil, err } - return newTracingConn(conn, false) + return newTracingConn(conn, l.collector, false) } diff --git a/p2p/transport/tcp/metrics_none.go b/p2p/transport/tcp/metrics_none.go index 8538b30c89..cbee982070 100644 --- a/p2p/transport/tcp/metrics_none.go +++ b/p2p/transport/tcp/metrics_none.go @@ -6,5 +6,9 @@ package tcp import manet "github.com/multiformats/go-multiaddr/net" -func newTracingConn(c manet.Conn, _ bool) (manet.Conn, error) { return c, nil } -func newTracingListener(l manet.Listener) manet.Listener { return l } +type aggregatingCollector struct{} + +func newTracingConn(c manet.Conn, collector *aggregatingCollector, isClient bool) (manet.Conn, error) { + return c, nil +} +func newTracingListener(l manet.Listener, collector *aggregatingCollector) manet.Listener { return l } diff --git a/p2p/transport/tcp/metrics_unix_test.go b/p2p/transport/tcp/metrics_unix_test.go new file mode 100644 index 0000000000..0a09526206 --- /dev/null +++ b/p2p/transport/tcp/metrics_unix_test.go @@ -0,0 +1,54 @@ +// go:build: unix + +package tcp + +import ( + "testing" + + tptu "github.com/libp2p/go-libp2p/p2p/net/upgrader" + "github.com/libp2p/go-libp2p/p2p/transport/tcpreuse" + ttransport "github.com/libp2p/go-libp2p/p2p/transport/testsuite" + + "github.com/stretchr/testify/require" +) + +func TestTcpTransportCollectsMetricsWithSharedTcpSocket(t *testing.T) { + + peerA, ia := makeInsecureMuxer(t) + _, ib := makeInsecureMuxer(t) + + sharedTCPSocketA := tcpreuse.NewConnMgr(false, nil, nil) + sharedTCPSocketB := tcpreuse.NewConnMgr(false, nil, nil) + + ua, err := tptu.New(ia, muxers, nil, nil, nil) + require.NoError(t, err) + ta, err := NewTCPTransport(ua, nil, sharedTCPSocketA, WithMetrics()) + require.NoError(t, err) + ub, err := tptu.New(ib, muxers, nil, nil, nil) + require.NoError(t, err) + tb, err := NewTCPTransport(ub, nil, sharedTCPSocketB, WithMetrics()) + require.NoError(t, err) + + zero := "/ip4/127.0.0.1/tcp/0" + + // Not running any test that needs more than 1 conn because the testsuite + // opens multiple conns via multiple listeners, which is not expected to work + // with the shared TCP socket. + subtestsToRun := []ttransport.TransportSubTestFn{ + ttransport.SubtestProtocols, + ttransport.SubtestBasic, + ttransport.SubtestCancel, + ttransport.SubtestPingPong, + + // Stolen from the stream muxer test suite. + ttransport.SubtestStress1Conn1Stream1Msg, + ttransport.SubtestStress1Conn1Stream100Msg, + ttransport.SubtestStress1Conn100Stream100Msg, + ttransport.SubtestStress1Conn1000Stream10Msg, + ttransport.SubtestStress1Conn100Stream100Msg10MB, + ttransport.SubtestStreamOpenStress, + ttransport.SubtestStreamReset, + } + + ttransport.SubtestTransportWithFs(t, ta, tb, zero, peerA, subtestsToRun) +} diff --git a/p2p/transport/tcp/tcp.go b/p2p/transport/tcp/tcp.go index d52bb96019..61e68941e2 100644 --- a/p2p/transport/tcp/tcp.go +++ b/p2p/transport/tcp/tcp.go @@ -13,6 +13,7 @@ import ( "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/transport" "github.com/libp2p/go-libp2p/p2p/net/reuseport" + "github.com/libp2p/go-libp2p/p2p/transport/tcpreuse" logging "github.com/ipfs/go-log/v2" ma "github.com/multiformats/go-multiaddr" @@ -33,6 +34,9 @@ type canKeepAlive interface { var _ canKeepAlive = &net.TCPConn{} +// Deprecated: Use tcpreuse.ReuseportIsAvailable +var ReuseportIsAvailable = tcpreuse.ReuseportIsAvailable + func tryKeepAlive(conn net.Conn, keepAlive bool) { keepAliveConn, ok := conn.(canKeepAlive) if !ok { @@ -122,20 +126,25 @@ type TcpTransport struct { disableReuseport bool // Explicitly disable reuseport. enableMetrics bool + // share and demultiplex TCP listeners across multiple transports + sharedTcp *tcpreuse.ConnMgr + // TCP connect timeout connectTimeout time.Duration rcmgr network.ResourceManager reuse reuseport.Transport + + metricsCollector *aggregatingCollector } var _ transport.Transport = &TcpTransport{} var _ transport.DialUpdater = &TcpTransport{} // NewTCPTransport creates a tcp transport object that tracks dialers and listeners -// created. It represents an entire TCP stack (though it might not necessarily be). -func NewTCPTransport(upgrader transport.Upgrader, rcmgr network.ResourceManager, opts ...Option) (*TcpTransport, error) { +// created. +func NewTCPTransport(upgrader transport.Upgrader, rcmgr network.ResourceManager, sharedTCP *tcpreuse.ConnMgr, opts ...Option) (*TcpTransport, error) { if rcmgr == nil { rcmgr = &network.NullResourceManager{} } @@ -143,6 +152,7 @@ func NewTCPTransport(upgrader transport.Upgrader, rcmgr network.ResourceManager, upgrader: upgrader, connectTimeout: defaultConnectTimeout, // can be set by using the WithConnectionTimeout option rcmgr: rcmgr, + sharedTcp: sharedTCP, } for _, o := range opts { if err := o(tr); err != nil { @@ -168,6 +178,10 @@ func (t *TcpTransport) maDial(ctx context.Context, raddr ma.Multiaddr) (manet.Co defer cancel() } + if t.sharedTcp != nil { + return t.sharedTcp.DialContext(ctx, raddr) + } + if t.UseReuseport() { return t.reuse.DialContext(ctx, raddr) } @@ -212,7 +226,7 @@ func (t *TcpTransport) dialWithScope(ctx context.Context, raddr ma.Multiaddr, p c := conn if t.enableMetrics { var err error - c, err = newTracingConn(conn, true) + c, err = newTracingConn(conn, t.metricsCollector, true) if err != nil { return nil, err } @@ -233,10 +247,10 @@ func (t *TcpTransport) dialWithScope(ctx context.Context, raddr ma.Multiaddr, p // UseReuseport returns true if reuseport is enabled and available. func (t *TcpTransport) UseReuseport() bool { - return !t.disableReuseport && ReuseportIsAvailable() + return !t.disableReuseport && tcpreuse.ReuseportIsAvailable() } -func (t *TcpTransport) maListen(laddr ma.Multiaddr) (manet.Listener, error) { +func (t *TcpTransport) unsharedMAListen(laddr ma.Multiaddr) (manet.Listener, error) { if t.UseReuseport() { return t.reuse.Listen(laddr) } @@ -245,12 +259,20 @@ func (t *TcpTransport) maListen(laddr ma.Multiaddr) (manet.Listener, error) { // Listen listens on the given multiaddr. func (t *TcpTransport) Listen(laddr ma.Multiaddr) (transport.Listener, error) { - list, err := t.maListen(laddr) + var list manet.Listener + var err error + + if t.sharedTcp == nil { + list, err = t.unsharedMAListen(laddr) + } else { + list, err = t.sharedTcp.DemultiplexedListen(laddr, tcpreuse.DemultiplexedConnType_MultistreamSelect) + } if err != nil { return nil, err } + if t.enableMetrics { - list = newTracingListener(&tcpListener{list, 0}) + list = newTracingListener(&tcpListener{list, 0}, t.metricsCollector) } return t.upgrader.UpgradeListener(t, list), nil } diff --git a/p2p/transport/tcp/tcp_test.go b/p2p/transport/tcp/tcp_test.go index a57a65e420..1f939d92be 100644 --- a/p2p/transport/tcp/tcp_test.go +++ b/p2p/transport/tcp/tcp_test.go @@ -14,6 +14,7 @@ import ( "github.com/libp2p/go-libp2p/core/transport" "github.com/libp2p/go-libp2p/p2p/muxer/yamux" tptu "github.com/libp2p/go-libp2p/p2p/net/upgrader" + "github.com/libp2p/go-libp2p/p2p/transport/tcpreuse" ttransport "github.com/libp2p/go-libp2p/p2p/transport/testsuite" ma "github.com/multiformats/go-multiaddr" @@ -31,19 +32,19 @@ func TestTcpTransport(t *testing.T) { ua, err := tptu.New(ia, muxers, nil, nil, nil) require.NoError(t, err) - ta, err := NewTCPTransport(ua, nil) + ta, err := NewTCPTransport(ua, nil, nil) require.NoError(t, err) ub, err := tptu.New(ib, muxers, nil, nil, nil) require.NoError(t, err) - tb, err := NewTCPTransport(ub, nil) + tb, err := NewTCPTransport(ub, nil, nil) require.NoError(t, err) zero := "/ip4/127.0.0.1/tcp/0" ttransport.SubtestTransport(t, ta, tb, zero, peerA) - envReuseportVal = false + tcpreuse.EnvReuseportVal = false } - envReuseportVal = true + tcpreuse.EnvReuseportVal = true } func TestTcpTransportWithMetrics(t *testing.T) { @@ -52,11 +53,11 @@ func TestTcpTransportWithMetrics(t *testing.T) { ua, err := tptu.New(ia, muxers, nil, nil, nil) require.NoError(t, err) - ta, err := NewTCPTransport(ua, nil, WithMetrics()) + ta, err := NewTCPTransport(ua, nil, nil, WithMetrics()) require.NoError(t, err) ub, err := tptu.New(ib, muxers, nil, nil, nil) require.NoError(t, err) - tb, err := NewTCPTransport(ub, nil, WithMetrics()) + tb, err := NewTCPTransport(ub, nil, nil, WithMetrics()) require.NoError(t, err) zero := "/ip4/127.0.0.1/tcp/0" @@ -72,7 +73,7 @@ func TestResourceManager(t *testing.T) { ua, err := tptu.New(ia, muxers, nil, nil, nil) require.NoError(t, err) - ta, err := NewTCPTransport(ua, nil) + ta, err := NewTCPTransport(ua, nil, nil) require.NoError(t, err) ln, err := ta.Listen(ma.StringCast("/ip4/127.0.0.1/tcp/0")) require.NoError(t, err) @@ -81,7 +82,7 @@ func TestResourceManager(t *testing.T) { ub, err := tptu.New(ib, muxers, nil, nil, nil) require.NoError(t, err) rcmgr := mocknetwork.NewMockResourceManager(ctrl) - tb, err := NewTCPTransport(ub, rcmgr) + tb, err := NewTCPTransport(ub, rcmgr, nil) require.NoError(t, err) t.Run("success", func(t *testing.T) { @@ -119,16 +120,16 @@ func TestTcpTransportCantDialDNS(t *testing.T) { require.NoError(t, err) var u transport.Upgrader - tpt, err := NewTCPTransport(u, nil) + tpt, err := NewTCPTransport(u, nil, nil) require.NoError(t, err) if tpt.CanDial(dnsa) { t.Fatal("shouldn't be able to dial dns") } - envReuseportVal = false + tcpreuse.EnvReuseportVal = false } - envReuseportVal = true + tcpreuse.EnvReuseportVal = true } func TestTcpTransportCantListenUtp(t *testing.T) { @@ -137,15 +138,15 @@ func TestTcpTransportCantListenUtp(t *testing.T) { require.NoError(t, err) var u transport.Upgrader - tpt, err := NewTCPTransport(u, nil) + tpt, err := NewTCPTransport(u, nil, nil) require.NoError(t, err) _, err = tpt.Listen(utpa) require.Error(t, err, "shouldn't be able to listen on utp addr with tcp transport") - envReuseportVal = false + tcpreuse.EnvReuseportVal = false } - envReuseportVal = true + tcpreuse.EnvReuseportVal = true } func TestDialWithUpdates(t *testing.T) { @@ -154,7 +155,7 @@ func TestDialWithUpdates(t *testing.T) { ua, err := tptu.New(ia, muxers, nil, nil, nil) require.NoError(t, err) - ta, err := NewTCPTransport(ua, nil) + ta, err := NewTCPTransport(ua, nil, nil) require.NoError(t, err) ln, err := ta.Listen(ma.StringCast("/ip4/127.0.0.1/tcp/0")) require.NoError(t, err) @@ -162,7 +163,7 @@ func TestDialWithUpdates(t *testing.T) { ub, err := tptu.New(ib, muxers, nil, nil, nil) require.NoError(t, err) - tb, err := NewTCPTransport(ub, nil) + tb, err := NewTCPTransport(ub, nil, nil) require.NoError(t, err) updCh := make(chan transport.DialUpdate, 1) diff --git a/p2p/transport/tcpreuse/connwithscope.go b/p2p/transport/tcpreuse/connwithscope.go new file mode 100644 index 0000000000..ca66f20325 --- /dev/null +++ b/p2p/transport/tcpreuse/connwithscope.go @@ -0,0 +1,26 @@ +package tcpreuse + +import ( + "fmt" + + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/p2p/transport/tcpreuse/internal/sampledconn" + manet "github.com/multiformats/go-multiaddr/net" +) + +type connWithScope struct { + sampledconn.ManetTCPConnInterface + scope network.ConnManagementScope +} + +func (c connWithScope) Scope() network.ConnManagementScope { + return c.scope +} + +func manetConnWithScope(c manet.Conn, scope network.ConnManagementScope) (manet.Conn, error) { + if tcpconn, ok := c.(sampledconn.ManetTCPConnInterface); ok { + return &connWithScope{tcpconn, scope}, nil + } + + return nil, fmt.Errorf("manet.Conn is not a TCP Conn") +} diff --git a/p2p/transport/tcpreuse/demultiplex.go b/p2p/transport/tcpreuse/demultiplex.go new file mode 100644 index 0000000000..f9175ecfdb --- /dev/null +++ b/p2p/transport/tcpreuse/demultiplex.go @@ -0,0 +1,100 @@ +package tcpreuse + +import ( + "errors" + "fmt" + "time" + + "github.com/libp2p/go-libp2p/p2p/transport/tcpreuse/internal/sampledconn" + manet "github.com/multiformats/go-multiaddr/net" +) + +// This is reading the first 3 bytes of the first packet after the handshake. +// It's set to the default TCP connect timeout in the TCP Transport. +// +// A var so we can change it in tests. +var identifyConnTimeout = 5 * time.Second + +type DemultiplexedConnType int + +const ( + DemultiplexedConnType_Unknown DemultiplexedConnType = iota + DemultiplexedConnType_MultistreamSelect + DemultiplexedConnType_HTTP + DemultiplexedConnType_TLS +) + +func (t DemultiplexedConnType) String() string { + switch t { + case DemultiplexedConnType_MultistreamSelect: + return "MultistreamSelect" + case DemultiplexedConnType_HTTP: + return "HTTP" + case DemultiplexedConnType_TLS: + return "TLS" + default: + return fmt.Sprintf("Unknown(%d)", int(t)) + } +} + +func (t DemultiplexedConnType) IsKnown() bool { + return t >= 1 || t <= 3 +} + +// identifyConnType attempts to identify the connection type by peeking at the +// first few bytes. +// Its Callers must not use the passed in Conn after this function returns. +// If an error is returned, the connection will be closed. +func identifyConnType(c manet.Conn) (DemultiplexedConnType, manet.Conn, error) { + if err := c.SetReadDeadline(time.Now().Add(identifyConnTimeout)); err != nil { + closeErr := c.Close() + return 0, nil, errors.Join(err, closeErr) + } + + s, peekedConn, err := sampledconn.PeekBytes(c) + if err != nil { + closeErr := c.Close() + return 0, nil, errors.Join(err, closeErr) + } + + if err := peekedConn.SetReadDeadline(time.Time{}); err != nil { + closeErr := peekedConn.Close() + return 0, nil, errors.Join(err, closeErr) + } + + if IsMultistreamSelect(s) { + return DemultiplexedConnType_MultistreamSelect, peekedConn, nil + } + if IsTLS(s) { + return DemultiplexedConnType_TLS, peekedConn, nil + } + if IsHTTP(s) { + return DemultiplexedConnType_HTTP, peekedConn, nil + } + return DemultiplexedConnType_Unknown, peekedConn, nil +} + +// Matchers are implemented here instead of in the transports so we can easily fuzz them together. +type Prefix = [3]byte + +func IsMultistreamSelect(s Prefix) bool { + return string(s[:]) == "\x13/m" +} + +func IsHTTP(s Prefix) bool { + switch string(s[:]) { + case "GET", "HEA", "POS", "PUT", "DEL", "CON", "OPT", "TRA", "PAT": + return true + default: + return false + } +} + +func IsTLS(s Prefix) bool { + switch string(s[:]) { + case "\x16\x03\x01", "\x16\x03\x02", "\x16\x03\x03": + return true + default: + return false + } +} diff --git a/p2p/transport/tcpreuse/demultiplex_test.go b/p2p/transport/tcpreuse/demultiplex_test.go new file mode 100644 index 0000000000..e201f2ca75 --- /dev/null +++ b/p2p/transport/tcpreuse/demultiplex_test.go @@ -0,0 +1,50 @@ +package tcpreuse + +import "testing" + +func FuzzClash(f *testing.F) { + // make untyped literals type correctly + add := func(a, b, c byte) { f.Add(a, b, c) } + + // multistream-select + add('\x13', '/', 'm') + // http + add('G', 'E', 'T') + add('H', 'E', 'A') + add('P', 'O', 'S') + add('P', 'U', 'T') + add('D', 'E', 'L') + add('C', 'O', 'N') + add('O', 'P', 'T') + add('T', 'R', 'A') + add('P', 'A', 'T') + // tls + add('\x16', '\x03', '\x01') + add('\x16', '\x03', '\x02') + add('\x16', '\x03', '\x03') + add('\x16', '\x03', '\x04') + + f.Fuzz(func(t *testing.T, a, b, c byte) { + s := Prefix{a, b, c} + var total uint + + ms := IsMultistreamSelect(s) + if ms { + total++ + } + + http := IsHTTP(s) + if http { + total++ + } + + tls := IsTLS(s) + if tls { + total++ + } + + if total > 1 { + t.Errorf("clash on: %q; ms: %v; http: %v; tls: %v", s, ms, http, tls) + } + }) +} diff --git a/p2p/transport/tcpreuse/dialer.go b/p2p/transport/tcpreuse/dialer.go new file mode 100644 index 0000000000..ad634583ed --- /dev/null +++ b/p2p/transport/tcpreuse/dialer.go @@ -0,0 +1,16 @@ +package tcpreuse + +import ( + "context" + ma "github.com/multiformats/go-multiaddr" + manet "github.com/multiformats/go-multiaddr/net" +) + +// DialContext is like Dial but takes a context. +func (t *ConnMgr) DialContext(ctx context.Context, raddr ma.Multiaddr) (manet.Conn, error) { + if t.useReuseport() { + return t.reuse.DialContext(ctx, raddr) + } + var d manet.Dialer + return d.DialContext(ctx, raddr) +} diff --git a/p2p/transport/tcpreuse/internal/sampledconn/sampledconn_common.go b/p2p/transport/tcpreuse/internal/sampledconn/sampledconn_common.go new file mode 100644 index 0000000000..7324b45849 --- /dev/null +++ b/p2p/transport/tcpreuse/internal/sampledconn/sampledconn_common.go @@ -0,0 +1,89 @@ +package sampledconn + +import ( + "errors" + "io" + "net" + "syscall" + "time" + + manet "github.com/multiformats/go-multiaddr/net" +) + +const peekSize = 3 + +type PeekedBytes = [peekSize]byte + +var errNotSupported = errors.New("not supported on this platform") + +var ErrNotTCPConn = errors.New("passed conn is not a TCPConn") + +func PeekBytes(conn manet.Conn) (PeekedBytes, manet.Conn, error) { + if c, ok := conn.(syscall.Conn); ok { + b, err := OSPeekConn(c) + if err == nil { + return b, conn, nil + } + if err != errNotSupported { + return PeekedBytes{}, nil, err + } + // Fallback to wrapping the coonn + } + + if c, ok := conn.(ManetTCPConnInterface); ok { + return newFallbackSampledConn(c) + } + + return PeekedBytes{}, nil, ErrNotTCPConn +} + +type fallbackPeekingConn struct { + ManetTCPConnInterface + peekedBytes PeekedBytes + bytesPeeked uint8 +} + +// tcpConnInterface is the interface for TCPConn's functions +// NOTE: `SyscallConn() (syscall.RawConn, error)` is here to make using this as +// a TCP Conn easier, but it's a potential footgun as you could skipped the +// peeked bytes if using the fallback +type tcpConnInterface interface { + net.Conn + syscall.Conn + + CloseRead() error + CloseWrite() error + + SetLinger(sec int) error + SetKeepAlive(keepalive bool) error + SetKeepAlivePeriod(d time.Duration) error + SetNoDelay(noDelay bool) error + MultipathTCP() (bool, error) + + io.ReaderFrom + io.WriterTo +} + +type ManetTCPConnInterface interface { + manet.Conn + tcpConnInterface +} + +func newFallbackSampledConn(conn ManetTCPConnInterface) (PeekedBytes, *fallbackPeekingConn, error) { + s := &fallbackPeekingConn{ManetTCPConnInterface: conn} + _, err := io.ReadFull(conn, s.peekedBytes[:]) + if err != nil { + return s.peekedBytes, nil, err + } + return s.peekedBytes, s, nil +} + +func (sc *fallbackPeekingConn) Read(b []byte) (int, error) { + if int(sc.bytesPeeked) != len(sc.peekedBytes) { + red := copy(b, sc.peekedBytes[sc.bytesPeeked:]) + sc.bytesPeeked += uint8(red) + return red, nil + } + + return sc.ManetTCPConnInterface.Read(b) +} diff --git a/p2p/transport/tcpreuse/internal/sampledconn/sampledconn_other.go b/p2p/transport/tcpreuse/internal/sampledconn/sampledconn_other.go new file mode 100644 index 0000000000..5197052fab --- /dev/null +++ b/p2p/transport/tcpreuse/internal/sampledconn/sampledconn_other.go @@ -0,0 +1,11 @@ +//go:build !unix + +package sampledconn + +import ( + "syscall" +) + +func OSPeekConn(conn syscall.Conn) (PeekedBytes, error) { + return PeekedBytes{}, errNotSupported +} diff --git a/p2p/transport/tcpreuse/internal/sampledconn/sampledconn_test.go b/p2p/transport/tcpreuse/internal/sampledconn/sampledconn_test.go new file mode 100644 index 0000000000..d5b31009e2 --- /dev/null +++ b/p2p/transport/tcpreuse/internal/sampledconn/sampledconn_test.go @@ -0,0 +1,78 @@ +package sampledconn + +import ( + "io" + "syscall" + "testing" + "time" + + ma "github.com/multiformats/go-multiaddr" + manet "github.com/multiformats/go-multiaddr/net" + + "github.com/stretchr/testify/assert" +) + +func TestSampledConn(t *testing.T) { + testCases := []string{ + "platform", + "fallback", + } + + // Start a TCP server + listener, err := manet.Listen(ma.StringCast("/ip4/127.0.0.1/tcp/0")) + assert.NoError(t, err) + defer listener.Close() + + serverAddr := listener.Multiaddr() + + // Server goroutine + go func() { + for i := 0; i < len(testCases); i++ { + conn, err := listener.Accept() + assert.NoError(t, err) + defer conn.Close() + + // Write some data to the connection + _, err = conn.Write([]byte("hello")) + assert.NoError(t, err) + } + }() + + // Give the server a moment to start + time.Sleep(100 * time.Millisecond) + + for _, tc := range testCases { + t.Run(tc, func(t *testing.T) { + // Create a TCP client + clientConn, err := manet.Dial(serverAddr) + assert.NoError(t, err) + defer clientConn.Close() + + if tc == "platform" { + // Wrap the client connection in SampledConn + peeked, clientConn, err := PeekBytes(clientConn.(interface { + manet.Conn + syscall.Conn + })) + assert.NoError(t, err) + assert.Equal(t, "hel", string(peeked[:])) + + buf := make([]byte, 5) + _, err = io.ReadFull(clientConn, buf) + assert.NoError(t, err) + assert.Equal(t, "hello", string(buf)) + } else { + // Wrap the client connection in SampledConn + sample, sampledConn, err := newFallbackSampledConn(clientConn.(ManetTCPConnInterface)) + assert.NoError(t, err) + assert.Equal(t, "hel", string(sample[:])) + + buf := make([]byte, 5) + _, err = io.ReadFull(sampledConn, buf) + assert.NoError(t, err) + assert.Equal(t, "hello", string(buf)) + + } + }) + } +} diff --git a/p2p/transport/tcpreuse/internal/sampledconn/sampledconn_unix.go b/p2p/transport/tcpreuse/internal/sampledconn/sampledconn_unix.go new file mode 100644 index 0000000000..9847e8d4be --- /dev/null +++ b/p2p/transport/tcpreuse/internal/sampledconn/sampledconn_unix.go @@ -0,0 +1,42 @@ +//go:build unix + +package sampledconn + +import ( + "errors" + "syscall" +) + +func OSPeekConn(conn syscall.Conn) (PeekedBytes, error) { + s := PeekedBytes{} + + rawConn, err := conn.SyscallConn() + if err != nil { + return s, err + } + + readBytes := 0 + var readErr error + err = rawConn.Read(func(fd uintptr) bool { + for readBytes < peekSize { + var n int + n, _, readErr = syscall.Recvfrom(int(fd), s[readBytes:], syscall.MSG_PEEK) + if errors.Is(readErr, syscall.EAGAIN) { + return false + } + if readErr != nil { + return true + } + readBytes += n + } + return true + }) + if readErr != nil { + return s, readErr + } + if err != nil { + return s, err + } + + return s, nil +} diff --git a/p2p/transport/tcpreuse/listener.go b/p2p/transport/tcpreuse/listener.go new file mode 100644 index 0000000000..d94186e7ec --- /dev/null +++ b/p2p/transport/tcpreuse/listener.go @@ -0,0 +1,327 @@ +package tcpreuse + +import ( + "context" + "errors" + "fmt" + "net" + "sync" + "time" + + logging "github.com/ipfs/go-log/v2" + "github.com/libp2p/go-libp2p/core/connmgr" + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/transport" + "github.com/libp2p/go-libp2p/p2p/net/reuseport" + ma "github.com/multiformats/go-multiaddr" + manet "github.com/multiformats/go-multiaddr/net" +) + +const acceptQueueSize = 64 // It is fine to read 3 bytes from 64 connections in parallel. + +// How long we wait for a connection to be accepted before dropping it. +const acceptTimeout = 30 * time.Second + +var log = logging.Logger("tcp-demultiplex") + +// ConnMgr enables you to share the same listen address between TCP and WebSocket transports. +type ConnMgr struct { + enableReuseport bool + reuse reuseport.Transport + connGater connmgr.ConnectionGater + rcmgr network.ResourceManager + + mx sync.Mutex + listeners map[string]*multiplexedListener +} + +func NewConnMgr(enableReuseport bool, gater connmgr.ConnectionGater, rcmgr network.ResourceManager) *ConnMgr { + if rcmgr == nil { + rcmgr = &network.NullResourceManager{} + } + return &ConnMgr{ + enableReuseport: enableReuseport, + reuse: reuseport.Transport{}, + connGater: gater, + rcmgr: rcmgr, + listeners: make(map[string]*multiplexedListener), + } +} + +func (t *ConnMgr) maListen(listenAddr ma.Multiaddr) (manet.Listener, error) { + if t.useReuseport() { + return t.reuse.Listen(listenAddr) + } else { + return manet.Listen(listenAddr) + } +} + +func (t *ConnMgr) useReuseport() bool { + return t.enableReuseport && ReuseportIsAvailable() +} + +func getTCPAddr(listenAddr ma.Multiaddr) (ma.Multiaddr, error) { + haveTCP := false + addr, _ := ma.SplitFunc(listenAddr, func(c ma.Component) bool { + if haveTCP { + return true + } + if c.Protocol().Code == ma.P_TCP { + haveTCP = true + } + return false + }) + if !haveTCP { + return nil, fmt.Errorf("invalid listen addr %s, need tcp address", listenAddr) + } + return addr, nil +} + +// DemultiplexedListen returns a listener for laddr listening for `connType` connections. The connections +// accepted from returned listeners need to be upgraded with a `transport.Upgrader`. +// NOTE: All listeners for port 0 share the same underlying socket, so they have the same specific port. +func (t *ConnMgr) DemultiplexedListen(laddr ma.Multiaddr, connType DemultiplexedConnType) (manet.Listener, error) { + if !connType.IsKnown() { + return nil, fmt.Errorf("unknown connection type: %s", connType) + } + laddr, err := getTCPAddr(laddr) + if err != nil { + return nil, err + } + + t.mx.Lock() + defer t.mx.Unlock() + ml, ok := t.listeners[laddr.String()] + if ok { + dl, err := ml.DemultiplexedListen(connType) + if err != nil { + return nil, err + } + return dl, nil + } + + l, err := t.maListen(laddr) + if err != nil { + return nil, err + } + + ctx, cancel := context.WithCancel(context.Background()) + cancelFunc := func() error { + cancel() + t.mx.Lock() + defer t.mx.Unlock() + delete(t.listeners, laddr.String()) + delete(t.listeners, l.Multiaddr().String()) + return l.Close() + } + ml = &multiplexedListener{ + Listener: l, + listeners: make(map[DemultiplexedConnType]*demultiplexedListener), + ctx: ctx, + closeFn: cancelFunc, + connGater: t.connGater, + rcmgr: t.rcmgr, + } + t.listeners[laddr.String()] = ml + t.listeners[l.Multiaddr().String()] = ml + + dl, err := ml.DemultiplexedListen(connType) + if err != nil { + cerr := ml.Close() + return nil, errors.Join(err, cerr) + } + + ml.wg.Add(1) + go ml.run() + + return dl, nil +} + +var _ manet.Listener = &demultiplexedListener{} + +type multiplexedListener struct { + manet.Listener + listeners map[DemultiplexedConnType]*demultiplexedListener + mx sync.RWMutex + + connGater connmgr.ConnectionGater + rcmgr network.ResourceManager + ctx context.Context + closeFn func() error + wg sync.WaitGroup +} + +var ErrListenerExists = errors.New("listener already exists for this conn type on this address") + +func (m *multiplexedListener) DemultiplexedListen(connType DemultiplexedConnType) (manet.Listener, error) { + if !connType.IsKnown() { + return nil, fmt.Errorf("unknown connection type: %s", connType) + } + + m.mx.Lock() + defer m.mx.Unlock() + if _, ok := m.listeners[connType]; ok { + return nil, ErrListenerExists + } + + ctx, cancel := context.WithCancel(m.ctx) + l := &demultiplexedListener{ + buffer: make(chan manet.Conn), + inner: m.Listener, + ctx: ctx, + cancelFunc: cancel, + closeFn: func() error { m.removeDemultiplexedListener(connType); return nil }, + } + + m.listeners[connType] = l + + return l, nil +} + +func (m *multiplexedListener) run() error { + defer m.Close() + defer m.wg.Done() + acceptQueue := make(chan struct{}, acceptQueueSize) + for { + c, err := m.Listener.Accept() + if err != nil { + return err + } + + // Gate and resource limit the connection here. + // If done after sampling the connection, we'll be vulnerable to DOS attacks by a single peer + // which clogs up our entire connection queue. + // This duplicates the responsibility of gating and resource limiting between here and the upgrader. The + // alternative without duplication requires moving the process of upgrading the connection here, which forces + // us to establish the websocket connection here. That is more duplication, or a significant breaking change. + // + // Bugs around multiple calls to OpenConnection or InterceptAccept are prevented by the transport + // integration tests. + if m.connGater != nil && !m.connGater.InterceptAccept(c) { + log.Debugf("gater blocked incoming connection on local addr %s from %s", + c.LocalMultiaddr(), c.RemoteMultiaddr()) + if err := c.Close(); err != nil { + log.Warnf("failed to close incoming connection rejected by gater: %s", err) + } + continue + } + connScope, err := m.rcmgr.OpenConnection(network.DirInbound, true, c.RemoteMultiaddr()) + if err != nil { + log.Debugw("resource manager blocked accept of new connection", "error", err) + if err := c.Close(); err != nil { + log.Warnf("failed to open incoming connection. Rejected by resource manager: %s", err) + } + continue + } + + select { + case acceptQueue <- struct{}{}: + // NOTE: We can drop the connection, but this is similar to the behaviour in the upgrader. + case <-m.ctx.Done(): + c.Close() + log.Debugf("accept queue full, dropping connection: %s", c.RemoteMultiaddr()) + } + + m.wg.Add(1) + go func() { + defer func() { <-acceptQueue }() + defer m.wg.Done() + ctx, cancelCtx := context.WithTimeout(m.ctx, acceptTimeout) + defer cancelCtx() + t, c, err := identifyConnType(c) + if err != nil { + connScope.Done() + log.Debugf("error demultiplexing connection: %s", err.Error()) + return + } + + connWithScope, err := manetConnWithScope(c, connScope) + if err != nil { + connScope.Done() + closeErr := c.Close() + err = errors.Join(err, closeErr) + log.Debugf("error wrapping connection with scope: %s", err.Error()) + return + } + + m.mx.RLock() + demux, ok := m.listeners[t] + m.mx.RUnlock() + if !ok { + closeErr := connWithScope.Close() + if closeErr != nil { + log.Debugf("no registered listener for demultiplex connection %s. Error closing the connection %s", t, closeErr.Error()) + } else { + log.Debugf("no registered listener for demultiplex connection %s", t) + } + return + } + + select { + case demux.buffer <- connWithScope: + case <-ctx.Done(): + connWithScope.Close() + } + }() + } +} + +func (m *multiplexedListener) Close() error { + m.mx.Lock() + for _, l := range m.listeners { + l.cancelFunc() + } + err := m.closeListener() + m.mx.Unlock() + m.wg.Wait() + return err +} + +func (m *multiplexedListener) closeListener() error { + lerr := m.Listener.Close() + cerr := m.closeFn() + return errors.Join(lerr, cerr) +} + +func (m *multiplexedListener) removeDemultiplexedListener(c DemultiplexedConnType) { + m.mx.Lock() + defer m.mx.Unlock() + + delete(m.listeners, c) + if len(m.listeners) == 0 { + m.closeListener() + m.mx.Unlock() + m.wg.Wait() + m.mx.Lock() + } +} + +type demultiplexedListener struct { + buffer chan manet.Conn + inner manet.Listener + ctx context.Context + cancelFunc context.CancelFunc + closeFn func() error +} + +func (m *demultiplexedListener) Accept() (manet.Conn, error) { + select { + case c := <-m.buffer: + return c, nil + case <-m.ctx.Done(): + return nil, transport.ErrListenerClosed + } +} + +func (m *demultiplexedListener) Close() error { + m.cancelFunc() + return m.closeFn() +} + +func (m *demultiplexedListener) Multiaddr() ma.Multiaddr { + return m.inner.Multiaddr() +} + +func (m *demultiplexedListener) Addr() net.Addr { + return m.inner.Addr() +} diff --git a/p2p/transport/tcpreuse/listener_test.go b/p2p/transport/tcpreuse/listener_test.go new file mode 100644 index 0000000000..b5dc49f2c1 --- /dev/null +++ b/p2p/transport/tcpreuse/listener_test.go @@ -0,0 +1,476 @@ +package tcpreuse + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "fmt" + "math/big" + "net" + "net/http" + "sync" + "testing" + "time" + + "github.com/gorilla/websocket" + ma "github.com/multiformats/go-multiaddr" + manet "github.com/multiformats/go-multiaddr/net" + "github.com/multiformats/go-multistream" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func selfSignedTLSConfig(t *testing.T) *tls.Config { + t.Helper() + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + certTemplate := x509.Certificate{ + SerialNumber: &big.Int{}, + Subject: pkix.Name{ + Organization: []string{"Test"}, + }, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + + derBytes, err := x509.CreateCertificate(rand.Reader, &certTemplate, &certTemplate, &priv.PublicKey, priv) + require.NoError(t, err) + + cert := tls.Certificate{ + Certificate: [][]byte{derBytes}, + PrivateKey: priv, + } + + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{cert}, + } + return tlsConfig +} + +type wsHandler struct{ conns chan *websocket.Conn } + +func (wh wsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + u := websocket.Upgrader{} + c, _ := u.Upgrade(w, r, http.Header{}) + wh.conns <- c +} + +func TestListenerSingle(t *testing.T) { + listenAddr := ma.StringCast("/ip4/0.0.0.0/tcp/0") + const N = 64 + for _, enableReuseport := range []bool{true, false} { + t.Run(fmt.Sprintf("multistream-reuseport:%v", enableReuseport), func(t *testing.T) { + cm := NewConnMgr(enableReuseport, nil, nil) + l, err := cm.DemultiplexedListen(listenAddr, DemultiplexedConnType_MultistreamSelect) + require.NoError(t, err) + go func() { + d := net.Dialer{} + for i := 0; i < N; i++ { + go func() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + conn, err := d.DialContext(ctx, l.Addr().Network(), l.Addr().String()) + if err != nil { + t.Error("failed to dial", err, i) + return + } + lconn := multistream.NewMSSelect(conn, "a") + buf := make([]byte, 10) + _, err = lconn.Write([]byte("hello-multistream")) + if err != nil { + t.Error(err) + } + _, err = lconn.Read(buf) + if err == nil { + t.Error("expected EOF got nil") + } + }() + } + }() + + var wg sync.WaitGroup + for i := 0; i < N; i++ { + c, err := l.Accept() + require.NoError(t, err) + wg.Add(1) + go func() { + defer wg.Done() + cc := multistream.NewMSSelect(c, "a") + defer cc.Close() + buf := make([]byte, 30) + n, err := cc.Read(buf) + if !assert.NoError(t, err) { + return + } + if !assert.Equal(t, "hello-multistream", string(buf[:n])) { + return + } + }() + } + wg.Wait() + }) + + t.Run(fmt.Sprintf("WebSocket-reuseport:%v", enableReuseport), func(t *testing.T) { + cm := NewConnMgr(enableReuseport, nil, nil) + l, err := cm.DemultiplexedListen(listenAddr, DemultiplexedConnType_HTTP) + require.NoError(t, err) + wh := wsHandler{conns: make(chan *websocket.Conn, acceptQueueSize)} + go func() { + http.Serve(manet.NetListener(l), wh) + }() + go func() { + d := websocket.Dialer{} + for i := 0; i < N; i++ { + go func() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + conn, _, err := d.DialContext(ctx, fmt.Sprintf("ws://%s", l.Addr().String()), http.Header{}) + if err != nil { + t.Error("failed to dial", err, i) + return + } + err = conn.WriteMessage(websocket.TextMessage, []byte("hello")) + if err != nil { + t.Error(err) + } + _, _, err = conn.ReadMessage() + if err == nil { + t.Error("expected EOF got nil") + } + }() + } + }() + var wg sync.WaitGroup + for i := 0; i < N; i++ { + c := <-wh.conns + wg.Add(1) + go func() { + defer wg.Done() + defer c.Close() + msgType, buf, err := c.ReadMessage() + if !assert.NoError(t, err) { + return + } + if !assert.Equal(t, msgType, websocket.TextMessage) { + return + } + if !assert.Equal(t, "hello", string(buf)) { + return + } + }() + } + wg.Wait() + }) + + t.Run(fmt.Sprintf("WebSocketTLS-reuseport:%v", enableReuseport), func(t *testing.T) { + cm := NewConnMgr(enableReuseport, nil, nil) + l, err := cm.DemultiplexedListen(listenAddr, DemultiplexedConnType_TLS) + require.NoError(t, err) + defer l.Close() + wh := wsHandler{conns: make(chan *websocket.Conn, acceptQueueSize)} + go func() { + s := http.Server{Handler: wh, TLSConfig: selfSignedTLSConfig(t)} + s.ServeTLS(manet.NetListener(l), "", "") + }() + go func() { + d := websocket.Dialer{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} + for i := 0; i < N; i++ { + go func() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + conn, _, err := d.DialContext(ctx, fmt.Sprintf("wss://%s", l.Addr().String()), http.Header{}) + if err != nil { + t.Error("failed to dial", err, i) + return + } + err = conn.WriteMessage(websocket.TextMessage, []byte("hello")) + if err != nil { + t.Error(err) + } + _, _, err = conn.ReadMessage() + if err == nil { + t.Error("expected EOF got nil") + } + }() + } + }() + var wg sync.WaitGroup + for i := 0; i < N; i++ { + c := <-wh.conns + wg.Add(1) + go func() { + defer wg.Done() + defer c.Close() + msgType, buf, err := c.ReadMessage() + if !assert.NoError(t, err) { + return + } + if !assert.Equal(t, msgType, websocket.TextMessage) { + return + } + if !assert.Equal(t, "hello", string(buf)) { + return + } + }() + } + wg.Wait() + }) + } +} + +func TestListenerMultiplexed(t *testing.T) { + listenAddr := ma.StringCast("/ip4/0.0.0.0/tcp/0") + const N = 20 + for _, enableReuseport := range []bool{true, false} { + cm := NewConnMgr(enableReuseport, nil, nil) + msl, err := cm.DemultiplexedListen(listenAddr, DemultiplexedConnType_MultistreamSelect) + require.NoError(t, err) + defer msl.Close() + + wsl, err := cm.DemultiplexedListen(listenAddr, DemultiplexedConnType_HTTP) + require.NoError(t, err) + defer wsl.Close() + require.Equal(t, wsl.Multiaddr(), msl.Multiaddr()) + wh := wsHandler{conns: make(chan *websocket.Conn, acceptQueueSize)} + go func() { + http.Serve(manet.NetListener(wsl), wh) + }() + + wssl, err := cm.DemultiplexedListen(listenAddr, DemultiplexedConnType_TLS) + require.NoError(t, err) + defer wssl.Close() + require.Equal(t, wssl.Multiaddr(), wsl.Multiaddr()) + whs := wsHandler{conns: make(chan *websocket.Conn, acceptQueueSize)} + go func() { + s := http.Server{Handler: whs, TLSConfig: selfSignedTLSConfig(t)} + s.ServeTLS(manet.NetListener(wssl), "", "") + }() + + // multistream connections + go func() { + d := net.Dialer{} + for i := 0; i < N; i++ { + go func() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + conn, err := d.DialContext(ctx, msl.Addr().Network(), msl.Addr().String()) + if err != nil { + t.Error("failed to dial", err, i) + return + } + lconn := multistream.NewMSSelect(conn, "a") + buf := make([]byte, 10) + _, err = lconn.Write([]byte("multistream")) + if err != nil { + t.Error(err) + } + _, err = lconn.Read(buf) + if err == nil { + t.Error("expected EOF got nil") + } + }() + } + }() + + // ws connections + go func() { + d := websocket.Dialer{} + for i := 0; i < N; i++ { + go func() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + conn, _, err := d.DialContext(ctx, fmt.Sprintf("ws://%s", msl.Addr().String()), http.Header{}) + if err != nil { + t.Error("failed to dial", err, i) + return + } + err = conn.WriteMessage(websocket.TextMessage, []byte("websocket")) + if err != nil { + t.Error(err) + } + _, _, err = conn.ReadMessage() + if err == nil { + t.Error("expected EOF got nil") + } + }() + } + }() + + // wss connections + go func() { + d := websocket.Dialer{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} + for i := 0; i < N; i++ { + go func() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + conn, _, err := d.DialContext(ctx, fmt.Sprintf("wss://%s", msl.Addr().String()), http.Header{}) + if err != nil { + t.Error("failed to dial", err, i) + return + } + err = conn.WriteMessage(websocket.TextMessage, []byte("websocket-tls")) + if err != nil { + t.Error(err) + } + _, _, err = conn.ReadMessage() + if err == nil { + t.Error("expected EOF got nil") + } + }() + } + }() + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < N; i++ { + c, err := msl.Accept() + if !assert.NoError(t, err) { + return + } + wg.Add(1) + go func() { + defer wg.Done() + cc := multistream.NewMSSelect(c, "a") + defer cc.Close() + buf := make([]byte, 20) + n, err := cc.Read(buf) + if !assert.NoError(t, err) { + return + } + if !assert.Equal(t, "multistream", string(buf[:n])) { + return + } + }() + } + }() + + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < N; i++ { + c := <-wh.conns + wg.Add(1) + go func() { + defer wg.Done() + defer c.Close() + msgType, buf, err := c.ReadMessage() + if !assert.NoError(t, err) { + return + } + if !assert.Equal(t, msgType, websocket.TextMessage) { + return + } + if !assert.Equal(t, "websocket", string(buf)) { + return + } + }() + } + }() + + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < N; i++ { + c := <-whs.conns + wg.Add(1) + go func() { + defer wg.Done() + defer c.Close() + msgType, buf, err := c.ReadMessage() + if !assert.NoError(t, err) { + return + } + if !assert.Equal(t, msgType, websocket.TextMessage) { + return + } + if !assert.Equal(t, "websocket-tls", string(buf)) { + return + } + }() + } + }() + wg.Wait() + } +} + +func TestListenerClose(t *testing.T) { + testClose := func(listenAddr ma.Multiaddr) { + // listen on port 0 + cm := NewConnMgr(false, nil, nil) + ml, err := cm.DemultiplexedListen(listenAddr, DemultiplexedConnType_MultistreamSelect) + require.NoError(t, err) + wl, err := cm.DemultiplexedListen(listenAddr, DemultiplexedConnType_HTTP) + require.NoError(t, err) + require.Equal(t, wl.Multiaddr(), ml.Multiaddr()) + wl.Close() + + wl, err = cm.DemultiplexedListen(listenAddr, DemultiplexedConnType_HTTP) + require.NoError(t, err) + require.Equal(t, wl.Multiaddr(), ml.Multiaddr()) + + ml.Close() + + mll, err := cm.DemultiplexedListen(listenAddr, DemultiplexedConnType_MultistreamSelect) + require.NoError(t, err) + require.Equal(t, wl.Multiaddr(), ml.Multiaddr()) + + mll.Close() + wl.Close() + + ml, err = cm.DemultiplexedListen(listenAddr, DemultiplexedConnType_MultistreamSelect) + require.NoError(t, err) + + // Now listen on the specific port previously used + listenAddr = ml.Multiaddr() + wl, err = cm.DemultiplexedListen(listenAddr, DemultiplexedConnType_HTTP) + require.NoError(t, err) + require.Equal(t, wl.Multiaddr(), ml.Multiaddr()) + wl.Close() + + wl, err = cm.DemultiplexedListen(listenAddr, DemultiplexedConnType_HTTP) + require.NoError(t, err) + require.Equal(t, wl.Multiaddr(), ml.Multiaddr()) + + ml.Close() + wl.Close() + } + listenAddrs := []ma.Multiaddr{ma.StringCast("/ip4/0.0.0.0/tcp/0"), ma.StringCast("/ip6/::/tcp/0")} + for _, listenAddr := range listenAddrs { + testClose(listenAddr) + } +} + +func setDeferReset[T any](t testing.TB, ptr *T, val T) { + t.Helper() + orig := *ptr + *ptr = val + t.Cleanup(func() { *ptr = orig }) +} + +// TestHitTimeout asserts that we don't panic in case we fail to peek at the connection. +func TestHitTimeout(t *testing.T) { + setDeferReset(t, &identifyConnTimeout, 100*time.Millisecond) + // listen on port 0 + cm := NewConnMgr(false, nil, nil) + + listenAddr := ma.StringCast("/ip4/127.0.0.1/tcp/0") + ml, err := cm.DemultiplexedListen(listenAddr, DemultiplexedConnType_MultistreamSelect) + require.NoError(t, err) + defer ml.Close() + + tcpConn, err := net.Dial(ml.Addr().Network(), ml.Addr().String()) + require.NoError(t, err) + + // Stall tcp conn for over the timeout. + time.Sleep(identifyConnTimeout + 100*time.Millisecond) + + tcpConn.Close() +} diff --git a/p2p/transport/tcp/reuseport.go b/p2p/transport/tcpreuse/reuseport.go similarity index 81% rename from p2p/transport/tcp/reuseport.go rename to p2p/transport/tcpreuse/reuseport.go index ba09304622..a2529c0bda 100644 --- a/p2p/transport/tcp/reuseport.go +++ b/p2p/transport/tcpreuse/reuseport.go @@ -1,4 +1,4 @@ -package tcp +package tcpreuse import ( "os" @@ -11,13 +11,13 @@ import ( // It default to true. const envReuseport = "LIBP2P_TCP_REUSEPORT" -// envReuseportVal stores the value of envReuseport. defaults to true. -var envReuseportVal = true +// EnvReuseportVal stores the value of envReuseport. defaults to true. +var EnvReuseportVal = true func init() { v := strings.ToLower(os.Getenv(envReuseport)) if v == "false" || v == "f" || v == "0" { - envReuseportVal = false + EnvReuseportVal = false log.Infof("REUSEPORT disabled (LIBP2P_TCP_REUSEPORT=%s)", v) } } @@ -31,5 +31,5 @@ func init() { // If this becomes a sought after feature, we could add this to the config. // In the end, reuseport is a stop-gap. func ReuseportIsAvailable() bool { - return envReuseportVal && reuseport.Available() + return EnvReuseportVal && reuseport.Available() } diff --git a/p2p/transport/testsuite/utils_suite.go b/p2p/transport/testsuite/utils_suite.go index 5e488397a5..8b002f8900 100644 --- a/p2p/transport/testsuite/utils_suite.go +++ b/p2p/transport/testsuite/utils_suite.go @@ -11,7 +11,9 @@ import ( ma "github.com/multiformats/go-multiaddr" ) -var Subtests = []func(t *testing.T, ta, tb transport.Transport, maddr ma.Multiaddr, peerA peer.ID){ +type TransportSubTestFn func(t *testing.T, ta, tb transport.Transport, maddr ma.Multiaddr, peerA peer.ID) + +var Subtests = []TransportSubTestFn{ SubtestProtocols, SubtestBasic, SubtestCancel, @@ -33,12 +35,17 @@ func getFunctionName(i interface{}) string { } func SubtestTransport(t *testing.T, ta, tb transport.Transport, addr string, peerA peer.ID) { + t.Helper() + SubtestTransportWithFs(t, ta, tb, addr, peerA, Subtests) +} + +func SubtestTransportWithFs(t *testing.T, ta, tb transport.Transport, addr string, peerA peer.ID, tests []TransportSubTestFn) { maddr, err := ma.NewMultiaddr(addr) if err != nil { t.Fatal(err) } - for _, f := range Subtests { + for _, f := range tests { t.Run(getFunctionName(f), func(t *testing.T) { f(t, ta, tb, maddr, peerA) }) diff --git a/p2p/transport/webrtc/listener.go b/p2p/transport/webrtc/listener.go index d4ba3c0550..c3e2f29799 100644 --- a/p2p/transport/webrtc/listener.go +++ b/p2p/transport/webrtc/listener.go @@ -33,8 +33,12 @@ func (c *connMultiaddrs) LocalMultiaddr() ma.Multiaddr { return c.local } func (c *connMultiaddrs) RemoteMultiaddr() ma.Multiaddr { return c.remote } const ( - candidateSetupTimeout = 20 * time.Second - DefaultMaxInFlightConnections = 10 + candidateSetupTimeout = 10 * time.Second + // This is higher than other transports(64) as there's no way to detect a peer that has gone away after + // sending the initial connection request message(STUN Binding request). Such peers take up a goroutine + // till connection timeout. As the number of handshakes in parallel is still guarded by the resource + // manager, this higher number is okay. + DefaultMaxInFlightConnections = 128 ) type listener struct { @@ -325,8 +329,7 @@ func (l *listener) Multiaddr() ma.Multiaddr { // addOnConnectionStateChangeCallback adds the OnConnectionStateChange to the PeerConnection. // The channel returned here: // * is closed when the state changes to Connection -// * receives an error when the state changes to Failed -// * doesn't receive anything (nor is closed) when the state changes to Disconnected +// * receives an error when the state changes to Failed or Closed or Disconnected func addOnConnectionStateChangeCallback(pc *webrtc.PeerConnection) <-chan error { errC := make(chan error, 1) var once sync.Once @@ -334,17 +337,15 @@ func addOnConnectionStateChangeCallback(pc *webrtc.PeerConnection) <-chan error switch pc.ConnectionState() { case webrtc.PeerConnectionStateConnected: once.Do(func() { close(errC) }) - case webrtc.PeerConnectionStateFailed: + // PeerConnectionStateFailed happens when we fail to negotiate the connection. + // PeerConnectionStateDisconnected happens when we disconnect immediately after connecting. + // PeerConnectionStateClosed happens when we close the peer connection locally, not when remote closes. We don't need + // to error in this case, but it's a no-op, so it doesn't hurt. + case webrtc.PeerConnectionStateFailed, webrtc.PeerConnectionStateClosed, webrtc.PeerConnectionStateDisconnected: once.Do(func() { errC <- errors.New("peerconnection failed") close(errC) }) - case webrtc.PeerConnectionStateDisconnected: - // the connection can move to a disconnected state and back to a connected state without ICE renegotiation. - // This could happen when underlying UDP packets are lost, and therefore the connection moves to the disconnected state. - // If the connection then receives packets on the connection, it can move back to the connected state. - // If no packets are received until the failed timeout is triggered, the connection moves to the failed state. - log.Warn("peerconnection disconnected") } }) return errC diff --git a/p2p/transport/webrtc/pb/message.pb.go b/p2p/transport/webrtc/pb/message.pb.go index d8e68a1aa9..d7d4d583af 100644 --- a/p2p/transport/webrtc/pb/message.pb.go +++ b/p2p/transport/webrtc/pb/message.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.34.2 -// protoc v5.27.3 +// protoc-gen-go v1.35.1 +// protoc v5.28.2 // source: p2p/transport/webrtc/pb/message.proto package pb @@ -101,11 +101,9 @@ type Message struct { func (x *Message) Reset() { *x = Message{} - if protoimpl.UnsafeEnabled { - mi := &file_p2p_transport_webrtc_pb_message_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_p2p_transport_webrtc_pb_message_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Message) String() string { @@ -116,7 +114,7 @@ func (*Message) ProtoMessage() {} func (x *Message) ProtoReflect() protoreflect.Message { mi := &file_p2p_transport_webrtc_pb_message_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -197,20 +195,6 @@ func file_p2p_transport_webrtc_pb_message_proto_init() { if File_p2p_transport_webrtc_pb_message_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_p2p_transport_webrtc_pb_message_proto_msgTypes[0].Exporter = func(v any, i int) any { - switch v := v.(*Message); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ diff --git a/p2p/transport/websocket/addrs_test.go b/p2p/transport/websocket/addrs_test.go index 3c5ba502a9..50a8b9e823 100644 --- a/p2p/transport/websocket/addrs_test.go +++ b/p2p/transport/websocket/addrs_test.go @@ -69,7 +69,7 @@ func TestConvertWebsocketMultiaddrToNetAddr(t *testing.T) { } func TestListeningOnDNSAddr(t *testing.T) { - ln, err := newListener(ma.StringCast("/dns/localhost/tcp/0/ws"), nil) + ln, err := newListener(ma.StringCast("/dns/localhost/tcp/0/ws"), nil, nil) require.NoError(t, err) addr := ln.Multiaddr() first, rest := ma.SplitFirst(addr) diff --git a/p2p/transport/websocket/conn.go b/p2p/transport/websocket/conn.go index 30b70055d0..ce51611703 100644 --- a/p2p/transport/websocket/conn.go +++ b/p2p/transport/websocket/conn.go @@ -1,6 +1,7 @@ package websocket import ( + "errors" "io" "net" "sync" @@ -8,6 +9,8 @@ import ( "github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/transport" + ma "github.com/multiformats/go-multiaddr" + manet "github.com/multiformats/go-multiaddr/net" ws "github.com/gorilla/websocket" ) @@ -22,20 +25,53 @@ type Conn struct { secure bool DefaultMessageType int reader io.Reader - closeOnce sync.Once + closeOnceVal func() error + laddr ma.Multiaddr + raddr ma.Multiaddr readLock, writeLock sync.Mutex } var _ net.Conn = (*Conn)(nil) +var _ manet.Conn = (*Conn)(nil) // NewConn creates a Conn given a regular gorilla/websocket Conn. +// +// Deprecated: There's no reason to use this method externally. It'll be unexported in a future release. func NewConn(raw *ws.Conn, secure bool) *Conn { - return &Conn{ + lna := NewAddrWithScheme(raw.LocalAddr().String(), secure) + laddr, err := manet.FromNetAddr(lna) + if err != nil { + log.Errorf("BUG: invalid localaddr on websocket conn", raw.LocalAddr()) + return nil + } + + rna := NewAddrWithScheme(raw.RemoteAddr().String(), secure) + raddr, err := manet.FromNetAddr(rna) + if err != nil { + log.Errorf("BUG: invalid remoteaddr on websocket conn", raw.RemoteAddr()) + return nil + } + + c := &Conn{ Conn: raw, secure: secure, DefaultMessageType: ws.BinaryMessage, + laddr: laddr, + raddr: raddr, } + c.closeOnceVal = sync.OnceValue(c.closeOnceFn) + return c +} + +// LocalMultiaddr implements manet.Conn. +func (c *Conn) LocalMultiaddr() ma.Multiaddr { + return c.laddr +} + +// RemoteMultiaddr implements manet.Conn. +func (c *Conn) RemoteMultiaddr() ma.Multiaddr { + return c.raddr } func (c *Conn) Read(b []byte) (int, error) { @@ -99,26 +135,31 @@ func (c *Conn) Write(b []byte) (n int, err error) { return len(b), nil } -// Close closes the connection. Only the first call to Close will receive the -// close error, subsequent and concurrent calls will return nil. +func (c *Conn) Scope() network.ConnManagementScope { + nc := c.NetConn() + if sc, ok := nc.(interface { + Scope() network.ConnManagementScope + }); ok { + return sc.Scope() + } + return nil +} + +// Close closes the connection. +// subsequent and concurrent calls will return the same error value. // This method is thread-safe. func (c *Conn) Close() error { - var err error - c.closeOnce.Do(func() { - err1 := c.Conn.WriteControl( - ws.CloseMessage, - ws.FormatCloseMessage(ws.CloseNormalClosure, "closed"), - time.Now().Add(GracefulCloseTimeout), - ) - err2 := c.Conn.Close() - switch { - case err1 != nil: - err = err1 - case err2 != nil: - err = err2 - } - }) - return err + return c.closeOnceVal() +} + +func (c *Conn) closeOnceFn() error { + err1 := c.Conn.WriteControl( + ws.CloseMessage, + ws.FormatCloseMessage(ws.CloseNormalClosure, "closed"), + time.Now().Add(GracefulCloseTimeout), + ) + err2 := c.Conn.Close() + return errors.Join(err1, err2) } func (c *Conn) LocalAddr() net.Addr { diff --git a/p2p/transport/websocket/listener.go b/p2p/transport/websocket/listener.go index 8071ddb814..dd399aa079 100644 --- a/p2p/transport/websocket/listener.go +++ b/p2p/transport/websocket/listener.go @@ -4,14 +4,16 @@ import ( "crypto/tls" "errors" "fmt" - "go.uber.org/zap" "net" "net/http" "sync" + "go.uber.org/zap" + logging "github.com/ipfs/go-log/v2" "github.com/libp2p/go-libp2p/core/transport" + "github.com/libp2p/go-libp2p/p2p/transport/tcpreuse" ma "github.com/multiformats/go-multiaddr" manet "github.com/multiformats/go-multiaddr/net" @@ -50,7 +52,7 @@ func (pwma *parsedWebsocketMultiaddr) toMultiaddr() ma.Multiaddr { // newListener creates a new listener from a raw net.Listener. // tlsConf may be nil (for unencrypted websockets). -func newListener(a ma.Multiaddr, tlsConf *tls.Config) (*listener, error) { +func newListener(a ma.Multiaddr, tlsConf *tls.Config, sharedTcp *tcpreuse.ConnMgr) (*listener, error) { parsed, err := parseWebsocketMultiaddr(a) if err != nil { return nil, err @@ -60,19 +62,36 @@ func newListener(a ma.Multiaddr, tlsConf *tls.Config) (*listener, error) { return nil, fmt.Errorf("cannot listen on wss address %s without a tls.Config", a) } - lnet, lnaddr, err := manet.DialArgs(parsed.restMultiaddr) - if err != nil { - return nil, err - } - nl, err := net.Listen(lnet, lnaddr) - if err != nil { - return nil, err + var nl net.Listener + + if sharedTcp == nil { + lnet, lnaddr, err := manet.DialArgs(parsed.restMultiaddr) + if err != nil { + return nil, err + } + nl, err = net.Listen(lnet, lnaddr) + if err != nil { + return nil, err + } + } else { + var connType tcpreuse.DemultiplexedConnType + if parsed.isWSS { + connType = tcpreuse.DemultiplexedConnType_TLS + } else { + connType = tcpreuse.DemultiplexedConnType_HTTP + } + mal, err := sharedTcp.DemultiplexedListen(parsed.restMultiaddr, connType) + if err != nil { + return nil, err + } + nl = manet.NetListener(mal) } laddr, err := manet.FromNetAddr(nl.Addr()) if err != nil { return nil, err } + first, _ := ma.SplitFirst(a) // Don't resolve dns addresses. // We want to be able to announce domain names, so the peer can validate the TLS certificate. @@ -111,7 +130,12 @@ func (l *listener) ServeHTTP(w http.ResponseWriter, r *http.Request) { // The upgrader writes a response for us. return } - + nc := NewConn(c, l.isWss) + if nc == nil { + c.Close() + w.WriteHeader(500) + return + } select { case l.incoming <- NewConn(c, l.isWss): case <-l.closed: @@ -126,13 +150,7 @@ func (l *listener) Accept() (manet.Conn, error) { if !ok { return nil, transport.ErrListenerClosed } - - mnc, err := manet.WrapNetConn(c) - if err != nil { - c.Close() - return nil, err - } - return mnc, nil + return c, nil case <-l.closed: return nil, transport.ErrListenerClosed } diff --git a/p2p/transport/websocket/websocket.go b/p2p/transport/websocket/websocket.go index 36818decee..e24cb88c6d 100644 --- a/p2p/transport/websocket/websocket.go +++ b/p2p/transport/websocket/websocket.go @@ -11,6 +11,7 @@ import ( "github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/transport" + "github.com/libp2p/go-libp2p/p2p/transport/tcpreuse" ma "github.com/multiformats/go-multiaddr" mafmt "github.com/multiformats/go-multiaddr-fmt" @@ -87,11 +88,13 @@ type WebsocketTransport struct { tlsClientConf *tls.Config tlsConf *tls.Config + + sharedTcp *tcpreuse.ConnMgr } var _ transport.Transport = (*WebsocketTransport)(nil) -func New(u transport.Upgrader, rcmgr network.ResourceManager, opts ...Option) (*WebsocketTransport, error) { +func New(u transport.Upgrader, rcmgr network.ResourceManager, sharedTCP *tcpreuse.ConnMgr, opts ...Option) (*WebsocketTransport, error) { if rcmgr == nil { rcmgr = &network.NullResourceManager{} } @@ -99,6 +102,7 @@ func New(u transport.Upgrader, rcmgr network.ResourceManager, opts ...Option) (* upgrader: u, rcmgr: rcmgr, tlsClientConf: &tls.Config{}, + sharedTcp: sharedTCP, } for _, opt := range opts { if err := opt(t); err != nil { @@ -133,7 +137,7 @@ func (t *WebsocketTransport) Resolve(_ context.Context, maddr ma.Multiaddr) ([]m if parsed.sni == nil { var err error - // We don't have an sni component, we'll use dns/dnsaddr + // We don't have an sni component, we'll use dns ma.ForEach(parsed.restMultiaddr, func(c ma.Component) bool { switch c.Protocol().Code { case ma.P_DNS, ma.P_DNS4, ma.P_DNS6: @@ -233,7 +237,7 @@ func (t *WebsocketTransport) maListen(a ma.Multiaddr) (manet.Listener, error) { if t.tlsConf != nil { tlsConf = t.tlsConf.Clone() } - l, err := newListener(a, tlsConf) + l, err := newListener(a, tlsConf, t.sharedTcp) if err != nil { return nil, err } diff --git a/p2p/transport/websocket/websocket_test.go b/p2p/transport/websocket/websocket_test.go index 8f912c4138..9ca03775a2 100644 --- a/p2p/transport/websocket/websocket_test.go +++ b/p2p/transport/websocket/websocket_test.go @@ -154,7 +154,7 @@ func testWSSServer(t *testing.T, listenAddr ma.Multiaddr) (ma.Multiaddr, peer.ID } id, u := newSecureUpgrader(t) - tpt, err := New(u, &network.NullResourceManager{}, WithTLSConfig(tlsConf)) + tpt, err := New(u, &network.NullResourceManager{}, nil, WithTLSConfig(tlsConf)) if err != nil { t.Fatal(err) } @@ -237,7 +237,7 @@ func TestHostHeaderWss(t *testing.T) { tlsConfig := &tls.Config{InsecureSkipVerify: true} // Our test server doesn't have a cert signed by a CA _, u := newSecureUpgrader(t) - tpt, err := New(u, &network.NullResourceManager{}, WithTLSClientConfig(tlsConfig)) + tpt, err := New(u, &network.NullResourceManager{}, nil, WithTLSClientConfig(tlsConfig)) require.NoError(t, err) masToDial, err := tpt.Resolve(context.Background(), serverMA) @@ -256,7 +256,7 @@ func TestDialWss(t *testing.T) { tlsConfig := &tls.Config{InsecureSkipVerify: true} // Our test server doesn't have a cert signed by a CA _, u := newSecureUpgrader(t) - tpt, err := New(u, &network.NullResourceManager{}, WithTLSClientConfig(tlsConfig)) + tpt, err := New(u, &network.NullResourceManager{}, nil, WithTLSClientConfig(tlsConfig)) require.NoError(t, err) masToDial, err := tpt.Resolve(context.Background(), serverMA) @@ -279,7 +279,7 @@ func TestDialWssNoClientCert(t *testing.T) { require.Contains(t, serverMA.String(), "tls") _, u := newSecureUpgrader(t) - tpt, err := New(u, &network.NullResourceManager{}) + tpt, err := New(u, &network.NullResourceManager{}, nil) require.NoError(t, err) masToDial, err := tpt.Resolve(context.Background(), serverMA) @@ -294,12 +294,12 @@ func TestDialWssNoClientCert(t *testing.T) { func TestWebsocketTransport(t *testing.T) { peerA, ua := newUpgrader(t) - ta, err := New(ua, nil) + ta, err := New(ua, nil, nil) if err != nil { t.Fatal(err) } _, ub := newUpgrader(t) - tb, err := New(ub, nil) + tb, err := New(ub, nil, nil) if err != nil { t.Fatal(err) } @@ -325,7 +325,7 @@ func connectAndExchangeData(t *testing.T, laddr ma.Multiaddr, secure bool) { opts = append(opts, WithTLSConfig(tlsConf)) } server, u := newUpgrader(t) - tpt, err := New(u, &network.NullResourceManager{}, opts...) + tpt, err := New(u, &network.NullResourceManager{}, nil, opts...) require.NoError(t, err) l, err := tpt.Listen(laddr) require.NoError(t, err) @@ -344,7 +344,7 @@ func connectAndExchangeData(t *testing.T, laddr ma.Multiaddr, secure bool) { opts = append(opts, WithTLSClientConfig(&tls.Config{InsecureSkipVerify: true})) } _, u := newUpgrader(t) - tpt, err := New(u, &network.NullResourceManager{}, opts...) + tpt, err := New(u, &network.NullResourceManager{}, nil, opts...) require.NoError(t, err) c, err := tpt.Dial(context.Background(), l.Multiaddr(), server) require.NoError(t, err) @@ -382,7 +382,7 @@ func TestWebsocketConnection(t *testing.T) { func TestWebsocketListenSecureFailWithoutTLSConfig(t *testing.T) { _, u := newUpgrader(t) - tpt, err := New(u, &network.NullResourceManager{}) + tpt, err := New(u, &network.NullResourceManager{}, nil) require.NoError(t, err) addr := ma.StringCast("/ip4/127.0.0.1/tcp/0/wss") _, err = tpt.Listen(addr) @@ -391,7 +391,7 @@ func TestWebsocketListenSecureFailWithoutTLSConfig(t *testing.T) { func TestWebsocketListenSecureAndInsecure(t *testing.T) { serverID, serverUpgrader := newUpgrader(t) - server, err := New(serverUpgrader, &network.NullResourceManager{}, WithTLSConfig(generateTLSConfig(t))) + server, err := New(serverUpgrader, &network.NullResourceManager{}, nil, WithTLSConfig(generateTLSConfig(t))) require.NoError(t, err) lnInsecure, err := server.Listen(ma.StringCast("/ip4/127.0.0.1/tcp/0/ws")) @@ -401,7 +401,7 @@ func TestWebsocketListenSecureAndInsecure(t *testing.T) { t.Run("insecure", func(t *testing.T) { _, clientUpgrader := newUpgrader(t) - client, err := New(clientUpgrader, &network.NullResourceManager{}, WithTLSClientConfig(&tls.Config{InsecureSkipVerify: true})) + client, err := New(clientUpgrader, &network.NullResourceManager{}, nil, WithTLSClientConfig(&tls.Config{InsecureSkipVerify: true})) require.NoError(t, err) // dialing the insecure address should succeed @@ -418,7 +418,7 @@ func TestWebsocketListenSecureAndInsecure(t *testing.T) { t.Run("secure", func(t *testing.T) { _, clientUpgrader := newUpgrader(t) - client, err := New(clientUpgrader, &network.NullResourceManager{}, WithTLSClientConfig(&tls.Config{InsecureSkipVerify: true})) + client, err := New(clientUpgrader, &network.NullResourceManager{}, nil, WithTLSClientConfig(&tls.Config{InsecureSkipVerify: true})) require.NoError(t, err) // dialing the insecure address should succeed @@ -436,7 +436,7 @@ func TestWebsocketListenSecureAndInsecure(t *testing.T) { func TestConcurrentClose(t *testing.T) { _, u := newUpgrader(t) - tpt, err := New(u, &network.NullResourceManager{}) + tpt, err := New(u, &network.NullResourceManager{}, nil) require.NoError(t, err) l, err := tpt.maListen(ma.StringCast("/ip4/127.0.0.1/tcp/0/ws")) if err != nil { @@ -474,7 +474,7 @@ func TestConcurrentClose(t *testing.T) { func TestWriteZero(t *testing.T) { _, u := newUpgrader(t) - tpt, err := New(u, &network.NullResourceManager{}) + tpt, err := New(u, &network.NullResourceManager{}, nil) if err != nil { t.Fatal(err) } diff --git a/p2p/transport/webtransport/conn.go b/p2p/transport/webtransport/conn.go index 0525124711..d914398e0e 100644 --- a/p2p/transport/webtransport/conn.go +++ b/p2p/transport/webtransport/conn.go @@ -71,7 +71,7 @@ func (c *conn) allowWindowIncrease(size uint64) bool { // It must be called even if the peer closed the connection in order for // garbage collection to properly work in this package. func (c *conn) Close() error { - c.scope.Done() + defer c.scope.Done() c.transport.removeConn(c.session) err := c.session.CloseWithError(0, "") _ = c.qconn.CloseWithError(1, "") diff --git a/p2p/transport/webtransport/mock_connection_gater_test.go b/p2p/transport/webtransport/mock_connection_gater_test.go index 245d254fcb..d0a4747dbe 100644 --- a/p2p/transport/webtransport/mock_connection_gater_test.go +++ b/p2p/transport/webtransport/mock_connection_gater_test.go @@ -23,6 +23,7 @@ import ( type MockConnectionGater struct { ctrl *gomock.Controller recorder *MockConnectionGaterMockRecorder + isgomock struct{} } // MockConnectionGaterMockRecorder is the mock recorder for MockConnectionGater. @@ -71,17 +72,17 @@ func (mr *MockConnectionGaterMockRecorder) InterceptAddrDial(arg0, arg1 any) *go } // InterceptPeerDial mocks base method. -func (m *MockConnectionGater) InterceptPeerDial(arg0 peer.ID) bool { +func (m *MockConnectionGater) InterceptPeerDial(p peer.ID) bool { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InterceptPeerDial", arg0) + ret := m.ctrl.Call(m, "InterceptPeerDial", p) ret0, _ := ret[0].(bool) return ret0 } // InterceptPeerDial indicates an expected call of InterceptPeerDial. -func (mr *MockConnectionGaterMockRecorder) InterceptPeerDial(arg0 any) *gomock.Call { +func (mr *MockConnectionGaterMockRecorder) InterceptPeerDial(p any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InterceptPeerDial", reflect.TypeOf((*MockConnectionGater)(nil).InterceptPeerDial), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InterceptPeerDial", reflect.TypeOf((*MockConnectionGater)(nil).InterceptPeerDial), p) } // InterceptSecured mocks base method. diff --git a/p2p/transport/webtransport/transport_test.go b/p2p/transport/webtransport/transport_test.go index bd41446218..f58f6cc009 100644 --- a/p2p/transport/webtransport/transport_test.go +++ b/p2p/transport/webtransport/transport_test.go @@ -845,10 +845,8 @@ func TestH3ConnClosed(t *testing.T) { NextProtos: []string{http3.NextProtoH3}, }, nil) require.NoError(t, err) - rt := &http3.SingleDestinationRoundTripper{ - Connection: conn, - } - rt.Start() + rt := &http3.Transport{} + rt.NewClientConn(conn) require.Eventually(t, func() bool { c := http.Client{ Transport: rt, diff --git a/test-plans/go.mod b/test-plans/go.mod index 466a55e911..be83100fcf 100644 --- a/test-plans/go.mod +++ b/test-plans/go.mod @@ -1,6 +1,8 @@ module github.com/libp2p/go-libp2p/test-plans/m/v2 -go 1.22 +go 1.22.0 + +toolchain go1.22.1 require ( github.com/go-redis/redis/v8 v8.11.5 @@ -26,7 +28,7 @@ require ( github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gopacket v1.1.19 // indirect - github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect + github.com/google/pprof v0.0.0-20241017200806-017d972448fc // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/huin/goupnp v1.3.0 // indirect @@ -34,11 +36,11 @@ require ( github.com/ipfs/go-log/v2 v2.5.1 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect - github.com/klauspost/compress v1.17.9 // indirect + github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/koron/go-ssdp v0.0.4 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect - github.com/libp2p/go-flow-metrics v0.1.0 // indirect + github.com/libp2p/go-flow-metrics v0.2.0 // indirect github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect github.com/libp2p/go-msgio v0.3.0 // indirect github.com/libp2p/go-nat v0.2.0 // indirect @@ -47,67 +49,67 @@ require ( github.com/libp2p/go-yamux/v4 v4.0.1 // indirect github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/miekg/dns v1.1.61 // indirect + github.com/miekg/dns v1.1.62 // indirect github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect github.com/minio/sha256-simd v1.0.1 // indirect github.com/mr-tron/base58 v1.2.0 // indirect github.com/multiformats/go-base32 v0.1.0 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect - github.com/multiformats/go-multiaddr-dns v0.3.1 // indirect + github.com/multiformats/go-multiaddr-dns v0.4.1 // indirect github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect github.com/multiformats/go-multibase v0.2.0 // indirect github.com/multiformats/go-multicodec v0.9.0 // indirect github.com/multiformats/go-multihash v0.2.3 // indirect - github.com/multiformats/go-multistream v0.5.0 // indirect + github.com/multiformats/go-multistream v0.6.0 // indirect github.com/multiformats/go-varint v0.0.7 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/onsi/ginkgo/v2 v2.19.1 // indirect + github.com/onsi/ginkgo/v2 v2.20.2 // indirect github.com/opencontainers/runtime-spec v1.2.0 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect - github.com/pion/datachannel v1.5.8 // indirect + github.com/pion/datachannel v1.5.9 // indirect github.com/pion/dtls/v2 v2.2.12 // indirect - github.com/pion/ice/v2 v2.3.34 // indirect - github.com/pion/interceptor v0.1.29 // indirect + github.com/pion/ice/v2 v2.3.36 // indirect + github.com/pion/interceptor v0.1.37 // indirect github.com/pion/logging v0.2.2 // indirect github.com/pion/mdns v0.0.12 // indirect github.com/pion/randutil v0.1.0 // indirect github.com/pion/rtcp v1.2.14 // indirect - github.com/pion/rtp v1.8.8 // indirect - github.com/pion/sctp v1.8.20 // indirect + github.com/pion/rtp v1.8.9 // indirect + github.com/pion/sctp v1.8.33 // indirect github.com/pion/sdp/v3 v3.0.9 // indirect github.com/pion/srtp/v2 v2.0.20 // indirect github.com/pion/stun v0.6.1 // indirect github.com/pion/transport/v2 v2.2.10 // indirect github.com/pion/turn/v2 v2.1.6 // indirect - github.com/pion/webrtc/v3 v3.3.0 // indirect + github.com/pion/webrtc/v3 v3.3.4 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_golang v1.19.1 // indirect + github.com/prometheus/client_golang v1.20.5 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/common v0.60.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect - github.com/quic-go/qpack v0.4.0 // indirect - github.com/quic-go/quic-go v0.45.2 // indirect - github.com/quic-go/webtransport-go v0.8.0 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.48.2 // indirect + github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 // indirect github.com/raulk/go-watchdog v1.3.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/stretchr/testify v1.9.0 // indirect - github.com/wlynxg/anet v0.0.3 // indirect - go.uber.org/dig v1.17.1 // indirect - go.uber.org/fx v1.22.1 // indirect - go.uber.org/mock v0.4.0 // indirect + github.com/wlynxg/anet v0.0.5 // indirect + go.uber.org/dig v1.18.0 // indirect + go.uber.org/fx v1.23.0 // indirect + go.uber.org/mock v0.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.25.0 // indirect - golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/mod v0.19.0 // indirect - golang.org/x/net v0.27.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.22.0 // indirect - golang.org/x/text v0.16.0 // indirect - golang.org/x/tools v0.23.0 // indirect - google.golang.org/protobuf v1.34.2 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect + golang.org/x/mod v0.21.0 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect + golang.org/x/tools v0.26.0 // indirect + google.golang.org/protobuf v1.35.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.3.0 // indirect ) diff --git a/test-plans/go.sum b/test-plans/go.sum index 45000bf25b..36002223f5 100644 --- a/test-plans/go.sum +++ b/test-plans/go.sum @@ -91,8 +91,8 @@ github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF 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/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k= -github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +github.com/google/pprof v0.0.0-20241017200806-017d972448fc h1:NGyrhhFhwvRAZg02jnYVg3GBQy0qGBKmFQJwaPmpmxs= +github.com/google/pprof v0.0.0-20241017200806-017d972448fc/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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= @@ -119,8 +119,8 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= -github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0= @@ -136,8 +136,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= -github.com/libp2p/go-flow-metrics v0.1.0 h1:0iPhMI8PskQwzh57jB9WxIuIOQ0r+15PChFGkx3Q3WM= -github.com/libp2p/go-flow-metrics v0.1.0/go.mod h1:4Xi8MX8wj5aWNDAZttg6UPmc0ZrnFNsMtpsYUClFtro= +github.com/libp2p/go-flow-metrics v0.2.0 h1:EIZzjmeOE6c8Dav0sNv35vhZxATIXWZg6j/C08XmmDw= +github.com/libp2p/go-flow-metrics v0.2.0/go.mod h1:st3qqfu8+pMfh+9Mzqb2GTiwrAGjIPszEjZmtksN8Jc= github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= @@ -161,9 +161,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= -github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= -github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs= -github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ= +github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= +github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8= github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms= github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc= @@ -184,11 +183,10 @@ github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYg github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo= -github.com/multiformats/go-multiaddr v0.2.0/go.mod h1:0nO36NvPpyV4QzvTLi/lafl2y95ncPj0vFwVF6k6wJ4= github.com/multiformats/go-multiaddr v0.13.0 h1:BCBzs61E3AGHcYYTv8dqRH43ZfyrqM8RXVPT8t13tLQ= github.com/multiformats/go-multiaddr v0.13.0/go.mod h1:sBXrNzucqkFJhvKOiwwLyqamGa/P5EIXNPLovyhQCII= -github.com/multiformats/go-multiaddr-dns v0.3.1 h1:QgQgR+LQVt3NPTjbrLLpsaT2ufAA2y0Mkk+QRVJbW3A= -github.com/multiformats/go-multiaddr-dns v0.3.1/go.mod h1:G/245BRQ6FJGmryJCrOuTdB37AMA5AMOVuO6NY3JwTk= +github.com/multiformats/go-multiaddr-dns v0.4.1 h1:whi/uCLbDS3mSEUMb1MsoT4uzUeZB0N32yzufqS0i5M= +github.com/multiformats/go-multiaddr-dns v0.4.1/go.mod h1:7hfthtB4E4pQwirrz+J0CcDUfbWzTqEzVyYKKIKpgkc= github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= @@ -198,9 +196,8 @@ github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI1 github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= -github.com/multiformats/go-multistream v0.5.0 h1:5htLSLl7lvJk3xx3qT/8Zm9J4K8vEOf/QGkvOGQAyiE= -github.com/multiformats/go-multistream v0.5.0/go.mod h1:n6tMZiwiP2wUsR8DgfDWw1dydlEqV3l6N3/GBsX6ILA= -github.com/multiformats/go-varint v0.0.1/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= +github.com/multiformats/go-multistream v0.6.0 h1:ZaHKbsL404720283o4c/IHQXiS6gb8qAN5EIJ4PN5EA= +github.com/multiformats/go-multistream v0.6.0/go.mod h1:MOyoG5otO24cHIg8kf9QW2/NozURlkP/rvi2FQJyCPg= github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= @@ -211,25 +208,25 @@ github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= 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/v2 v2.19.1 h1:QXgq3Z8Crl5EL1WBAC98A5sEBHARrAJNzAmMxzLcRF0= -github.com/onsi/ginkgo/v2 v2.19.1/go.mod h1:O3DtEWQkPa/F7fBMgmZQKKsluAy8pd3rEQdrjkPb9zA= -github.com/onsi/gomega v1.34.0 h1:eSSPsPNp6ZpsG8X1OVmOTxig+CblTc4AxpPBykhe2Os= -github.com/onsi/gomega v1.34.0/go.mod h1:MIKI8c+f+QLWk+hxbePD4i0LMJSExPaZOVfkoex4cAo= +github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= +github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= 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/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= -github.com/pion/datachannel v1.5.8 h1:ph1P1NsGkazkjrvyMfhRBUAWMxugJjq2HfQifaOoSNo= -github.com/pion/datachannel v1.5.8/go.mod h1:PgmdpoaNBLX9HNzNClmdki4DYW5JtI7Yibu8QzbL3tI= +github.com/pion/datachannel v1.5.9 h1:LpIWAOYPyDrXtU+BW7X0Yt/vGtYxtXQ8ql7dFfYUVZA= +github.com/pion/datachannel v1.5.9/go.mod h1:kDUuk4CU4Uxp82NH4LQZbISULkX/HtzKa4P7ldf9izE= github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= -github.com/pion/ice/v2 v2.3.34 h1:Ic1ppYCj4tUOcPAp76U6F3fVrlSw8A9JtRXLqw6BbUM= -github.com/pion/ice/v2 v2.3.34/go.mod h1:mBF7lnigdqgtB+YHkaY/Y6s6tsyRyo4u4rPGRuOjUBQ= -github.com/pion/interceptor v0.1.29 h1:39fsnlP1U8gw2JzOFWdfCU82vHvhW9o0rZnZF56wF+M= -github.com/pion/interceptor v0.1.29/go.mod h1:ri+LGNjRUc5xUNtDEPzfdkmSqISixVTBF/z/Zms/6T4= +github.com/pion/ice/v2 v2.3.36 h1:SopeXiVbbcooUg2EIR8sq4b13RQ8gzrkkldOVg+bBsc= +github.com/pion/ice/v2 v2.3.36/go.mod h1:mBF7lnigdqgtB+YHkaY/Y6s6tsyRyo4u4rPGRuOjUBQ= +github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI= +github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y= github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8= @@ -240,10 +237,10 @@ github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9 github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE= github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= -github.com/pion/rtp v1.8.8 h1:EtYFHI0rpUEjT/RMnGfb1vdJhbYmPG77szD72uUnSxs= -github.com/pion/rtp v1.8.8/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= -github.com/pion/sctp v1.8.20 h1:sOc3lkV/tQaP57ZUEXIMdM2V92IIB2ia5v/ygnBxaEg= -github.com/pion/sctp v1.8.20/go.mod h1:oTxw8i5m+WbDHZJL/xUpe6CPIn1Y0GIKKwTLF4h53H8= +github.com/pion/rtp v1.8.9 h1:E2HX740TZKaqdcPmf4pw6ZZuG8u5RlMMt+l3dxeu6Wk= +github.com/pion/rtp v1.8.9/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= +github.com/pion/sctp v1.8.33 h1:dSE4wX6uTJBcNm8+YlMg7lw1wqyKHggsP5uKbdj+NZw= +github.com/pion/sctp v1.8.33/go.mod h1:beTnqSzewI53KWoG3nqB282oDMGrhNxBdb+JZnkCwRM= github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY= github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M= github.com/pion/srtp/v2 v2.0.20 h1:HNNny4s+OUmG280ETrCdgFndp4ufx3/uy85EawYEhTk= @@ -256,36 +253,36 @@ github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLh github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= -github.com/pion/transport/v3 v3.0.6 h1:k1mQU06bmmX143qSWgXFqSH1KUJceQvIUuVH/K5ELWw= -github.com/pion/transport/v3 v3.0.6/go.mod h1:HvJr2N/JwNJAfipsRleqwFoR3t/pWyHeZUs89v3+t5s= +github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= +github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= github.com/pion/turn/v2 v2.1.6 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc= github.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= -github.com/pion/webrtc/v3 v3.3.0 h1:Rf4u6n6U5t5sUxhYPQk/samzU/oDv7jk6BA5hyO2F9I= -github.com/pion/webrtc/v3 v3.3.0/go.mod h1:hVmrDJvwhEertRWObeb1xzulzHGeVUoPlWvxdGzcfU0= +github.com/pion/webrtc/v3 v3.3.4 h1:v2heQVnXTSqNRXcaFQVOhIOYkLMxOu1iJG8uy1djvkk= +github.com/pion/webrtc/v3 v3.3.4/go.mod h1:liNa+E1iwyzyXqNUwvoMRNQ10x8h8FOeJKL8RkIbamE= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= -github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= -github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/common v0.60.0 h1:+V9PAREWNvJMAuJ1x1BaWl9dewMW4YrHZQbx0sJNllA= +github.com/prometheus/common v0.60.0/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= -github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= -github.com/quic-go/quic-go v0.45.2 h1:DfqBmqjb4ExSdxRIb/+qXhPC+7k6+DUNZha4oeiC9fY= -github.com/quic-go/quic-go v0.45.2/go.mod h1:1dLehS7TIR64+vxGR70GDcatWTOtMX2PUtnKsjbTurI= -github.com/quic-go/webtransport-go v0.8.0 h1:HxSrwun11U+LlmwpgM1kEqIqH90IT4N8auv/cD7QFJg= -github.com/quic-go/webtransport-go v0.8.0/go.mod h1:N99tjprW432Ut5ONql/aUhSLT0YVSlwHohQsuac9WaM= +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/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE= +github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs= +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/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk= github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= @@ -339,23 +336,24 @@ github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cb github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= -github.com/wlynxg/anet v0.0.3 h1:PvR53psxFXstc12jelG6f1Lv4MWqE0tI76/hHGjh9rg= github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 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.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc= -go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= -go.uber.org/fx v1.22.1 h1:nvvln7mwyT5s1q201YE29V/BFrGor6vMiDNpU/78Mys= -go.uber.org/fx v1.22.1/go.mod h1:HT2M7d7RHo+ebKGh9NRcrsrHHfpZ60nW3QRubMRfv48= +go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw= +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/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.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= -go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= 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/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= @@ -376,11 +374,11 @@ 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.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.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -392,8 +390,8 @@ golang.org/x/mod v0.3.0/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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= -golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -416,8 +414,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.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= 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-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -433,8 +431,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/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-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.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/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= @@ -447,7 +445,6 @@ golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -461,8 +458,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.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.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -478,8 +475,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.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.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 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.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= @@ -498,8 +495,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f 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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= -golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= 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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -520,8 +517,8 @@ google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmE google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/version.json b/version.json index 53072426c1..707e97ed5b 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "v0.36.2" + "version": "v0.37.0" }