monitor/core/modules/WebServer/middlewares/globalRateLimiter.ts
2025-04-16 22:30:27 +07:00

98 lines
3.2 KiB
TypeScript

const modulename = 'WebServer:RateLimiter';
import consoleFactory from '@lib/console';
import { isIpAddressLocal } from '@lib/host/isIpAddressLocal';
const console = consoleFactory(modulename);
/*
Expected requests per user per minute:
50 usual
800 live console with ingame + 2 web pages
2300 very very very heavy behavior
*/
//Config
const DDOS_THRESHOLD = 20_000;
const MAX_RPM_DEFAULT = 5000;
const MAX_RPM_UNDER_ATTACK = 2500;
const DDOS_COOLDOWN_MINUTES = 15;
//Vars
const bannedIps = new Set<string>();
const reqsPerIp = new Map<string, number>();
let httpRequestsCounter = 0;
let bansPendingWarn: string[] = [];
let minutesSinceLastAttack = Number.MAX_SAFE_INTEGER;
/**
* Process the counts and declares a DDoS or not, as well as warns of new banned IPs.
* Note if the requests are under the DDOS_THRESHOLD, banned ips will be immediately unbanned, so
* in this case the rate limiter will only serve to limit instead of banning these IPs.
*/
setInterval(() => {
if (httpRequestsCounter > DDOS_THRESHOLD) {
minutesSinceLastAttack = 0;
const numberFormatter = new Intl.NumberFormat('en-US');
console.majorMultilineError([
'You might be under a DDoS attack!',
`txAdmin got ${numberFormatter.format(httpRequestsCounter)} HTTP requests in the last minute.`,
`The attacker IP addresses have been blocked until ${DDOS_COOLDOWN_MINUTES} mins after the attack stops.`,
'Make sure you have a proper firewall setup and/or a reverse proxy with rate limiting.',
'You can join https://discord.gg/txAdmin for support.'
]);
} else {
minutesSinceLastAttack++;
if (minutesSinceLastAttack > DDOS_COOLDOWN_MINUTES) {
bannedIps.clear();
}
}
httpRequestsCounter = 0;
reqsPerIp.clear();
if (bansPendingWarn.length) {
console.warn('IPs blocked:', bansPendingWarn.join(', '));
bansPendingWarn = [];
}
}, 60_000);
/**
* Checks if an IP is allowed to make a request based on the rate limit per IP.
* The rate limit ignores local IPs.
* The limits are calculated based on requests per minute, which varies if under attack or not.
* All bans are cleared 15 minutes after the attack stops.
*/
const checkRateLimit = (remoteAddress: string) => {
// Sanity check on the ip
if (typeof remoteAddress !== 'string' || !remoteAddress.length) return false;
// Counting requests per minute
httpRequestsCounter++;
// Whitelist all local addresses
if (isIpAddressLocal(remoteAddress)) return true;
// Checking if the IP is banned
if (bannedIps.has(remoteAddress)) return false;
// Check rate and count request
const reqsCount = reqsPerIp.get(remoteAddress);
if (reqsCount !== undefined) {
const limit = minutesSinceLastAttack < DDOS_COOLDOWN_MINUTES
? MAX_RPM_UNDER_ATTACK
: MAX_RPM_DEFAULT;
if (reqsCount > limit) {
bannedIps.add(remoteAddress);
bansPendingWarn.push(remoteAddress);
return false;
}
reqsPerIp.set(remoteAddress, reqsCount + 1);
} else {
reqsPerIp.set(remoteAddress, 1);
}
return true;
}
export default checkRateLimit;