Skip to content

OF-3171: Implement basic rate limiting for new connections (C2S and S2S)#3127

Open
guusdk wants to merge 4 commits intoigniterealtime:mainfrom
guusdk:OF-3170_Rate-Limit-New-Connections
Open

OF-3171: Implement basic rate limiting for new connections (C2S and S2S)#3127
guusdk wants to merge 4 commits intoigniterealtime:mainfrom
guusdk:OF-3170_Rate-Limit-New-Connections

Conversation

@guusdk
Copy link
Member

@guusdk guusdk commented Jan 22, 2026

  • 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.

@guusdk
Copy link
Member Author

guusdk commented Feb 12, 2026

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.

@guusdk guusdk force-pushed the OF-3170_Rate-Limit-New-Connections branch 2 times, most recently from d903e5e to 20e1dd6 Compare March 6, 2026 16:19
@akrherz akrherz requested a review from Copilot March 6, 2026 20:03
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 NewConnectionLimiterRegistry to 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.

Comment on lines +3784 to +3791
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.
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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).

Copilot uses AI. Check for mistakes.
Comment on lines +3800 to +3808
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
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines +136 to +138
// 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;
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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;
}

Copilot uses AI. Check for mistakes.
Comment on lines +286 to +298
// 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; }
// }
// );
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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; }
}
);

Copilot uses AI. Check for mistakes.
@guusdk guusdk force-pushed the OF-3170_Rate-Limit-New-Connections branch from b86a998 to 23953f1 Compare March 6, 2026 21:07
guusdk added 4 commits March 6, 2026 22:10
- 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.
@guusdk guusdk force-pushed the OF-3170_Rate-Limit-New-Connections branch from 23953f1 to 7f8def7 Compare March 6, 2026 21:12
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.

2 participants