darshankabariya e2a34e365d feat: DoS guards for store sync - rate limit, session guards, bounded reads
The sync server had no protection (the old transfer guard was
'removed DOS prototection until we can design something better'):
any peer could open unlimited reconciliation sessions, claim an
int.high-sized payload with one length prefix, and push unsolicited
transfer messages for free.

- reconciliation: a RequestRateLimiter now fronts the server handler
  (new 'storesync' bucket in --rate-limit, default 30 sessions/5 min,
  the cheap check running before any processing); one active session
  per peer at a time, enforced on both the serving and initiating
  side; payload reads bounded to 64 MiB (large legitimate diffs are
  ~40 B per difference, so the largest honest rounds stay well under)
- transfer: messages are only accepted from peers with a
  reconciliation session in the last 10 minutes; each session
  (re)opens that peer's window, unsolicited pushes are dropped,
  counted and disconnected. A stricter accept-only-granted-hashes
  model was tried and rejected: for an empty or far-behind receiver
  the sender computes the diff, so the receiver cannot enumerate the
  hashes it is owed
- re-enables the transfer test disabled 'until we impl. DOS
  protection again'; adds session-guard, rate-limit and
  unsolicited-drop regression tests

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 23:31:37 +05:30

76 lines
1.8 KiB
Nim

import std/random, results, chronos, chronicles
import
logos_delivery/waku/[
node/peer_manager,
waku_core,
waku_store_sync/common,
waku_store_sync/reconciliation,
waku_store_sync/transfer,
common/rate_limit/setting,
],
../testlib/wakucore
randomize()
proc randomHash*(rng: var Rand): WakuMessageHash =
var hash = EmptyWakuMessageHash
for i in 0 ..< hash.len:
hash[i] = rng.rand(uint8)
return hash
proc newTestWakuRecon*(
switch: Switch,
pubsubTopics: seq[PubsubTopic] = @[],
contentTopics: seq[ContentTopic] = @[],
syncRange: timer.Duration = DefaultSyncRange,
idsRx: AsyncQueue[(SyncID, PubsubTopic, ContentTopic)],
wantsTx: AsyncQueue[PeerId],
needsTx: AsyncQueue[(PeerId, WakuMessageHash)],
rateLimitSetting: Opt[RateLimitSetting] = Opt.none(RateLimitSetting),
): Future[SyncReconciliation] {.async.} =
let peerManager = PeerManager.new(switch)
let res = await SyncReconciliation.new(
pubsubTopics = pubsubTopics,
contentTopics = contentTopics,
peerManager = peerManager,
wakuArchive = nil,
syncRange = syncRange,
relayJitter = 0.seconds,
idsRx = idsRx,
localWantsTx = wantsTx,
remoteNeedsTx = needsTx,
rateLimitSetting = rateLimitSetting,
)
let proto = res.get()
await proto.start()
switch.mount(proto)
return proto
proc newTestWakuTransfer*(
switch: Switch,
idsTx: AsyncQueue[(SyncID, PubsubTopic, ContentTopic)],
wantsRx: AsyncQueue[PeerId],
needsRx: AsyncQueue[(PeerId, WakuMessageHash)],
): Future[SyncTransfer] {.async.} =
let peerManager = PeerManager.new(switch)
let proto = SyncTransfer.new(
peerManager = peerManager,
wakuArchive = nil,
idsTx = idsTx,
localWantsRx = wantsRx,
remoteNeedsRx = needsRx,
)
await proto.start()
switch.mount(proto)
return proto