From 707820697ab2e9bf74b70e6e4c3a9ef5d394ffc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B3=E4=BD=A9=E5=BA=AD?= Date: Tue, 7 Oct 2025 17:57:17 +0800 Subject: [PATCH] =?UTF-8?q?=E6=AD=A3=E7=A2=BA=E9=A1=AF=E7=A4=BA=20ip=20?= =?UTF-8?q?=E7=B5=90=E6=9E=9C=20(=20cloudflare=20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/ip-diagnostic/route.ts | 193 ++++++++++++++++++ app/api/ip/route.ts | 13 +- app/test-ip-blocking/page.tsx | 224 +++++++++++++++++++++ app/test/ip-debug/page.tsx | 16 +- app/test/ip-diagnostic/page.tsx | 339 ++++++++++++++++++++++++++++++++ components/ip-display.tsx | 30 ++- lib/ip-utils.ts | 151 +++++++++++--- middleware.ts | 15 +- 8 files changed, 935 insertions(+), 46 deletions(-) create mode 100644 app/api/ip-diagnostic/route.ts create mode 100644 app/test-ip-blocking/page.tsx create mode 100644 app/test/ip-diagnostic/page.tsx diff --git a/app/api/ip-diagnostic/route.ts b/app/api/ip-diagnostic/route.ts new file mode 100644 index 0000000..a2dd032 --- /dev/null +++ b/app/api/ip-diagnostic/route.ts @@ -0,0 +1,193 @@ +import { NextRequest, NextResponse } from 'next/server' + +export async function GET(request: NextRequest) { + try { + // 收集所有可能的IP相关头部 + const allHeaders = Object.fromEntries(request.headers.entries()) + + // 专门收集IP相关的头部 + const ipHeaders = { + 'x-forwarded-for': request.headers.get('x-forwarded-for'), + 'x-real-ip': request.headers.get('x-real-ip'), + 'x-client-ip': request.headers.get('x-client-ip'), + 'cf-connecting-ip': request.headers.get('cf-connecting-ip'), // Cloudflare + 'cf-ray': request.headers.get('cf-ray'), // Cloudflare Ray ID + 'cf-visitor': request.headers.get('cf-visitor'), // Cloudflare visitor info + 'cf-ipcountry': request.headers.get('cf-ipcountry'), // Cloudflare country + 'x-forwarded': request.headers.get('x-forwarded'), + 'forwarded-for': request.headers.get('forwarded-for'), + 'forwarded': request.headers.get('forwarded'), + 'x-original-forwarded-for': request.headers.get('x-original-forwarded-for'), + 'x-cluster-client-ip': request.headers.get('x-cluster-client-ip'), + 'x-1panel-client-ip': request.headers.get('x-1panel-client-ip'), + 'x-nginx-proxy-real-ip': request.headers.get('x-nginx-proxy-real-ip'), + 'x-original-remote-addr': request.headers.get('x-original-remote-addr'), + 'x-remote-addr': request.headers.get('x-remote-addr'), + 'x-client-real-ip': request.headers.get('x-client-real-ip'), + 'true-client-ip': request.headers.get('true-client-ip'), + } + + // 获取其他有用的头部信息 + const otherHeaders = { + 'user-agent': request.headers.get('user-agent'), + 'host': request.headers.get('host'), + 'referer': request.headers.get('referer'), + 'origin': request.headers.get('origin'), + 'x-forwarded-proto': request.headers.get('x-forwarded-proto'), + 'x-forwarded-host': request.headers.get('x-forwarded-host'), + 'x-forwarded-port': request.headers.get('x-forwarded-port'), + 'via': request.headers.get('via'), + 'connection': request.headers.get('connection'), + 'upgrade-insecure-requests': request.headers.get('upgrade-insecure-requests'), + } + + // 尝试从不同来源获取IP + const ipAnalysis = { + // 标准代理头部 + xForwardedFor: request.headers.get('x-forwarded-for'), + xRealIp: request.headers.get('x-real-ip'), + xClientIp: request.headers.get('x-client-ip'), + + // Cloudflare 特定头部 + cfConnectingIp: request.headers.get('cf-connecting-ip'), + + // 其他可能的头部 + forwarded: request.headers.get('forwarded'), + xOriginalForwardedFor: request.headers.get('x-original-forwarded-for'), + + // 分析结果 + analysis: { + hasCloudflare: !!request.headers.get('cf-connecting-ip') || !!request.headers.get('cf-ray'), + hasXForwardedFor: !!request.headers.get('x-forwarded-for'), + hasXRealIp: !!request.headers.get('x-real-ip'), + hasForwarded: !!request.headers.get('forwarded'), + recommendedIpSource: '', + recommendedIp: '', + } + } + + // 分析并推荐最佳IP来源 + if (ipAnalysis.cfConnectingIp) { + ipAnalysis.analysis.recommendedIpSource = 'cf-connecting-ip (Cloudflare)' + ipAnalysis.analysis.recommendedIp = ipAnalysis.cfConnectingIp + } else if (ipAnalysis.xForwardedFor) { + // 解析 x-forwarded-for,通常格式为 "client-ip, proxy1-ip, proxy2-ip" + const forwardedIps = ipAnalysis.xForwardedFor.split(',').map(ip => ip.trim()) + const clientIp = forwardedIps[0] // 第一个IP通常是客户端IP + ipAnalysis.analysis.recommendedIpSource = 'x-forwarded-for (first IP)' + ipAnalysis.analysis.recommendedIp = clientIp + } else if (ipAnalysis.xRealIp) { + ipAnalysis.analysis.recommendedIpSource = 'x-real-ip' + ipAnalysis.analysis.recommendedIp = ipAnalysis.xRealIp + } else if (ipAnalysis.forwarded) { + ipAnalysis.analysis.recommendedIpSource = 'forwarded header' + ipAnalysis.analysis.recommendedIp = ipAnalysis.forwarded + } else { + ipAnalysis.analysis.recommendedIpSource = 'no reliable IP source found' + ipAnalysis.analysis.recommendedIp = 'unknown' + } + + // 检查是否是Cloudflare + const isCloudflare = { + hasCfConnectingIp: !!request.headers.get('cf-connecting-ip'), + hasCfRay: !!request.headers.get('cf-ray'), + hasCfVisitor: !!request.headers.get('cf-visitor'), + hasCfIpCountry: !!request.headers.get('cf-ipcountry'), + cfRay: request.headers.get('cf-ray'), + cfCountry: request.headers.get('cf-ipcountry'), + cfVisitor: request.headers.get('cf-visitor'), + } + + // 检查是否是1Panel或Nginx代理 + const proxyInfo = { + hasNginxProxy: !!request.headers.get('x-nginx-proxy-real-ip'), + has1Panel: !!request.headers.get('x-1panel-client-ip'), + nginxProxyIp: request.headers.get('x-nginx-proxy-real-ip'), + panelClientIp: request.headers.get('x-1panel-client-ip'), + } + + // 解析 x-forwarded-for 中的所有IP + const parseXForwardedFor = (xff: string | null) => { + if (!xff) return [] + return xff.split(',').map(ip => ip.trim()).filter(ip => ip) + } + + // 分析IP来源链 + const ipChain = { + xForwardedForChain: parseXForwardedFor(ipAnalysis.xForwardedFor), + recommendedClientIp: ipAnalysis.analysis.recommendedIp, + isPublicIp: (ip: string) => { + // 简单的公网IP检查 + return !ip.match(/^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|127\.|169\.254\.)/) + } + } + + return NextResponse.json({ + timestamp: new Date().toISOString(), + environment: process.env.NODE_ENV, + url: request.url, + + // 主要分析结果 + ipAnalysis, + isCloudflare, + proxyInfo, + ipChain, + + // 所有IP相关头部 + ipHeaders, + + // 其他有用头部 + otherHeaders, + + // 完整头部列表(用于调试) + allHeaders, + + // 建议 + recommendations: { + primaryIpSource: ipAnalysis.analysis.recommendedIpSource, + primaryIp: ipAnalysis.analysis.recommendedIp, + isCloudflareSetup: isCloudflare.hasCfConnectingIp, + isProxySetup: proxyInfo.hasNginxProxy || proxyInfo.has1Panel, + suggestedConfig: generateSuggestedConfig(ipAnalysis, isCloudflare, proxyInfo) + } + }) + + } catch (error) { + console.error('IP诊断错误:', error) + return NextResponse.json( + { error: 'IP诊断失败', details: error.message }, + { status: 500 } + ) + } +} + +function generateSuggestedConfig(ipAnalysis: any, isCloudflare: any, proxyInfo: any): string[] { + const suggestions = [] + + if (isCloudflare.hasCfConnectingIp) { + suggestions.push('检测到Cloudflare代理,建议优先使用 cf-connecting-ip 头部') + suggestions.push('确保1Panel/反向代理正确转发 cf-connecting-ip 头部') + } + + if (ipAnalysis.xForwardedFor) { + suggestions.push('检测到 x-forwarded-for 头部,建议解析第一个IP作为客户端IP') + const ips = ipAnalysis.xForwardedFor.split(',').map((ip: string) => ip.trim()) + if (ips.length > 1) { + suggestions.push(`IP链: ${ips.join(' -> ')}`) + } + } + + if (proxyInfo.hasNginxProxy) { + suggestions.push('检测到Nginx代理,建议配置 nginx.conf 正确转发客户端IP') + } + + if (proxyInfo.has1Panel) { + suggestions.push('检测到1Panel,建议检查1Panel的反向代理配置') + } + + if (!isCloudflare.hasCfConnectingIp && !ipAnalysis.xForwardedFor && !ipAnalysis.xRealIp) { + suggestions.push('警告: 未检测到可靠的IP来源头部,请检查代理配置') + } + + return suggestions +} diff --git a/app/api/ip/route.ts b/app/api/ip/route.ts index ade61ad..c914878 100644 --- a/app/api/ip/route.ts +++ b/app/api/ip/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { getClientIp, isIpAllowed, getIpLocation, getDetailedIpInfo } from '@/lib/ip-utils' +import { getClientIp, isIpAllowed, getIpLocation, getDetailedIpInfo, isValidIp, isValidIPv6 } from '@/lib/ip-utils' // 強制動態渲染 export const dynamic = 'force-dynamic' @@ -10,6 +10,17 @@ export async function GET(request: NextRequest) { const detailedInfo = getDetailedIpInfo(request); let clientIp = detailedInfo.detectedIp; + // 根據你的環境,優先使用 cf-connecting-ip (支持IPv4和IPv6) + const cfConnectingIp = request.headers.get('cf-connecting-ip'); + if (cfConnectingIp && cfConnectingIp.trim() !== '') { + const cleanCfIp = cfConnectingIp.trim(); + // 检查是否是有效的IPv4或IPv6地址 + if (isValidIp(cleanCfIp) || isValidIPv6(cleanCfIp)) { + clientIp = cleanCfIp; + console.log('使用 cf-connecting-ip:', clientIp, isValidIPv6(clientIp) ? '(IPv6)' : '(IPv4)'); + } + } + // 確保返回IPv4格式的地址 function ensureIPv4Format(ip: string): string { if (!ip) return '127.0.0.1'; diff --git a/app/test-ip-blocking/page.tsx b/app/test-ip-blocking/page.tsx new file mode 100644 index 0000000..1b73435 --- /dev/null +++ b/app/test-ip-blocking/page.tsx @@ -0,0 +1,224 @@ +"use client" + +import { useEffect, useState } from "react" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { RefreshCw, Shield, AlertTriangle, CheckCircle, Lock } from "lucide-react" + +interface IpInfo { + ip: string + isAllowed: boolean + enableIpWhitelist: boolean + allowedIps: string[] + timestamp: string +} + +export default function IpBlockingTestPage() { + const [ipInfo, setIpInfo] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + const fetchIpInfo = async () => { + setLoading(true) + setError(null) + try { + const response = await fetch('/api/ip') + if (!response.ok) { + throw new Error('無法獲取IP信息') + } + const data = await response.json() + setIpInfo(data) + } catch (error) { + console.error("無法獲取IP信息:", error) + setError("無法獲取IP信息") + } finally { + setLoading(false) + } + } + + useEffect(() => { + fetchIpInfo() + }, []) + + if (loading) { + return ( +
+
+
+ + 載入中... +
+
+
+ ) + } + + if (error || !ipInfo) { + return ( +
+ + + + + 錯誤 + + + +

{error}

+ +
+
+
+ ) + } + + return ( +
+
+

IP 阻擋測試

+ +
+ + {/* IP狀態卡片 */} + + + + {ipInfo.enableIpWhitelist && !ipInfo.isAllowed ? ( + <> + + IP 被阻擋 + + ) : ( + <> + + IP 允許訪問 + + )} + + + +
+
+ +

+ {ipInfo.ip} +

+
+
+ +
+ + {ipInfo.isAllowed ? "允許" : "阻擋"} + +
+
+
+ +
+ +
+ + + {ipInfo.enableIpWhitelist ? "已啟用" : "未啟用"} + +
+
+
+
+ + {/* 允許的IP列表 */} + {ipInfo.enableIpWhitelist && ipInfo.allowedIps.length > 0 && ( + + + 允許的IP列表 + + +
+ {ipInfo.allowedIps.map((ip, index) => ( +
+ + {ip === ipInfo.ip ? "當前" : ""} + + {ip} +
+ ))} +
+
+
+ )} + + {/* 阻擋測試說明 */} + + + IP 阻擋測試說明 + + +
+

✅ 如果你能看到這個頁面

+

+ 說明你的IP ({ipInfo.ip}) 在白名單中,可以正常訪問網站。 +

+
+ +
+

⚠️ 測試阻擋功能

+

+ 要測試IP阻擋功能,可以: +

+
    +
  • 暫時從 .env.local 的 ALLOWED_IPS 中移除你的IP
  • +
  • 重啟應用後訪問網站
  • +
  • 應該會看到403禁止訪問頁面
  • +
  • 記得測試完成後將IP加回白名單
  • +
+
+ +
+

🚫 被阻擋的訪問

+

+ 如果IP不在白名單中,訪問者會看到: +

+
    +
  • 403 Forbidden 錯誤頁面
  • +
  • 顯示被阻擋的IP地址
  • +
  • 無法訪問任何網頁內容
  • +
  • 只有IP檢測API可以訪問
  • +
+
+
+
+ + {/* 當前配置 */} + + + 當前配置 + + +
+ 白名單狀態: + + {ipInfo.enableIpWhitelist ? "已啟用" : "未啟用"} + +
+
+ 當前IP: + {ipInfo.ip} +
+
+ 訪問權限: + + {ipInfo.isAllowed ? "允許" : "阻擋"} + +
+
+
+
+ ) +} diff --git a/app/test/ip-debug/page.tsx b/app/test/ip-debug/page.tsx index 017b719..6107da6 100644 --- a/app/test/ip-debug/page.tsx +++ b/app/test/ip-debug/page.tsx @@ -100,10 +100,18 @@ export default function IpDebugPage() {

IP 檢測調試

- +
+ + +
{/* 主要IP信息 */} diff --git a/app/test/ip-diagnostic/page.tsx b/app/test/ip-diagnostic/page.tsx new file mode 100644 index 0000000..4bd0441 --- /dev/null +++ b/app/test/ip-diagnostic/page.tsx @@ -0,0 +1,339 @@ +"use client" + +import { useEffect, useState } from "react" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { RefreshCw, Shield, AlertTriangle, CheckCircle, Info, Cloud, Server } from "lucide-react" + +interface DiagnosticData { + timestamp: string + environment: string + url: string + ipAnalysis: { + xForwardedFor: string | null + xRealIp: string | null + xClientIp: string | null + cfConnectingIp: string | null + forwarded: string | null + xOriginalForwardedFor: string | null + analysis: { + hasCloudflare: boolean + hasXForwardedFor: boolean + hasXRealIp: boolean + hasForwarded: boolean + recommendedIpSource: string + recommendedIp: string + } + } + isCloudflare: { + hasCfConnectingIp: boolean + hasCfRay: boolean + hasCfVisitor: boolean + hasCfIpCountry: boolean + cfRay: string | null + cfCountry: string | null + cfVisitor: string | null + } + proxyInfo: { + hasNginxProxy: boolean + has1Panel: boolean + nginxProxyIp: string | null + panelClientIp: string | null + } + ipChain: { + xForwardedForChain: string[] + recommendedClientIp: string + } + ipHeaders: Record + otherHeaders: Record + allHeaders: Record + recommendations: { + primaryIpSource: string + primaryIp: string + isCloudflareSetup: boolean + isProxySetup: boolean + suggestedConfig: string[] + } +} + +export default function IpDiagnosticPage() { + const [diagnosticData, setDiagnosticData] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + const fetchDiagnosticData = async () => { + setLoading(true) + setError(null) + try { + const response = await fetch('/api/ip-diagnostic') + if (!response.ok) { + throw new Error('無法獲取診斷數據') + } + const data = await response.json() + setDiagnosticData(data) + } catch (error) { + console.error("無法獲取診斷數據:", error) + setError("無法獲取診斷數據") + } finally { + setLoading(false) + } + } + + useEffect(() => { + fetchDiagnosticData() + }, []) + + if (loading) { + return ( +
+
+
+ + 載入診斷數據中... +
+
+
+ ) + } + + if (error || !diagnosticData) { + return ( +
+ + + + + 診斷失敗 + + + +

{error}

+ +
+
+
+ ) + } + + return ( +
+
+

IP 診斷工具

+ +
+ + {/* 環境檢測結果 */} + + + + + 環境檢測結果 + + + +
+
+ + Cloudflare: + + {diagnosticData.isCloudflare.hasCfConnectingIp ? "檢測到" : "未檢測到"} + +
+
+ + 代理服務器: + + {diagnosticData.proxyInfo.hasNginxProxy || diagnosticData.proxyInfo.has1Panel ? "檢測到" : "未檢測到"} + +
+
+ + IP來源: + + {diagnosticData.recommendations.primaryIpSource} + +
+
+ + {/* 推薦的IP */} +
+

推薦使用的客戶端IP

+

+ {diagnosticData.recommendations.primaryIp} +

+

+ 來源: {diagnosticData.recommendations.primaryIpSource} +

+
+
+
+ + {/* Cloudflare 信息 */} + {diagnosticData.isCloudflare.hasCfConnectingIp && ( + + + + + Cloudflare 信息 + + + +
+
+ +

+ {diagnosticData.ipAnalysis.cfConnectingIp || 'null'} +

+
+
+ +

+ {diagnosticData.isCloudflare.cfRay || 'null'} +

+
+
+ +

+ {diagnosticData.isCloudflare.cfCountry || 'null'} +

+
+
+ +

+ {diagnosticData.isCloudflare.cfVisitor || 'null'} +

+
+
+
+
+ )} + + {/* 代理信息 */} + {(diagnosticData.proxyInfo.hasNginxProxy || diagnosticData.proxyInfo.has1Panel) && ( + + + + + 代理服務器信息 + + + + {diagnosticData.proxyInfo.hasNginxProxy && ( +
+ +

+ {diagnosticData.proxyInfo.nginxProxyIp || 'null'} +

+
+ )} + {diagnosticData.proxyInfo.has1Panel && ( +
+ +

+ {diagnosticData.proxyInfo.panelClientIp || 'null'} +

+
+ )} +
+
+ )} + + {/* IP鏈分析 */} + + + IP 鏈分析 + + +
+ + {diagnosticData.ipChain.xForwardedForChain.length > 0 ? ( +
+ {diagnosticData.ipChain.xForwardedForChain.map((ip, index) => ( +
+ + {index === 0 ? "客戶端" : `代理${index}`} + + {ip} +
+ ))} +
+ ) : ( +

無 X-Forwarded-For 頭部

+ )} +
+
+
+ + {/* 所有IP相關頭部 */} + + + 所有 IP 相關頭部 + + +
+ {Object.entries(diagnosticData.ipHeaders).map(([key, value]) => ( +
+ {key}: + {value || 'null'} +
+ ))} +
+
+
+ + {/* 配置建議 */} + + + 配置建議 + + +
+ {diagnosticData.recommendations.suggestedConfig.map((suggestion, index) => ( +
+ +

{suggestion}

+
+ ))} +
+
+
+ + {/* 完整頭部列表 */} + + + 完整 HTTP 頭部列表 + + +
+ {Object.entries(diagnosticData.allHeaders).map(([key, value]) => ( +
+ {key}: + {value} +
+ ))} +
+
+
+ + {/* 使用說明 */} + + + 使用說明 + + +

1. 查看上面的診斷結果,確認你的環境類型(Cloudflare、代理、直接連接)

+

2. 檢查「推薦使用的客戶端IP」是否為你的真實IP (114.33.18.13)

+

3. 如果不是,請將診斷結果截圖發給我,我會根據實際的頭部信息調整IP檢測邏輯

+

4. 特別注意 X-Forwarded-For 鏈和 Cloudflare 相關頭部

+
+
+
+ ) +} diff --git a/components/ip-display.tsx b/components/ip-display.tsx index ef30476..916ddb6 100644 --- a/components/ip-display.tsx +++ b/components/ip-display.tsx @@ -28,7 +28,7 @@ interface IpInfo { } } -// 清理IP地址,確保顯示IPv4格式 +// 清理IP地址,支持IPv4和IPv6格式 function cleanIpForDisplay(ip: string): string { if (!ip) return '127.0.0.1'; @@ -51,7 +51,13 @@ function cleanIpForDisplay(ip: string): string { return ip; } - // 如果不是有效的IPv4,返回默認值 + // 驗證是否為有效的IPv6地址 + const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^::1$|^::$|^([0-9a-fA-F]{1,4}:)*::([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4}$|^([0-9a-fA-F]{1,4}:)*::[0-9a-fA-F]{1,4}$|^([0-9a-fA-F]{1,4}:)+::$|^::$/; + if (ipv6Regex.test(ip)) { + return ip; // 直接返回IPv6地址 + } + + // 如果不是有效的IP,返回默認值 return '127.0.0.1'; } @@ -74,11 +80,18 @@ export default function IpDisplay({ mobileSimplified = false }: IpDisplayProps) useEffect(() => { const fetchIpInfo = async () => { try { - const response = await fetch('/api/ip') + // 添加时间戳防止缓存,确保获取最新的IP检测结果 + const response = await fetch(`/api/ip?t=${Date.now()}`, { + cache: 'no-store', + headers: { + 'Cache-Control': 'no-cache' + } + }) if (!response.ok) { throw new Error('無法獲取IP信息') } const data = await response.json() + console.log('IP显示组件获取到数据:', data) setIpInfo(data) } catch (error) { console.error("無法獲取IP信息:", error) @@ -109,9 +122,12 @@ export default function IpDisplay({ mobileSimplified = false }: IpDisplayProps) ) } - // 清理IP地址確保顯示IPv4格式 + // 清理IP地址,支持IPv4和IPv6格式 const displayIp = cleanIpForDisplay(ipInfo.ip); + // 检测是否是真正的IPv6地址 + const isRealIPv6 = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^([0-9a-fA-F]{1,4}:)*::([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4}$|^([0-9a-fA-F]{1,4}:)*::[0-9a-fA-F]{1,4}$|^([0-9a-fA-F]{1,4}:)+::$|^::$/.test(displayIp); + // 使用API返回的IPv6信息,如果沒有則回退到本地檢測 const isIPv6Mapped = ipInfo.ipv6Info?.isIPv6Mapped || isIPv6MappedIPv4(ipInfo.debug?.originalDetectedIp || ipInfo.ip); const ipv6Format = ipInfo.ipv6Info?.ipv6Format || getIPv6MappedFormat(displayIp); @@ -131,7 +147,7 @@ export default function IpDisplay({ mobileSimplified = false }: IpDisplayProps) )} - {isIPv6Mapped ? 'IPv6' : 'IPv4'}: {displayIp.split('.').slice(0, 2).join('.')}... + {isRealIPv6 ? 'IPv6' : 'IPv4'}: {isRealIPv6 ? displayIp.substring(0, 10) + '...' : displayIp.split('.').slice(0, 2).join('.') + '...'}
) @@ -152,9 +168,9 @@ export default function IpDisplay({ mobileSimplified = false }: IpDisplayProps)
- {isIPv6Mapped ? 'IPv6' : 'IPv4'}: {displayIp} + {isRealIPv6 ? 'IPv6' : 'IPv4'}: {displayIp} - {isIPv6Mapped && ( + {isIPv6Mapped && !isRealIPv6 && ( {ipv6Format} diff --git a/lib/ip-utils.ts b/lib/ip-utils.ts index 29efebc..b5d7c5b 100644 --- a/lib/ip-utils.ts +++ b/lib/ip-utils.ts @@ -43,7 +43,12 @@ export function isIpAllowed(clientIp: string, allowedIps: string): boolean { for (const allowedIp of allowedIpList) { try { - if (isIpInRange(clientIp, allowedIp)) { + // 如果是IPv6地址,直接比较字符串 + if (isValidIPv6(clientIp)) { + if (clientIp === allowedIp) { + return true; + } + } else if (isIpInRange(clientIp, allowedIp)) { return true; } } catch (error) { @@ -71,40 +76,112 @@ function cleanIpAddress(ip: string): string | null { return '127.0.0.1'; } - // 驗證IP格式 - if (!isValidIp(ip)) { + // 驗證IP格式 - 支持IPv4和IPv6 + if (!isValidIp(ip) && !isValidIPv6(ip)) { return null; } return ip; } +// 檢測IP策略 - 根據請求頭判斷環境類型 +function detectIpStrategy(req: any): { + isCloudflare: boolean; + isProxy: boolean; + isDirect: boolean; + strategy: string; +} { + const hasCfConnectingIp = !!req.headers['cf-connecting-ip']; + const hasCfRay = !!req.headers['cf-ray']; + const hasCfVisitor = !!req.headers['cf-visitor']; + const hasXForwardedFor = !!req.headers['x-forwarded-for']; + const hasXRealIp = !!req.headers['x-real-ip']; + const hasNginxProxy = !!req.headers['x-nginx-proxy-real-ip']; + const has1Panel = !!req.headers['x-1panel-client-ip']; + + const isCloudflare = hasCfConnectingIp || hasCfRay || hasCfVisitor; + const isProxy = hasXForwardedFor || hasXRealIp || hasNginxProxy || has1Panel; + const isDirect = !isCloudflare && !isProxy; + + let strategy = 'unknown'; + if (isCloudflare) { + strategy = 'cloudflare'; + } else if (isProxy) { + strategy = 'proxy'; + } else { + strategy = 'direct'; + } + + return { + isCloudflare, + isProxy, + isDirect, + strategy + }; +} + // 獲取客戶端真實IP export function getClientIp(req: any): string { - // 按優先順序檢查各種IP來源 - const ipSources = [ - // 1Panel 和常見代理伺服器轉發的IP (優先級最高) - req.headers['x-forwarded-for'], - req.headers['x-real-ip'], - req.headers['x-client-ip'], - req.headers['cf-connecting-ip'], // Cloudflare - req.headers['x-original-forwarded-for'], // 某些代理伺服器 - req.headers['x-cluster-client-ip'], // 集群環境 - - // 其他代理頭 - req.headers['x-forwarded'], // 舊版代理頭 - req.headers['forwarded-for'], - req.headers['forwarded'], - - // 1Panel 特殊頭部 - req.headers['x-1panel-client-ip'], // 1Panel 可能使用的頭部 - req.headers['x-nginx-proxy-real-ip'], // Nginx 代理 - - // 直接連接的IP - req.connection?.remoteAddress, - req.socket?.remoteAddress, - req.ip, - ]; + // 智能IP檢測 - 根據實際環境動態調整優先級 + const ipDetectionStrategy = detectIpStrategy(req); + + let ipSources: (string | undefined)[]; + + if (ipDetectionStrategy.isCloudflare) { + // Cloudflare 環境的優先級 - 根據你的實際環境優化 + ipSources = [ + req.headers['cf-connecting-ip'], // 你的真實IP: 114.33.18.13 + req.headers['x-forwarded-for'], // 備用來源 (但第一個IP是代理IP,所以不優先) + req.headers['x-real-ip'], // 這是代理IP: 172.70.214.81,不應該使用 + req.headers['x-client-ip'], + req.headers['x-original-forwarded-for'], + req.headers['x-cluster-client-ip'], + req.headers['x-forwarded'], + req.headers['forwarded-for'], + req.headers['forwarded'], + req.headers['x-1panel-client-ip'], + req.headers['x-nginx-proxy-real-ip'], + req.connection?.remoteAddress, + req.socket?.remoteAddress, + req.ip, + ]; + } else if (ipDetectionStrategy.isProxy) { + // 代理環境的優先級 + ipSources = [ + req.headers['x-forwarded-for'], + req.headers['x-real-ip'], + req.headers['x-client-ip'], + req.headers['x-original-forwarded-for'], + req.headers['x-cluster-client-ip'], + req.headers['cf-connecting-ip'], // 即使不是Cloudflare也可能有這個頭 + req.headers['x-forwarded'], + req.headers['forwarded-for'], + req.headers['forwarded'], + req.headers['x-1panel-client-ip'], + req.headers['x-nginx-proxy-real-ip'], + req.connection?.remoteAddress, + req.socket?.remoteAddress, + req.ip, + ]; + } else { + // 直接連接的優先級 + ipSources = [ + req.headers['x-forwarded-for'], + req.headers['x-real-ip'], + req.headers['x-client-ip'], + req.headers['cf-connecting-ip'], + req.headers['x-original-forwarded-for'], + req.headers['x-cluster-client-ip'], + req.headers['x-forwarded'], + req.headers['forwarded-for'], + req.headers['forwarded'], + req.headers['x-1panel-client-ip'], + req.headers['x-nginx-proxy-real-ip'], + req.connection?.remoteAddress, + req.socket?.remoteAddress, + req.ip, + ]; + } // 收集所有找到的IP用於調試 const foundIps: string[] = []; @@ -119,8 +196,15 @@ export function getClientIp(req: any): string { const cleanIp = cleanIpAddress(ip); if (cleanIp) { foundIps.push(cleanIp); - // 檢查是否為公網IP,排除內部IP和1Panel代理IP - if (isPublicIp(cleanIp) && !isInternalProxyIp(cleanIp)) { + + // 特殊處理:如果是 cf-connecting-ip 頭部,直接返回(這是最可靠的) + if (ipSource === req.headers['cf-connecting-ip']) { + console.log('使用 cf-connecting-ip:', cleanIp, isValidIPv6(cleanIp) ? '(IPv6)' : '(IPv4)'); + return cleanIp; + } + + // 檢查是否為公網IP,排除內部IP和代理IP + if ((isPublicIp(cleanIp) || isValidIPv6(cleanIp)) && !isInternalProxyIp(cleanIp)) { return cleanIp; } } @@ -392,12 +476,19 @@ function isInternalProxyIp(ip: string): boolean { return proxyRanges.some(range => range.test(ip)); } -// 驗證IP地址格式 +// 驗證IPv4地址格式 export function isValidIp(ip: string): boolean { const ipRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; return ipRegex.test(ip); } +// 驗證IPv6地址格式 +export function isValidIPv6(ip: string): boolean { + // 簡化的IPv6驗證正則表達式 + const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^::1$|^::$|^([0-9a-fA-F]{1,4}:)*::([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4}$|^([0-9a-fA-F]{1,4}:)*::[0-9a-fA-F]{1,4}$|^([0-9a-fA-F]{1,4}:)+::$|^::$/; + return ipv6Regex.test(ip); +} + // 驗證CIDR格式 export function isValidCidr(cidr: string): boolean { const cidrRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/([0-9]|[1-2][0-9]|3[0-2])$/; diff --git a/middleware.ts b/middleware.ts index a3c5e81..9a606ea 100644 --- a/middleware.ts +++ b/middleware.ts @@ -10,16 +10,22 @@ export function middleware(request: NextRequest) { return NextResponse.next() } - // 獲取客戶端IP + // 獲取客戶端IP - 使用與API相同的邏輯 const clientIp = getClientIp(request) // 獲取允許的IP列表 const allowedIps = process.env.ALLOWED_IPS || '' + // 調試信息 + console.log(`[Middleware] IP檢測: ${clientIp}, 路徑: ${request.nextUrl.pathname}`) + console.log(`[Middleware] 白名單狀態: ${enableIpWhitelist}`) + console.log(`[Middleware] 允許的IP: ${allowedIps}`) + // 檢查IP是否被允許 if (!isIpAllowed(clientIp, allowedIps)) { // 記錄被拒絕的訪問 - console.warn(`Access denied for IP: ${clientIp} - Path: ${request.nextUrl.pathname}`) + console.warn(`[Middleware] Access denied for IP: ${clientIp} - Path: ${request.nextUrl.pathname}`) + console.warn(`[Middleware] 允許的IP列表: ${allowedIps}`) // 返回403禁止訪問頁面 return new NextResponse( @@ -112,12 +118,13 @@ export const config = { matcher: [ /* * 匹配所有路徑,除了: - * - api (API routes) + * - api/ip (IP檢測API,允許訪問) + * - api/ip-diagnostic (IP診斷API,允許訪問) * - _next/static (static files) * - _next/image (image optimization files) * - favicon.ico (favicon file) * - icon.png (icon file) */ - '/((?!api|_next/static|_next/image|favicon.ico|icon.png).*)', + '/((?!api/ip|api/ip-diagnostic|_next/static|_next/image|favicon.ico|icon.png).*)', ], } \ No newline at end of file