mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-06-28 02:42:44 +08:00
feat: add ASN organization blacklist for IP filtering (#755)
- Add asnBlacklist field to IpBlacklistSettings (optional) - Create shared isBlacklisted() function for IP and ASN matching - Add isAsnBlacklisted() function with case-insensitive matching - Extend checkIpBlacklist() to also check ASN organizations - Update admin API to validate and save ASN blacklist - Add ASN blacklist input to admin UI (below IP blacklist) - Support text matching and regex for ASN organization names - ASN data from request.cf.asOrganization (Cloudflare) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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 () => {
|
||||
</template>
|
||||
</n-select>
|
||||
</n-form-item-row>
|
||||
|
||||
<n-form-item-row :label="t('asn_blacklist')">
|
||||
<n-select
|
||||
v-model:value="asnBlacklist"
|
||||
filterable
|
||||
multiple
|
||||
tag
|
||||
:placeholder="t('asn_blacklist_placeholder')"
|
||||
:disabled="!enabled">
|
||||
<template #empty>
|
||||
<n-text depth="3">
|
||||
{{ t('manualInputPrompt') }}
|
||||
</n-text>
|
||||
</template>
|
||||
</n-select>
|
||||
</n-form-item-row>
|
||||
|
||||
<n-alert :show-icon="false" :bordered="false" type="default">
|
||||
<n-text depth="3" style="font-size: 12px;">
|
||||
{{ t('asn_tip') }}
|
||||
</n-text>
|
||||
</n-alert>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</div>
|
||||
|
||||
@@ -48,9 +48,29 @@ async function saveIpBlacklistSettings(c: Context<HonoCustomType>): Promise<Resp
|
||||
.map(pattern => 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(
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user