From 103d6347e9c8248b339cbfaadc4d255869bd4ded Mon Sep 17 00:00:00 2001 From: bdchatham Date: Tue, 28 Jul 2026 14:07:47 -0700 Subject: [PATCH] feat(seed): bound concurrent connections per source IP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- config_test.go | 18 +++++++++++++++++- defaults.go | 11 +++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/config_test.go b/config_test.go index 3a3bc1b..ad5c885 100644 --- a/config_test.go +++ b/config_test.go @@ -63,13 +63,29 @@ func TestDefaultForMode_SeedHighConnections(t *testing.T) { t.Errorf("seed max_connections: got %d, want 1000", cfg.Network.P2P.MaxConnections) } if !cfg.Network.P2P.AllowDuplicateIP { - t.Error("seed should allow duplicate IPs") + t.Error("seed should allow duplicate IPs — records the intent only; sei-tendermint never reads this field") } if cfg.Storage.PruningStrategy != PruningEverything { t.Errorf("seed pruning: got %s, want everything", cfg.Storage.PruningStrategy) } } +func TestDefaultForMode_SeedBoundsPerSourceIPConnections(t *testing.T) { + cfg := DefaultForMode(ModeSeed) + + got := cfg.Network.P2P.MaxIncomingConnectionAttempts + if got != 32 { + t.Errorf("seed max_incoming_connection_attempts: got %d, want 32", got) + } + // The cap is per source IP and counts concurrent connections, so it constrains + // nothing unless it sits below the total. Upstream leaves the two equal, where a + // single source can hold every slot. + if got >= uint(cfg.Network.P2P.MaxConnections) { + t.Errorf("seed per-IP cap %d must be below max_connections %d, or one source can fill the node", + got, cfg.Network.P2P.MaxConnections) + } +} + func TestDefaultForMode_SeedBindsP2POnAllInterfaces(t *testing.T) { cfg := DefaultForMode(ModeSeed) diff --git a/defaults.go b/defaults.go index b868939..cdc10b7 100644 --- a/defaults.go +++ b/defaults.go @@ -282,7 +282,18 @@ func applySeedOverrides(cfg *SeiConfig) { cfg.TxIndex.Indexer = []string{"null"} cfg.Network.P2P.ListenAddress = p2pListenAddrAllInterfaces cfg.Network.P2P.MaxConnections = 1000 + // Inert in sei-tendermint: declared and rendered into config.toml, never read. + // Kept because true is the right intent for a seed, but the limit that actually + // applies is MaxIncomingConnectionAttempts below. cfg.Network.P2P.AllowDuplicateIP = true + // Caps CONCURRENT connections per source IP, despite the name. Upstream leaves + // it equal to max_connections (100/100) where it can never bind; raising the + // total to 1000 makes it the first real constraint. 32 gives one source ~3% of + // inbound — ample for an operator running several nodes behind one NAT, while + // forcing a flood across ~31 addresses. This assumes the load balancer in front + // preserves client IPs; under SNAT it multiplies against the LB's address count + // rather than the peers'. + cfg.Network.P2P.MaxIncomingConnectionAttempts = 32 // Bounds the PEX recv path only, where ReadSizedMsg allocates a peer-declared // size before the channel's ~26 KB RecvMessageCapacity can reject it. The // multiplier is maxInbound (max_connections less the outbound reserve), so at