feat(relay): 接管 _reconnectFast,改写战场服地址并按原 host 切换上游
This commit is contained in:
1 parent
42e1944cce
commit
2197f23517
2 files changed
+165
-7
No files matched your search
@@ -49,7 +49,8 @@ param(
|
||||
[int]$DumpEvery = 1, # 中继:每 N 帧写一个 .bin(默认 1 = 每帧都写;0 = 不写抓包文件)
|
||||
[switch]$NoLog, # 中继:不写 log.txt(默认写)
|
||||
[switch]$NoHex, # 中继:log.txt 不写十六进制(日志体积大幅减小)
|
||||
[switch]$Quiet # 中继:只写 log.txt,不刷控制台
|
||||
[switch]$Quiet, # 中继:只写 log.txt,不刷控制台
|
||||
[switch]$NoTunnel # 中继:关闭 _reconnectFast 隧道接管(默认开启:战场连接也走中继)
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
@@ -170,7 +171,7 @@ if (Test-Path $PackScript) {
|
||||
$RelayProc = $null
|
||||
if ($WithRelay) {
|
||||
Write-Host ("[中继] wss_server.js relay ws://localhost:{0}/ -> {1}" -f $RelayPort, $Upstream) -ForegroundColor Green
|
||||
$RelayArgs = @('wss_server.js', '--port', "$RelayPort", '--upstream', $Upstream, '--upstream-alt', $UpstreamAlt, '--label', $Label, '--dump-every', "$DumpEvery")
|
||||
$RelayArgs = @('wss_server.js', '--port', "$RelayPort", '--upstream', $Upstream, '--upstream-alt', $UpstreamAlt, '--label', $Label, '--dump-every', "$DumpEvery", '--tunnel-reconnect', $(if ($NoTunnel) { '0' } else { '1' }))
|
||||
if (-not $NoLog) { $RelayArgs += @('--log', '1') }
|
||||
if ($NoHex) { $RelayArgs += @('--no-hex', '1') }
|
||||
if ($Quiet) { $RelayArgs += @('--quiet', '1') }
|
||||
@@ -203,7 +204,8 @@ Write-Host '================= 已启动 =================' -ForegroundColor Yell
|
||||
Write-Host ("浏览器打开 : http://localhost:{0}/" -f $MirrorPort) -ForegroundColor White
|
||||
Write-Host ("工具链 : 默认已注入(纯净页 http://localhost:{0}/#nohornet);切勿加查询参数 ?xxx —— 会被游戏当成 config 参数,导致握手失败 1005" -f $MirrorPort) -ForegroundColor White
|
||||
if ($WithRelay) {
|
||||
Write-Host ("连接路径 : 页面 → ws://localhost:{0}/ (中继)→ {1}" -f $RelayPort, $Upstream) -ForegroundColor White
|
||||
Write-Host ("连接路径 : 页面 → ws://localhost:{0}/ (中继)→ {1}" -f $RelayPort, $Upstream) -ForegroundColor White
|
||||
Write-Host ("隧道接管 : {0}" -f ($(if ($NoTunnel) {'关闭(客户端直连战场服)'} else {'开启(_reconnectFast 改写为本地,战场连接继续走中继)'}))) -ForegroundColor White
|
||||
$DiskInfo = if ($DumpEvery -gt 0 -or -not $NoLog) { "dump-every=$DumpEvery log=$(if ($NoLog) {'off'} else {'on'}) hex=$(if ($NoHex) {'off'} else {'on'})" } else { '不落盘(只转发 + 实时解析)' }
|
||||
Write-Host ("抓包目录 : {0} (仅落盘时使用)" -f $PacketDir) -ForegroundColor White
|
||||
Write-Host ("落盘策略 : {0} (默认落盘保存;不落盘加 -DumpEvery 0 -NoLog)" -f $DiskInfo) -ForegroundColor White
|
||||
|
||||
+160
-4
@@ -29,7 +29,7 @@ const fs = require('fs');
|
||||
const http = require('http');
|
||||
const path = require('path');
|
||||
const zlib = require('zlib');
|
||||
const { LoadTables, MakeReader, FormatValue } = require('./lib_space_codec');
|
||||
const { LoadTables, MakeReader, ReadVarint, FormatValue } = require('./lib_space_codec');
|
||||
|
||||
const PROJECT = path.resolve(__dirname, '..');
|
||||
const ROOT = path.resolve(PROJECT, '..');
|
||||
@@ -43,6 +43,18 @@ const PORT = Number(Arg('port', '9091'));
|
||||
const UPSTREAM = Arg('upstream', 'wss://c2.3dtank.com:9090/'); // 主上游(大厅:首帧 = Hash Request)
|
||||
// 备用上游:首帧直接是 Auth/EnterSpace 的连接(复用会话哈希的"二次进空间",官方走的是另一台服务器)→ 默认 c1
|
||||
const UPSTREAM_ALT = Arg('upstream-alt', 'wss://c1.3dtank.com:9090/');
|
||||
// 隧道接管:服务端在进入战斗时会下发 _reconnectFast(RemoteEndpointData{host,ports,wsProto}),
|
||||
// 让客户端连到它指定的战场服(可能是另一台 c1/c2)。开启后中继会:
|
||||
// ① 把该包里的 host/port 改写成 localhost:<中继端口>,让客户端继续连回中继;
|
||||
// ② 记住原始 host:port,作为**下一条**客户端连接的实际上游。
|
||||
// 这样战场连接也能被抓包/解密。--tunnel-reconnect 0 可关闭(客户端将直连真实战场服)。
|
||||
const TUNNEL_RECONNECT = Arg('tunnel-reconnect', '1') !== '0';
|
||||
const RECONNECT_METHOD_ID = '4504535536312151081'; // _reconnectFast(见 names/downlink_table.json)
|
||||
const RECONNECT_ID_BUF = (() => {
|
||||
const B = Buffer.alloc(8);
|
||||
B.writeBigInt64BE(BigInt(RECONNECT_METHOD_ID));
|
||||
return B;
|
||||
})();
|
||||
const LABEL = Arg('label', new Date().toISOString().replace(/[:.]/g, '-'));
|
||||
const PACKET_DIR = path.resolve(Arg('dir', path.join(PROJECT, 'packets', LABEL)));
|
||||
|
||||
@@ -240,6 +252,131 @@ class Cipher {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// _reconnectFast 隧道接管(s2c 方向改包)
|
||||
// ============================================================
|
||||
// 下一条客户端连接要连的上游(由服务端下发的 RemoteEndpointData 决定)
|
||||
let TunnelUpstream = null;
|
||||
let TunnelUpstreamAt = 0;
|
||||
const TUNNEL_TTL_MS = 15000;
|
||||
|
||||
function WriteVarint(N) {
|
||||
const V = Number(N) | 0;
|
||||
if (V < 0x80) return Buffer.from([V & 0x7f]);
|
||||
if (V < 0x4000) return Buffer.from([0x80 | ((V >> 8) & 0x3f), V & 0xff]);
|
||||
return Buffer.from([0xc0 | ((V >> 16) & 0x3f), (V >> 8) & 0xff, V & 0xff]);
|
||||
}
|
||||
|
||||
// 按 PeekFrame 的逆过程重建帧头
|
||||
function BuildFrame(Body, Compressed) {
|
||||
const Size = Body.length;
|
||||
const C = Compressed ? 0x40 : 0;
|
||||
if (Size < 0x4000) {
|
||||
return Buffer.concat([Buffer.from([C | ((Size >> 8) & 0x3f), Size & 0xff]), Body]);
|
||||
}
|
||||
if (Size < 0x400000) {
|
||||
return Buffer.concat([Buffer.from([0x80 | C | ((Size >> 16) & 0x3f), (Size >> 8) & 0xff, Size & 0xff]), Body]);
|
||||
}
|
||||
const H = Buffer.alloc(4);
|
||||
H[0] = 0x80 | C | ((Size >> 24) & 0x3f);
|
||||
H[1] = (Size >> 16) & 0xff;
|
||||
H[2] = (Size >> 8) & 0xff;
|
||||
H[3] = Size & 0xff;
|
||||
return Buffer.concat([H, Body]);
|
||||
}
|
||||
|
||||
// 解析 RemoteEndpointData:host(string) / ports(list<i32>) / wsProto(string)
|
||||
function ParseEndpoint(Buf, Off) {
|
||||
const H = ReadVarint(Buf, Off);
|
||||
if (H.Bad) return null;
|
||||
const Host = Buf.slice(H.Next, H.Next + H.Value).toString('utf8');
|
||||
let At = H.Next + H.Value;
|
||||
const N = ReadVarint(Buf, At);
|
||||
if (N.Bad) return null;
|
||||
At = N.Next;
|
||||
const Ports = [];
|
||||
for (let I = 0; I < N.Value; I++) {
|
||||
const R = ReadVarint(Buf, At);
|
||||
if (R.Bad) return null;
|
||||
Ports.push(R.Value);
|
||||
At = R.Next;
|
||||
}
|
||||
const S = ReadVarint(Buf, At);
|
||||
if (S.Bad) return null;
|
||||
const Proto = Buf.slice(S.Next, S.Next + S.Value).toString('utf8');
|
||||
return { Host, Ports, Proto, Next: S.Next + S.Value };
|
||||
}
|
||||
|
||||
// 在一条 s2c 明文消息里找 _reconnectFast;命中则改写成 localhost:<中继端口> 并返回新明文
|
||||
function RewriteReconnect(State, Plain) {
|
||||
const Frames = ParseFrames(Plain);
|
||||
if (!Frames) return null;
|
||||
const Out = [];
|
||||
let Changed = false;
|
||||
for (const F of Frames) {
|
||||
let NewBody = null;
|
||||
if (!F.Compressed) {
|
||||
const Hit = F.Body.indexOf(RECONNECT_ID_BUF);
|
||||
if (Hit >= 0) {
|
||||
const Info = ParseEndpoint(F.Body, Hit + 8);
|
||||
if (Info && Info.Host && Info.Ports.length) {
|
||||
const Proto = String(Info.Proto || 'WSS').toUpperCase();
|
||||
const Scheme = Proto === 'WSS' ? 'wss' : 'ws';
|
||||
const Target = Scheme + '://' + Info.Host + ':' + Info.Ports[0] + '/';
|
||||
TunnelUpstream = Target;
|
||||
TunnelUpstreamAt = Date.now();
|
||||
State.Log('[_reconnectFast] 服务端指定战场服 ' + Target + ' → 已接管为下一条连接的上游');
|
||||
const Local = Buffer.from('localhost', 'utf8');
|
||||
const Ws = Buffer.from('WS', 'utf8');
|
||||
NewBody = Buffer.concat([
|
||||
F.Body.slice(0, Hit + 8), // nullmap + objectId + methodId 原样保留
|
||||
WriteVarint(Local.length), Local,
|
||||
WriteVarint(1), WriteVarint(PORT),
|
||||
WriteVarint(Ws.length), Ws,
|
||||
]);
|
||||
Changed = true;
|
||||
} else {
|
||||
State.Log('[_reconnectFast] 命中但字段解不动: ' + Hex(F.Body.slice(Hit, Hit + 48), 48));
|
||||
}
|
||||
}
|
||||
}
|
||||
Out.push(BuildFrame(NewBody || F.Body, F.Compressed ? true : false));
|
||||
}
|
||||
return Changed ? Buffer.concat(Out) : null;
|
||||
}
|
||||
|
||||
// s2c 解密 + 可选改写。返回 { Send, FollowUp }
|
||||
// 说明:改包会改变长度,本连接的密码流会与真实服务器错位;因此 FollowUp=true 时调用方
|
||||
// 需要在发完这一帧后主动关闭该连接(客户端本来也会因切服而断开)。
|
||||
function DecodeS2c(State, Decoder, Buf) {
|
||||
if (Decoder.Mode !== 'cipher' || !Decoder.Cipher) {
|
||||
// 明文段也可能是空间帧(少数地图/阶段),同样尝试接管
|
||||
const NewPlain = TUNNEL_RECONNECT ? RewriteReconnect(State, Buf) : null;
|
||||
if (NewPlain) {
|
||||
Decoder.Feed(NewPlain);
|
||||
return { Send: NewPlain, FollowUp: true };
|
||||
}
|
||||
Decoder.Feed(Buf);
|
||||
return { Send: Buf, FollowUp: false };
|
||||
}
|
||||
const Backup = new Cipher(Decoder.Key, Decoder.Encoded);
|
||||
Backup.Slot.set(Decoder.Cipher.Slot);
|
||||
Backup.Idx = Decoder.Cipher.Idx;
|
||||
const Plain = Decoder.Cipher.Apply(Buf);
|
||||
const NewPlain = TUNNEL_RECONNECT ? RewriteReconnect(State, Plain) : null;
|
||||
if (!NewPlain) {
|
||||
Decoder.Report(Plain, true);
|
||||
return { Send: Buf, FollowUp: false };
|
||||
}
|
||||
const Enc = new Cipher(Decoder.Key, Decoder.Encoded);
|
||||
Enc.Slot.set(Backup.Slot);
|
||||
Enc.Idx = Backup.Idx;
|
||||
const NewBuf = Enc.Apply(NewPlain);
|
||||
Decoder.Cipher = Enc;
|
||||
Decoder.Report(NewPlain, true);
|
||||
return { Send: NewBuf, FollowUp: true };
|
||||
}
|
||||
|
||||
function Hex(Buf, N) {
|
||||
return Buf.slice(0, N || 32).toString('hex');
|
||||
}
|
||||
@@ -570,9 +707,17 @@ WsServer.on('connection', (Client, Req) => {
|
||||
U.on('message', (Data) => {
|
||||
const Buf = Buffer.from(Data);
|
||||
SaveFrame(State, 's2c', Buf);
|
||||
Decoders.s2c.Feed(Buf);
|
||||
const Res = DecodeS2c(State, Decoders.s2c, Buf);
|
||||
SyncKeys(Decoders);
|
||||
if (Client.readyState === Ws.OPEN) Client.send(Buf);
|
||||
if (Client.readyState === Ws.OPEN) Client.send(Res.Send);
|
||||
if (Res.FollowUp) {
|
||||
// 改包后密码流已与服务器错位;客户端本来也会切服,这里主动收尾
|
||||
State.Log('[_reconnectFast] 已改写并接管,关闭当前连接等待客户端重连');
|
||||
setTimeout(() => {
|
||||
try { if (Client.readyState === Ws.OPEN) Client.close(); } catch (E) {}
|
||||
try { if (U.readyState === Ws.OPEN) U.close(); } catch (E) {}
|
||||
}, 200);
|
||||
}
|
||||
});
|
||||
U.on('close', (Code) => {
|
||||
State.Log('上游关闭 code=' + Code);
|
||||
@@ -581,7 +726,15 @@ WsServer.on('connection', (Client, Req) => {
|
||||
U.on('error', (Err) => State.Log('上游错误: ' + Err.message));
|
||||
Upstream = U;
|
||||
};
|
||||
StartUpstream(UPSTREAM);
|
||||
|
||||
// 若上一条连接收到 _reconnectFast,则本连接直接连它指定的战场服
|
||||
let InitialTarget = UPSTREAM;
|
||||
if (TUNNEL_RECONNECT && TunnelUpstream && Date.now() - TunnelUpstreamAt < TUNNEL_TTL_MS) {
|
||||
InitialTarget = TunnelUpstream;
|
||||
TunnelUpstream = null;
|
||||
State.Log('沿用 _reconnectFast 指定的上游: ' + InitialTarget);
|
||||
}
|
||||
StartUpstream(InitialTarget);
|
||||
|
||||
Client.on('message', (Data) => {
|
||||
const Buf = Buffer.from(Data);
|
||||
@@ -609,5 +762,8 @@ Server.listen(PORT, () => {
|
||||
console.log(' 抓包目录: ' + PACKET_DIR);
|
||||
console.log(' 落盘策略: ' + (NeedDisk ? ('dump-every=' + DUMP_EVERY + ' log=' + (WRITE_LOG ? 'on' : 'off')) : '不落盘(只转发+实时解析;要抓包加 --dump-every 1 --log 1)'));
|
||||
console.log(' 转发策略: 原样直发;上游未就绪时暂存竞态帧,一连上立即按原序补发');
|
||||
console.log(' 隧道接管: ' + (TUNNEL_RECONNECT
|
||||
? '_reconnectFast → 改写 host/port 为 localhost:' + PORT + ',并按原 host 切换上游'
|
||||
: '关闭(客户端会直连真实战场服)'));
|
||||
console.log(' 实时解析: 明文直解;加密段自动穷举密钥后实时解密;zlib 压缩帧自动解压');
|
||||
});
|
||||
Reference in new issue
Block a user