OF-3171: Implement basic rate limiting for new connections (C2S and S2S)#3127
OF-3171: Implement basic rate limiting for new connections (C2S and S2S)#3127guusdk wants to merge 4 commits intoigniterealtime:mainfrom
Conversation
|
I've now added exposure of rate-limiting statistics via Statistics API Integrate rate-limiting counters into Openfire's Statistics API so they are automatically available via JMX and the Monitoring plugin. This adds real-time, thread-safe statistics for rate limiters used for all connection types (eg: socket_c2s, socket_s2s), tracking accepted and rejected connection attempts. Metrics are incremented on every connection attempt, but reset after rate limit configuration changes. Acceptance ratio is intentionally not exposed. Ratios would be derived from cumulative totals since the last rate-limiter reset, causing them to converge over time and potentially mislead users expecting a time-windowed value. Consumers can derive meaningful ratios themselves from the provided accepted and rejected counters. |
d903e5e to
20e1dd6
Compare
There was a problem hiding this comment.
Pull request overview
Implements a first iteration of connection-rate limiting for inbound connections in Openfire by introducing a shared token-bucket limiter per logical connection group (C2S, S2S), and wiring it into Netty (TCP), BOSH and WebSocket entry points while exposing basic metrics via the StatisticsManager/i18n bundles.
Changes:
- Add a generic
TokenBucketRateLimiter(with metrics) plus unit tests. - Add
NewConnectionLimiterRegistryto manage shared limiters for C2S/S2S, dynamic reconfiguration via system properties, and optional rejection logging. - Enforce rate limiting on new connections for TCP (Netty handler), BOSH session creation, and WebSocket creation; add i18n strings and tests for the registry.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| xmppserver/src/main/java/org/jivesoftware/util/TokenBucketRateLimiter.java | Adds synchronized token-bucket rate limiter with metrics and unlimited mode. |
| xmppserver/src/test/java/org/jivesoftware/util/TokenBucketRateLimiterTest.java | Adds unit tests for token-bucket behavior and metrics. |
| xmppserver/src/main/java/org/jivesoftware/openfire/ratelimit/NewConnectionLimiterRegistry.java | Adds shared limiter registry, system properties, rejection logging, and StatisticsManager integration. |
| xmppserver/src/test/java/org/jivesoftware/openfire/ratelimit/NewConnectionLimiterRegistryTest.java | Tests limiter sharing, unsupported types, and dynamic property-driven updates. |
| xmppserver/src/main/java/org/jivesoftware/openfire/nio/NewConnectionRateLimitHandler.java | Adds Netty handler that closes channels immediately when rate limited. |
| xmppserver/src/main/java/org/jivesoftware/openfire/spi/NettyServerInitializer.java | Inserts rate-limit handler at the start of the Netty child pipeline. |
| xmppserver/src/main/java/org/jivesoftware/openfire/http/HttpBindServlet.java | Applies limiter to BOSH new-session creation path. |
| xmppserver/src/main/java/org/jivesoftware/openfire/websocket/OpenfireWebSocketServlet.java | Applies limiter to WebSocket connection creation path. |
| i18n/src/main/resources/openfire_i18n.properties | Adds i18n for new system properties and rate-limit stats (EN). |
| i18n/src/main/resources/openfire_i18n_nl.properties | Adds i18n for new system properties and rate-limit stats (NL). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| stat.ratelimit.newconnections.socket_c2s.total.name=RL: C2S Totaal (TCP) | ||
| stat.ratelimit.newconnections.socket_c2s.total.desc=Totaal aantal client-naar-server (TCP) verbindingen dat door de rate limiter is beoordeeld, zijnde de som van alle toegestane en geweigerde verbindingen. Dit is een absoluut aantal sinds de laatste reset van de rate limiter. | ||
| stat.ratelimit.newconnections.socket_c2s.total.units=verbindingen | ||
| stat.ratelimit.newconnections.socket_c2s.ratio.name=RL: C2S Ratio (TCP) | ||
| stat.ratelimit.newconnections.socket_c2s.ratio.desc=Verhouding van client-naar-server (TCP) verbindingen die door de rate limiter zijn toegestaan ten opzichte van het totale aantal verbindingspogingen sinds de laatste reset. | ||
| stat.ratelimit.newconnections.socket_c2s.ratio.units=% | ||
| stat.ratelimit.newconnections.socket_c2s.tokens.name=RL: C2S Tokens (TCP) | ||
| stat.ratelimit.newconnections.socket_c2s.tokens.desc=Momenteel beschikbare tokens in de rate limiter voor client-naar-server (TCP) verbindingen. |
There was a problem hiding this comment.
The socket_c2s stat texts mention TCP-only metrics, but the implementation aggregates all C2S connection attempts (TCP+BOSH+WebSocket) into one shared limiter/counter. Update these Dutch strings (and/or the stat key naming) so they reflect what is actually measured.
| stat.ratelimit.newconnections.socket_c2s.total.name=RL: C2S Totaal (TCP) | |
| stat.ratelimit.newconnections.socket_c2s.total.desc=Totaal aantal client-naar-server (TCP) verbindingen dat door de rate limiter is beoordeeld, zijnde de som van alle toegestane en geweigerde verbindingen. Dit is een absoluut aantal sinds de laatste reset van de rate limiter. | |
| stat.ratelimit.newconnections.socket_c2s.total.units=verbindingen | |
| stat.ratelimit.newconnections.socket_c2s.ratio.name=RL: C2S Ratio (TCP) | |
| stat.ratelimit.newconnections.socket_c2s.ratio.desc=Verhouding van client-naar-server (TCP) verbindingen die door de rate limiter zijn toegestaan ten opzichte van het totale aantal verbindingspogingen sinds de laatste reset. | |
| stat.ratelimit.newconnections.socket_c2s.ratio.units=% | |
| stat.ratelimit.newconnections.socket_c2s.tokens.name=RL: C2S Tokens (TCP) | |
| stat.ratelimit.newconnections.socket_c2s.tokens.desc=Momenteel beschikbare tokens in de rate limiter voor client-naar-server (TCP) verbindingen. | |
| stat.ratelimit.newconnections.socket_c2s.total.name=RL: C2S Totaal | |
| stat.ratelimit.newconnections.socket_c2s.total.desc=Totaal aantal client-naar-server verbindingen (TCP, websocket en BOSH) dat door de rate limiter is beoordeeld, zijnde de som van alle toegestane en geweigerde verbindingen. Dit is een absoluut aantal sinds de laatste reset van de rate limiter. | |
| stat.ratelimit.newconnections.socket_c2s.total.units=verbindingen | |
| stat.ratelimit.newconnections.socket_c2s.ratio.name=RL: C2S Ratio | |
| stat.ratelimit.newconnections.socket_c2s.ratio.desc=Verhouding van client-naar-server verbindingen (TCP, websocket en BOSH) die door de rate limiter zijn toegestaan ten opzichte van het totale aantal verbindingspogingen sinds de laatste reset. | |
| stat.ratelimit.newconnections.socket_c2s.ratio.units=% | |
| stat.ratelimit.newconnections.socket_c2s.tokens.name=RL: C2S Tokens | |
| stat.ratelimit.newconnections.socket_c2s.tokens.desc=Momenteel beschikbare tokens in de rate limiter voor client-naar-server verbindingen (TCP, websocket en BOSH). |
xmppserver/src/main/java/org/jivesoftware/openfire/ratelimit/NewConnectionLimiterRegistry.java
Show resolved
Hide resolved
xmppserver/src/main/java/org/jivesoftware/openfire/ratelimit/NewConnectionLimiterRegistry.java
Show resolved
Hide resolved
| stat.ratelimit.newconnections.bosh_c2s.total.name=RL: C2S Totaal (web) | ||
| stat.ratelimit.newconnections.bosh_c2s.total.desc=Totaal aantal client-naar-server (websocket en BOSH) verbindingen dat door de rate limiter is beoordeeld, zijnde de som van alle toegestane en geweigerde verbindingen. Dit is een absoluut aantal sinds de laatste reset van de rate limiter. | ||
| stat.ratelimit.newconnections.bosh_c2s.total.units=verbindingen | ||
| stat.ratelimit.newconnections.bosh_c2s.ratio.name=RL: C2S Ratio (web) | ||
| stat.ratelimit.newconnections.bosh_c2s.ratio.desc=Verhouding van client-naar-server (websocket en BOSH) verbindingen die door de rate limiter zijn toegestaan ten opzichte van het totale aantal verbindingspogingen sinds de laatste reset. | ||
| stat.ratelimit.newconnections.bosh_c2s.ratio.units=% | ||
| stat.ratelimit.newconnections.bosh_c2s.tokens.name=RL: C2S Tokens (web) | ||
| stat.ratelimit.newconnections.bosh_c2s.tokens.desc=Momenteel beschikbare tokens in de rate limiter voor client-naar-server (websocket en BOSH) verbindingen. | ||
| stat.ratelimit.newconnections.bosh_c2s.tokens.units=tokens |
There was a problem hiding this comment.
The i18n bundle defines bosh_c2s rate-limit stat strings, but NewConnectionLimiterRegistry currently registers stats only for SOCKET_C2S and SOCKET_S2S (not BOSH_C2S). Either register the BOSH_C2S stat keys too, or remove these entries to avoid unused resources.
| stat.ratelimit.newconnections.bosh_c2s.total.name=RL: C2S Totaal (web) | |
| stat.ratelimit.newconnections.bosh_c2s.total.desc=Totaal aantal client-naar-server (websocket en BOSH) verbindingen dat door de rate limiter is beoordeeld, zijnde de som van alle toegestane en geweigerde verbindingen. Dit is een absoluut aantal sinds de laatste reset van de rate limiter. | |
| stat.ratelimit.newconnections.bosh_c2s.total.units=verbindingen | |
| stat.ratelimit.newconnections.bosh_c2s.ratio.name=RL: C2S Ratio (web) | |
| stat.ratelimit.newconnections.bosh_c2s.ratio.desc=Verhouding van client-naar-server (websocket en BOSH) verbindingen die door de rate limiter zijn toegestaan ten opzichte van het totale aantal verbindingspogingen sinds de laatste reset. | |
| stat.ratelimit.newconnections.bosh_c2s.ratio.units=% | |
| stat.ratelimit.newconnections.bosh_c2s.tokens.name=RL: C2S Tokens (web) | |
| stat.ratelimit.newconnections.bosh_c2s.tokens.desc=Momenteel beschikbare tokens in de rate limiter voor client-naar-server (websocket en BOSH) verbindingen. | |
| stat.ratelimit.newconnections.bosh_c2s.tokens.units=tokens |
| // Only advance the timestamp when tokens are actually added, so that sub-token elapsed time is preserved | ||
| // and contributes to the next refill rather than being discarded. | ||
| lastRefillTimeNanos = now; |
There was a problem hiding this comment.
In refillIfNeeded(), lastRefillTimeNanos is set to now when tokens are added. That discards any fractional elapsed time beyond the generated whole tokens (eg. 1.5s at 1 token/s adds 1 token but loses the extra 0.5s), making the limiter systematically stricter than configured. Consider advancing lastRefillTimeNanos by the exact amount of time that corresponds to the number of tokens actually added (or otherwise preserve the remainder) instead of setting it to now.
| // Only advance the timestamp when tokens are actually added, so that sub-token elapsed time is preserved | |
| // and contributes to the next refill rather than being discarded. | |
| lastRefillTimeNanos = now; | |
| // Advance the timestamp only by the exact amount of time that corresponds to the tokens that were added. | |
| // This preserves any remaining elapsed time (sub-token fraction) so it contributes to the next refill. | |
| final long nanosPerToken = 1_000_000_000L / refillTokensPerSecond; | |
| if (nanosPerToken > 0) { | |
| final long maxTokensWithoutOverflow = Long.MAX_VALUE / nanosPerToken; | |
| final long advance; | |
| if (tokensToAdd > maxTokensWithoutOverflow) { | |
| // In the unlikely event that tokensToAdd * nanosPerToken would overflow, | |
| // fall back to consuming all elapsed time. | |
| advance = elapsed; | |
| } else { | |
| long candidate = tokensToAdd * nanosPerToken; | |
| // Guard against any rounding anomalies that might otherwise advance beyond 'now'. | |
| advance = candidate > elapsed ? elapsed : candidate; | |
| } | |
| lastRefillTimeNanos += advance; | |
| } else { | |
| // Extremely high refill rate (sub-nanosecond per token); we cannot represent finer granularity, | |
| // so fall back to advancing to 'now'. | |
| lastRefillTimeNanos = now; | |
| } |
| // TODO: Decide whether this should be exposed. The value may be misleading because the 'ratio' is always calculated | ||
| // from totals since the last rate-limiter reset. Over time, this causes the ratio to converge and lose usefulness, | ||
| // while users may reasonably expect it to reflect a recent time window. Since absolute 'accepted' and 'rejected' | ||
| // counts are already provided, consumers who need meaningful statistics can derive ratios for any period of | ||
| // interest themselves. | ||
| // // Acceptance ratio | ||
| // stats.removeStatistic(statKeyPrefix + "ratio"); | ||
| // stats.addStatistic(statKeyPrefix + "ratio", | ||
| // new i18nStatistic("ratelimit.newconnections."+type.name().toLowerCase()+".ratio", Statistic.Type.amount) { | ||
| // @Override public double sample() { return getLimiter(type).getAcceptanceRatio() * 100; } | ||
| // @Override public boolean isPartialSample() { return true; } | ||
| // } | ||
| // ); |
There was a problem hiding this comment.
registerLimiterStatistics() currently does not register the total and ratio statistics (the implementation is commented out), but the i18n bundles add stat.ratelimit.newconnections.*.total.* and *.ratio.* entries. Either remove those unused i18n entries, or re-enable/implement these statistics so that the resource bundles match the actually exposed stats.
| // TODO: Decide whether this should be exposed. The value may be misleading because the 'ratio' is always calculated | |
| // from totals since the last rate-limiter reset. Over time, this causes the ratio to converge and lose usefulness, | |
| // while users may reasonably expect it to reflect a recent time window. Since absolute 'accepted' and 'rejected' | |
| // counts are already provided, consumers who need meaningful statistics can derive ratios for any period of | |
| // interest themselves. | |
| // // Acceptance ratio | |
| // stats.removeStatistic(statKeyPrefix + "ratio"); | |
| // stats.addStatistic(statKeyPrefix + "ratio", | |
| // new i18nStatistic("ratelimit.newconnections."+type.name().toLowerCase()+".ratio", Statistic.Type.amount) { | |
| // @Override public double sample() { return getLimiter(type).getAcceptanceRatio() * 100; } | |
| // @Override public boolean isPartialSample() { return true; } | |
| // } | |
| // ); | |
| // Acceptance ratio (percentage). Note: this is calculated from totals since the last rate-limiter reset. | |
| // Over time, this causes the ratio to converge and lose usefulness, while users may reasonably expect it to | |
| // reflect a recent time window. Since absolute 'accepted' and 'rejected' counts are already provided, | |
| // consumers who need meaningful statistics can derive ratios for any period of interest themselves. | |
| final String statKeyRatio = statKeyPrefix + "ratio"; | |
| stats.removeStatistic(statKeyRatio); | |
| stats.addStatistic(statKeyRatio, | |
| new i18nStatistic("ratelimit.newconnections."+type.name().toLowerCase()+".ratio", Statistic.Type.amount) { | |
| @Override public double sample() { return getLimiter(type).getAcceptanceRatio() * 100; } | |
| @Override public boolean isPartialSample() { return true; } | |
| } | |
| ); |
b86a998 to
23953f1
Compare
- Introduce NewConnectionLimiterRegistry to track new connections per type. - Add per-group rate limiting: client-to-server (C2S) and server-to-server (S2S). - By default, rate limiting is disabled for both C2S and S2S. - Support dynamic updates via system properties for permits per second, max burst, and enabled flag. - Add optional logging for rejected connections with configurable suppression interval. - Ensure unsupported connection types receive unlimited limiters while still collecting metrics. This lays the foundation for controlling the rate of new connections, without yet exposing admin console configuration or statistics.
Integrate rate-limiting counters into Openfire's Statistics API so they are automatically available via JMX and the Monitoring plugin. This adds real-time, thread-safe statistics for rate limiters used for all connection types (eg: socket_c2s, socket_s2s), tracking accepted and rejected connection attempts. Metrics are incremented on every connection attempt, but reset after rate limit configuration changes. Acceptance ratio is intentionally not exposed. Ratios would be derived from cumulative totals since the last rate-limiter reset, causing them to converge over time and potentially mislead users expecting a time-windowed value. Consumers can derive meaningful ratios themselves from the provided accepted and rejected counters.
Add NewConnectionRateLimitHandler, a @sharable ChannelInboundHandlerAdapter that intercepts channelActive at the head of the child channel pipeline. Rejected connections are now closed before any downstream handler runs, avoiding TLS negotiation, XML parser allocation, and session scaffolding for connections that would be discarded anyway.
…kenBucketRateLimiter Replace AtomicLong/LongAdder with plain longs guarded by synchronized methods, fixing a race condition between refill and consume. Fix overflow in refillIfNeeded for large elapsed times and capacity values. Fix unlimited() instances eventually exhausting by introducing a dedicated code path that bypasses token accounting. Expand test coverage accordingly.
23953f1 to
7f8def7
Compare
This lays the foundation for controlling the rate of new connections, without yet exposing admin console configuration or statistics.