IP Lists and Login Blocks
How the address allow and deny lists are built and checked, and how the failed logins turn into temporary address blocks stored in the same ip_lists table.
Lists Sources
- Environment: RELDENS_IP_LISTS_ENABLED (0 or 1), RELDENS_IP_ALLOW_LIST and RELDENS_IP_DENY_LIST (comma separated addresses or CIDR ranges), read by EnvironmentVariablesReader.fetchIpListsFromEnvironmentVariables() into the server/appServerConfig/ipLists configuration (the environmentConfig passed to the ConfigManager constructor).
- config rows (scope server): security/ipLists/enabled (boolean, overrides the environment switch), security/ipLists/allow and security/ipLists/deny (comma separated, appended to the environment entries). The basic configuration installs security/ipLists/enabled as 0, so RELDENS_IP_LISTS_ENABLED=1 alone does not enable the lists: set the row to 1 (the environment switch only applies when the row is missing, and before the config rows are loaded at startup).
- ip_lists table rows without expires_at: permanent entries, list_type is allow or deny, the address and list_type pair is unique. They are managed in the administration panel settings menu as "IP Allow And Deny Lists" (entity ipLists).
For example, to enable the lists from the database (or edit the same row in the admin panel Settings, config):
UPDATE `config`
SET `value` = '1'
WHERE `scope` = 'server' AND `path` = 'security/ipLists/enabled';
Startup Flow
- ServerManager.createAppServer() calls AppServerFactory.createAppServer(appServerConfig), setupIpLists() pushes the environment lists into the IpListsConfigurer and registers its Express middleware before the security, CORS, rate limit and body parsing middlewares.
- ServerManager.initializeConfigManager() loads the config rows, creates the IpListsUpgradeGuard (lib/game/server/ip-lists-upgrade-guard.js, kept as serverManager.ipListsUpgradeGuard) and calls its refresh(), which rebuilds the lists: the enabled flag comes from the config row with the environment value as default, and each list joins the environment entries, the config row entries and the permanent ip_lists rows (loadStoredEntries() skips the rows with expires_at).
- The Colyseus transport receives beforeUpgrade: serverManager.ipListsUpgradeGuard.createBeforeUpgradeHandler().
- ServerManagersInitializer.initializeLoginManager() calls loginManager.loginAttempts.restoreAddressBlocks(Date.now()) right after the LoginManager is created, see "Login Blocks" below.
- After gameServer.listen() the ServerManager calls appServerFactory.attachClientAddressGuard(gameServer.transport.server), see "Client Address" below.
The ip_lists rows saved or deleted in the administration panel refresh the lists immediately: IpListsEntitySubscriber (lib/admin/server/subscribers/ip-lists-entity-subscriber.js) runs ipListsUpgradeGuard.refresh() on reldens.adminAfterEntitySave and reldens.adminAfterEntityDelete for the ipLists entity. Only the security/ipLists/* config rows and the environment values need a restart.
Client Address
The ClientAddressGuard of @reldens/server-utils resolves the client address with proxy-addr and the Express trust proxy function: the socket peer address, or the forwarded address only when the peer is a trusted proxy (RELDENS_EXPRESS_TRUSTED_PROXY). On every WebSocket upgrade and every /matchmake/* request it removes the client sent X-Real-IP, X-Forwarded-For and X-Client-IP headers and sets X-Real-IP to the resolved address, so the Colyseus auth context ip (used by the upgrade guard and every per address limit) can not be spoofed.
Where the Lists Are Checked
- Express routes: the middleware answers 403 with Forbidden. for a not allowed req.ip. Behind a reverse proxy RELDENS_EXPRESS_TRUSTED_PROXY sets the Express trust proxy setting so req.ip is the client address.
- Colyseus matchmaking: the /matchmake/* requests are answered by the Colyseus router before the request reaches Express (the @colyseus/core router only hands the other routes to the Express app), so the ClientAddressGuard checks them before the Colyseus listener and answers 403 for a not allowed address.
- WebSocket upgrade: the beforeUpgrade handler returns 403 and logs Denied WebSocket upgrade for address: followed by the address, so a denied address can not join any room, also from a page that was loaded before the address was denied. The browser error of a refused upgrade carries no message, so GameClient.joinOrCreate() stores GameConst.JOIN_GAME_ERROR_MESSAGE and the login form shows it.
Matching Rules
IpListsConfigurer.isAllowed(address) from @reldens/server-utils:
- Disabled lists allow every address.
- IPv4 mapped IPv6 addresses (::ffff:127.0.0.1) are compared as IPv4, a value that is not an IP address is allowed only when the allow list is empty.
- When the allow list has entries only those addresses are accepted and the deny list is ignored.
- Without allow entries the addresses in the deny list are rejected.
- Entries are single addresses or CIDR ranges, matched with the Node net.BlockList. A range needs an integer prefix up to 32 (IPv4) or 128 (IPv6), any other entry is ignored.
Login Blocks
LoginAttempts (lib/game/server/memory/login-attempts.js) is created by the LoginManager with the server/security/loginAttempts configuration (environment values overridden by the configuration rows) and the ipLists repository, and it is shared by the game login and the administration panel login. The security/loginAttempts/enabled row (installed as 1) overrides RELDENS_LOGIN_ATTEMPTS_ENABLED; when disabled no failure is counted and no key is blocked.
- Every failed login calls LoginAttempts.registerLoginFailure(), which registers a hit for the identity: key (the username or email) and for the address: key (the request address), see GameConst.LOGIN_ATTEMPTS_KEYS.
- When a key reaches maxAttempts inside the window (the security/loginAttempts/maxAttempts row, installed as 10, overrides RELDENS_LOGIN_ATTEMPTS_MAX; the window defaults to the block time) the key is blocked for blockTimeMs (the security/loginAttempts/blockTimeMs row, installed as 900000, overrides RELDENS_LOGIN_ATTEMPTS_BLOCK_MS).
- An address block is stored as a deny row with the reason Login attempts limit reached. and the expires_at of the block. A repeated block for the same address updates that row, and a permanent deny row (without expires_at) is never replaced. The identity blocks are kept in memory only.
- LoginAttempts.isLoginBlocked() rejects the login while the identity or the address is blocked, in LoginManager.processUserRequest() for the game login and in LoginManager.roleAuthenticationCallback() for the administration panel login, with the same invalid login message as a wrong password.
- On the startup restoreAddressBlocks() loads the deny rows whose expires_at is still in the future back into memory, so a restart does not lift the block. The expired rows stay in the table and are ignored.
The temporary rows never enter the Express or WebSocket lists, they only block the logins.
Other Counters Per Address
The same LoginAttempts registry counts, in memory only. Every limit below is read from its config row (scope server, all installed by the basic configuration), which overrides the environment variable; the environment value only applies when the row is missing:
- joins:{room type}: and joinsIdentity:{room type}: - room joins per address and per username in RoomLogin.isJoinsLimitReached(). A maximum of 0 or lower disables the check.
- Game room: security/gameLogin/maxJoins (installed 20, overrides RELDENS_GAME_LOGIN_MAX_JOINS) in security/gameLogin/windowMs (installed 60000, overrides RELDENS_GAME_LOGIN_WINDOW_MS).
- Scene and feature rooms: security/roomsLogin/maxJoins (installed 60, overrides RELDENS_ROOMS_LOGIN_MAX_JOINS) in security/roomsLogin/windowMs (installed 60000, overrides RELDENS_ROOMS_LOGIN_WINDOW_MS).
- guests: - guest accounts created per address (security/guests/maxPerIp, installed 20, overrides RELDENS_GUESTS_MAX_PER_IP).
- registration: - accounts registered per address (security/registration/maxPerIp, installed 10, overrides RELDENS_REGISTRATION_MAX_PER_IP).
- forgotAddress: - forgot password requests per address, with the registration maximum (security/registration/maxPerIp).
The registry keys are Map entries: the expired hits and blocks are swept once per window, the identities are truncated to 255 characters in the keys, and at 50000 tracked keys the oldest key is evicted.
The LoginManager also caps the password validations running at the same time (the security/maxConcurrentPasswordValidations row, installed as 8, overrides RELDENS_MAX_CONCURRENT_PASSWORD_VALIDATIONS): each game or administration login reserves its slot before the user lookup and releases it when the login ends, so concurrent requests can not pass the check together. The validation uses the asynchronous pbkdf2 of Encryptor.validatePassword() so it does not block the event loop.
Administration Panel Login Limiter
Independent from the lists: CreateAdminSubscriber.applyLoginRateLimit() mounts an express-rate-limit limiter on the administration login POST, keyed by the address and the submitted email, where only the failed logins count, and the next request gets 429:
- security/adminLogin/maxAttempts - installed as 5, overrides RELDENS_ADMIN_LOGIN_MAX_ATTEMPTS.
- security/adminLogin/windowMs - installed as 900000, overrides RELDENS_ADMIN_LOGIN_WINDOW_MS.
In development mode (NODE_ENV is development, dev or test, or the domain of a plain http:// RELDENS_APP_HOST or RELDENS_PUBLIC_URL matches a development pattern like localhost, 127.0.0.1 or .local) RateLimitConfigurer.createLimiter() multiplies the limit by the developmentMultiplier (10). The end-to-end tests read the real limit from the RateLimit response header.
Related Documentation
- Admin Panel Guide - Admin sessions, request token and the Settings section.
- Administration Panel - Admin access and login.
- Environment Variables - The RELDENS_* security variables.
- Guest System - Guest accounts and the per address guests limit.
- E2E Testing - The login and admin security end-to-end tests.
- Configuration - The config table and how the rows override the environment.
reldens