Skip to content

p2p: size the inbound throttle to one peer's connections - #1018

Open
hyunsooda wants to merge 1 commit into
kaiachain:devfrom
hyunsooda:fix/multichannel-inbound-throttle
Open

p2p: size the inbound throttle to one peer's connections#1018
hyunsooda wants to merge 1 commit into
kaiachain:devfrom
hyunsooda:fix/multichannel-inbound-throttle

Conversation

@hyunsooda

@hyunsooda hyunsooda commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Proposed changes

The inbound throttle is keyed on the remote IP and allowed one connection per 30s
window, but a multichannel peer opens one connection to every listen port from that
same IP. Every channel after the first was closed before its handshake, and a peer is
only complete once all of them arrive, so multichannel peering could not form at all
between public IPs — LAN is exempt, which is why loopback tests never showed it. The
throttle now allows one peer's worth of connections: len(ListenAddrs) + 1 for a
multichannel node — one per listener, plus the single-channel dial a peer makes before
the handshake tells it there are more ports — and 1 for a single-channel node.

Types of changes

  • 🐛 Bug fix
  • ✨ Non-hardfork changes (node upgrade not required)
  • 💥 Hardfork / consensus-breaking changes
  • 🧪 Test improvements
  • 🧰 CI / build tool
  • ♻️ Chore / Refactor / Non-functional changes

Checklist

  • 📖 I have read the CONTRIBUTING GUIDELINES doc
  • 📝 I have signed in the PR comment I have read the CLA Document and I hereby sign the CLA in first time contribute after having read CLA
  • 🟢 Lint and unit tests pass locally with my changes ($ make test)

Related issues

Regression from #926, present in v3.0.0-rc.1.

Further comments

Giving each listener its own throttle history does not work: the initial
single-channel dial and its retry both land on the main port, so that listener still
sees the same IP twice. Two nodes behind one public IP still share an allowance, as
upstream.

The per-IP inbound throttle admitted one connection per 30s window, which fits
a single listener but not a multichannel node. Discovery carries one TCP port
per node, so a peer's first dial is single-channel; the handshake then reports
the extra listen ports and the peer redials all of them. One peer therefore
opens one connection per listener plus that first dial, from the same IP within
microseconds, and every connection after the first was closed before its
handshake. handleAddPeerConn only builds a Peer once all channels arrive, so a
peer on a public IP never assembled and expired out of CandidateConns instead.
LAN addresses are exempt, which is why loopback tests and same-subnet
deployments never saw it.

Count the entries per IP instead of testing for presence, and let each server
declare how many belong to one peer: one for a single-channel node, one per
listener plus the initial dial for a multichannel one.

Constraint: discovery carries one TCP port per node, so the first dial to a
  multichannel peer is always single-channel and is always retried
Rejected: give each listener its own history | listener 0 still sees the redial
  as a second attempt from the same IP
Rejected: drop the history entry when its connection closes | turns the
  throttle into a concurrency cap that a connect/disconnect flood evades
Confidence: high
Scope-risk: narrow
Directive: the allowance is derived from the listener count, not a constant -
  adding a listener must not require touching the throttle
Not-tested: the listenLoop call sites; the test drives inboundAllowance() and
  checkInboundConn directly
Not-tested: two nodes behind one public IP, which still share one allowance

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@hyunsooda hyunsooda self-assigned this Aug 6, 2026
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.

1 participant