diff --git a/packages/core/src/core/checks/helpers.ts b/packages/core/src/core/checks/helpers.ts index 6ce0b33..b57ea0e 100644 --- a/packages/core/src/core/checks/helpers.ts +++ b/packages/core/src/core/checks/helpers.ts @@ -60,17 +60,20 @@ export async function checkRouteIpAccess( ): Promise { try { /* Reference check_route_ip_access: a configured route whitelist alone - decides; the route blacklist is consulted only when it is absent. */ + decides among the IP lists (the route blacklist is consulted only when + it is absent), then the route country verdict is combined: a blocked + verdict from either denies, an allowed verdict from either allows. */ + let ipResult: boolean | null = null; if (routeConfig.ipWhitelist && routeConfig.ipWhitelist.length > 0) { - const whitelistResult = isIpInWhitelist(clientIp, routeConfig.ipWhitelist); - if (whitelistResult !== null) return whitelistResult; + ipResult = isIpInWhitelist(clientIp, routeConfig.ipWhitelist); } else if (routeConfig.ipBlacklist && routeConfig.ipBlacklist.length > 0) { - if (isIpInBlacklist(clientIp, routeConfig.ipBlacklist)) return false; + if (isIpInBlacklist(clientIp, routeConfig.ipBlacklist)) ipResult = false; } const countryResult = checkCountryAccess(clientIp, routeConfig, middleware.geoIpHandler); - if (countryResult !== null) return countryResult; + if (ipResult === false || countryResult === false) return false; + if (ipResult === true || countryResult === true) return true; return null; /* v8 ignore start -- catch block requires ipaddr.parse to throw on a value that already passed validation */ } catch { diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index 7afe5d3..6b1da1f 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -4,7 +4,7 @@ import ipaddr from 'ipaddr.js'; import { multipartPartEntries, parseFormPairs, parseMediaTypeParams, parseMultipartParts } from './detection-engine/body-form-scan.js'; import type { ResolvedSecurityConfig } from './models/config.js'; import { defaultLogger } from './models/logger.js'; -import { requestClientIp } from './core/client-identity.js'; +import { requestClientIp, UNKNOWN_CLIENT_IDENTITY } from './core/client-identity.js'; import { canonicalIpText, canonicalizeIp, ipInList } from './ip-identity.js'; import type { Logger } from './models/logger.js'; import type { AgentHandlerProtocol } from './protocols/agent.js'; @@ -445,7 +445,9 @@ export async function checkIpCountry( const country = geoIpHandler.getCountry(ip); if (!country) { logCountryCheckResult(ip, null, 'no_geolocation', config); - return false; + /* Reference _resolve_country_verdict: an ungeolocated client is blocked + only when a country whitelist is configured. */ + return config.whitelistCountries.length > 0; } if (config.whitelistCountries.length > 0) { @@ -471,18 +473,27 @@ export async function isIpAllowed( geoIpHandler?: GeoIPHandler | null, ): Promise { try { + /* Reference _check_unknown_identity_access: an unknown client identity + only blocks when a whitelist or country whitelist is configured. */ + if (ip === UNKNOWN_CLIENT_IDENTITY) { + return !((config.whitelist?.length ?? 0) > 0 || config.whitelistCountries.length > 0); + } + if (canonicalIpText(ip) === null) return false; /* Reference _check_ip_lists_detail: a configured whitelist alone decides; - the blacklist applies only when no whitelist is configured. */ + the blacklist applies only when no whitelist is configured, and a + whitelist match skips the country verdict. */ + let whitelisted = false; if (config.whitelist && config.whitelist.length > 0) { if (!ipInList(ip, config.whitelist)) return false; + whitelisted = true; } else if (ipInList(ip, config.blacklist)) { return false; } /* v8 ignore start -- geoIpHandler branch unreachable in recursive JSON check because dangerousPatterns check returns first */ - if (geoIpHandler) { + if (geoIpHandler && !whitelisted) { const blocked = await checkIpCountry(ip, config, geoIpHandler); if (blocked) return false; } diff --git a/packages/core/tests/test-core/websocket-guard.test.ts b/packages/core/tests/test-core/websocket-guard.test.ts index 53fc007..6ae161c 100644 --- a/packages/core/tests/test-core/websocket-guard.test.ts +++ b/packages/core/tests/test-core/websocket-guard.test.ts @@ -51,9 +51,15 @@ describe('guardWebSocketUpgrade', () => { }); }); - it('falls through to the allow check for an unknown identity when failSecure is off', async () => { + it('lets an unknown identity through the allow check when failSecure is off and no whitelist is set', async () => { const components = await buildComponents({ failSecure: false }); const verdict = await guardWebSocketUpgrade(wsRequest({ clientHost: null }), components); + expect(verdict).toMatchObject({ allowed: true }); + }); + + it('rejects an unknown identity at the allow check when a whitelist is configured and failSecure is off', async () => { + const components = await buildComponents({ failSecure: false, whitelist: ['8.8.4.4'] }); + const verdict = await guardWebSocketUpgrade(wsRequest({ clientHost: null }), components); expect(verdict).toMatchObject({ allowed: false, close: WS_CLOSE_IP_NOT_ALLOWED }); }); diff --git a/packages/core/tests/test-utils/ip-list-route-country-cases.json b/packages/core/tests/test-utils/ip-list-route-country-cases.json new file mode 100644 index 0000000..7c2efbb --- /dev/null +++ b/packages/core/tests/test-utils/ip-list-route-country-cases.json @@ -0,0 +1,2035 @@ +{ + "countries": { + "10.0.0.1": "US", + "10.0.0.2": "CN", + "192.168.1.5": null + }, + "route": [ + { + "routeWhitelist": [], + "routeBlacklist": [], + "blockedCountries": [], + "whitelistCountries": [], + "ip": "10.0.0.1", + "route": null + }, + { + "routeWhitelist": [], + "routeBlacklist": [], + "blockedCountries": [], + "whitelistCountries": [], + "ip": "10.0.0.2", + "route": null + }, + { + "routeWhitelist": [], + "routeBlacklist": [], + "blockedCountries": [], + "whitelistCountries": [], + "ip": "192.168.1.5", + "route": null + }, + { + "routeWhitelist": [], + "routeBlacklist": [], + "blockedCountries": [], + "whitelistCountries": [], + "ip": "unknown", + "route": null + }, + { + "routeWhitelist": [], + "routeBlacklist": [], + "blockedCountries": [], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.1", + "route": true + }, + { + "routeWhitelist": [], + "routeBlacklist": [], + "blockedCountries": [], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.2", + "route": false + }, + { + "routeWhitelist": [], + "routeBlacklist": [], + "blockedCountries": [], + "whitelistCountries": [ + "US" + ], + "ip": "192.168.1.5", + "route": false + }, + { + "routeWhitelist": [], + "routeBlacklist": [], + "blockedCountries": [], + "whitelistCountries": [ + "US" + ], + "ip": "unknown", + "route": false + }, + { + "routeWhitelist": [], + "routeBlacklist": [], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [], + "ip": "10.0.0.1", + "route": null + }, + { + "routeWhitelist": [], + "routeBlacklist": [], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [], + "ip": "10.0.0.2", + "route": false + }, + { + "routeWhitelist": [], + "routeBlacklist": [], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [], + "ip": "192.168.1.5", + "route": null + }, + { + "routeWhitelist": [], + "routeBlacklist": [], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [], + "ip": "unknown", + "route": null + }, + { + "routeWhitelist": [], + "routeBlacklist": [], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.1", + "route": true + }, + { + "routeWhitelist": [], + "routeBlacklist": [], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.2", + "route": false + }, + { + "routeWhitelist": [], + "routeBlacklist": [], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [ + "US" + ], + "ip": "192.168.1.5", + "route": false + }, + { + "routeWhitelist": [], + "routeBlacklist": [], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [ + "US" + ], + "ip": "unknown", + "route": false + }, + { + "routeWhitelist": [], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [], + "whitelistCountries": [], + "ip": "10.0.0.1", + "route": false + }, + { + "routeWhitelist": [], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [], + "whitelistCountries": [], + "ip": "10.0.0.2", + "route": null + }, + { + "routeWhitelist": [], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [], + "whitelistCountries": [], + "ip": "192.168.1.5", + "route": null + }, + { + "routeWhitelist": [], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [], + "whitelistCountries": [], + "ip": "unknown", + "route": null + }, + { + "routeWhitelist": [], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.1", + "route": false + }, + { + "routeWhitelist": [], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.2", + "route": false + }, + { + "routeWhitelist": [], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [], + "whitelistCountries": [ + "US" + ], + "ip": "192.168.1.5", + "route": false + }, + { + "routeWhitelist": [], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [], + "whitelistCountries": [ + "US" + ], + "ip": "unknown", + "route": false + }, + { + "routeWhitelist": [], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [], + "ip": "10.0.0.1", + "route": false + }, + { + "routeWhitelist": [], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [], + "ip": "10.0.0.2", + "route": false + }, + { + "routeWhitelist": [], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [], + "ip": "192.168.1.5", + "route": null + }, + { + "routeWhitelist": [], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [], + "ip": "unknown", + "route": null + }, + { + "routeWhitelist": [], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.1", + "route": false + }, + { + "routeWhitelist": [], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.2", + "route": false + }, + { + "routeWhitelist": [], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [ + "US" + ], + "ip": "192.168.1.5", + "route": false + }, + { + "routeWhitelist": [], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [ + "US" + ], + "ip": "unknown", + "route": false + }, + { + "routeWhitelist": [], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [], + "whitelistCountries": [], + "ip": "10.0.0.1", + "route": null + }, + { + "routeWhitelist": [], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [], + "whitelistCountries": [], + "ip": "10.0.0.2", + "route": false + }, + { + "routeWhitelist": [], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [], + "whitelistCountries": [], + "ip": "192.168.1.5", + "route": null + }, + { + "routeWhitelist": [], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [], + "whitelistCountries": [], + "ip": "unknown", + "route": null + }, + { + "routeWhitelist": [], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.1", + "route": true + }, + { + "routeWhitelist": [], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.2", + "route": false + }, + { + "routeWhitelist": [], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [], + "whitelistCountries": [ + "US" + ], + "ip": "192.168.1.5", + "route": false + }, + { + "routeWhitelist": [], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [], + "whitelistCountries": [ + "US" + ], + "ip": "unknown", + "route": false + }, + { + "routeWhitelist": [], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [], + "ip": "10.0.0.1", + "route": null + }, + { + "routeWhitelist": [], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [], + "ip": "10.0.0.2", + "route": false + }, + { + "routeWhitelist": [], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [], + "ip": "192.168.1.5", + "route": null + }, + { + "routeWhitelist": [], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [], + "ip": "unknown", + "route": null + }, + { + "routeWhitelist": [], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.1", + "route": true + }, + { + "routeWhitelist": [], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.2", + "route": false + }, + { + "routeWhitelist": [], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [ + "US" + ], + "ip": "192.168.1.5", + "route": false + }, + { + "routeWhitelist": [], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [ + "US" + ], + "ip": "unknown", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [], + "blockedCountries": [], + "whitelistCountries": [], + "ip": "10.0.0.1", + "route": true + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [], + "blockedCountries": [], + "whitelistCountries": [], + "ip": "10.0.0.2", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [], + "blockedCountries": [], + "whitelistCountries": [], + "ip": "192.168.1.5", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [], + "blockedCountries": [], + "whitelistCountries": [], + "ip": "unknown", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [], + "blockedCountries": [], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.1", + "route": true + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [], + "blockedCountries": [], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.2", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [], + "blockedCountries": [], + "whitelistCountries": [ + "US" + ], + "ip": "192.168.1.5", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [], + "blockedCountries": [], + "whitelistCountries": [ + "US" + ], + "ip": "unknown", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [], + "ip": "10.0.0.1", + "route": true + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [], + "ip": "10.0.0.2", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [], + "ip": "192.168.1.5", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [], + "ip": "unknown", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.1", + "route": true + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.2", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [ + "US" + ], + "ip": "192.168.1.5", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [ + "US" + ], + "ip": "unknown", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [], + "whitelistCountries": [], + "ip": "10.0.0.1", + "route": true + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [], + "whitelistCountries": [], + "ip": "10.0.0.2", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [], + "whitelistCountries": [], + "ip": "192.168.1.5", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [], + "whitelistCountries": [], + "ip": "unknown", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.1", + "route": true + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.2", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [], + "whitelistCountries": [ + "US" + ], + "ip": "192.168.1.5", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [], + "whitelistCountries": [ + "US" + ], + "ip": "unknown", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [], + "ip": "10.0.0.1", + "route": true + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [], + "ip": "10.0.0.2", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [], + "ip": "192.168.1.5", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [], + "ip": "unknown", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.1", + "route": true + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.2", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [ + "US" + ], + "ip": "192.168.1.5", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [ + "US" + ], + "ip": "unknown", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [], + "whitelistCountries": [], + "ip": "10.0.0.1", + "route": true + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [], + "whitelistCountries": [], + "ip": "10.0.0.2", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [], + "whitelistCountries": [], + "ip": "192.168.1.5", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [], + "whitelistCountries": [], + "ip": "unknown", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.1", + "route": true + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.2", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [], + "whitelistCountries": [ + "US" + ], + "ip": "192.168.1.5", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [], + "whitelistCountries": [ + "US" + ], + "ip": "unknown", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [], + "ip": "10.0.0.1", + "route": true + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [], + "ip": "10.0.0.2", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [], + "ip": "192.168.1.5", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [], + "ip": "unknown", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.1", + "route": true + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.2", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [ + "US" + ], + "ip": "192.168.1.5", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.1" + ], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [ + "US" + ], + "ip": "unknown", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [], + "blockedCountries": [], + "whitelistCountries": [], + "ip": "10.0.0.1", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [], + "blockedCountries": [], + "whitelistCountries": [], + "ip": "10.0.0.2", + "route": true + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [], + "blockedCountries": [], + "whitelistCountries": [], + "ip": "192.168.1.5", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [], + "blockedCountries": [], + "whitelistCountries": [], + "ip": "unknown", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [], + "blockedCountries": [], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.1", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [], + "blockedCountries": [], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.2", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [], + "blockedCountries": [], + "whitelistCountries": [ + "US" + ], + "ip": "192.168.1.5", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [], + "blockedCountries": [], + "whitelistCountries": [ + "US" + ], + "ip": "unknown", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [], + "ip": "10.0.0.1", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [], + "ip": "10.0.0.2", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [], + "ip": "192.168.1.5", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [], + "ip": "unknown", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.1", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.2", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [ + "US" + ], + "ip": "192.168.1.5", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [ + "US" + ], + "ip": "unknown", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [], + "whitelistCountries": [], + "ip": "10.0.0.1", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [], + "whitelistCountries": [], + "ip": "10.0.0.2", + "route": true + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [], + "whitelistCountries": [], + "ip": "192.168.1.5", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [], + "whitelistCountries": [], + "ip": "unknown", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.1", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.2", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [], + "whitelistCountries": [ + "US" + ], + "ip": "192.168.1.5", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [], + "whitelistCountries": [ + "US" + ], + "ip": "unknown", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [], + "ip": "10.0.0.1", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [], + "ip": "10.0.0.2", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [], + "ip": "192.168.1.5", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [], + "ip": "unknown", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.1", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.2", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [ + "US" + ], + "ip": "192.168.1.5", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [ + "10.0.0.1" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [ + "US" + ], + "ip": "unknown", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [], + "whitelistCountries": [], + "ip": "10.0.0.1", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [], + "whitelistCountries": [], + "ip": "10.0.0.2", + "route": true + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [], + "whitelistCountries": [], + "ip": "192.168.1.5", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [], + "whitelistCountries": [], + "ip": "unknown", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.1", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.2", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [], + "whitelistCountries": [ + "US" + ], + "ip": "192.168.1.5", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [], + "whitelistCountries": [ + "US" + ], + "ip": "unknown", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [], + "ip": "10.0.0.1", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [], + "ip": "10.0.0.2", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [], + "ip": "192.168.1.5", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [], + "ip": "unknown", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.1", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.2", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [ + "US" + ], + "ip": "192.168.1.5", + "route": false + }, + { + "routeWhitelist": [ + "10.0.0.2" + ], + "routeBlacklist": [ + "10.0.0.2" + ], + "blockedCountries": [ + "CN" + ], + "whitelistCountries": [ + "US" + ], + "ip": "unknown", + "route": false + } + ], + "global": [ + { + "whitelist": [], + "whitelistCountries": [], + "ip": "10.0.0.1", + "allowed": true + }, + { + "whitelist": [], + "whitelistCountries": [], + "ip": "10.0.0.2", + "allowed": true + }, + { + "whitelist": [], + "whitelistCountries": [], + "ip": "192.168.1.5", + "allowed": true + }, + { + "whitelist": [], + "whitelistCountries": [], + "ip": "unknown", + "allowed": true + }, + { + "whitelist": [], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.1", + "allowed": true + }, + { + "whitelist": [], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.2", + "allowed": false + }, + { + "whitelist": [], + "whitelistCountries": [ + "US" + ], + "ip": "192.168.1.5", + "allowed": false + }, + { + "whitelist": [], + "whitelistCountries": [ + "US" + ], + "ip": "unknown", + "allowed": false + }, + { + "whitelist": [ + "10.0.0.1" + ], + "whitelistCountries": [], + "ip": "10.0.0.1", + "allowed": true + }, + { + "whitelist": [ + "10.0.0.1" + ], + "whitelistCountries": [], + "ip": "10.0.0.2", + "allowed": false + }, + { + "whitelist": [ + "10.0.0.1" + ], + "whitelistCountries": [], + "ip": "192.168.1.5", + "allowed": false + }, + { + "whitelist": [ + "10.0.0.1" + ], + "whitelistCountries": [], + "ip": "unknown", + "allowed": false + }, + { + "whitelist": [ + "10.0.0.1" + ], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.1", + "allowed": true + }, + { + "whitelist": [ + "10.0.0.1" + ], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.2", + "allowed": false + }, + { + "whitelist": [ + "10.0.0.1" + ], + "whitelistCountries": [ + "US" + ], + "ip": "192.168.1.5", + "allowed": false + }, + { + "whitelist": [ + "10.0.0.1" + ], + "whitelistCountries": [ + "US" + ], + "ip": "unknown", + "allowed": false + }, + { + "whitelist": [ + "10.0.0.2" + ], + "whitelistCountries": [], + "ip": "10.0.0.1", + "allowed": false + }, + { + "whitelist": [ + "10.0.0.2" + ], + "whitelistCountries": [], + "ip": "10.0.0.2", + "allowed": true + }, + { + "whitelist": [ + "10.0.0.2" + ], + "whitelistCountries": [], + "ip": "192.168.1.5", + "allowed": false + }, + { + "whitelist": [ + "10.0.0.2" + ], + "whitelistCountries": [], + "ip": "unknown", + "allowed": false + }, + { + "whitelist": [ + "10.0.0.2" + ], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.1", + "allowed": false + }, + { + "whitelist": [ + "10.0.0.2" + ], + "whitelistCountries": [ + "US" + ], + "ip": "10.0.0.2", + "allowed": true + }, + { + "whitelist": [ + "10.0.0.2" + ], + "whitelistCountries": [ + "US" + ], + "ip": "192.168.1.5", + "allowed": false + }, + { + "whitelist": [ + "10.0.0.2" + ], + "whitelistCountries": [ + "US" + ], + "ip": "unknown", + "allowed": false + } + ] +} \ No newline at end of file diff --git a/packages/core/tests/test-utils/ip-list-route-country.test.ts b/packages/core/tests/test-utils/ip-list-route-country.test.ts new file mode 100644 index 0000000..f514e75 --- /dev/null +++ b/packages/core/tests/test-utils/ip-list-route-country.test.ts @@ -0,0 +1,72 @@ +/** + * Route country verdict combination and unknown-identity parity with the + * reference engine (guard_core/core/checks/helpers.py `check_route_ip_access`, + * guard_core/_utils/access_control.py `_check_unknown_identity_access`): a + * blocked verdict from either the route IP lists or the route countries + * denies, an allowed verdict from either allows, otherwise no verdict; the + * unknown client identity is allowed unless a whitelist or country whitelist + * is configured. The expectations in ip-list-route-country-cases.json are + * generated by running the reference. + */ +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; +import { isIpAllowed } from '../../src/utils.js'; +import { checkIpAccess } from '../../src/core/checks/ip-access.js'; +import { checkRouteIpAccess } from '../../src/core/checks/helpers.js'; +import { SecurityConfigSchema } from '../../src/models/config.js'; +import { RouteConfig } from '../../src/models/route-config.js'; +import type { GeoIPHandler } from '../../src/protocols/geo-ip.js'; +import { createMockMiddleware } from '../helpers.js'; + +interface Cases { + countries: Record; + route: Array<{ + routeWhitelist: string[]; + routeBlacklist: string[]; + blockedCountries: string[]; + whitelistCountries: string[]; + ip: string; + route: boolean | null; + }>; + global: Array<{ whitelist: string[]; whitelistCountries: string[]; ip: string; allowed: boolean }>; +} + +const CASES: Cases = JSON.parse( + readFileSync(new URL('./ip-list-route-country-cases.json', import.meta.url), 'utf8'), +); + +const geo = { + isInitialized: true, + async initialize() {}, + getCountry: (ip: string) => CASES.countries[ip] ?? null, +} as unknown as GeoIPHandler; + +describe('route IP lists and countries combine like the reference', () => { + it.each(CASES.route.map((r) => [JSON.stringify(r), r] as const))('%s', async (_l, r) => { + const rc = new RouteConfig(); + rc.ipWhitelist = r.routeWhitelist.length > 0 ? r.routeWhitelist : null; + rc.ipBlacklist = r.routeBlacklist.length > 0 ? r.routeBlacklist : null; + rc.blockedCountries = r.blockedCountries; + rc.whitelistCountries = r.whitelistCountries; + const mw = { ...createMockMiddleware(), geoIpHandler: geo }; + expect(await checkRouteIpAccess(r.ip, rc, mw)).toBe(r.route); + }); +}); + +describe('global access with country whitelist matches the reference', () => { + const cfg = (r: Cases['global'][number]) => + SecurityConfigSchema.parse({ + enableRedis: false, + whitelist: r.whitelist.length > 0 ? r.whitelist : null, + whitelistCountries: r.whitelistCountries, + geoIpHandler: geo, + }); + + it.each(CASES.global.map((r) => [JSON.stringify(r), r] as const))('checkIpAccess %s', async (_l, r) => { + expect((await checkIpAccess(r.ip, cfg(r), geo)).allowed).toBe(r.allowed); + }); + + it.each(CASES.global.map((r) => [JSON.stringify(r), r] as const))('isIpAllowed %s', async (_l, r) => { + expect(await isIpAllowed(r.ip, cfg(r), geo)).toBe(r.allowed); + }); +});