SOCKS5 サーバーは 4 バイトを別々に書き込み、クライアントはそれらの書き込みが結合されて初めて動作します。結合すればタイミングは変わりえますが、TCP は依然として書き込みごとに 1 回の読み取りを約束しません。

クライアントには応答パーサーが必要です。メソッド選択と必要な認証の後、SOCKS5 コマンド応答は 4 バイトで始まります。バージョン、結果、予約バイト、アドレス種別です。残りの長さはそのアドレス種別に依存します。

成功した 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);
}

最後の 2 バイトはバインドされたポートです。IPv4 は 4 バイト、IPv6 は 16 バイトを必要とし、ドメインアドレスは名前の前に長さバイトを持ちます。

想定したネゴシエーション期限を伴うキャンセルトークンを渡します。ReadExactlyAsync は短い個別の読み取りを処理し、要求された長さの前にストリームが終了すると例外を投げます。失敗した場合は、所有者のクリーンアップ経路でその接続を破棄します。

成功したら応答の境界で停止します。その後のバイトは宛先プロトコルに属します。大きめの先読みを行う場合は、アプリケーションストリームの先頭を捨てるのではなく、余剰バイトを保持しなければなりません。

関連記事