fix(relay): 按首帧分流上游 —— Hash Request 走 c2(大厅),EnterSpace 走 c1(二次进空间)

- 取证:短命连接是「复用首个会话哈希、跳过 Hash Request 直接 EnterSpace」,被服务器 code=1000 关闭(约 1.2 秒);完整握手的连接可稳定存活(2 分半)

- wss_server:上游改为「首帧到达后再建」——StreamDecoder 记录 FirstCmdId,首帧 ID=3 走备用上游、其余走主上游;首帧仍走 Pending 原序补发

- 新增 --upstream-alt / -UpstreamAlt(默认 wss://c1.3dtank.com:9090/)
This commit is contained in:
TermiNexus committed 2026-10-15 16:16:16 +08:00
1 parent 529868b194
commit 941dde0913
2 files changed
+44 -22

No files matched your search

+3 -2
View File
@@ -26,7 +26,8 @@
param(
[int]$MirrorPort = 8080,
[int]$RelayPort = 9091,
[string]$Upstream = 'wss://c2.3dtank.com:9090/', # 官方客户端实际连 c2(c1/c2 是两个独立服;要切回 c1 用 -Upstream wss://c1.3dtank.com:9090/)
[string]$Upstream = 'wss://c2.3dtank.com:9090/', # 主上游(大厅:首帧 = Hash Request)
[string]$UpstreamAlt = 'wss://c1.3dtank.com:9090/', # 备用上游(首帧直接 EnterSpace 的「二次进空间」连接)
[string]$Label = (Get-Date -Format 'yyyyMMdd-HHmmss'),
[switch]$FetchAssets, # 先跑 fetch_assets.js 镜像资源(首次或资源缺失时用)
[switch]$KillExisting, # 端口被占用时,先结束占用进程
@@ -135,7 +136,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, '--label', $Label, '--dump-every', "$DumpEvery")
$RelayArgs = @('wss_server.js', '--port', "$RelayPort", '--upstream', $Upstream, '--upstream-alt', $UpstreamAlt, '--label', $Label, '--dump-every', "$DumpEvery")
if (-not $NoLog) { $RelayArgs += @('--log', '1') }
if ($NoHex) { $RelayArgs += @('--no-hex', '1') }
if ($Quiet) { $RelayArgs += @('--quiet', '1') }
+41 -20
View File
@@ -40,7 +40,9 @@ const Arg = (Name, Def) => {
};
const PORT = Number(Arg('port', '9091'));
const UPSTREAM = Arg('upstream', 'wss://c2.3dtank.com:9090/'); // 官方客户端实际连 c2(c1/c2 是两个独立服;要切回来用 --upstream wss://c1.3dtank.com:9090/)
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/');
const LABEL = Arg('label', new Date().toISOString().replace(/[:.]/g, '-'));
const PACKET_DIR = path.resolve(Arg('dir', path.join(PROJECT, 'packets', LABEL)));
@@ -277,6 +279,7 @@ class StreamDecoder {
this.Key = null;
this.Hint = null; // 对向方向破解出的密钥
this.Window = [];
this.FirstCmdId = null; // 本方向解出的**第一条**命令 ID(用于按首帧选上游)
}
Feed(Buf) {
@@ -372,6 +375,7 @@ class StreamDecoder {
continue;
}
const Id = Body[At];
if (this.FirstCmdId === null) this.FirstCmdId = Id; // 首条命令 ID(供上游分流)
const Payload = Body.slice(At + 1, At + 41);
const Name = (CMD_NAME[this.Direction] && CMD_NAME[this.Direction][Id]) || '未知';
const Extra = this.Direction === 'c2s' && Id === 32 ? ' ' + StreamDecoder.DescribeClientLog(Body.slice(At + 1)) : '';
@@ -551,31 +555,48 @@ WsServer.on('connection', (Client, Req) => {
: undefined;
// 转发:客户端帧**原样直发**;上游未就绪时暂存,open 后按原序补发(不重放、不改字节)。
Upstream = new Ws(UPSTREAM, { headers: UpHeaders, protocols: UpProtocols, rejectUnauthorized: false });
Upstream.binaryType = 'nodebuffer';
Upstream.on('open', () => {
State.Log('上游已连接: ' + UPSTREAM + ',补发暂存 ' + Pending.length + ' 帧');
while (Pending.length) Upstream.send(Pending.shift());
});
Upstream.on('message', (Data) => {
const Buf = Buffer.from(Data);
SaveFrame(State, 's2c', Buf);
Decoders.s2c.Feed(Buf);
SyncKeys(Decoders);
if (Client.readyState === Ws.OPEN) Client.send(Buf);
});
Upstream.on('close', (Code) => {
State.Log('上游关闭 code=' + Code);
if (Client.readyState === Ws.OPEN) Client.close();
});
Upstream.on('error', (Err) => State.Log('上游错误: ' + Err.message));
// ★ 上游不再"连上就建":等客户端首帧到达、解出命令 ID 后再选上游(见下方 Client.on('message'))。
const StartUpstream = (Target) => {
State.Log('上游选择: ' + Target + (Target === UPSTREAM ? '(主:大厅连接)' : '(备:复用哈希的二次进空间)'));
const U = new Ws(Target, { headers: UpHeaders, protocols: UpProtocols, rejectUnauthorized: false });
U.binaryType = 'nodebuffer';
U.on('open', () => {
State.Log('上游已连接: ' + Target + ',补发暂存 ' + Pending.length + ' 帧');
while (Pending.length) U.send(Pending.shift());
});
U.on('message', (Data) => {
const Buf = Buffer.from(Data);
SaveFrame(State, 's2c', Buf);
Decoders.s2c.Feed(Buf);
SyncKeys(Decoders);
if (Client.readyState === Ws.OPEN) Client.send(Buf);
});
U.on('close', (Code) => {
State.Log('上游关闭 code=' + Code);
if (Client.readyState === Ws.OPEN) Client.close();
});
U.on('error', (Err) => State.Log('上游错误: ' + Err.message));
Upstream = U;
};
Client.on('message', (Data) => {
const Buf = Buffer.from(Data);
SaveFrame(State, 'c2s', Buf);
Decoders.c2s.Feed(Buf);
SyncKeys(Decoders);
if (Upstream && Upstream.readyState === Ws.OPEN) Upstream.send(Buf);
if (!Upstream) {
// 上游还没定:先暂存,并尝试用首帧的命令 ID 决定去主上游还是备用上游
Pending.push(Buf);
const Id = Decoders.c2s.FirstCmdId;
if (Id === null) {
State.Log('等待首帧命令 ID 以决定上游(暂存 ' + Buf.length + ' 字节)');
return;
}
// 1 = Hash Request(大厅)→ 主上游;3 = Auth/EnterSpace(复用会话哈希的二次进空间)→ 备用上游
StartUpstream(Id === 3 ? UPSTREAM_ALT : UPSTREAM);
return;
}
if (Upstream.readyState === Ws.OPEN) Upstream.send(Buf);
else {
State.Log('上游尚未就绪,暂存 ' + Buf.length + ' 字节待补发');
Pending.push(Buf);