.NET 7+ 中的 SOCKS5 应答:处理部分 TCP 读取
在不依赖服务器 TCP 写入边界的前提下,读取 SOCKS5 命令应答的固定前缀和依赖 ATYP 的后缀。
SOCKS5 服务器会分多次写入四个字节,客户端只有等这些写入合并后才能工作。合并可能改变时序,但 TCP 仍然不承诺每次写入对应一次读取。
客户端需要一个应答解析器。完成方法选择和所需认证后,SOCKS5 命令应答以四个字节开始:版本、结果、保留字节和地址类型。其余长度取决于该地址类型。
只消费成功 CONNECT 应答的 .NET 7+ 解析器可以这样写:
static async Task ReadConnectReply(Stream stream, CancellationToken ct)
{
byte[] prefix = new byte[4];
await stream.ReadExactlyAsync(prefix, ct);
if (prefix[0] != 5 || prefix[2] != 0)
throw new IOException("Invalid SOCKS5 reply");
if (prefix[1] != 0)
throw new IOException($"SOCKS5 request failed: {prefix[1]}");
int remaining;
switch (prefix[3])
{
case 1: remaining = 4 + 2; break;
case 4: remaining = 16 + 2; break;
case 3:
byte[] length = new byte[1];
await stream.ReadExactlyAsync(length, ct);
remaining = length[0] + 2;
break;
default:
throw new IOException("Unknown SOCKS5 address type");
}
await stream.ReadExactlyAsync(new byte[remaining], ct);
}
最后两个字节是绑定端口。IPv4 需要四个字节,IPv6 需要十六个字节,域名地址在名称前带一个长度字节。
传入带有预期协商截止时间的取消令牌。ReadExactlyAsync 会处理较短的单个读取,并在流在请求长度之前结束时抛出异常。失败时,通过其所有者的清理路径丢弃失败的连接。
成功时,停在应答边界处。其后的字节属于目标协议。更大的投机性读取必须保留多余的字节,而不是丢掉应用流的开头。