github.com/onflow/flow-go@v0.33.17/network/p2p/connection/connection_gater_test.go (about) 1 package connection_test 2 3 import ( 4 "context" 5 "fmt" 6 "testing" 7 "time" 8 9 "github.com/libp2p/go-libp2p/core/control" 10 "github.com/libp2p/go-libp2p/core/network" 11 "github.com/libp2p/go-libp2p/core/peer" 12 "github.com/stretchr/testify/mock" 13 "github.com/stretchr/testify/require" 14 15 "github.com/onflow/flow-go/model/flow" 16 "github.com/onflow/flow-go/module/irrecoverable" 17 mockmodule "github.com/onflow/flow-go/module/mock" 18 "github.com/onflow/flow-go/network/channels" 19 "github.com/onflow/flow-go/network/internal/p2pfixtures" 20 "github.com/onflow/flow-go/network/p2p" 21 p2pbuilderconfig "github.com/onflow/flow-go/network/p2p/builder/config" 22 "github.com/onflow/flow-go/network/p2p/connection" 23 p2plogging "github.com/onflow/flow-go/network/p2p/logging" 24 mockp2p "github.com/onflow/flow-go/network/p2p/mock" 25 p2ptest "github.com/onflow/flow-go/network/p2p/test" 26 "github.com/onflow/flow-go/network/p2p/unicast/stream" 27 "github.com/onflow/flow-go/utils/unittest" 28 ) 29 30 // TestConnectionGating tests node allow listing by peer ID. 31 func TestConnectionGating(t *testing.T) { 32 ctx, cancel := context.WithCancel(context.Background()) 33 signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx) 34 35 sporkID := unittest.IdentifierFixture() 36 idProvider := mockmodule.NewIdentityProvider(t) 37 // create 2 nodes 38 node1Peers := unittest.NewProtectedMap[peer.ID, struct{}]() 39 node1, node1Id := p2ptest.NodeFixture( 40 t, 41 sporkID, 42 t.Name(), 43 idProvider, 44 p2ptest.WithConnectionGater(p2ptest.NewConnectionGater(idProvider, func(p peer.ID) error { 45 if !node1Peers.Has(p) { 46 return fmt.Errorf("id not found: %s", p2plogging.PeerId(p)) 47 } 48 return nil 49 }))) 50 idProvider.On("ByPeerID", node1.ID()).Return(&node1Id, true).Maybe() 51 52 node2Peers := unittest.NewProtectedMap[peer.ID, struct{}]() 53 node2, node2Id := p2ptest.NodeFixture( 54 t, 55 sporkID, 56 t.Name(), 57 idProvider, 58 p2ptest.WithConnectionGater(p2ptest.NewConnectionGater(idProvider, func(p peer.ID) error { 59 if !node2Peers.Has(p) { 60 return fmt.Errorf("id not found: %s", p2plogging.PeerId(p)) 61 } 62 return nil 63 }))) 64 idProvider.On("ByPeerID", node2.ID()).Return(&node2Id, true).Maybe() 65 66 nodes := []p2p.LibP2PNode{node1, node2} 67 ids := flow.IdentityList{&node1Id, &node2Id} 68 p2ptest.StartNodes(t, signalerCtx, nodes) 69 defer p2ptest.StopNodes(t, nodes, cancel) 70 71 p2pfixtures.AddNodesToEachOthersPeerStore(t, nodes, ids) 72 73 t.Run("outbound connection to a disallowed node is rejected", func(t *testing.T) { 74 // although nodes have each other addresses, they are not in the allow-lists of each other. 75 // so they should not be able to connect to each other. 76 p2pfixtures.EnsureNoStreamCreationBetweenGroups(t, ctx, []p2p.LibP2PNode{node1}, []p2p.LibP2PNode{node2}, func(t *testing.T, err error) { 77 require.Truef(t, stream.IsErrGaterDisallowedConnection(err), "expected ErrGaterDisallowedConnection, got: %v", err) 78 }) 79 }) 80 81 t.Run("inbound connection from an allowed node is rejected", func(t *testing.T) { 82 // for an inbound connection to be established both nodes should be in each other's allow-lists. 83 // the connection gater on the dialing node is checking the allow-list upon dialing. 84 // the connection gater on the listening node is checking the allow-list upon accepting the connection. 85 86 // add node2 to node1's allow list, but not the other way around. 87 node1Peers.Add(node2.ID(), struct{}{}) 88 89 // from node2 -> node1 should also NOT work, since node 1 is not in node2's allow list for dialing! 90 p2pfixtures.EnsureNoStreamCreation(t, ctx, []p2p.LibP2PNode{node2}, []p2p.LibP2PNode{node1}, func(t *testing.T, err error) { 91 // dialing node-1 by node-2 should fail locally at the connection gater of node-2. 92 require.Truef(t, stream.IsErrGaterDisallowedConnection(err), "expected ErrGaterDisallowedConnection, got: %v", err) 93 }) 94 95 // now node2 should be able to connect to node1. 96 // from node1 -> node2 shouldn't work 97 p2pfixtures.EnsureNoStreamCreation(t, ctx, []p2p.LibP2PNode{node1}, []p2p.LibP2PNode{node2}) 98 }) 99 100 t.Run("outbound connection to an approved node is allowed", func(t *testing.T) { 101 // adding both nodes to each other's allow lists. 102 node1Peers.Add(node2.ID(), struct{}{}) 103 node2Peers.Add(node1.ID(), struct{}{}) 104 105 // now both nodes should be able to connect to each other. 106 p2ptest.EnsureStreamCreationInBothDirections(t, ctx, []p2p.LibP2PNode{node1, node2}) 107 }) 108 } 109 110 // TestConnectionGating_ResourceAllocation_AllowListing tests resource allocation when a connection from an allow-listed node is established. 111 // The test directly mocks the underlying resource manager metrics of the libp2p native resource manager to ensure that the 112 // expected set of resources are allocated for the connection upon establishment. 113 func TestConnectionGating_ResourceAllocation_AllowListing(t *testing.T) { 114 ctx, cancel := context.WithCancel(context.Background()) 115 signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx) 116 117 sporkID := unittest.IdentifierFixture() 118 idProvider := mockmodule.NewIdentityProvider(t) 119 120 node1, node1Id := p2ptest.NodeFixture( 121 t, 122 sporkID, 123 t.Name(), 124 idProvider, 125 p2ptest.WithRole(flow.RoleConsensus)) 126 127 node2Metrics := mockmodule.NewNetworkMetrics(t) 128 // libp2p native resource manager metrics: 129 // we expect exactly 1 connection to be established from node1 to node2 (inbound for node 2). 130 node2Metrics.On("AllowConn", network.DirInbound, true).Return().Once() 131 // we expect the libp2p.identify service to be used to establish the connection. 132 node2Metrics.On("AllowService", "libp2p.identify").Return() 133 // we expect the node2 attaching node1 to the incoming connection. 134 node2Metrics.On("AllowPeer", node1.ID()).Return() 135 // we expect node2 allocate memory for the incoming connection. 136 node2Metrics.On("AllowMemory", mock.Anything) 137 // we expect node2 to allow the stream to be created. 138 node2Metrics.On("AllowStream", node1.ID(), mock.Anything) 139 // we expect node2 to attach protocol to the created stream. 140 node2Metrics.On("AllowProtocol", mock.Anything).Return() 141 142 // Flow-level resource allocation metrics: 143 // We expect both of the following to be called as they are called together in the same function. 144 node2Metrics.On("InboundConnections", mock.Anything).Return() 145 node2Metrics.On("OutboundConnections", mock.Anything).Return() 146 147 // Libp2p control message validation metrics, these may or may not be called depending on the machine the test is running on and how long 148 // the nodes in the test run for. 149 node2Metrics.On("BlockingPreProcessingStarted", mock.Anything, mock.Anything).Maybe() 150 node2Metrics.On("BlockingPreProcessingFinished", mock.Anything, mock.Anything, mock.Anything).Maybe() 151 node2Metrics.On("AsyncProcessingStarted", mock.Anything).Maybe() 152 node2Metrics.On("AsyncProcessingFinished", mock.Anything, mock.Anything).Maybe() 153 154 // we create node2 with a connection gater that allows all connections and the mocked metrics collector. 155 node2, node2Id := p2ptest.NodeFixture( 156 t, 157 sporkID, 158 t.Name(), 159 idProvider, 160 p2ptest.WithRole(flow.RoleConsensus), 161 p2ptest.WithMetricsCollector(node2Metrics), 162 // we use default resource manager rather than the test resource manager to ensure that the metrics are called. 163 p2ptest.WithDefaultResourceManager(), 164 p2ptest.WithConnectionGater(p2ptest.NewConnectionGater(idProvider, func(p peer.ID) error { 165 return nil // allow all connections. 166 }))) 167 idProvider.On("ByPeerID", node1.ID()).Return(&node1Id, true).Maybe() 168 idProvider.On("ByPeerID", node2.ID()).Return(&node2Id, true).Maybe() 169 170 nodes := []p2p.LibP2PNode{node1, node2} 171 ids := flow.IdentityList{&node1Id, &node2Id} 172 p2ptest.StartNodes(t, signalerCtx, nodes) 173 defer p2ptest.StopNodes(t, nodes, cancel) 174 175 p2pfixtures.AddNodesToEachOthersPeerStore(t, nodes, ids) 176 177 // now node-1 should be able to connect to node-2. 178 p2pfixtures.EnsureStreamCreation(t, ctx, []p2p.LibP2PNode{node1}, []p2p.LibP2PNode{node2}) 179 180 node2Metrics.AssertExpectations(t) 181 } 182 183 // TestConnectionGating_ResourceAllocation_DisAllowListing tests resource allocation when a connection from an allow-listed node is established. 184 func TestConnectionGating_ResourceAllocation_DisAllowListing(t *testing.T) { 185 ctx, cancel := context.WithCancel(context.Background()) 186 signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx) 187 188 sporkID := unittest.IdentifierFixture() 189 idProvider := mockmodule.NewIdentityProvider(t) 190 191 node1, node1Id := p2ptest.NodeFixture( 192 t, 193 sporkID, 194 t.Name(), 195 idProvider, 196 p2ptest.WithRole(flow.RoleConsensus)) 197 198 node2Metrics := mockmodule.NewNetworkMetrics(t) 199 node2Metrics.On("AllowConn", network.DirInbound, true).Return() 200 node2, node2Id := p2ptest.NodeFixture( 201 t, 202 sporkID, 203 t.Name(), 204 idProvider, 205 p2ptest.WithRole(flow.RoleConsensus), 206 p2ptest.WithMetricsCollector(node2Metrics), 207 // we use default resource manager rather than the test resource manager to ensure that the metrics are called. 208 p2ptest.WithDefaultResourceManager(), 209 p2ptest.WithConnectionGater(p2ptest.NewConnectionGater(idProvider, func(p peer.ID) error { 210 return fmt.Errorf("disallowed connection") // rejecting all connections. 211 }))) 212 idProvider.On("ByPeerID", node1.ID()).Return(&node1Id, true).Maybe() 213 idProvider.On("ByPeerID", node2.ID()).Return(&node2Id, true).Maybe() 214 215 nodes := []p2p.LibP2PNode{node1, node2} 216 ids := flow.IdentityList{&node1Id, &node2Id} 217 p2ptest.StartNodes(t, signalerCtx, nodes) 218 defer p2ptest.StopNodes(t, nodes, cancel) 219 220 p2pfixtures.AddNodesToEachOthersPeerStore(t, nodes, ids) 221 222 // now node2 should be able to connect to node1. 223 p2pfixtures.EnsureNoStreamCreation(t, ctx, []p2p.LibP2PNode{node1}, []p2p.LibP2PNode{node2}) 224 225 // as node-2 connection gater is rejecting all connections, none of the following resource allocation methods should be called. 226 node2Metrics.AssertNotCalled(t, "AllowService", mock.Anything) // no service is allowed, e.g., libp2p.identify 227 node2Metrics.AssertNotCalled(t, "AllowPeer", mock.Anything) // no peer is allowed to be attached to the connection. 228 node2Metrics.AssertNotCalled(t, "AllowMemory", mock.Anything) // no memory is EVER allowed to be used during the test. 229 node2Metrics.AssertNotCalled(t, "AllowStream", mock.Anything, mock.Anything) // no stream is allowed to be created. 230 } 231 232 // TestConnectionGater_InterceptUpgrade tests the connection gater only upgrades the connections to the allow-listed peers. 233 // Upgrading a connection means that the connection is the last phase of the connection establishment process. 234 // It means that the connection is ready to be used for sending and receiving messages. 235 // It checks that no disallowed peer can upgrade the connection. 236 func TestConnectionGater_InterceptUpgrade(t *testing.T) { 237 unittest.SkipUnless(t, unittest.TEST_FLAKY, "fails locally and on CI regularly") 238 ctx, cancel := context.WithCancel(context.Background()) 239 signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx) 240 sporkId := unittest.IdentifierFixture() 241 defer cancel() 242 243 count := 5 244 nodes := make([]p2p.LibP2PNode, 0, count) 245 inbounds := make([]chan string, 0, count) 246 identities := make(flow.IdentityList, 0, count) 247 248 disallowedPeerIds := unittest.NewProtectedMap[peer.ID, struct{}]() 249 allPeerIds := make(peer.IDSlice, 0, count) 250 idProvider := mockmodule.NewIdentityProvider(t) 251 connectionGater := mockp2p.NewConnectionGater(t) 252 for i := 0; i < count; i++ { 253 handler, inbound := p2ptest.StreamHandlerFixture(t) 254 node, id := p2ptest.NodeFixture( 255 t, 256 sporkId, 257 t.Name(), 258 idProvider, 259 p2ptest.WithRole(flow.RoleConsensus), 260 p2ptest.WithDefaultStreamHandler(handler), 261 // enable peer manager, with a 1-second refresh rate, and connection pruning enabled. 262 p2ptest.WithPeerManagerEnabled(&p2pbuilderconfig.PeerManagerConfig{ 263 ConnectionPruning: true, 264 UpdateInterval: 1 * time.Second, 265 ConnectorFactory: connection.DefaultLibp2pBackoffConnectorFactory(), 266 }, func() peer.IDSlice { 267 list := make(peer.IDSlice, 0) 268 for _, pid := range allPeerIds { 269 if !disallowedPeerIds.Has(pid) { 270 list = append(list, pid) 271 } 272 } 273 return list 274 }), 275 p2ptest.WithConnectionGater(connectionGater)) 276 idProvider.On("ByPeerID", node.ID()).Return(&id, true).Maybe() 277 nodes = append(nodes, node) 278 identities = append(identities, &id) 279 allPeerIds = append(allPeerIds, node.ID()) 280 inbounds = append(inbounds, inbound) 281 } 282 283 connectionGater.On("InterceptSecured", mock.Anything, mock.Anything, mock.Anything). 284 Return(func(_ network.Direction, p peer.ID, _ network.ConnMultiaddrs) bool { 285 return !disallowedPeerIds.Has(p) 286 }) 287 288 connectionGater.On("InterceptPeerDial", mock.Anything).Return(func(p peer.ID) bool { 289 return !disallowedPeerIds.Has(p) 290 }) 291 292 // we don't inspect connections during "accept" and "dial" phases as the peer IDs are not available at those phases. 293 connectionGater.On("InterceptAddrDial", mock.Anything, mock.Anything).Return(true) 294 connectionGater.On("InterceptAccept", mock.Anything).Return(true) 295 296 // adds first node to disallowed list 297 disallowedPeerIds.Add(nodes[0].ID(), struct{}{}) 298 299 // starts the nodes 300 p2ptest.StartNodes(t, signalerCtx, nodes) 301 defer p2ptest.StopNodes(t, nodes, cancel) 302 303 // Checks that only an allowed REMOTE node can establish an upgradable connection. 304 connectionGater.On("InterceptUpgraded", mock.Anything).Run(func(args mock.Arguments) { 305 conn, ok := args.Get(0).(network.Conn) 306 require.True(t, ok) 307 308 // we don't check for the local peer as with v0.24 of libp2p, the local peer may be able to upgrade an empty connection 309 // even though the remote peer has already disconnected and rejected the connection. 310 remote := conn.RemotePeer() 311 require.False(t, disallowedPeerIds.Has(remote)) 312 }).Return(true, control.DisconnectReason(0)) 313 314 ensureCommunicationSilenceAmongGroups(t, ctx, sporkId, nodes[:1], identities[:1].NodeIDs(), nodes[1:], identities[1:].NodeIDs()) 315 ensureCommunicationOverAllProtocols(t, ctx, sporkId, nodes[1:], inbounds[1:]) 316 } 317 318 // TestConnectionGater_Disallow_Integration tests that when a peer is disallowed, it is disconnected from all other peers, and 319 // cannot connect, exchange unicast, or pubsub messages to any other peers. 320 // It also checked that the allowed peers can still communicate with each other. 321 func TestConnectionGater_Disallow_Integration(t *testing.T) { 322 ctx, cancel := context.WithCancel(context.Background()) 323 signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx) 324 sporkId := unittest.IdentifierFixture() 325 idProvider := mockmodule.NewIdentityProvider(t) 326 defer cancel() 327 328 count := 5 329 nodes := make([]p2p.LibP2PNode, 0, 5) 330 ids := flow.IdentityList{} 331 inbounds := make([]chan string, 0, 5) 332 333 disallowedList := unittest.NewProtectedMap[*flow.Identity, struct{}]() 334 335 for i := 0; i < count; i++ { 336 handler, inbound := p2ptest.StreamHandlerFixture(t) 337 node, id := p2ptest.NodeFixture( 338 t, 339 sporkId, 340 t.Name(), 341 idProvider, 342 p2ptest.WithRole(flow.RoleConsensus), 343 p2ptest.WithDefaultStreamHandler(handler), 344 // enable peer manager, with a 1-second refresh rate, and connection pruning enabled. 345 p2ptest.WithPeerManagerEnabled(&p2pbuilderconfig.PeerManagerConfig{ 346 ConnectionPruning: true, 347 UpdateInterval: 1 * time.Second, 348 ConnectorFactory: connection.DefaultLibp2pBackoffConnectorFactory(), 349 }, func() peer.IDSlice { 350 list := make(peer.IDSlice, 0) 351 for _, id := range ids { 352 if disallowedList.Has(id) { 353 continue 354 } 355 356 pid, err := unittest.PeerIDFromFlowID(id) 357 require.NoError(t, err) 358 359 list = append(list, pid) 360 } 361 return list 362 }), 363 p2ptest.WithConnectionGater(p2ptest.NewConnectionGater(idProvider, func(pid peer.ID) error { 364 return disallowedList.ForEach(func(id *flow.Identity, _ struct{}) error { 365 bid, err := unittest.PeerIDFromFlowID(id) 366 require.NoError(t, err) 367 if bid == pid { 368 return fmt.Errorf("disallow-listed") 369 } 370 return nil 371 }) 372 }))) 373 idProvider.On("ByPeerID", node.ID()).Return(&id, true).Maybe() 374 375 nodes = append(nodes, node) 376 ids = append(ids, &id) 377 inbounds = append(inbounds, inbound) 378 } 379 380 p2ptest.StartNodes(t, signalerCtx, nodes) 381 defer p2ptest.StopNodes(t, nodes, cancel) 382 383 p2ptest.LetNodesDiscoverEachOther(t, ctx, nodes, ids) 384 385 // ensures that all nodes are connected to each other, and they can exchange messages over the pubsub and unicast. 386 ensureCommunicationOverAllProtocols(t, ctx, sporkId, nodes, inbounds) 387 388 // now we add one of the nodes (the last node) to the disallow-list. 389 disallowedList.Add(ids[len(ids)-1], struct{}{}) 390 // let peer manager prune the connections to the disallow-listed node. 391 time.Sleep(1 * time.Second) 392 // ensures no connection, unicast, or pubsub going to or coming from the disallow-listed node. 393 ensureCommunicationSilenceAmongGroups(t, ctx, sporkId, nodes[:count-1], ids[:count-1].NodeIDs(), nodes[count-1:], ids[count-1:].NodeIDs()) 394 395 // now we add another node (the second last node) to the disallowed list. 396 disallowedList.Add(ids[len(ids)-2], struct{}{}) 397 // let peer manager prune the connections to the disallow-listed node. 398 time.Sleep(1 * time.Second) 399 // ensures no connection, unicast, or pubsub going to and coming from the disallow-listed nodes. 400 ensureCommunicationSilenceAmongGroups(t, ctx, sporkId, nodes[:count-2], ids[:count-2].NodeIDs(), nodes[count-2:], ids[count-2:].NodeIDs()) 401 // ensures that all nodes are other non-disallow-listed nodes can exchange messages over the pubsub and unicast. 402 ensureCommunicationOverAllProtocols(t, ctx, sporkId, nodes[:count-2], inbounds[:count-2]) 403 } 404 405 // ensureCommunicationSilenceAmongGroups ensures no connection, unicast, or pubsub going to or coming from between the two groups of nodes. 406 func ensureCommunicationSilenceAmongGroups( 407 t *testing.T, 408 ctx context.Context, 409 sporkId flow.Identifier, 410 groupANodes []p2p.LibP2PNode, 411 groupAIdentifiers flow.IdentifierList, 412 groupBNodes []p2p.LibP2PNode, 413 groupBIdentifiers flow.IdentifierList) { 414 // ensures no connection, unicast, or pubsub going to the disallow-listed nodes 415 p2ptest.EnsureNotConnectedBetweenGroups(t, ctx, groupANodes, groupBNodes) 416 417 blockTopic := channels.TopicFromChannel(channels.PushBlocks, sporkId) 418 p2ptest.EnsureNoPubsubExchangeBetweenGroups( 419 t, 420 ctx, 421 groupANodes, 422 groupAIdentifiers, 423 groupBNodes, 424 groupBIdentifiers, 425 blockTopic, 426 1, 427 func() interface{} { 428 return unittest.ProposalFixture() 429 }) 430 p2pfixtures.EnsureNoStreamCreationBetweenGroups(t, ctx, groupANodes, groupBNodes) 431 } 432 433 // ensureCommunicationOverAllProtocols ensures that all nodes are connected to each other, and they can exchange messages over the pubsub and unicast. 434 func ensureCommunicationOverAllProtocols(t *testing.T, ctx context.Context, sporkId flow.Identifier, nodes []p2p.LibP2PNode, inbounds []chan string) { 435 blockTopic := channels.TopicFromChannel(channels.PushBlocks, sporkId) 436 p2ptest.TryConnectionAndEnsureConnected(t, ctx, nodes) 437 p2ptest.EnsurePubsubMessageExchange(t, ctx, nodes, blockTopic, 1, func() interface{} { 438 return unittest.ProposalFixture() 439 }) 440 p2pfixtures.EnsureMessageExchangeOverUnicast(t, ctx, nodes, inbounds, p2pfixtures.LongStringMessageFactoryFixture(t)) 441 }