feat: 连接列表显示客户端真实 IP,支持反代和 CDN 场景

新增可配置的 real_ip_headers(默认 CF-Connecting-IP > X-Real-IP >
X-Forwarded-For 降级取值),trusted_proxies 用于 gin 代理信任链。
WebSocket 连接建立时捕获真实 IP 并在 /profile 和 /admin 连接列表展示,
登录会话 IP 同步改用真实 IP。
This commit is contained in:
2026-07-10 16:58:14 +08:00
parent a98cdb0cac
commit 8398067acb
13 changed files with 67 additions and 13 deletions
+1
View File
@@ -154,6 +154,7 @@ export default {
user: 'User',
ipv4: 'IPv4',
ipv6: 'IPv6',
realIp: 'Real IP',
connectTime: 'Connected At',
noOnlineClients: 'No online clients',
staticIpReservation: 'Static IP Reservation',
+1
View File
@@ -153,6 +153,7 @@ export default {
user: '用户',
ipv4: 'IPv4',
ipv6: 'IPv6',
realIp: '真实 IP',
connectTime: '连接时间',
noOnlineClients: '暂无在线客户端',
staticIpReservation: '静态 IP 预留',
+4 -1
View File
@@ -25,6 +25,7 @@ interface ClientInfo {
username: string
ip: string
ip6?: string
real_ip: string
connected_at: string
rx_bytes: number
tx_bytes: number
@@ -215,6 +216,7 @@ function handleStatClick(route: string) {
<thead>
<tr class="border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50">
<th class="px-6 py-3 text-left font-medium text-gray-500 dark:text-gray-400">{{ t('vpn.user') }}</th>
<th class="px-6 py-3 text-left font-medium text-gray-500 dark:text-gray-400">{{ t('vpn.realIp') }}</th>
<th class="px-6 py-3 text-left font-medium text-gray-500 dark:text-gray-400">{{ t('vpn.ipv4') }}</th>
<th class="px-6 py-3 text-left font-medium text-gray-500 dark:text-gray-400">{{ t('vpn.ipv6') }}</th>
<th class="px-6 py-3 text-left font-medium text-gray-500 dark:text-gray-400">{{ t('vpn.connectTime') }}</th>
@@ -225,10 +227,11 @@ function handleStatClick(route: string) {
</thead>
<tbody>
<tr v-if="!vpnClients.length">
<td colspan="7" class="px-6 py-6 text-center text-gray-400">{{ t('vpn.noOnlineClients') }}</td>
<td colspan="8" class="px-6 py-6 text-center text-gray-400">{{ t('vpn.noOnlineClients') }}</td>
</tr>
<tr v-for="(c, i) in vpnClients" :key="i" class="border-b border-gray-100 dark:border-gray-700/50">
<td class="px-6 py-3 text-gray-900 dark:text-white font-medium">{{ c.username }}</td>
<td class="px-6 py-3 text-gray-700 dark:text-gray-300">{{ c.real_ip || '-' }}</td>
<td class="px-6 py-3 text-gray-700 dark:text-gray-300">{{ c.ip }}</td>
<td class="px-6 py-3 text-gray-700 dark:text-gray-300">{{ c.ip6 || '-' }}</td>
<td class="px-6 py-3 text-gray-500 dark:text-gray-400">{{ c.connected_at }}</td>
+4 -1
View File
@@ -12,6 +12,7 @@ const { t } = useI18n()
interface VpnConnection {
ip: string
ip6?: string
real_ip: string
connected_at: string
}
const vpnConnections = ref<VpnConnection[]>([])
@@ -155,6 +156,7 @@ async function handleChangePassword() {
<table class="w-full text-sm">
<thead>
<tr class="border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50">
<th class="px-6 py-3 text-left font-medium text-gray-500 dark:text-gray-400">{{ t('vpn.realIp') }}</th>
<th class="px-6 py-3 text-left font-medium text-gray-500 dark:text-gray-400">{{ t('vpn.ipv4') }}</th>
<th class="px-6 py-3 text-left font-medium text-gray-500 dark:text-gray-400">{{ t('vpn.ipv6') }}</th>
<th class="px-6 py-3 text-left font-medium text-gray-500 dark:text-gray-400">{{ t('vpn.connectTime') }}</th>
@@ -162,9 +164,10 @@ async function handleChangePassword() {
</thead>
<tbody>
<tr v-if="!vpnConnections.length">
<td colspan="3" class="px-6 py-6 text-center text-gray-400">{{ t('profile.noConnections') }}</td>
<td colspan="4" class="px-6 py-6 text-center text-gray-400">{{ t('profile.noConnections') }}</td>
</tr>
<tr v-for="(c, i) in vpnConnections" :key="i" class="border-b border-gray-100 dark:border-gray-700/50">
<td class="px-6 py-3 text-gray-700 dark:text-gray-300">{{ c.real_ip || '-' }}</td>
<td class="px-6 py-3 text-gray-700 dark:text-gray-300">{{ c.ip }}</td>
<td class="px-6 py-3 text-gray-700 dark:text-gray-300">{{ c.ip6 || '-' }}</td>
<td class="px-6 py-3 text-gray-500 dark:text-gray-400">{{ c.connected_at }}</td>
+1
View File
@@ -23,6 +23,7 @@ interface ClientInfo {
username: string
ip: string
ip6?: string
real_ip: string
connected_at: string
}
interface Status {