mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-17 22:31:26 +02:00
The rate limiter caps new connections (10/min/IP) and game starts (15/min/IP), but one IP could still hold open unbounded sockets by trickling them in under the connection rate and keeping them alive — hoarding spectator slots and server resources ahead of a public launch. Track live sockets per IP in ConnectionHandler and reject new ones past a cap (MAX_SOCKETS_PER_IP, default 10). Slots are freed on disconnect and reconciled against live sockets in the periodic cleanup so a missed disconnect can't wrongly lock out an IP.
68 lines
2.7 KiB
JavaScript
68 lines
2.7 KiB
JavaScript
/**
|
|
* Per-IP concurrent-socket cap (anti-abuse / resource hoarding).
|
|
*
|
|
* The rate limiter caps NEW connections and game starts per minute, but nothing capped how
|
|
* many sockets a single IP could hold OPEN at once. ConnectionHandler now tracks live sockets
|
|
* per IP and refuses new ones past the cap; these tests exercise that bookkeeping directly.
|
|
*/
|
|
|
|
const ConnectionHandler = require('../src/network/connectionHandler');
|
|
|
|
function makeHandler(maxPerIp) {
|
|
if (maxPerIp != null) process.env.MAX_SOCKETS_PER_IP = String(maxPerIp);
|
|
else delete process.env.MAX_SOCKETS_PER_IP;
|
|
return new ConnectionHandler({
|
|
io: { sockets: { sockets: new Map() } },
|
|
broadcastManager: { broadcastUserCount() {}, sendStatusUpdate() {} },
|
|
debugManager: { CONSOLE_LOGGING: false, getCurrentBlockHeight: () => 1 },
|
|
sessionManager: null,
|
|
rateLimiter: { getStats: () => ({}) }
|
|
});
|
|
}
|
|
|
|
describe('per-IP concurrent socket cap', () => {
|
|
let h;
|
|
afterEach(() => { if (h) h.shutdown(); h = null; delete process.env.MAX_SOCKETS_PER_IP; });
|
|
|
|
test('allows up to the cap, then rejects further sockets from the same IP', () => {
|
|
h = makeHandler(3);
|
|
expect(h._trackIpSocket('1.2.3.4', 's1')).toBe(true);
|
|
expect(h._trackIpSocket('1.2.3.4', 's2')).toBe(true);
|
|
expect(h._trackIpSocket('1.2.3.4', 's3')).toBe(true);
|
|
expect(h._trackIpSocket('1.2.3.4', 's4')).toBe(false); // over cap
|
|
});
|
|
|
|
test('a freed slot lets a new socket in', () => {
|
|
h = makeHandler(2);
|
|
expect(h._trackIpSocket('5.5.5.5', 'a')).toBe(true);
|
|
expect(h._trackIpSocket('5.5.5.5', 'b')).toBe(true);
|
|
expect(h._trackIpSocket('5.5.5.5', 'c')).toBe(false);
|
|
h._untrackIpSocket('a');
|
|
expect(h._trackIpSocket('5.5.5.5', 'c')).toBe(true);
|
|
});
|
|
|
|
test('different IPs have independent budgets', () => {
|
|
h = makeHandler(1);
|
|
expect(h._trackIpSocket('1.1.1.1', 'x')).toBe(true);
|
|
expect(h._trackIpSocket('2.2.2.2', 'y')).toBe(true); // different IP, fine
|
|
expect(h._trackIpSocket('1.1.1.1', 'z')).toBe(false);
|
|
});
|
|
|
|
test('re-tracking the same socket id is idempotent (reconnect within same id)', () => {
|
|
h = makeHandler(1);
|
|
expect(h._trackIpSocket('9.9.9.9', 'same')).toBe(true);
|
|
expect(h._trackIpSocket('9.9.9.9', 'same')).toBe(true); // not double-counted
|
|
});
|
|
|
|
test('missing IP is never blocked (cannot attribute)', () => {
|
|
h = makeHandler(1);
|
|
expect(h._trackIpSocket(null, 's1')).toBe(true);
|
|
expect(h._trackIpSocket(undefined, 's2')).toBe(true);
|
|
});
|
|
|
|
test('defaults to 10 when MAX_SOCKETS_PER_IP unset', () => {
|
|
h = makeHandler(null);
|
|
expect(h.maxSocketsPerIp).toBe(10);
|
|
});
|
|
});
|