Skip to content

feat: per-connection serverbound packet rate limiter#691

Merged
robinbraemer merged 1 commit into
masterfrom
feat/packet-limiter
May 27, 2026
Merged

feat: per-connection serverbound packet rate limiter#691
robinbraemer merged 1 commit into
masterfrom
feat/packet-limiter

Conversation

@robinbraemer

Copy link
Copy Markdown
Member

Gap

Gate's existing rate limiting (config.quota) only applies at connect time and login time per IP. Once a client is connected, it can send packets unthrottled — there's no per-connection packet/bytes-per-second limit during play. This PR closes that gap.

(The acute payload/decompression/pre-join-queue DoS vectors are addressed separately in #689; this adds the missing per-connection flood limit. There's even a long-standing TODO in config.Quota anticipating a bytes-per-sec limiter.)

What it does

A per-connection limiter accounts every serverbound packet (client→proxy) and closes the connection when a configured rate is exceeded. Backend connections are trusted and pass no limiter.

Piece File
Sliding-window counter (ring buffer) pkg/internal/packetlimiter/counter.go
Limiter with Account(bytes) bool (nil = disabled) pkg/internal/packetlimiter/limiter.go
Config: packetLimiter section + defaults + validation pkg/edition/java/config/config.go, config.yml
Read-loop hook on client connections pkg/edition/java/netmc/connection.go
Caller wiring (client = configured, backend = nil) pkg/edition/java/proxy/{proxy,server}.go

Config (defaults)

config:
  packetLimiter:
    interval: 7s            # sliding window
    packetsPerSecond: 500   # <=0 disables this dimension
    bytesPerSecond: -1      # disabled by default

A limit <= 0 disables that dimension; both <= 0 (or interval <= 0) disables the limiter entirely. Defaults match Velocity's vetted values (7s / 500 pps). 500 pps sustained is far above normal play (movement is ~20/s), so legitimate clients are unaffected; the value is tunable.

Note: this is on by default. If you'd prefer it off-by-default for backwards-compatibility, that's a one-line change to the default — happy to flip it.

Tests (TDD)

  • counter_test.go — window accumulation, expiry, per-second rate, ring-buffer resize.
  • limiter_test.go — allows under limit, rejects over packet rate, rejects over byte rate, nil/disabled is a no-op (race-clean).
  • connection_limiter_test.go — drives real handshake frames through a net.Pipe read loop: a flood closes the connection, and a negative control confirms an unlimited connection is not closed (so the flood test can't pass for an unrelated reason).

Design note

This adopts the rate-limiting approach Velocity uses, fit to Gate's Go read loop (accounting packetCtx.BytesRead per packet) rather than a Netty pipeline handler. The sliding-window counter mirrors the Velocity/Paper IntervalledCounter.

Verification

  • go build ./... — clean
  • go vet (touched packages) — clean
  • go test ./... — all packages pass (0 failures); limiter + netmc tests pass under -race
  • gofmt — clean

Independent of #689 and #690 (all branch from master).

Gate's existing quotas only limit new connections and logins per IP; an
already-connected client could flood packets unchecked. Add a per-connection
serverbound packet/byte rate limiter, closing the connection when a limit is
exceeded.

- pkg/internal/packetlimiter: a sliding-window counter (ring buffer) and a
  Limiter with Account(bytes) bool. A nil/zero-limit Limiter is disabled.
- config: new packetLimiter section (interval, packetsPerSecond, bytesPerSecond)
  with defaults of a 7s window and 500 packets/s (bytes disabled). A limit <= 0
  disables that dimension; both <= 0 (or interval <= 0) disables the limiter.
- netmc: client connections (serverbound reads) account each packet's bytes in
  the read loop and are closed when over the limit. Backend connections pass a
  nil limiter (trusted, no limit).

This adopts the rate-limiting approach Velocity added for the same purpose,
fit to Gate's Go read-loop rather than its Netty pipeline. The sliding-window
counter, the limiter, and the read-loop close behaviour each have tests
(including a negative control that an unlimited connection is not closed).
@robinbraemer robinbraemer merged commit c8ed9bb into master May 27, 2026
12 checks passed
@robinbraemer robinbraemer deleted the feat/packet-limiter branch May 27, 2026 21:30
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