.NET 7+의 SOCKS5 응답: 부분 TCP 읽기 처리
서버의 TCP 쓰기 경계에 의존하지 않고 SOCKS5 명령 응답의 고정 접두사와 ATYP 의존 접미사를 읽습니다.
SOCKS5 서버는 4바이트를 따로 쓰고, 클라이언트는 이 쓰기들이 합쳐진 뒤에야 동작합니다. 합치면 타이밍이 달라질 수 있지만 TCP는 여전히 쓰기마다 한 번의 읽기를 약속하지 않습니다.
클라이언트에는 응답 파서가 필요합니다. 메서드 선택과 필요한 인증 뒤에 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);
}
마지막 두 바이트는 바인딩된 포트입니다. IPv4는 4바이트, IPv6는 16바이트가 필요하며, 도메인 주소는 이름 앞에 길이 바이트를 가집니다.
의도한 협상 기한과 함께 취소 토큰을 넘깁니다. ReadExactlyAsync는 더 짧은 개별 읽기를 처리하고 요청한 길이 전에 스트림이 끝나면 예외를 던집니다. 실패하면 소유자의 정리 경로를 통해 실패한 연결을 버립니다.
성공하면 응답 경계에서 멈춥니다. 그 뒤의 바이트는 목적지 프로토콜에 속합니다. 더 큰 추측 읽기는 애플리케이션 스트림의 시작을 버리지 말고 남는 바이트를 보존해야 합니다.