.NET 7+ の SOCKS5 応答:部分的な TCP 読み取りの扱い
サーバーの TCP 書き込み境界に依存せず、SOCKS5 コマンド応答の固定プレフィックスと ATYP 依存のサフィックスを読み取ります。
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 は短い個別の読み取りを処理し、要求された長さの前にストリームが終了すると例外を投げます。失敗した場合は、所有者のクリーンアップ経路でその接続を破棄します。
成功したら応答の境界で停止します。その後のバイトは宛先プロトコルに属します。大きめの先読みを行う場合は、アプリケーションストリームの先頭を捨てるのではなく、余剰バイトを保持しなければなりません。