feat: 优化clientIp获取方式

This commit is contained in:
chaos-zhu
2026-04-26 18:06:40 +08:00
parent 3e2f60ea48
commit a81ac19f62
7 changed files with 56 additions and 15 deletions

2
.gitignore vendored
View File

@@ -16,4 +16,4 @@ plan.md
local-script
版本发布.md
.vscode
.windsurfrules
plan

View File

@@ -18,7 +18,8 @@ async function initKeyDB() {
const { _id, ipWhiteList = [] } = keyData
try {
let { ipWhiteList = [] } = await keyDB.findOneAsync({})
if (ipWhiteList.filter(ip => Boolean(ip)).length > 0) global.ALLOWED_IPS = ipWhiteList
const filteredList = ipWhiteList.filter(ip => typeof ip === 'string' && ip.trim() !== '')
if (filteredList.length > 0) global.ALLOWED_IPS = filteredList
} catch (error) {
logger.error('设置全局IP白名单失败:', error)
}

View File

@@ -1,13 +1,14 @@
// 白名单IP
const fs = require('fs')
const path = require('path')
const { isAllowedIp } = require('../utils/tools')
const { isAllowedIp, getClientIP } = require('../utils/tools')
const htmlPath = path.join(__dirname, '../template/ipForbidden.html')
const ipForbiddenHtml = fs.readFileSync(htmlPath, 'utf8')
const ipFilter = async (ctx, next) => {
if (isAllowedIp(ctx.request.ip)) return await next()
const requestIP = getClientIP(ctx.socket.remoteAddress, ctx.get('x-forwarded-for'))
if (isAllowedIp(requestIP)) return await next()
ctx.status = 403
ctx.body = ipForbiddenHtml
}

View File

@@ -1,5 +1,5 @@
// log4.js
const { DEBUG } = require('../config').logConfig
const { getClientIP } = require('../utils/tools')
// ------------------ 脱敏 ------------------
// 可能包含敏感信息的 header key小写比较
@@ -108,9 +108,10 @@ const useLog = () => {
query,
body,
headers,
ip
} = ctx.request
const ip = getClientIP(ctx.socket.remoteAddress, ctx.get('x-forwarded-for'))
const start = Date.now()
// 先让后续中间件 / 路由处理

View File

@@ -12,7 +12,7 @@ const wsDocker = require('./socket/docker')
const wsOnekey = require('./socket/onekey')
const wsServerStatus = require('./socket/server-status')
const wsFileTransfer = require('./socket/file-transfer')
const { throwError, isAllowedIp } = require('./utils/tools')
const { throwError, isAllowedIp, getClientIP } = require('./utils/tools')
const { SessionDB } = require('./utils/db-class')
const { parseCookies } = require('./utils/verify-auth')
const { generateSelfSignedCert } = require('./utils/ssl-cert')
@@ -89,8 +89,7 @@ const createServer = () => {
if (request.url.startsWith('/rdp-proxy')) {
try {
// 验证 IP 白名单
const requestIP = request.headers['x-forwarded-for']?.split(',')[0]?.trim() ||
request.socket.remoteAddress
const requestIP = getClientIP(request.socket.remoteAddress, request.headers['x-forwarded-for'])
if (!isAllowedIp(requestIP)) {
logger.warn(`RDP 连接被拒绝: IP ${ requestIP } 不在白名单中`)
socket.write('HTTP/1.1 403 Forbidden\r\n\r\n')
@@ -180,4 +179,4 @@ function serverHandler(app, server, httpsServer) {
module.exports = {
createServer
}
}

View File

@@ -271,6 +271,41 @@ const isProd = () => {
return EXEC_ENV === 'production'
}
// 将 IPv6 映射的 IPv4 地址(::ffff:x.x.x.x规范化为纯 IPv4保证比较一致性
const normalizeIP = (ip) => {
if (typeof ip !== 'string') return ''
ip = ip.trim().toLowerCase()
if (ip.startsWith('::ffff:')) {
const ipv4Part = ip.slice(7)
if (net.isIPv4(ipv4Part)) return ipv4Part
}
return ip
}
const isLoopbackIP = (ip) => {
ip = normalizeIP(ip)
if (ip === '::1') return true
if (!net.isIPv4(ip)) return false
const firstOctet = Number(ip.split('.')[0])
return firstOctet === 127
}
/**
* 从连接中提取可信客户端 IP
* - 优先使用 TCP 层真实地址socketRemoteAddress不可被客户端伪造
* - 仅当该地址为 loopback即经过本机可信反向代理才从 x-forwarded-for 取客户端 IP
* - nginx 使用 proxy_add_x_forwarded_for 时会将真实 IP 追加到末尾,故取最后一个值
* 以防御客户端在头部预置伪造 IP 的攻击XFF 首个值可伪造,末尾值由代理追加不可伪造)
*/
const getClientIP = (socketRemoteAddress, xForwardedFor) => {
const socketIP = normalizeIP(socketRemoteAddress || '')
if (!isLoopbackIP(socketIP)) return socketIP
if (!xForwardedFor) return socketIP
const parts = xForwardedFor.split(',')
const last = parts[parts.length - 1].trim()
return normalizeIP(last) || socketIP
}
const isAllowedIp = (requestIP) => {
let allowedIPs = Array.isArray(global.ALLOWED_IPS) ? global.ALLOWED_IPS : []
if (allowedIPs.length === 0) return true
@@ -378,6 +413,8 @@ module.exports = {
getLocalNetIP,
throwError,
isIP,
isLocalIP,
isLoopbackIP,
randomStr,
getUTCDate,
formatTimestamp,
@@ -385,8 +422,10 @@ module.exports = {
shellThrottle,
fileTransferThrottle,
isProd,
normalizeIP,
getClientIP,
isAllowedIp,
ping,
requestWithFailover,
timingSafeEqual
}
}

View File

@@ -1,6 +1,6 @@
const jwt = require('jsonwebtoken')
const { AESDecryptAsync } = require('./encrypt')
const { isAllowedIp } = require('../utils/tools')
const { isAllowedIp, getClientIP } = require('../utils/tools')
const { SHA256Encrypt } = require('../utils/encrypt')
const { KeyDB, SessionDB } = require('./db-class')
const keyDB = new KeyDB().getInstance()
@@ -57,8 +57,8 @@ const verifyAuthSync = async (token, session) => {
}
}
const verifyWsAuthSync = async (socket, next) => {
const requestIP = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address
const verifyWsAuthSync = async (socket, next) => {
const requestIP = getClientIP(socket.conn.remoteAddress, socket.handshake.headers['x-forwarded-for'])
// console.log('ws terminal requestIP:', requestIP)
// IP 白名单检查
if (!isAllowedIp(requestIP)) {
@@ -100,4 +100,4 @@ module.exports = {
verifyAuthSync,
verifyWsAuthSync,
parseCookies
}
}