工具链(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/(含数据库口令)不进库,可用上述工具重新生成。
84 lines
3.4 KiB
JavaScript
84 lines
3.4 KiB
JavaScript
// 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 };
|