Files

90 lines
4.1 KiB
C#
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.
using System.Buffers.Binary;
using System.Net.Sockets;
namespace Ananta.SDK.Network;
public static class FrameCodec
{
// ⛔ 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";
// 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);
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)}");
}
}
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;
}
}