Node.js服务端如何读取Hello数据包生成JA3指纹?
JA3/JA3S NPM库推荐及Node.js实现指南
一、现成的NPM库
直接用这些库可以快速实现JA3/JA3S指纹生成,不用自己造轮子:
ja3:最常用的实现,支持从TLS连接提取并生成JA3/JA3S哈希,API简单,直接集成到Node.js服务端即可。tls-parser:通用TLS数据包解析库,能解析Client/Server Hello的所有字段,适合需要自定义指纹逻辑的场景,你可以基于它的解析结果拼接JA3字符串。node-ja3:轻量版JA3实现,专注于核心指纹生成,适合资源有限的轻量服务端。
二、自行开发的Node.js实现思路
完全不需要依赖Python库,Node.js原生tls模块就能搞定,核心是在TLS握手阶段拦截并解析Client Hello数据包:
1. 拦截Client Hello数据包
在TLS服务器建立连接的初期,还没完成握手前,捕获原始的网络数据。可以通过监听connection事件(此时是原始TCP连接)来获取数据,判断是否为Client Hello包后再交给TLS层处理:
const tls = require('tls'); const fs = require('fs'); const crypto = require('crypto'); const server = tls.createServer({ key: fs.readFileSync('server-key.pem'), cert: fs.readFileSync('server-cert.pem') }, (socket) => { socket.end('Hello!\n'); }); // 拦截原始TCP连接,捕获Client Hello server.on('connection', (rawSocket) => { let buffer = Buffer.alloc(0); const onData = (data) => { buffer = Buffer.concat([buffer, data]); // 检查是否是Client Hello:TLS记录类型22(握手),握手消息类型1(Client Hello) if (buffer.length >= 5 && buffer[0] === 22 && buffer[5] === 1) { // 解析Client Hello并生成JA3哈希 const ja3Components = parseClientHello(buffer); const ja3String = `${ja3Components.version},${ja3Components.cipherSuites.join('-')},${ja3Components.extensions.join('-')},${ja3Components.ecCurves.join('-')},${ja3Components.ecPointFormats.join('-')}`; const ja3Hash = crypto.createHash('md5').update(ja3String).digest('hex'); console.log('JA3 Hash:', ja3Hash); // 停止监听数据,把连接交给TLS层继续握手 rawSocket.removeListener('data', onData); server._connections[rawSocket._handle.fd] = rawSocket; server._tlsWrap.handshake(rawSocket); } }; rawSocket.on('data', onData); }); server.listen(443, () => console.log('TLS server running on port 443'));
2. 解析Client Hello的核心逻辑
按照TLS协议结构解析缓冲区,提取JA3需要的字段:
function parseClientHello(buffer) { // 跳过TLS记录头(5字节)和握手消息头(4字节),直接到Client Hello内容 let offset = 9; // 读取TLS版本(2字节,转十进制) const version = buffer.readUInt16BE(offset); offset += 2; // 跳过随机数(32字节)和会话ID(1字节长度 + 对应长度内容) offset += 32; const sessionIdLen = buffer.readUInt8(offset); offset += 1 + sessionIdLen; // 读取密码套件(2字节长度,然后每个套件2字节) const cipherSuiteLen = buffer.readUInt16BE(offset); offset += 2; const cipherSuites = []; for (let i = 0; i < cipherSuiteLen; i += 2) { cipherSuites.push(buffer.readUInt16BE(offset + i).toString()); } offset += cipherSuiteLen; // 跳过压缩方法(1字节长度 + 对应长度内容) const compressMethodLen = buffer.readUInt8(offset); offset += 1 + compressMethodLen; // 读取扩展列表(2字节长度,然后每个扩展:2字节类型 + 2字节长度 + 内容) const extensionsLen = buffer.readUInt16BE(offset); offset += 2; const extensions = []; const ecCurves = []; const ecPointFormats = []; let extOffset = offset; while (extOffset < offset + extensionsLen) { const extType = buffer.readUInt16BE(extOffset); extensions.push(extType.toString()); const extDataLen = buffer.readUInt16BE(extOffset + 2); extOffset += 4; // 提取椭圆曲线扩展(类型10) if (extType === 10) { const ecLen = buffer.readUInt16BE(extOffset); extOffset += 2; for (let i = 0; i < ecLen; i += 2) { ecCurves.push(buffer.readUInt16BE(extOffset + i).toString()); } } // 提取椭圆曲线点格式扩展(类型11) else if (extType === 11) { const ecFormatLen = buffer.readUInt8(extOffset); extOffset += 1; for (let i = 0; i < ecFormatLen; i++) { ecPointFormats.push(buffer.readUInt8(extOffset + i).toString()); } } else { extOffset += extDataLen; } } return { version: version.toString(), cipherSuites, extensions, ecCurves, ecPointFormats }; }
3. 生成JA3哈希
把解析得到的字段按规则拼接成字符串,再做MD5哈希就是最终的JA3指纹。JA3S的实现逻辑类似,只是解析Server Hello数据包即可。
三、关键注意事项
- 处理数据包时要注意粘包问题,可以通过记录头的长度字段判断是否捕获到完整的Client Hello包。
- 拦截数据后必须把连接交还给TLS层,否则握手会失败。
- 不同TLS版本的Client Hello结构略有差异,需要兼容TLS 1.0/1.1/1.2/1.3(JA3对TLS 1.3的支持需要调整字段提取逻辑)。
内容的提问来源于stack exchange,提问作者charnould
相关产品推荐
相关产品推荐

