@@ -250,3 +250,69 @@ fn test_reconnection_peers_skips_recently_updated_ip<
250250 assert_ne ! ( next_reconnection_peer, None , ) ;
251251 }
252252}
253+
254+ /// Regression test for <https://github.com/ZcashFoundation/zebra/issues/11134>.
255+ ///
256+ /// `by_addr` is ordered by reconnection order, not grouped by IP, so the ban path's old
257+ /// `skip_while(ip != banned).take_while(ip == banned)` scan stopped at the first entry for a
258+ /// different IP, and any later entry on the banned IP survived. That survivor then stayed at the
259+ /// front of the reconnection order for the lifetime of the process: it was selected as a candidate
260+ /// on every crawl, and `update()` rejected the resulting `UpdateAttempt` because the IP was
261+ /// banned, so its state never changed.
262+ #[ test]
263+ fn ban_removes_every_entry_for_the_banned_ip ( ) {
264+ let banned_addr: crate :: PeerSocketAddr = "127.0.0.1:8233" . parse ( ) . unwrap ( ) ;
265+ let unrelated_addr: crate :: PeerSocketAddr = "127.0.0.2:8233" . parse ( ) . unwrap ( ) ;
266+ // An ephemeral-port entry for the same IP, like the one in #11134.
267+ let zombie_addr: crate :: PeerSocketAddr = "127.0.0.1:43562" . parse ( ) . unwrap ( ) ;
268+
269+ // `max_connections_per_ip` is above one, so `reconnection_peers` does not skip the second
270+ // entry on the banned IP for being a duplicate IP.
271+ let mut address_book =
272+ AddressBook :: new ( "0.0.0.0:0" . parse ( ) . unwrap ( ) , & Mainnet , 2 , Span :: current ( ) ) ;
273+
274+ // `MetaAddr`'s `Ord` sorts more recently gossiped addresses first, so these last seen times
275+ // place the unrelated IP between the two entries for the banned IP.
276+ for ( addr, last_seen) in [ ( banned_addr, 2 ) , ( unrelated_addr, 1 ) , ( zombie_addr, 0 ) ] {
277+ address_book. update ( gossiped_change (
278+ addr,
279+ PeerServices :: NODE_NETWORK ,
280+ DateTime32 :: MIN . saturating_add ( Duration32 :: from_seconds ( last_seen) ) ,
281+ ) ) ;
282+ }
283+
284+ // Without this ordering the test would also pass before the fix, because a contiguous scan
285+ // removes contiguous entries correctly.
286+ assert_eq ! (
287+ address_book. by_addr. descending_keys( ) . collect:: <Vec <_>>( ) ,
288+ vec![ & banned_addr, & unrelated_addr, & zombie_addr] ,
289+ "test setup: the unrelated IP must sort between the two entries for the banned IP" ,
290+ ) ;
291+
292+ address_book. update ( MetaAddrChange :: UpdateMisbehavior {
293+ addr : banned_addr,
294+ score_increment : MAX_PEER_MISBEHAVIOR_SCORE ,
295+ } ) ;
296+
297+ assert ! (
298+ address_book. bans( ) . contains_key( & banned_addr. ip( ) ) ,
299+ "ban-threshold misbehavior should ban the peer IP" ,
300+ ) ;
301+ assert_eq ! (
302+ address_book. by_addr. descending_keys( ) . collect:: <Vec <_>>( ) ,
303+ vec![ & unrelated_addr] ,
304+ "the ban should remove every entry for the banned IP, including the one that does not \
305+ sort next to the banned address",
306+ ) ;
307+
308+ let candidates: Vec < _ > = address_book
309+ . reconnection_peers ( Instant :: now ( ) , Utc :: now ( ) )
310+ . map ( |peer| peer. addr )
311+ . collect ( ) ;
312+
313+ assert_eq ! (
314+ candidates,
315+ vec![ unrelated_addr] ,
316+ "a banned IP must never be a reconnection candidate" ,
317+ ) ;
318+ }
0 commit comments