mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-08-31 13:08:15 +00:00
feat(server): raise event rate limit to 50 and name rate-limit constants
Raise the per-socket event budget from 30 to 50 per 10s to give legitimate bursts (episode joins, force-sync, ACK flurries) more headroom. Extract the previously inline connection/event/health/admin windows and limits into named constants (CONNECTION_RATE_LIMIT, EVENT_RATE_LIMIT, *_WINDOW_MS). Tests now derive their thresholds from the exported constants instead of hardcoding them. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,9 @@ import {
|
|||||||
roomListCooldowns,
|
roomListCooldowns,
|
||||||
rateLimitDenied,
|
rateLimitDenied,
|
||||||
startRateLimitCleanup,
|
startRateLimitCleanup,
|
||||||
stopRateLimitCleanup
|
stopRateLimitCleanup,
|
||||||
|
CONNECTION_RATE_LIMIT,
|
||||||
|
EVENT_RATE_LIMIT
|
||||||
} from '../server/rate-limiter.js';
|
} from '../server/rate-limiter.js';
|
||||||
|
|
||||||
// Helper: mock io for cleanup
|
// Helper: mock io for cleanup
|
||||||
@@ -31,8 +33,9 @@ function reset() {
|
|||||||
// --- checkConnectionRate ---
|
// --- checkConnectionRate ---
|
||||||
reset();
|
reset();
|
||||||
assert.equal(checkConnectionRate('1.1.1.1'), true, 'first connection allowed');
|
assert.equal(checkConnectionRate('1.1.1.1'), true, 'first connection allowed');
|
||||||
for (let i = 0; i < 9; i++) checkConnectionRate('1.1.1.1');
|
// Exhaust the rest of the budget (first call above counted as 1).
|
||||||
assert.equal(checkConnectionRate('1.1.1.1'), false, '11th connection blocked');
|
for (let i = 0; i < CONNECTION_RATE_LIMIT - 1; i++) checkConnectionRate('1.1.1.1');
|
||||||
|
assert.equal(checkConnectionRate('1.1.1.1'), false, `connection beyond ${CONNECTION_RATE_LIMIT}/window blocked`);
|
||||||
assert.equal(rateLimitDenied.connections, 1, 'denial counter incremented');
|
assert.equal(rateLimitDenied.connections, 1, 'denial counter incremented');
|
||||||
|
|
||||||
reset();
|
reset();
|
||||||
@@ -41,8 +44,9 @@ assert.equal(checkConnectionRate('2.2.2.2'), true, 'separate IP independent');
|
|||||||
// --- checkEventRate ---
|
// --- checkEventRate ---
|
||||||
reset();
|
reset();
|
||||||
assert.equal(checkEventRate('sock1'), true, 'first event allowed');
|
assert.equal(checkEventRate('sock1'), true, 'first event allowed');
|
||||||
for (let i = 0; i < 29; i++) checkEventRate('sock1');
|
// Exhaust the rest of the budget (first call above counted as 1).
|
||||||
assert.equal(checkEventRate('sock1'), false, '31st event blocked');
|
for (let i = 0; i < EVENT_RATE_LIMIT - 1; i++) checkEventRate('sock1');
|
||||||
|
assert.equal(checkEventRate('sock1'), false, `event beyond ${EVENT_RATE_LIMIT}/window blocked`);
|
||||||
assert.equal(rateLimitDenied.events, 1);
|
assert.equal(rateLimitDenied.events, 1);
|
||||||
|
|
||||||
reset();
|
reset();
|
||||||
|
|||||||
+18
-10
@@ -7,6 +7,14 @@ export const ROOM_LIST_COOLDOWN_MS = 10000;
|
|||||||
export const HEALTH_RATE_LIMIT_PER_MINUTE = 10;
|
export const HEALTH_RATE_LIMIT_PER_MINUTE = 10;
|
||||||
export const ADMIN_METRICS_AUTH_RATE_LIMIT_PER_MINUTE = 5;
|
export const ADMIN_METRICS_AUTH_RATE_LIMIT_PER_MINUTE = 5;
|
||||||
|
|
||||||
|
// --- Connection & event budgets (formerly inline magic numbers) ---
|
||||||
|
export const CONNECTION_RATE_LIMIT = 10; // max new connections per IP per window
|
||||||
|
export const CONNECTION_RATE_WINDOW_MS = 60000; // 1 minute
|
||||||
|
export const EVENT_RATE_LIMIT = 50; // max relayed events per socket per window
|
||||||
|
export const EVENT_RATE_WINDOW_MS = 10000; // 10 seconds
|
||||||
|
export const HEALTH_RATE_WINDOW_MS = 60000; // 1 minute
|
||||||
|
export const ADMIN_METRICS_AUTH_WINDOW_MS = 60000; // 1 minute
|
||||||
|
|
||||||
export const connectionCounts = new Map(); // ip -> { count, resetTime }
|
export const connectionCounts = new Map(); // ip -> { count, resetTime }
|
||||||
export const failedAuthAttempts = new Map(); // Map<IP+RoomID, {count, lastAttempt}>
|
export const failedAuthAttempts = new Map(); // Map<IP+RoomID, {count, lastAttempt}>
|
||||||
export const eventCounts = new Map(); // socketId -> { count, resetTime }
|
export const eventCounts = new Map(); // socketId -> { count, resetTime }
|
||||||
@@ -72,30 +80,30 @@ export function recordAuthFailure(ip, roomId) {
|
|||||||
|
|
||||||
export function checkConnectionRate(ip) {
|
export function checkConnectionRate(ip) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const entry = connectionCounts.get(ip) || { count: 0, resetTime: now + 60000 };
|
const entry = connectionCounts.get(ip) || { count: 0, resetTime: now + CONNECTION_RATE_WINDOW_MS };
|
||||||
if (now > entry.resetTime) { entry.count = 0; entry.resetTime = now + 60000; }
|
if (now > entry.resetTime) { entry.count = 0; entry.resetTime = now + CONNECTION_RATE_WINDOW_MS; }
|
||||||
entry.count++;
|
entry.count++;
|
||||||
connectionCounts.set(ip, entry);
|
connectionCounts.set(ip, entry);
|
||||||
if (entry.count <= 10) return true;
|
if (entry.count <= CONNECTION_RATE_LIMIT) return true;
|
||||||
rateLimitDenied.connections++;
|
rateLimitDenied.connections++;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkEventRate(socketId) {
|
export function checkEventRate(socketId) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const entry = eventCounts.get(socketId) || { count: 0, resetTime: now + 10000 };
|
const entry = eventCounts.get(socketId) || { count: 0, resetTime: now + EVENT_RATE_WINDOW_MS };
|
||||||
if (now > entry.resetTime) { entry.count = 0; entry.resetTime = now + 10000; }
|
if (now > entry.resetTime) { entry.count = 0; entry.resetTime = now + EVENT_RATE_WINDOW_MS; }
|
||||||
entry.count++;
|
entry.count++;
|
||||||
eventCounts.set(socketId, entry);
|
eventCounts.set(socketId, entry);
|
||||||
if (entry.count <= 30) return true;
|
if (entry.count <= EVENT_RATE_LIMIT) return true;
|
||||||
rateLimitDenied.events++;
|
rateLimitDenied.events++;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkHealthRate(ip) {
|
export function checkHealthRate(ip) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const entry = healthCounts.get(ip) || { count: 0, resetTime: now + 60000 };
|
const entry = healthCounts.get(ip) || { count: 0, resetTime: now + HEALTH_RATE_WINDOW_MS };
|
||||||
if (now > entry.resetTime) { entry.count = 0; entry.resetTime = now + 60000; }
|
if (now > entry.resetTime) { entry.count = 0; entry.resetTime = now + HEALTH_RATE_WINDOW_MS; }
|
||||||
entry.count++;
|
entry.count++;
|
||||||
healthCounts.set(ip, entry);
|
healthCounts.set(ip, entry);
|
||||||
if (entry.count <= HEALTH_RATE_LIMIT_PER_MINUTE) return true;
|
if (entry.count <= HEALTH_RATE_LIMIT_PER_MINUTE) return true;
|
||||||
@@ -105,8 +113,8 @@ export function checkHealthRate(ip) {
|
|||||||
|
|
||||||
export function checkAdminMetricsAuthRate(ip) {
|
export function checkAdminMetricsAuthRate(ip) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const entry = adminMetricsAuthCounts.get(ip) || { count: 0, resetTime: now + 60000 };
|
const entry = adminMetricsAuthCounts.get(ip) || { count: 0, resetTime: now + ADMIN_METRICS_AUTH_WINDOW_MS };
|
||||||
if (now > entry.resetTime) { entry.count = 0; entry.resetTime = now + 60000; }
|
if (now > entry.resetTime) { entry.count = 0; entry.resetTime = now + ADMIN_METRICS_AUTH_WINDOW_MS; }
|
||||||
entry.count++;
|
entry.count++;
|
||||||
adminMetricsAuthCounts.set(ip, entry);
|
adminMetricsAuthCounts.set(ip, entry);
|
||||||
if (entry.count <= ADMIN_METRICS_AUTH_RATE_LIMIT_PER_MINUTE) return true;
|
if (entry.count <= ADMIN_METRICS_AUTH_RATE_LIMIT_PER_MINUTE) return true;
|
||||||
|
|||||||
Reference in New Issue
Block a user