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는 더 짧은 개별 읽기를 처리하고 요청한 길이 전에 스트림이 끝나면 예외를 던집니다. 실패하면 소유자의 정리 경로를 통해 실패한 연결을 버립니다.

성공하면 응답 경계에서 멈춥니다. 그 뒤의 바이트는 목적지 프로토콜에 속합니다. 더 큰 추측 읽기는 애플리케이션 스트림의 시작을 버리지 말고 남는 바이트를 보존해야 합니다.

관련 글