Сервер SOCKS5 записывает четыре байта по отдельности, и клиент работает только после того, как эти записи объединены. Объединение может изменить тайминг, но TCP по-прежнему не обещает одно чтение на одну запись.

Клиенту нужен анализатор ответа. После выбора метода и любой требуемой аутентификации ответ команды SOCKS5 начинается с четырёх байтов: версия, результат, зарезервированный байт и тип адреса. Оставшаяся длина зависит от этого типа адреса.

Анализатор для .NET 7+, которому нужно обработать только успешный ответ CONNECT, может использовать:

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 обрабатывает более короткие отдельные чтения и выдаёт исключение, если поток заканчивается раньше запрошенной длины. При сбое отбросьте неудачное соединение через путь очистки его владельца.

При успехе остановитесь на границе ответа. Байты после неё принадлежат протоколу назначения. Более крупное упреждающее чтение должно сохранять лишние байты, а не выбрасывать начало потока приложения.

Похожие статьи