Files
TermiNexus b6f2cb71ee feat(3dtank): 协议逆向工程与自研服务器入库(工具链 + 协议表 + 空间回放 + MySQL)
工具链(3DTank/tools/):extract(模块提取)、mine_evidence/apply_names(命名证据与别名回填)、extract_codec_fields(字段表 + 全库槽位类型 + PrimClassWire/ResourceClasses)、mine_downlink(下行字段表)、extract_space_init/rebase_space_init/audit_space_init(空间模板与记录切分审计)、extract_space_session(把真实会话切成按上行触发的回放模板)、pair_flow(请求→应答配对)、probe_containers、ingest/decrypt_frames(抓包入库与解密)、serve/fetch_assets(本地站点镜像)、import_db(抓包对象仓库导入 MySQL)。

服务器(3DTank/tools/server/):link/entry/codec/container/dispatch/world/space/semantic(协议层与 lt/pt/zt 三个容器布局)、session(会话回放,顺序推进)、db + schema.sql(MySQL 连接层与对象仓库表结构)、store/responder(查库组帧与定向应答;接管范围默认只开 ba.lcr)。

同时修 pair_flow 的时间轴偏移:timeline_c2s 与 timeline_s2c 的 0 点相差 144~4453ms,改前配对整体偏约 2 秒(实测 space-100073070 偏 2084ms),改后 22.4% 的触发者被纠正。

src/、src_named/、packets/、3DTank/local/(含数据库口令)不进库,可用上述工具重新生成。
2026-09-17 20:00:41 +08:00

84 lines
3.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// link.js - 链路层:帧边界 / nullmap / 明文协商
//
// 帧 = [2 或 4 字节大端长度][帧体]
// 长度首字节 bit7=1 → 4 字节头;bit6=1 → 帧体 zlib 压缩;低 6 位参与长度
// 帧体 = [nullmap][payload]
// payload = [1 字节入口命令 ID][命令体](entry.js)
// | [8 字节目标 id][空间命令记录…](dispatch.js)
// nullmap:长度 = NullmapSize()。本服务器发出的帧一律 1 字节 0x00(没有空字段);
// 读入按同一函数算长度,兼容抓包里的变长 nullmap。
// 加密:自变体 XOR(k[i] = K ^ (i<<3),上行再 ^87)。本服务器在 Hash Response 里把
// 「是否启用链路加密」置 0,整条链路走明文;解密只在 relay 模式做(见 ../wss_server.js)。
'use strict';
const zlib = require('zlib');
// 一帧的头部信息 → { TotalSize, HeaderSize, Compressed };字节不够返回 null
function PeekFrame(Buf, Offset) {
const Off = Offset || 0;
if (Buf.length - Off < 2) return null;
const B0 = Buf[Off];
if (B0 & 0x80) {
if (Buf.length - Off < 4) return null;
const Size = ((((B0 & 0x3f) << 24) | (Buf[Off + 1] << 16) | (Buf[Off + 2] << 8) | Buf[Off + 3]) >>> 0) + 4;
return { TotalSize: Size, HeaderSize: 4, Compressed: (B0 & 0x40) !== 0 };
}
return { TotalSize: 2 + (((B0 & 0x3f) << 8) | Buf[Off + 1]), HeaderSize: 2, Compressed: (B0 & 0x40) !== 0 };
}
// 把一串字节拆成整帧;有任何残余(说明不是整帧流)返回 null
function ParseFrames(Buf) {
const Frames = [];
let Off = 0;
while (Off < Buf.length) {
const Info = PeekFrame(Buf, Off);
if (!Info || Off + Info.TotalSize > Buf.length) return null;
Frames.push({ Body: Buf.slice(Off + Info.HeaderSize, Off + Info.TotalSize), Compressed: Info.Compressed });
Off += Info.TotalSize;
}
return Frames.length ? Frames : null;
}
// nullmap 长度(对应客户端的 x7g)
// 长格式实测:`c0 <2 字节长度>` ⇒ 总长 = 3 + 长度(**不是 4 +**)。
// 这一字节之差会让「大 nullmap」的帧整体偏 1 字节,进而所有后续解析全错——
// 车库那几帧(16771B / 137119B)的 nullmap 分别是 800B / 269B,正是踩在这个坑上。
function NullmapSize(Buf, Offset) {
const R = Buf[Offset];
if (R === undefined) return 0;
if (R & 0x80) {
if (R & 0x40) return 3 + (((R & 0x3f) << 16) | (Buf[Offset + 1] << 8) | Buf[Offset + 2]);
return 1 + (R & 0x3f);
}
return 1 + ((R & 0x60) >>> 5);
}
function DecodeFrame(Frame) {
return Frame.Compressed ? zlib.inflateSync(Frame.Body) : Frame.Body;
}
// 组帧:Length < 0x4000 用 2 字节头,否则 4 字节头(bit7)
function EncodeFrame(Payload, Nullmap) {
const Nm = Nullmap || Buffer.from([0x00]);
const Length = Nm.length + Payload.length;
if (Length < 0x4000) {
return Buffer.concat([Buffer.from([(Length >> 8) & 0xff, Length & 0xff]), Nm, Payload]);
}
return Buffer.concat([
Buffer.from([((Length >>> 24) & 0x3f) | 0x80, (Length >>> 16) & 0xff, (Length >>> 8) & 0xff, Length & 0xff]),
Nm,
Payload,
]);
}
// 保活帧 = 负载为空的帧(实测 3 字节:00 01 00)
function PingFrame() {
return EncodeFrame(Buffer.alloc(0));
}
// 帧体只有 nullmap、没有 payload
function IsPingFrame(Frame) {
return Frame.Body.length > 0 && NullmapSize(Frame.Body, 0) >= Frame.Body.length;
}
module.exports = { PeekFrame, ParseFrames, NullmapSize, DecodeFrame, EncodeFrame, PingFrame, IsPingFrame };