feat(kad_dht): enforce IP subnet diversity in k-buckets (#1383) - #1399
Open
yashksaini-coder wants to merge 3 commits into
Open
feat(kad_dht): enforce IP subnet diversity in k-buckets (#1383)#1399yashksaini-coder wants to merge 3 commits into
yashksaini-coder wants to merge 3 commits into
Conversation
Contributor
Author
|
Guards against eclipse attacks that grind cheap peer IDs from a single subnet to occupy the closest-K slots for a target key. Before admitting a new peer, KBucket.add_peer now rejects it if its globally-routable /24 (IPv4) or /48 (IPv6) subnet already holds MAX_PEERS_PER_SUBNET (default 2) peers in that bucket. - Only globally-routable addresses are grouped (ip.is_global), so loopback, private (RFC1918/ULA), CGNAT, link-local, and documentation ranges are exempt — CI and local testnets are unaffected. - DNS-named and relayed (p2p-circuit) peers are exempt: a circuit address exposes the relay's IP, not the peer's, and is skipped to avoid grouping distinct peers behind a shared relay. - Multi-homed peers are grouped by their first globally-routable address. - Opt-out: set MAX_PEERS_PER_SUBNET <= 0 to disable the check entirely. - _should_split_bucket now only splits genuinely full buckets, so a subnet rejection (add_peer returning False on a non-full bucket) cannot trigger a spurious split. Reject-only (no eviction) matches go-libp2p's TryAddPeer and avoids a churn/DoS vector. No protocol or wire change. Adds unit tests for saturation, exemptions, opt-out, IPv6 grouping, and multi-homing. Closes libp2p#1383.
yashksaini-coder
force-pushed
the
security/kad-dht-subnet-diversity-1383
branch
from
July 21, 2026 05:08
1d77070 to
913fcc3
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #1383.
py-libp2p's k-buckets accept peers with no constraint on network origin. An attacker can grind many valid peer IDs (peer ID = multihash(pubkey), free to mint) that land closest to a target key in XOR space, then run them all from a small number of IPs/subnets and occupy the closest-K slots for that key in every honest node's routing table. Because the messages are authentically signed, record-level defenses don't catch this — the attack lives in the routing layer.
This PR enforces IP/subnet diversity in
KBucket.add_peer: before admitting a new peer, it is rejected if its globally-routable subnet already holdsMAX_PEERS_PER_SUBNETpeers in that bucket.Design
MAX_PEERS_PER_SUBNET2<= 0disables the checkSUBNET_PREFIX_LEN_V424SUBNET_PREFIX_LEN_V648ipaddress.is_global). Loopback, private (RFC1918/ULA), CGNAT (100.64.0.0/10), link-local, and documentation ranges are all exempt — CI and local testnets are unaffected. Usingis_global(rather than enumerating private ranges) keeps behaviour stable across CPython versions.p2p-circuit) peers are exempt. A circuit address exposes the relay's IP, not the peer's, so it is skipped to avoid grouping distinct peers behind a shared relay. DNS names are not resolved in the routing hot path.TryAddPeerand avoids a churn/DoS vector where an attacker forces out established peers._should_split_bucketnow only splits genuinely full buckets, so a subnet rejection (add_peerreturningFalseon a non-full bucket) can no longer trigger a spurious bucket split.No protocol or wire-format change. Self-contained routing-layer change.
Test plan
uv run python -m pytest tests/core/kad_dht/test_unit_routing_table.py→ 23 passed (16 existing + 7 new)/48grouping, multi-homed grouping-by-first-global, and the_subnet_keyhelper (incl. DNS + relay exemption)ruff check/ruff format --checkclean;mypyclean on changed modulesOpen questions for maintainers
/24(stricter than go-libp2p's/16)./24matches the realistic granularity an attacker rents;/16over-blocks large shared ISP blocks less aggressively. Prefer/24, or widen to/16?MaxPeersPerIPGroup=3(needs RoutingTable-level counter bookkeeping). Deferred here as a follow-up — acceptable, or wanted now?MAX_PEERS_PER_SUBNET <= 0) rather than aRoutingTable/KadDHTconstructor kwarg. Want a threaded kwarg instead/in addition?Non-goals
No crypto-puzzle peer IDs or reputation scoring (spec/ecosystem-level). Path-steering hardening is tracked separately in #1384.
Notes
/48(site-allocation boundary); go-libp2p's ASN-based IPv6 grouping is intentionally not ported (it needs a bundled ASN dataset py-libp2p doesn't ship).