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 会处理较短的单个读取,并在流在请求长度之前结束时抛出异常。失败时,通过其所有者的清理路径丢弃失败的连接。

成功时,停在应答边界处。其后的字节属于目标协议。更大的投机性读取必须保留多余的字节,而不是丢掉应用流的开头。

相关文章