Skip to content

Rate limit and bound chat from remote clients - #11457

Open
Tyrathalis wants to merge 1 commit into
Card-Forge:masterfrom
Tyrathalis:chat-anti-spam
Open

Rate limit and bound chat from remote clients#11457
Tyrathalis wants to merge 1 commit into
Card-Forge:masterfrom
Tyrathalis:chat-anti-spam

Conversation

@Tyrathalis

@Tyrathalis Tyrathalis commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Chat from a remote client is accepted before login and rebroadcast to every peer, and nothing bounds how fast those messages arrive. A client sending chat in a loop costs the host's CPU, everyone's bandwidth and everyone's scrollback at once.

This adds a per-client token bucket, and makes the existing length bound on the rebroadcast configurable.

Behaviour

  • Token bucket sized for a person typing: 10 message burst, one refilled per second. Over-rate messages are dropped rather than queued, since queuing is what the sender wants.
  • The rebroadcast length cap becomes a property instead of LogSafe's default.

Both are policy rather than correctness, so both are tunable and both can be switched off outright:

property default off
forge.net.chatBurst 10 0
forge.net.chatRefillMillis 1000
forge.net.maxChatLength 512 0

These sit alongside the existing forge.net.heartbeatTimeout. The defaults are a guess at "generous for a human" — if they are wrong for how people actually use the lobby, they are one property away rather than a rebuild.

Switching the cap off still strips control characters from the rebroadcast: that part is a correctness property, not an anti-spam measure, so only the truncation is optional.

Values are read per call rather than into static final fields, because Integer.getInteger in a static initialiser is fixed at class-load and surefire shares one JVM, so a test could not otherwise vary them.

Deliberately not included

A general per-message rate limit covering all inbound traffic. That risks throttling legitimate high-rate game traffic, and there is no measured baseline here for what a busy turn sends — guessing a number and stalling a real game is how this kind of change gets reverted.

Tests

ChatRateLimitTest, 2 unit tests, no server and no socket: the bucket empties and refills, and the disable switch never refuses.


Portions authored with an AI assistant (Claude), reviewed by a human.

@MostCromulent

MostCromulent commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

No problem in principle, but what was the situation that led to the need for this?

Chat is accepted from a remote client before it has logged in and is
rebroadcast to every peer, so a client sending it in a loop costs the host's
CPU, everyone's bandwidth and everyone's scrollback at once. Nothing bounds the
rate, and the length bound is fixed.

Adds a per-client token bucket sized for a person typing (10 message burst, one
refilled per second). Over-rate messages are dropped rather than queued, since
queuing is what the sender wants. The existing LogSafe length bound on the
rebroadcast becomes configurable at the same time.

Both are policy rather than correctness, so both are tunable and both can be
switched off outright: forge.net.chatBurst, forge.net.chatRefillMillis and
forge.net.maxChatLength, alongside the existing forge.net.heartbeatTimeout.
Zero or less on the burst disables rate limiting; zero or less on the length
disables truncation. The defaults are a guess at "generous for a human" -- if
they are wrong for how people actually use the lobby, they are one property
away rather than a rebuild.

Note that switching the cap off still strips control characters, since that is
a correctness property of the rebroadcast rather than an anti-spam measure --
only the truncation is optional.

Values are read per call rather than into static final fields, because
Integer.getInteger in a static initialiser is fixed at class-load and surefire
shares one JVM, so a test could not otherwise vary them.

Deliberately not included: a general per-message rate limit covering all
inbound traffic. That risks throttling legitimate high-rate game traffic, and
there is no measured baseline here for what a busy turn sends -- guessing a
number and stalling a real game is how this kind of change gets reverted.

Portions authored with an AI assistant (Claude), reviewed by a human.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Tyrathalis

Copy link
Copy Markdown
Contributor Author

No inciting incident. I was looking at ways clients could overload each other (for 2df8aaa) and noticed there were no spam protections. Spam isn't much of an issue with no public rooms or matchmaking, but it seemed like an easy fix, so here it is if you're interested. The limits are off-switchable if the defaults turn out wrong for how people actually use the lobby.

@MostCromulent

Copy link
Copy Markdown
Contributor

@tool4ever I don't have a hard view against this, but it does seem to be solving a problem that doesn't actually exist in practice.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants