github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/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 unittest.SkipUnless(t, unittest.TEST_FLAKY, "flakey tests") 115 ctx, cancel := context.WithCancel(context.Background()) 116 signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx) 117 118 sporkID := unittest.IdentifierFixture() 119 idProvider := mockmodule.NewIdentityProvider(t) 120 121 node1, node1Id := p2ptest.NodeFixture( 122 t, 123 sporkID, 124 t.Name(), 125 idProvider, 126 p2ptest.WithRole(flow.RoleConsensus)) 127 128 node2Metrics := mockmodule.NewNetworkMetrics(t) 129 // libp2p native resource manager metrics: 130 // we expect exactly 1 connection to be established from node1 to node2 (inbound for node 2). 131 node2Metrics.On("AllowConn", network.DirInbound, true).Return().Once() 132 // we expect the libp2p.identify service to be used to establish the connection. 133 node2Metrics.On("AllowService", "libp2p.identify").Return() 134 // we expect the node2 attaching node1 to the incoming connection. 135 node2Metrics.On("AllowPeer", node1.ID()).Return() 136 // we expect node2 allocate memory for the incoming connection. 137 node2Metrics.On("AllowMemory", mock.Anything) 138 // we expect node2 to allow the stream to be created. 139 node2Metrics.On("AllowStream", node1.ID(), mock.Anything) 140 // we expect node2 to attach protocol to the created stream. 141 node2Metrics.On("AllowProtocol", mock.Anything).Return() 142 143 // Flow-level resource allocation metrics: 144 // We expect both of the following to be called as they are called together in the same function. 145 node2Metrics.On("InboundConnections", mock.Anything).Return() 146 node2Metrics.On("OutboundConnections", mock.Anything).Return() 147 148 // Libp2p control message validation metrics, these may or may not be called depending on the machine the test is running on and how long 149 // the nodes in the test run for. 150 node2Metrics.On("BlockingPreProcessingStarted", mock.Anything, mock.Anything).Maybe() 151 node2Metrics.On("BlockingPreProcessingFinished", mock.Anything, mock.Anything, mock.Anything).Maybe() 152 node2Metrics.On("AsyncProcessingStarted", mock.Anything).Maybe() 153 node2Metrics.On("AsyncProcessingFinished", mock.Anything, mock.Anything).Maybe() 154 155 // we create node2 with a connection gater that allows all connections and the mocked metrics collector. 156 node2, node2Id := p2ptest.NodeFixture( 157 t, 158 sporkID, 159 t.Name(), 160 idProvider, 161 p2ptest.WithRole(flow.RoleConsensus), 162 p2ptest.WithMetricsCollector(node2Metrics), 163 // we use default resource manager rather than the test resource manager to ensure that the metrics are called. 164 p2ptest.WithDefaultResourceManager(), 165 p2ptest.WithConnectionGater(p2ptest.NewConnectionGater(idProvider, func(p peer.ID) error { 166 return nil // allow all connections. 167 }))) 168 idProvider.On("ByPeerID", node1.ID()).Return(&node1Id, true).Maybe() 169 idProvider.On("ByPeerID", node2.ID()).Return(&node2Id, true).Maybe() 170 171 nodes := []p2p.LibP2PNode{node1, node2} 172 ids := flow.IdentityList{&node1Id, &node2Id} 173 p2ptest.StartNodes(t, signalerCtx, nodes) 174 defer p2ptest.StopNodes(t, nodes, cancel) 175 176 p2pfixtures.AddNodesToEachOthersPeerStore(t, nodes, ids) 177 178 // now node-1 should be able to connect to node-2. 179 p2pfixtures.EnsureStreamCreation(t, ctx, []p2p.LibP2PNode{node1}, []p2p.LibP2PNode{node2}) 180 181 node2Metrics.AssertExpectations(t) 182 } 183 184 // TestConnectionGating_ResourceAllocation_DisAllowListing tests resource allocation when a connection from an allow-listed node is established. 185 func TestConnectionGating_ResourceAllocation_DisAllowListing(t *testing.T) { 186 ctx, cancel := context.WithCancel(context.Background()) 187 signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx) 188 189 sporkID := unittest.IdentifierFixture() 190 idProvider := mockmodule.NewIdentityProvider(t) 191 192 node1, node1Id := p2ptest.NodeFixture( 193 t, 194 sporkID, 195 t.Name(), 196 idProvider, 197 p2ptest.WithRole(flow.RoleConsensus)) 198 199 node2Metrics := mockmodule.NewNetworkMetrics(t) 200 node2Metrics.On("AllowConn", network.DirInbound, true).Return() 201 node2, node2Id := p2ptest.NodeFixture( 202 t, 203 sporkID, 204 t.Name(), 205 idProvider, 206 p2ptest.WithRole(flow.RoleConsensus), 207 p2ptest.WithMetricsCollector(node2Metrics), 208 // we use default resource manager rather than the test resource manager to ensure that the metrics are called. 209 p2ptest.WithDefaultResourceManager(), 210 p2ptest.WithConnectionGater(p2ptest.NewConnectionGater(idProvider, func(p peer.ID) error { 211 return fmt.Errorf("disallowed connection") // rejecting all connections. 212 }))) 213 idProvider.On("ByPeerID", node1.ID()).Return(&node1Id, true).Maybe() 214 idProvider.On("ByPeerID", node2.ID()).Return(&node2Id, true).Maybe() 215 216 nodes := []p2p.LibP2PNode{node1, node2} 217 ids := flow.IdentityList{&node1Id, &node2Id} 218 p2ptest.StartNodes(t, signalerCtx, nodes) 219 defer p2ptest.StopNodes(t, nodes, cancel) 220 221 p2pfixtures.AddNodesToEachOthersPeerStore(t, nodes, ids) 222 223 // now node2 should be able to connect to node1. 224 p2pfixtures.EnsureNoStreamCreation(t, ctx, []p2p.LibP2PNode{node1}, []p2p.LibP2PNode{node2}) 225 226 // as node-2 connection gater is rejecting all connections, none of the following resource allocation methods should be called. 227 node2Metrics.AssertNotCalled(t, "AllowService", mock.Anything) // no service is allowed, e.g., libp2p.identify 228 node2Metrics.AssertNotCalled(t, "AllowPeer", mock.Anything) // no peer is allowed to be attached to the connection. 229 node2Metrics.AssertNotCalled(t, "AllowMemory", mock.Anything) // no memory is EVER allowed to be used during the test. 230 node2Metrics.AssertNotCalled(t, "AllowStream", mock.Anything, mock.Anything) // no stream is allowed to be created. 231 } 232 233 // TestConnectionGater_InterceptUpgrade tests the connection gater only upgrades the connections to the allow-listed peers. 234 // Upgrading a connection means that the connection is the last phase of the connection establishment process. 235 // It means that the connection is ready to be used for sending and receiving messages. 236 // It checks that no disallowed peer can upgrade the connection. 237 func TestConnectionGater_InterceptUpgrade(t *testing.T) { 238 unittest.SkipUnless(t, unittest.TEST_FLAKY, "fails locally and on CI regularly") 239 ctx, cancel := context.WithCancel(context.Background()) 240 signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx) 241 sporkId := unittest.IdentifierFixture() 242 defer cancel() 243 244 count := 5 245 nodes := make([]p2p.LibP2PNode, 0, count) 246 inbounds := make([]chan string, 0, count) 247 identities := make(flow.IdentityList, 0, count) 248 249 disallowedPeerIds := unittest.NewProtectedMap[peer.ID, struct{}]() 250 allPeerIds := make(peer.IDSlice, 0, count) 251 idProvider := mockmodule.NewIdentityProvider(t) 252 connectionGater := mockp2p.NewConnectionGater(t) 253 for i := 0; i < count; i++ { 254 handler, inbound := p2ptest.StreamHandlerFixture(t) 255 node, id := p2ptest.NodeFixture( 256 t, 257 sporkId, 258 t.Name(), 259 idProvider, 260 p2ptest.WithRole(flow.RoleConsensus), 261 p2ptest.WithDefaultStreamHandler(handler), 262 // enable peer manager, with a 1-second refresh rate, and connection pruning enabled. 263 p2ptest.WithPeerManagerEnabled(&p2pbuilderconfig.PeerManagerConfig{ 264 ConnectionPruning: true, 265 UpdateInterval: 1 * time.Second, 266 ConnectorFactory: connection.DefaultLibp2pBackoffConnectorFactory(), 267 }, func() peer.IDSlice { 268 list := make(peer.IDSlice, 0) 269 for _, pid := range allPeerIds { 270 if !disallowedPeerIds.Has(pid) { 271 list = append(list, pid) 272 } 273 } 274 return list 275 }), 276 p2ptest.WithConnectionGater(connectionGater)) 277 idProvider.On("ByPeerID", node.ID()).Return(&id, true).Maybe() 278 nodes = append(nodes, node) 279 identities = append(identities, &id) 280 allPeerIds = append(allPeerIds, node.ID()) 281 inbounds = append(inbounds, inbound) 282 } 283 284 connectionGater.On("InterceptSecured", mock.Anything, mock.Anything, mock.Anything). 285 Return(func(_ network.Direction, p peer.ID, _ network.ConnMultiaddrs) bool { 286 return !disallowedPeerIds.Has(p) 287 }) 288 289 connectionGater.On("InterceptPeerDial", mock.Anything).Return(func(p peer.ID) bool { 290 return !disallowedPeerIds.Has(p) 291 }) 292 293 // we don't inspect connections during "accept" and "dial" phases as the peer IDs are not available at those phases. 294 connectionGater.On("InterceptAddrDial", mock.Anything, mock.Anything).Return(true) 295 connectionGater.On("InterceptAccept", mock.Anything).Return(true) 296 297 // adds first node to disallowed list 298 disallowedPeerIds.Add(nodes[0].ID(), struct{}{}) 299 300 // starts the nodes 301 p2ptest.StartNodes(t, signalerCtx, nodes) 302 defer p2ptest.StopNodes(t, nodes, cancel) 303 304 // Checks that only an allowed REMOTE node can establish an upgradable connection. 305 connectionGater.On("InterceptUpgraded", mock.Anything).Run(func(args mock.Arguments) { 306 conn, ok := args.Get(0).(network.Conn) 307 require.True(t, ok) 308 309 // 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 310 // even though the remote peer has already disconnected and rejected the connection. 311 remote := conn.RemotePeer() 312 require.False(t, disallowedPeerIds.Has(remote)) 313 }).Return(true, control.DisconnectReason(0)) 314 315 ensureCommunicationSilenceAmongGroups(t, ctx, sporkId, nodes[:1], identities[:1].NodeIDs(), nodes[1:], identities[1:].NodeIDs()) 316 ensureCommunicationOverAllProtocols(t, ctx, sporkId, nodes[1:], inbounds[1:]) 317 } 318 319 // TestConnectionGater_Disallow_Integration tests that when a peer is disallowed, it is disconnected from all other peers, and 320 // cannot connect, exchange unicast, or pubsub messages to any other peers. 321 // It also checked that the allowed peers can still communicate with each other. 322 func TestConnectionGater_Disallow_Integration(t *testing.T) { 323 ctx, cancel := context.WithCancel(context.Background()) 324 signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx) 325 sporkId := unittest.IdentifierFixture() 326 idProvider := mockmodule.NewIdentityProvider(t) 327 defer cancel() 328 329 count := 5 330 nodes := make([]p2p.LibP2PNode, 0, 5) 331 ids := flow.IdentityList{} 332 inbounds := make([]chan string, 0, 5) 333 334 disallowedList := unittest.NewProtectedMap[*flow.Identity, struct{}]() 335 336 for i := 0; i < count; i++ { 337 handler, inbound := p2ptest.StreamHandlerFixture(t) 338 node, id := p2ptest.NodeFixture( 339 t, 340 sporkId, 341 t.Name(), 342 idProvider, 343 p2ptest.WithRole(flow.RoleConsensus), 344 p2ptest.WithDefaultStreamHandler(handler), 345 // enable peer manager, with a 1-second refresh rate, and connection pruning enabled. 346 p2ptest.WithPeerManagerEnabled(&p2pbuilderconfig.PeerManagerConfig{ 347 ConnectionPruning: true, 348 UpdateInterval: 1 * time.Second, 349 ConnectorFactory: connection.DefaultLibp2pBackoffConnectorFactory(), 350 }, func() peer.IDSlice { 351 list := make(peer.IDSlice, 0) 352 for _, id := range ids { 353 if disallowedList.Has(id) { 354 continue 355 } 356 357 pid, err := unittest.PeerIDFromFlowID(id) 358 require.NoError(t, err) 359 360 list = append(list, pid) 361 } 362 return list 363 }), 364 p2ptest.WithConnectionGater(p2ptest.NewConnectionGater(idProvider, func(pid peer.ID) error { 365 return disallowedList.ForEach(func(id *flow.Identity, _ struct{}) error { 366 bid, err := unittest.PeerIDFromFlowID(id) 367 require.NoError(t, err) 368 if bid == pid { 369 return fmt.Errorf("disallow-listed") 370 } 371 return nil 372 }) 373 }))) 374 idProvider.On("ByPeerID", node.ID()).Return(&id, true).Maybe() 375 376 nodes = append(nodes, node) 377 ids = append(ids, &id) 378 inbounds = append(inbounds, inbound) 379 } 380 381 p2ptest.StartNodes(t, signalerCtx, nodes) 382 defer p2ptest.StopNodes(t, nodes, cancel) 383 384 p2ptest.LetNodesDiscoverEachOther(t, ctx, nodes, ids) 385 386 // ensures that all nodes are connected to each other, and they can exchange messages over the pubsub and unicast. 387 ensureCommunicationOverAllProtocols(t, ctx, sporkId, nodes, inbounds) 388 389 // now we add one of the nodes (the last node) to the disallow-list. 390 disallowedList.Add(ids[len(ids)-1], struct{}{}) 391 // let peer manager prune the connections to the disallow-listed node. 392 time.Sleep(1 * time.Second) 393 // ensures no connection, unicast, or pubsub going to or coming from the disallow-listed node. 394 ensureCommunicationSilenceAmongGroups(t, ctx, sporkId, nodes[:count-1], ids[:count-1].NodeIDs(), nodes[count-1:], ids[count-1:].NodeIDs()) 395 396 // now we add another node (the second last node) to the disallowed list. 397 disallowedList.Add(ids[len(ids)-2], struct{}{}) 398 // let peer manager prune the connections to the disallow-listed node. 399 time.Sleep(1 * time.Second) 400 // ensures no connection, unicast, or pubsub going to and coming from the disallow-listed nodes. 401 ensureCommunicationSilenceAmongGroups(t, ctx, sporkId, nodes[:count-2], ids[:count-2].NodeIDs(), nodes[count-2:], ids[count-2:].NodeIDs()) 402 // ensures that all nodes are other non-disallow-listed nodes can exchange messages over the pubsub and unicast. 403 ensureCommunicationOverAllProtocols(t, ctx, sporkId, nodes[:count-2], inbounds[:count-2]) 404 } 405 406 // ensureCommunicationSilenceAmongGroups ensures no connection, unicast, or pubsub going to or coming from between the two groups of nodes. 407 func ensureCommunicationSilenceAmongGroups( 408 t *testing.T, 409 ctx context.Context, 410 sporkId flow.Identifier, 411 groupANodes []p2p.LibP2PNode, 412 groupAIdentifiers flow.IdentifierList, 413 groupBNodes []p2p.LibP2PNode, 414 groupBIdentifiers flow.IdentifierList) { 415 // ensures no connection, unicast, or pubsub going to the disallow-listed nodes 416 p2ptest.EnsureNotConnectedBetweenGroups(t, ctx, groupANodes, groupBNodes) 417 418 blockTopic := channels.TopicFromChannel(channels.PushBlocks, sporkId) 419 p2ptest.EnsureNoPubsubExchangeBetweenGroups( 420 t, 421 ctx, 422 groupANodes, 423 groupAIdentifiers, 424 groupBNodes, 425 groupBIdentifiers, 426 blockTopic, 427 1, 428 func() interface{} { 429 return unittest.ProposalFixture() 430 }) 431 p2pfixtures.EnsureNoStreamCreationBetweenGroups(t, ctx, groupANodes, groupBNodes) 432 } 433 434 // ensureCommunicationOverAllProtocols ensures that all nodes are connected to each other, and they can exchange messages over the pubsub and unicast. 435 func ensureCommunicationOverAllProtocols(t *testing.T, ctx context.Context, sporkId flow.Identifier, nodes []p2p.LibP2PNode, inbounds []chan string) { 436 blockTopic := channels.TopicFromChannel(channels.PushBlocks, sporkId) 437 p2ptest.TryConnectionAndEnsureConnected(t, ctx, nodes) 438 p2ptest.EnsurePubsubMessageExchange(t, ctx, nodes, blockTopic, 1, func() interface{} { 439 return unittest.ProposalFixture() 440 }) 441 p2pfixtures.EnsureMessageExchangeOverUnicast(t, ctx, nodes, inbounds, p2pfixtures.LongStringMessageFactoryFixture(t)) 442 }