Skip to content

Commit 9487509

Browse files
authored
fix(network): remove every address book entry for a banned peer IP (#11173)
2 parents da29357 + fd2dc73 commit 9487509

4 files changed

Lines changed: 86 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ and this project adheres to [Semantic Versioning](https://semver.org).
5050
- Reject blocks whose total chain value pool balance would exceed `MAX_MONEY`,
5151
enforcing the cap on the total monetary base
5252
([#10817](https://github.com/ZcashFoundation/zebra/pull/10817))
53+
- Banning a misbehaving peer now removes every address book entry for that IP, and a banned IP is
54+
never selected as a reconnection candidate. Previously an entry on a different port could survive
55+
the ban and occupy the first candidate slot until the node restarted
56+
([#11134](https://github.com/ZcashFoundation/zebra/issues/11134)).
5357

5458
### Security
5559

zebra-network/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2323

2424
- Peer-set, crawler-handshake, and address-book gauges now include a `network` label, so multiple
2525
network instances in one process do not overwrite each other's values.
26+
- `AddressBook::update()` logs a change rejected for a banned peer IP at `debug` instead of `warn`,
27+
since remote peers control how often it fires
28+
([#11134](https://github.com/ZcashFoundation/zebra/issues/11134)).
29+
30+
### Fixed
31+
32+
- Banning a peer IP now removes every address book entry for that IP, and `reconnection_peers()`
33+
never returns an address whose IP is banned. Previously an entry for the banned IP on another
34+
port could survive the ban and stay at the front of the reconnection order for the lifetime of
35+
the process ([#11134](https://github.com/ZcashFoundation/zebra/issues/11134)).
2636

2737
### Security
2838

zebra-network/src/address_book.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,8 @@ impl AddressBook {
416416
#[allow(clippy::unwrap_in_result)]
417417
pub fn update(&mut self, change: MetaAddrChange) -> Option<MetaAddr> {
418418
if self.bans_by_ip.contains_key(&change.addr().ip()) {
419-
tracing::warn!(
419+
// Remote peers control how often this fires, so keep it below `warn` (#11134).
420+
tracing::debug!(
420421
?change,
421422
"attempted to add a banned peer addr to address book"
422423
);
@@ -464,8 +465,7 @@ impl AddressBook {
464465
let banned_addrs: Vec<_> = self
465466
.by_addr
466467
.descending_keys()
467-
.skip_while(|addr| addr.ip() != banned_ip)
468-
.take_while(|addr| addr.ip() == banned_ip)
468+
.filter(|addr| addr.ip() == banned_ip)
469469
.cloned()
470470
.collect();
471471

@@ -647,12 +647,13 @@ impl AddressBook {
647647
) -> impl DoubleEndedIterator<Item = MetaAddr> + '_ {
648648
let _guard = self.span.enter();
649649

650-
// Skip live peers, and peers pending a reconnect attempt.
650+
// Skip live peers, banned peers, and peers pending a reconnect attempt.
651651
// The peers are already stored in sorted order.
652652
self.by_addr
653653
.descending_values()
654654
.filter(move |peer| {
655-
peer.is_ready_for_connection_attempt(instant_now, chrono_now, &self.network)
655+
!self.bans_by_ip.contains_key(&peer.addr.ip())
656+
&& peer.is_ready_for_connection_attempt(instant_now, chrono_now, &self.network)
656657
&& self.is_ready_for_connection_attempt_with_ip(&peer.addr.ip(), chrono_now)
657658
})
658659
.cloned()

zebra-network/src/address_book/tests/vectors.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)