A SOCKS5 server writes four bytes separately, and the client works only after those writes are combined. Combining them can change timing, but TCP still does not promise one read per write.

The client needs a reply parser. After method selection and any required authentication, a SOCKS5 command reply begins with four bytes: version, result, reserved byte and address type. The remaining length depends on that address type.

A .NET 7+ parser that only needs to consume a successful CONNECT reply can use:

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);
}

The final two bytes are the bound port. An IPv4 address needs four bytes; IPv6 needs sixteen; a domain address carries a length byte before its name.

Supply a cancellation token with the intended negotiation deadline. ReadExactlyAsync handles shorter individual reads and throws when the stream ends before the requested length. On failure, discard the failed connection through its owner's cleanup path.

On success, stop at the reply boundary. Bytes after it belong to the destination protocol. A larger speculative read must preserve surplus bytes instead of throwing away the beginning of the application stream.

Ũhoro ũhĩtĩte