diff --git a/.gitignore b/.gitignore index 4ee23b9..b8de4e3 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,4 @@ plan.md local-script 版本发布.md .vscode -.windsurfrules \ No newline at end of file +plan \ No newline at end of file diff --git a/server/app/db.js b/server/app/db.js index 085dbe6..9285c6a 100644 --- a/server/app/db.js +++ b/server/app/db.js @@ -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) } diff --git a/server/app/middlewares/ipFilter.js b/server/app/middlewares/ipFilter.js index a4efe69..448b49c 100644 --- a/server/app/middlewares/ipFilter.js +++ b/server/app/middlewares/ipFilter.js @@ -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 } diff --git a/server/app/middlewares/useLog.js b/server/app/middlewares/useLog.js index b81ed3e..393283c 100644 --- a/server/app/middlewares/useLog.js +++ b/server/app/middlewares/useLog.js @@ -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() // 先让后续中间件 / 路由处理 diff --git a/server/app/server.js b/server/app/server.js index 08bcfe7..13e2acf 100644 --- a/server/app/server.js +++ b/server/app/server.js @@ -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 -} \ No newline at end of file +} diff --git a/server/app/utils/tools.js b/server/app/utils/tools.js index 602f1a8..02f2ada 100644 --- a/server/app/utils/tools.js +++ b/server/app/utils/tools.js @@ -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 -} \ No newline at end of file +} diff --git a/server/app/utils/verify-auth.js b/server/app/utils/verify-auth.js index 12f986d..7a246a3 100644 --- a/server/app/utils/verify-auth.js +++ b/server/app/utils/verify-auth.js @@ -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 -} \ No newline at end of file +}