Skip to content

feat(kad_dht): enforce IP subnet diversity in k-buckets (#1383) - #1399

Open
yashksaini-coder wants to merge 3 commits into
libp2p:mainfrom
yashksaini-coder:security/kad-dht-subnet-diversity-1383
Open

feat(kad_dht): enforce IP subnet diversity in k-buckets (#1383)#1399
yashksaini-coder wants to merge 3 commits into
libp2p:mainfrom
yashksaini-coder:security/kad-dht-subnet-diversity-1383

Conversation

@yashksaini-coder

Copy link
Copy Markdown
Contributor

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 holds MAX_PEERS_PER_SUBNET peers in that bucket.

Design

Constant Default Meaning
MAX_PEERS_PER_SUBNET 2 max peers sharing a subnet per bucket; <= 0 disables the check
SUBNET_PREFIX_LEN_V4 24 IPv4 grouping prefix
SUBNET_PREFIX_LEN_V6 48 IPv6 grouping prefix
  • Only globally-routable addresses are grouped (ipaddress.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. Using is_global (rather than enumerating private ranges) keeps behaviour stable across CPython versions.
  • DNS-named and relayed (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.
  • Multi-homed peers are grouped by their first globally-routable address.
  • Reject-only, no eviction — matches go-libp2p's TryAddPeer and avoids a churn/DoS vector where an attacker forces out established peers.
  • _should_split_bucket now only splits genuinely full buckets, so a subnet rejection (add_peer returning False on 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.py23 passed (16 existing + 7 new)
  • New tests cover: subnet saturation rejection, loopback/private exemption, opt-out flag, distinct-subnet acceptance, IPv6 /48 grouping, multi-homed grouping-by-first-global, and the _subnet_key helper (incl. DNS + relay exemption)
  • ruff check / ruff format --check clean; mypy clean on changed modules

Open questions for maintainers

  1. IPv4 prefix: this ships /24 (stricter than go-libp2p's /16). /24 matches the realistic granularity an attacker rents; /16 over-blocks large shared ISP blocks less aggressively. Prefer /24, or widen to /16?
  2. Table-wide cap: go-libp2p also has a table-wide MaxPeersPerIPGroup=3 (needs RoutingTable-level counter bookkeeping). Deferred here as a follow-up — acceptable, or wanted now?
  3. Opt-out surface: disable is a module-level constant (MAX_PEERS_PER_SUBNET <= 0) rather than a RoutingTable/KadDHT constructor kwarg. Want a threaded kwarg instead/in addition?
  4. Multi-homed posture: grouped by first global address (lenient), the opposite of go-libp2p's reject-if-any-group-full. Confirm this posture.

Non-goals

No crypto-puzzle peer IDs or reputation scoring (spec/ecosystem-level). Path-steering hardening is tracked separately in #1384.

Notes

  • Background: [Stretch] Network attack simulation #57, and the eclipse-attack write-up linked from the issue.
  • IPv6 uses a fixed /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).

@yashksaini-coder

Copy link
Copy Markdown
Contributor Author

📖 Context / reasoning: This PR is Layer 1 of a two-part eclipse-hardening effort for the kad-dht. If you'd like the full thought process behind the approach — why subnet diversity here, and why the related #1384 is moving to disjoint lookup paths rather than randomized candidate selection — it's written up in plain language here: #1400 (discussion). Happy to discuss any of the open questions above either here or there.

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
yashksaini-coder force-pushed the security/kad-dht-subnet-diversity-1383 branch from 1d77070 to 913fcc3 Compare July 21, 2026 05:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

security(kad-dht): add IP/subnet diversity enforcement to KBucket

2 participants