2026-09-15 18:13:46 +07:00
|
|
|
|
using System.Buffers.Binary;
|
|
|
|
|
|
using System.Net.Sockets;
|
|
|
|
|
|
|
|
|
|
|
|
namespace Ananta.SDK.Network;
|
|
|
|
|
|
|
|
|
|
|
|
public static class FrameCodec
|
|
|
|
|
|
{
|
2026-10-29 02:44:24 +07:00
|
|
|
|
// ⛔ donor(kpect 参考实现)在**正常收包路径**上留的每帧调试输出,四份副本里逐行相同。
|
|
|
|
|
|
// 无条件执行时它占全部日志约 44%(实测一场 30 分钟 18715/42685 行),
|
|
|
|
|
|
// 且每个入站帧都要做一次 hex 格式化 + 一次 stderr 同步写(自动 flush ⇒ 每帧一个系统调用)。
|
|
|
|
|
|
// 2026-09-28 起默认关闭;排查 wire / 解密问题时设环境变量 DRMK_FRAME_DBG=1 再启动即可,不用重编译。
|
|
|
|
|
|
// ⛑ 下边「帧长非法」分支里那个同名 [FRAME-DBG] 是真出错才打的(且打完就 throw),**保留**。
|
|
|
|
|
|
// 它上面那句 rawHead 必须在 XOR 之前取(head 是就地解密的),所以不能延后到分支里 —— 留着。
|
|
|
|
|
|
private static readonly bool FrameDebugEnabled =
|
|
|
|
|
|
Environment.GetEnvironmentVariable("DRMK_FRAME_DBG") == "1";
|
|
|
|
|
|
|
2026-09-15 18:13:46 +07:00
|
|
|
|
// Public/plain overload retained for handshake/debug compatibility.
|
|
|
|
|
|
public static Task<Frame?> ReadFrameAsync(NetworkStream stream, CancellationToken token)
|
|
|
|
|
|
=> ReadFrameAsync(stream, null, token);
|
|
|
|
|
|
|
|
|
|
|
|
internal static async Task<Frame?> ReadFrameAsync(NetworkStream stream, UxChaCha8? cryptor, CancellationToken token)
|
|
|
|
|
|
{
|
|
|
|
|
|
// UXStreamSocket decrypts the first five bytes (length + mode) as one
|
|
|
|
|
|
// continuous stream, then decrypts the payload with the SAME cryptor state.
|
|
|
|
|
|
var head = new byte[5];
|
|
|
|
|
|
if (!await ReadExactAsync(stream, head, token)) return null;
|
|
|
|
|
|
var rawHead = Convert.ToHexString(head);
|
|
|
|
|
|
cryptor?.XorInPlace(head);
|
|
|
|
|
|
|
|
|
|
|
|
var payloadLength = BinaryPrimitives.ReadInt32LittleEndian(head.AsSpan(0, 4));
|
|
|
|
|
|
if (payloadLength < 0 || payloadLength > 8 * 1024 * 1024)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Raw bytes kept on purpose: a frame sent before the peer installed
|
|
|
|
|
|
// its cryptor arrives as plaintext and fails this check.
|
|
|
|
|
|
Console.Error.WriteLine($"[FRAME-DBG] rawHead={rawHead} decryptedLen={payloadLength} mode={head[4]}");
|
|
|
|
|
|
throw new InvalidDataException($"Bad frame payload length after decrypt: {payloadLength}");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var payload = new byte[payloadLength];
|
|
|
|
|
|
if (payloadLength != 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!await ReadExactAsync(stream, payload, token)) return null;
|
|
|
|
|
|
cryptor?.XorInPlace(payload);
|
2026-10-29 02:44:24 +07:00
|
|
|
|
if (FrameDebugEnabled)
|
|
|
|
|
|
{
|
|
|
|
|
|
var rawPayloadHead = Convert.ToHexString(payload.AsSpan(0, Math.Min(16, payload.Length)));
|
|
|
|
|
|
Console.Error.WriteLine($"[FRAME-DBG] ok len={payloadLength} mode={head[4]} rawPayloadHead={rawPayloadHead} decHead={Convert.ToHexString(head)}");
|
|
|
|
|
|
}
|
2026-09-15 18:13:46 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return new Frame(head[4], payload);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Public/plain overload retained for the plaintext handshake reply.
|
|
|
|
|
|
public static Task WriteFrameAsync(NetworkStream stream, byte mode, ReadOnlyMemory<byte> payload, CancellationToken token)
|
|
|
|
|
|
=> WriteFrameAsync(stream, mode, payload, null, token);
|
|
|
|
|
|
|
|
|
|
|
|
internal static async Task WriteFrameAsync(
|
|
|
|
|
|
NetworkStream stream,
|
|
|
|
|
|
byte mode,
|
|
|
|
|
|
ReadOnlyMemory<byte> payload,
|
|
|
|
|
|
UxChaCha8? cryptor,
|
|
|
|
|
|
CancellationToken token)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Client Send() constructs [LE payloadLength][mode][payload] and, once
|
|
|
|
|
|
// CryptorWrite exists, encrypts that full range in one continuous call.
|
|
|
|
|
|
var frame = new byte[5 + payload.Length];
|
|
|
|
|
|
BinaryPrimitives.WriteInt32LittleEndian(frame.AsSpan(0, 4), payload.Length);
|
|
|
|
|
|
frame[4] = mode;
|
|
|
|
|
|
payload.Span.CopyTo(frame.AsSpan(5));
|
|
|
|
|
|
|
|
|
|
|
|
cryptor?.XorInPlace(frame);
|
|
|
|
|
|
|
|
|
|
|
|
await stream.WriteAsync(frame, token);
|
|
|
|
|
|
await stream.FlushAsync(token);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static async Task<bool> ReadExactAsync(NetworkStream stream, byte[] buffer, CancellationToken token)
|
|
|
|
|
|
{
|
|
|
|
|
|
var offset = 0;
|
|
|
|
|
|
while (offset < buffer.Length)
|
|
|
|
|
|
{
|
|
|
|
|
|
var read = await stream.ReadAsync(buffer.AsMemory(offset, buffer.Length - offset), token);
|
|
|
|
|
|
if (read == 0) return false;
|
|
|
|
|
|
offset += read;
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|