diff --git a/frontend/src/views/admin/IpBlacklistSettings.vue b/frontend/src/views/admin/IpBlacklistSettings.vue index 72efdf62..a3dfec9c 100644 --- a/frontend/src/views/admin/IpBlacklistSettings.vue +++ b/frontend/src/views/admin/IpBlacklistSettings.vue @@ -20,6 +20,9 @@ const { t } = useI18n({ enable_tip: 'Block IPs matching blacklist patterns from accessing rate-limited APIs', ip_blacklist: 'IP Blacklist Patterns', ip_blacklist_placeholder: 'Enter pattern (e.g., 192.168.1 or ^10\\.0\\.0\\.5$)', + asn_blacklist: 'ASN Organization Blacklist', + asn_blacklist_placeholder: 'Enter ASN organization (e.g., Google, Amazon)', + asn_tip: 'Block by ASN organization (ISP/provider). Case-insensitive text matching or regex.', }, zh: { title: 'IP 黑名单设置', @@ -31,12 +34,16 @@ const { t } = useI18n({ enable_tip: '阻止匹配黑名单的 IP 访问限流 API', ip_blacklist: 'IP 黑名单匹配模式', ip_blacklist_placeholder: '输入匹配模式(例如:192.168.1 或 ^10\\.0\\.0\\.5$)', + asn_blacklist: 'ASN 组织(运营商)黑名单', + asn_blacklist_placeholder: '输入 ASN 组织名称(例如:Google, Amazon)', + asn_tip: '根据 ASN 组织(运营商/ISP)拉黑。支持不区分大小写的文本匹配或正则表达式。', } } }); const enabled = ref(false) const ipBlacklist = ref([]) +const asnBlacklist = ref([]) const fetchData = async () => { try { @@ -44,6 +51,7 @@ const fetchData = async () => { const res = await api.fetch(`/admin/ip_blacklist/settings`) enabled.value = res.enabled || false ipBlacklist.value = res.blacklist || [] + asnBlacklist.value = res.asnBlacklist || [] } catch (error) { message.error(error.message || "error"); } finally { @@ -59,6 +67,7 @@ const save = async () => { body: JSON.stringify({ enabled: enabled.value, blacklist: ipBlacklist.value || [], + asnBlacklist: asnBlacklist.value || [], }) }) message.success(t('successTip')) @@ -110,6 +119,28 @@ onMounted(async () => { + + + + + + + + + + {{ t('asn_tip') }} + + diff --git a/worker/src/admin_api/ip_blacklist_settings.ts b/worker/src/admin_api/ip_blacklist_settings.ts index 420e0b34..65936c95 100644 --- a/worker/src/admin_api/ip_blacklist_settings.ts +++ b/worker/src/admin_api/ip_blacklist_settings.ts @@ -48,9 +48,29 @@ async function saveIpBlacklistSettings(c: Context): Promise pattern.trim()) .filter(pattern => pattern.length > 0); + // Validate and sanitize ASN blacklist if provided + let sanitizedAsnBlacklist: string[] = []; + if (settings.asnBlacklist) { + if (!Array.isArray(settings.asnBlacklist)) { + return c.text("Invalid asnBlacklist value", 400); + } + + if (settings.asnBlacklist.length > MAX_BLACKLIST_SIZE) { + return c.text( + `ASN blacklist exceeds maximum size (${MAX_BLACKLIST_SIZE} entries)`, + 400 + ); + } + + sanitizedAsnBlacklist = settings.asnBlacklist + .map(pattern => pattern.trim()) + .filter(pattern => pattern.length > 0); + } + const sanitizedSettings: IpBlacklistSettings = { enabled: settings.enabled, - blacklist: sanitizedBlacklist + blacklist: sanitizedBlacklist, + asnBlacklist: sanitizedAsnBlacklist }; await saveSetting( diff --git a/worker/src/ip_blacklist.ts b/worker/src/ip_blacklist.ts index bc56d4bc..9abe3d87 100644 --- a/worker/src/ip_blacklist.ts +++ b/worker/src/ip_blacklist.ts @@ -8,6 +8,7 @@ import { CONSTANTS } from './constants'; export type IpBlacklistSettings = { enabled: boolean; blacklist: string[]; // Array of regex patterns or plain strings + asnBlacklist?: string[]; // Array of ASN organization patterns (e.g., "Google LLC", "Amazon") } /** @@ -16,40 +17,35 @@ export type IpBlacklistSettings = { */ function looksLikeRegex(pattern: string): boolean { // Check if pattern contains common regex metacharacters + // eslint-disable-next-line no-useless-escape return /[\^$.*+?\[\]{}()|\\]/.test(pattern); } /** - * Check if an IP address matches any blacklist pattern + * Check if a value matches any blacklist pattern * Supports both regex patterns and plain string matching * - * @param ip - The IP address to check (e.g., "192.168.1.100") + * @param value - The value to check (e.g., IP address, ASN organization) * @param blacklist - Array of patterns (regex or plain strings) - * @returns true if IP is blacklisted, false otherwise + * @param caseSensitive - Whether to use case-sensitive matching for plain strings (default: true for IP, false for ASN) + * @returns true if value is blacklisted, false otherwise * * @example - * // Regex mode (has special chars: ^ $ . * + ? [ ] { } ( ) | \): - * isIpBlacklisted("192.168.1.100", ["^192\\.168\\.1\\."]) // true (regex match) - * isIpBlacklisted("10.0.0.5", ["^10\\.0\\.0\\.5$"]) // true (exact match) - * isIpBlacklisted("192.168.10.1", ["^192\\.168\\.1\\."]) // false (no match) + * // IP address matching (case-sensitive): + * isBlacklisted("192.168.1.100", ["192.168.1"], true) // true (substring match) + * isBlacklisted("10.0.0.5", ["^10\\.0\\.0\\.5$"], true) // true (regex match) * - * // Plain string mode (no special chars - substring matching): - * // Rule: IP contains the pattern string - * isIpBlacklisted("192.168.1.100", ["192.168.1"]) // true (IP包含"192.168.1") - * isIpBlacklisted("192.168.1.255", ["192.168.1"]) // true (IP包含"192.168.1") - * isIpBlacklisted("10.0.0.5", ["10.0.0"]) // true (IP包含"10.0.0") - * isIpBlacklisted("192.168.2.100", ["192.168.1"]) // false (IP不包含"192.168.1") - * isIpBlacklisted("192.168.10.1", ["192.168.1"]) // true (IP包含"192.168.1") + * // ASN organization matching (case-insensitive): + * isBlacklisted("Google LLC", ["google"], false) // true (case-insensitive) + * isBlacklisted("Amazon.com, Inc.", ["amazon"], false) // true */ -export function isIpBlacklisted(ip: string | null, blacklist: string[]): boolean { - if (!ip || !blacklist || blacklist.length === 0) { +function isBlacklisted(value: string | null | undefined, blacklist: string[], caseSensitive: boolean = true): boolean { + if (!value || !blacklist || blacklist.length === 0) { return false; } - // Normalize IP (trim whitespace) - const normalizedIp = ip.trim(); + const normalizedValue = value.trim(); - // Check if IP matches any pattern in blacklist return blacklist.some(pattern => { const normalizedPattern = pattern.trim(); if (!normalizedPattern) { @@ -57,21 +53,24 @@ export function isIpBlacklisted(ip: string | null, blacklist: string[]): boolean } try { - // Try to detect if this is a regex pattern if (looksLikeRegex(normalizedPattern)) { - // Regex mode: test as regular expression const regex = new RegExp(normalizedPattern); - return regex.test(normalizedIp); + return regex.test(normalizedValue); } else { // Plain string mode: substring matching - // 匹配规则:IP中包含设置的字符串就算匹配 - // Example: "192.168.1.100".includes("192.168.1") → true - return normalizedIp.includes(normalizedPattern); + if (caseSensitive) { + return normalizedValue.includes(normalizedPattern); + } else { + return normalizedValue.toLowerCase().includes(normalizedPattern.toLowerCase()); + } } } catch (error) { - // If regex parsing fails, fall back to plain string matching console.warn(`Pattern "${normalizedPattern}" failed regex parsing, using plain matching`); - return normalizedIp.includes(normalizedPattern); + if (caseSensitive) { + return normalizedValue.includes(normalizedPattern); + } else { + return normalizedValue.toLowerCase().includes(normalizedPattern.toLowerCase()); + } } }); } @@ -133,12 +132,22 @@ export async function checkIpBlacklist( return null; } - // Check if IP is blacklisted - if (isIpBlacklisted(reqIp, settings.blacklist)) { + // Check if IP is blacklisted (case-sensitive matching) + if (isBlacklisted(reqIp, settings.blacklist, true)) { console.warn(`Blocked blacklisted IP: ${reqIp} for path: ${c.req.path}`); return c.text(`Access denied: IP ${reqIp} is blacklisted`, 403); } + // Check ASN organization blacklist + if (settings.asnBlacklist && settings.asnBlacklist.length > 0) { + const asOrganization = c.req.raw.cf?.asOrganization; + // Check ASN with case-insensitive matching + if (asOrganization && isBlacklisted(asOrganization as string, settings.asnBlacklist, false)) { + console.warn(`Blocked blacklisted ASN: ${asOrganization} (IP: ${reqIp}) for path: ${c.req.path}`); + return c.text(`Access denied: ASN organization is blacklisted`, 403); + } + } + return null; } catch (error) { // Log error but don't block request