feat(seed): bound concurrent connections per source IP - #45
Conversation
max_incoming_connection_attempts caps CONCURRENT connections per source IP (conn_tracker.go), not attempt rate as the name suggests. Upstream leaves it equal to max_connections at 100/100, where it can never bind. The seed profile raises the total to 1000, which makes the per-IP cap the first real constraint — and it was still carrying the inherited 100, letting one source hold ~10% of a seed's inbound capacity. Set it to 32: ample for an operator running several nodes behind one NAT, while forcing a flood to spread across ~31 addresses. This is only meaningful where the load balancer preserves client IPs. Under SNAT the cap multiplies against the balancer's address count rather than the peers', so a lower value shrinks total inbound capacity instead of limiting abuse. Also records that AllowDuplicateIP is inert — sei-tendermint declares and renders it but never reads it, so it documents intent rather than providing the protection its name implies. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PR SummaryMedium Risk Overview Comments clarify that A new test Reviewed by Cursor Bugbot for commit 103d634. Bugbot is set up for automated code reviews on this repo. Configure here. |
* chore: release v0.0.25 (seed per-source-IP connection cap) Carries #45: applySeedOverrides sets max_incoming_connection_attempts to 32. That key caps CONCURRENT connections per source IP rather than attempt rate, and upstream leaves it equal to max_connections (100/100) where it can never bind — so raising the seed total to 1000 had left one source able to hold ~10% of a seed's inbound. Also records that AllowDuplicateIP is inert: sei-tendermint declares and renders it but never reads it. Consumed next by seictl, whose sidecar renders config.toml (sidecar/tasks/config_apply.go) and is therefore what makes this real on a running node, and by sei-k8s-controller to keep its pin in sync. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(defaults): state consequences in present tense per xreview Comment-only; no behavior change. The giga comment described a prior tree state ("previously rendered"), which a reader cannot verify against the code in front of them. Restated as present fact, keeping the pin instruction for nodes that must stay on the v2 executor. The seed cap comment gave the mechanism ("multiplies against the LB's address count") without the consequence, so it read as benign re-accounting: under SNAT the ceiling collapses and legitimate peers are dropped, silently, because seed mode serves no metrics. It now leads with the silent-rejection property, says outright that this tightens 100 to 32, carries the measured basis for 32 (a fleet maximum of 8 real peers per address) so the number stays checkable, and records that the accept path is NodeID-blind and so bounds inbound persistent and unconditional peers too. Raised independently by idiomatic-reviewer and prose-steward while blinded. Ledger: bdchatham-designs designs/seed-node-mode/xreview/sei-config-v0.0.25-rollout.md Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Problem
max_incoming_connection_attemptscaps concurrent connections per source IP (internal/p2p/conn_tracker.go:33-38), not attempt rate as the name suggests. Upstream sei-tendermint sets it equal tomax_connections— 100/100 — where it can never bind.applySeedOverridesraisesmax_connectionsto 1000 (≈980 inbound after the outbound reserve), which makes the per-IP cap the first real constraint on a seed. It was still carrying the inherited 100, letting a single source hold ~10% of the seed's inbound capacity, and requiring only ~10 addresses to fill it.AllowDuplicateIP = truelooks like it covers this, but sei-tendermint declares the field, defaults it, and renders it intoconfig.tomlwithout ever reading it. It documents intent; it provides nothing.Change
MaxIncomingConnectionAttempts = 32for seed mode. One source gets ~3% of inbound — ample for an operator running several nodes behind one NAT, while forcing a flood across ~31 addresses.AllowDuplicateIPis inert, pointing at the knob that actually applies.TestDefaultForMode_SeedBoundsPerSourceIPConnectionspins the value and the relationship (the cap must sit belowmax_connections, or it constrains nothing). Both assertions verified to fail under mutation.Sequencing — read before merging
The cap multiplies against distinct source addresses as seid observes them. Behind a SNAT'ing load balancer that is the balancer's ENI count, not the peer count, so lowering this value reduces total inbound capacity instead of limiting abuse.
arctic-1's
seed-0is in exactly that state today: its NLB isip-target withpreserve_client_ip.enabled=false, and all 34 of its inbound connections present one of 3 ENI addresses. At 32 its ceiling would be 3 × 32 = 96. sei-protocol/platform#1440 must land and be verified first.seed-1/seed-2areinstance-target, already preserve client IPs, and see 11 distinct peer addresses.This is inert until a seed rolls — config reaches a running SeiNode only through an update plan, which fires on image or sidecar drift.
Verification
make test/make vetclean.make lintreports 47 issues, all pre-existing and identical on the unmodified baseline; none indefaults.go.Open item
The value is the judgment call here. 32 comes from our own observed pattern (~7 nodes per NAT address per cell) with 4–5× headroom, not from measured external data — and nothing today can distinguish "cap working" from "cap rejecting legitimate peers", since the rejection path at
router.go:212emits no metric and seed mode serves no Prometheus listener on the deployed build. Worth revisiting once that counter exists.🤖 Generated with Claude Code