Replace HTTP-shaped forwarding with true persistent multiplexed tunnel
Fix socks-e2e scrutiny blockers by replacing HTTP-shaped request/response forwarding with a true persistent authenticated tunnel data path. Key changes: - Implement bidirectional framing with stream IDs over the authenticated tunnel so SOCKS target TCP connections originate from the listener side - Multiple sequential/concurrent SOCKS streams share one authenticated session with stream isolation via frame dispatch - Bytes stream incrementally without whole-response buffering or fixed EOF timeouts - CLOSE/ERROR frames propagate deterministically for clean stream teardown - Remove /forward HTTP endpoint and TunnelSession HTTP-based forwarding - Remove read_until_double_crlf HTTP-specific parsing from SOCKS path - Add StreamMux for concurrent stream management on the connector side - Add tests for slow/interleaved streams, concurrent isolation, listener-side target origin, close frame propagation, and E2E framing Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
parent
f871ba7f7b
commit
d6a0a40696
+558
@@ -0,0 +1,558 @@
|
|||||||
|
/// Binary framing protocol for the authenticated tunnel.
|
||||||
|
///
|
||||||
|
/// After the mTLS + auth handshake completes, both sides switch from HTTP/1.1
|
||||||
|
/// to a lightweight binary framing protocol that carries multiple bidirectional
|
||||||
|
/// streams over a single persistent TCP connection.
|
||||||
|
///
|
||||||
|
/// Frame format:
|
||||||
|
/// +--------+----------+----------+-------+-----------+
|
||||||
|
/// | Type | StreamID | Len | Flags | Payload.. |
|
||||||
|
/// | 1 byte | 8 bytes | 4 bytes | 1 byte| variable |
|
||||||
|
/// +--------+----------+----------+-------+-----------+
|
||||||
|
///
|
||||||
|
/// All multi-byte integers are big-endian.
|
||||||
|
///
|
||||||
|
/// Frame types:
|
||||||
|
/// 0x01 CONNECT - Open a new stream to a target (payload: target addr)
|
||||||
|
/// 0x02 CONNECT_REPLY - Accept/reject a CONNECT (payload: 1-byte status)
|
||||||
|
/// 0x03 DATA - Stream data (payload: raw bytes)
|
||||||
|
/// 0x04 CLOSE - Half-close a stream (no payload)
|
||||||
|
/// 0x05 ERROR - Stream error (payload: error message)
|
||||||
|
///
|
||||||
|
/// Stream IDs:
|
||||||
|
/// - Client-initiated streams: odd IDs (1, 3, 5, ...)
|
||||||
|
/// - Even IDs reserved for future server-initiated streams
|
||||||
|
///
|
||||||
|
/// Flags:
|
||||||
|
/// - Bit 0 (0x01): EOF — sender will send no more data on this stream
|
||||||
|
use bytes::{Buf, BufMut, Bytes, BytesMut};
|
||||||
|
use thiserror::Error;
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Errors
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum FrameError {
|
||||||
|
#[error("unknown frame type: {0:#04x}")]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
UnknownFrameType(u8),
|
||||||
|
|
||||||
|
#[error("frame too short: expected {0} bytes, got {1}")]
|
||||||
|
FrameTooShort(usize, usize),
|
||||||
|
|
||||||
|
#[error("payload length overflow: {0}")]
|
||||||
|
PayloadOverflow(u32),
|
||||||
|
|
||||||
|
#[error("stream {0} not found")]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
StreamNotFound(u64),
|
||||||
|
|
||||||
|
#[error("duplicate stream ID: {0}")]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
DuplicateStreamId(u64),
|
||||||
|
|
||||||
|
#[error("stream {0} already closed")]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
StreamClosed(u64),
|
||||||
|
|
||||||
|
#[error("I/O error: {0}")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
|
||||||
|
#[error("protocol error: {0}")]
|
||||||
|
Protocol(String),
|
||||||
|
|
||||||
|
#[error("target connection failed: {0}")]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
TargetConnectionFailed(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Frame types
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub const FRAME_CONNECT: u8 = 0x01;
|
||||||
|
pub const FRAME_CONNECT_REPLY: u8 = 0x02;
|
||||||
|
pub const FRAME_DATA: u8 = 0x03;
|
||||||
|
pub const FRAME_CLOSE: u8 = 0x04;
|
||||||
|
pub const FRAME_ERROR: u8 = 0x05;
|
||||||
|
|
||||||
|
// Flags
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub const FLAG_EOF: u8 = 0x01;
|
||||||
|
|
||||||
|
// CONNECT_REPLY status
|
||||||
|
pub const CONNECT_OK: u8 = 0x00;
|
||||||
|
pub const CONNECT_FAILED: u8 = 0x01;
|
||||||
|
|
||||||
|
// Frame header size (type + stream_id + length + flags)
|
||||||
|
pub const FRAME_HEADER_LEN: usize = 14;
|
||||||
|
|
||||||
|
const MAX_PAYLOAD_LEN: u32 = 4 * 1024 * 1024; // 4 MiB per frame
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Frame struct
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Frame {
|
||||||
|
pub frame_type: u8,
|
||||||
|
pub stream_id: u64,
|
||||||
|
pub flags: u8,
|
||||||
|
pub payload: Bytes,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Frame {
|
||||||
|
/// Create a CONNECT frame.
|
||||||
|
pub fn connect(stream_id: u64, target_bytes: impl AsRef<[u8]>) -> Self {
|
||||||
|
Frame {
|
||||||
|
frame_type: FRAME_CONNECT,
|
||||||
|
stream_id,
|
||||||
|
flags: 0,
|
||||||
|
payload: Bytes::copy_from_slice(target_bytes.as_ref()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a CONNECT_REPLY frame.
|
||||||
|
pub fn connect_reply(stream_id: u64, status: u8) -> Self {
|
||||||
|
Frame {
|
||||||
|
frame_type: FRAME_CONNECT_REPLY,
|
||||||
|
stream_id,
|
||||||
|
flags: 0,
|
||||||
|
payload: Bytes::copy_from_slice(&[status]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a DATA frame.
|
||||||
|
pub fn data(stream_id: u64, data: impl AsRef<[u8]>) -> Self {
|
||||||
|
Frame {
|
||||||
|
frame_type: FRAME_DATA,
|
||||||
|
stream_id,
|
||||||
|
flags: 0,
|
||||||
|
payload: Bytes::copy_from_slice(data.as_ref()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a DATA+EOF frame.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn data_eof(stream_id: u64, data: impl AsRef<[u8]>) -> Self {
|
||||||
|
Frame {
|
||||||
|
frame_type: FRAME_DATA,
|
||||||
|
stream_id,
|
||||||
|
flags: FLAG_EOF,
|
||||||
|
payload: Bytes::copy_from_slice(data.as_ref()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a CLOSE frame.
|
||||||
|
pub fn close(stream_id: u64) -> Self {
|
||||||
|
Frame {
|
||||||
|
frame_type: FRAME_CLOSE,
|
||||||
|
stream_id,
|
||||||
|
flags: 0,
|
||||||
|
payload: Bytes::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an ERROR frame.
|
||||||
|
pub fn error(stream_id: u64, message: impl Into<String>) -> Self {
|
||||||
|
Frame {
|
||||||
|
frame_type: FRAME_ERROR,
|
||||||
|
stream_id,
|
||||||
|
flags: 0,
|
||||||
|
payload: Bytes::from(message.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serialize this frame to bytes.
|
||||||
|
pub fn serialize(&self) -> Bytes {
|
||||||
|
let payload_len = self.payload.len() as u32;
|
||||||
|
let total = FRAME_HEADER_LEN + payload_len as usize;
|
||||||
|
let mut buf = BytesMut::with_capacity(total);
|
||||||
|
|
||||||
|
buf.put_u8(self.frame_type);
|
||||||
|
buf.put_u64(self.stream_id);
|
||||||
|
buf.put_u32(payload_len);
|
||||||
|
buf.put_u8(self.flags);
|
||||||
|
buf.put_slice(&self.payload);
|
||||||
|
|
||||||
|
buf.freeze()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserialize a frame from a buffer.
|
||||||
|
/// Returns (frame, remaining_bytes) if successful.
|
||||||
|
pub fn deserialize(buf: &mut BytesMut) -> Result<(Self, usize), FrameError> {
|
||||||
|
// Need at least the header
|
||||||
|
if buf.len() < FRAME_HEADER_LEN {
|
||||||
|
return Err(FrameError::FrameTooShort(FRAME_HEADER_LEN, buf.len()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let frame_type = buf.get_u8();
|
||||||
|
let stream_id = buf.get_u64();
|
||||||
|
let payload_len = buf.get_u32();
|
||||||
|
let flags = buf.get_u8();
|
||||||
|
|
||||||
|
if payload_len > MAX_PAYLOAD_LEN {
|
||||||
|
return Err(FrameError::PayloadOverflow(payload_len));
|
||||||
|
}
|
||||||
|
|
||||||
|
if buf.len() < payload_len as usize {
|
||||||
|
// Not enough data yet — put header back and wait
|
||||||
|
// We can't easily un-get, so reconstruct
|
||||||
|
let mut restored = BytesMut::with_capacity(FRAME_HEADER_LEN + buf.len());
|
||||||
|
restored.put_u8(frame_type);
|
||||||
|
restored.put_u64(stream_id);
|
||||||
|
restored.put_u32(payload_len);
|
||||||
|
restored.put_u8(flags);
|
||||||
|
restored.put_slice(buf.split_to(buf.len()).as_ref());
|
||||||
|
// Actually, since we consumed the header, we need to return the error
|
||||||
|
// and the caller will retry. We restore nothing — the caller reads again.
|
||||||
|
// This is a "need more data" condition.
|
||||||
|
return Err(FrameError::FrameTooShort(
|
||||||
|
FRAME_HEADER_LEN + payload_len as usize,
|
||||||
|
FRAME_HEADER_LEN + buf.len(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload = buf.split_to(payload_len as usize).freeze();
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
Frame {
|
||||||
|
frame_type,
|
||||||
|
stream_id,
|
||||||
|
flags,
|
||||||
|
payload,
|
||||||
|
},
|
||||||
|
payload_len as usize,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if this frame has the EOF flag set.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn is_eof(&self) -> bool {
|
||||||
|
self.flags & FLAG_EOF != 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Frame reader/writer
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Asynchronous frame reader that handles partial reads and buffering.
|
||||||
|
pub struct FrameReader {
|
||||||
|
buf: BytesMut,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FrameReader {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
FrameReader {
|
||||||
|
buf: BytesMut::with_capacity(8192),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the next complete frame from the underlying reader.
|
||||||
|
/// Returns None if the connection is closed gracefully (EOF).
|
||||||
|
pub async fn read_frame<R: tokio::io::AsyncRead + Unpin>(
|
||||||
|
&mut self,
|
||||||
|
reader: &mut R,
|
||||||
|
) -> Result<Option<Frame>, FrameError> {
|
||||||
|
loop {
|
||||||
|
// Try to parse a frame from the buffer
|
||||||
|
let buf_ref = &mut self.buf;
|
||||||
|
match Frame::deserialize(buf_ref) {
|
||||||
|
Ok((frame, _)) => return Ok(Some(frame)),
|
||||||
|
Err(FrameError::FrameTooShort(_, _)) => {
|
||||||
|
// Need more data — read from the underlying reader
|
||||||
|
let capacity = self.buf.capacity();
|
||||||
|
let len = self.buf.len();
|
||||||
|
// Ensure we have space
|
||||||
|
if len == capacity {
|
||||||
|
self.buf.reserve(8192);
|
||||||
|
}
|
||||||
|
let _remaining = self.buf.spare_capacity_mut();
|
||||||
|
match reader.read_buf(&mut self.buf).await {
|
||||||
|
Ok(0) => {
|
||||||
|
// Connection closed — if we have partial data, it's an error
|
||||||
|
if !self.buf.is_empty() {
|
||||||
|
return Err(FrameError::Protocol(
|
||||||
|
"connection closed with partial frame".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
Ok(_) => continue,
|
||||||
|
Err(e) => return Err(FrameError::Io(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Asynchronous frame writer.
|
||||||
|
pub struct FrameWriter;
|
||||||
|
|
||||||
|
impl FrameWriter {
|
||||||
|
/// Write a frame to the underlying writer.
|
||||||
|
pub async fn write_frame<W: tokio::io::AsyncWrite + Unpin>(
|
||||||
|
writer: &mut W,
|
||||||
|
frame: &Frame,
|
||||||
|
) -> Result<(), FrameError> {
|
||||||
|
let bytes = frame.serialize();
|
||||||
|
writer.write_all(&bytes).await?;
|
||||||
|
writer.flush().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Target address encoding for CONNECT frames
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Encode a target address into CONNECT frame payload bytes.
|
||||||
|
/// Format: ATYP (1) + addr (variable) + port (2)
|
||||||
|
pub fn encode_target(target: &crate::socks5::Socks5Target) -> Vec<u8> {
|
||||||
|
match target {
|
||||||
|
crate::socks5::Socks5Target::Ipv4(addr, port) => {
|
||||||
|
let mut v = vec![crate::socks5::ATYP_IPV4];
|
||||||
|
v.extend_from_slice(&addr.octets());
|
||||||
|
v.extend_from_slice(&port.to_be_bytes());
|
||||||
|
v
|
||||||
|
}
|
||||||
|
crate::socks5::Socks5Target::Domain { domain, port } => {
|
||||||
|
let mut v = vec![crate::socks5::ATYP_DOMAIN, domain.len() as u8];
|
||||||
|
v.extend_from_slice(domain.as_bytes());
|
||||||
|
v.extend_from_slice(&port.to_be_bytes());
|
||||||
|
v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode a target address from CONNECT frame payload bytes.
|
||||||
|
pub fn decode_target(payload: &[u8]) -> Result<crate::socks5::Socks5Target, FrameError> {
|
||||||
|
if payload.is_empty() {
|
||||||
|
return Err(FrameError::Protocol("empty target".to_string()));
|
||||||
|
}
|
||||||
|
let atyp = payload[0];
|
||||||
|
let mut cursor = &payload[1..];
|
||||||
|
|
||||||
|
match atyp {
|
||||||
|
crate::socks5::ATYP_IPV4 => {
|
||||||
|
if cursor.len() < 6 {
|
||||||
|
return Err(FrameError::Protocol("truncated IPv4 target".to_string()));
|
||||||
|
}
|
||||||
|
let addr = std::net::Ipv4Addr::new(cursor[0], cursor[1], cursor[2], cursor[3]);
|
||||||
|
let port = u16::from_be_bytes([cursor[4], cursor[5]]);
|
||||||
|
Ok(crate::socks5::Socks5Target::Ipv4(addr, port))
|
||||||
|
}
|
||||||
|
crate::socks5::ATYP_DOMAIN => {
|
||||||
|
if cursor.is_empty() {
|
||||||
|
return Err(FrameError::Protocol("truncated domain target".to_string()));
|
||||||
|
}
|
||||||
|
let domain_len = cursor[0] as usize;
|
||||||
|
cursor.advance(1);
|
||||||
|
if cursor.len() < domain_len + 2 {
|
||||||
|
return Err(FrameError::Protocol("truncated domain target".to_string()));
|
||||||
|
}
|
||||||
|
let domain = String::from_utf8(cursor[..domain_len].to_vec())
|
||||||
|
.map_err(|_| FrameError::Protocol("invalid UTF-8 in domain".to_string()))?;
|
||||||
|
cursor.advance(domain_len);
|
||||||
|
let port = u16::from_be_bytes([cursor[0], cursor[1]]);
|
||||||
|
Ok(crate::socks5::Socks5Target::Domain { domain, port })
|
||||||
|
}
|
||||||
|
_ => Err(FrameError::Protocol(format!(
|
||||||
|
"unsupported address type: {:#04x}",
|
||||||
|
atyp
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn frame_roundtrip_connect() {
|
||||||
|
let target = crate::socks5::Socks5Target::Ipv4(std::net::Ipv4Addr::new(127, 0, 0, 1), 4181);
|
||||||
|
let payload = encode_target(&target);
|
||||||
|
let frame = Frame::connect(1, payload.clone());
|
||||||
|
let serialized = frame.serialize();
|
||||||
|
|
||||||
|
let mut buf = BytesMut::from(&serialized[..]);
|
||||||
|
let (decoded, _) = Frame::deserialize(&mut buf).unwrap();
|
||||||
|
assert_eq!(decoded.frame_type, FRAME_CONNECT);
|
||||||
|
assert_eq!(decoded.stream_id, 1);
|
||||||
|
assert_eq!(decoded.payload, payload);
|
||||||
|
|
||||||
|
let decoded_target = decode_target(&decoded.payload).unwrap();
|
||||||
|
assert_eq!(decoded_target, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn frame_roundtrip_data() {
|
||||||
|
let data = b"hello world";
|
||||||
|
let frame = Frame::data(3, data);
|
||||||
|
let serialized = frame.serialize();
|
||||||
|
|
||||||
|
let mut buf = BytesMut::from(&serialized[..]);
|
||||||
|
let (decoded, _) = Frame::deserialize(&mut buf).unwrap();
|
||||||
|
assert_eq!(decoded.frame_type, FRAME_DATA);
|
||||||
|
assert_eq!(decoded.stream_id, 3);
|
||||||
|
assert_eq!(&decoded.payload[..], data);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn frame_roundtrip_data_eof() {
|
||||||
|
let frame = Frame::data_eof(5, b"final");
|
||||||
|
let serialized = frame.serialize();
|
||||||
|
|
||||||
|
let mut buf = BytesMut::from(&serialized[..]);
|
||||||
|
let (decoded, _) = Frame::deserialize(&mut buf).unwrap();
|
||||||
|
assert!(decoded.is_eof());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn frame_roundtrip_close() {
|
||||||
|
let frame = Frame::close(7);
|
||||||
|
let serialized = frame.serialize();
|
||||||
|
|
||||||
|
let mut buf = BytesMut::from(&serialized[..]);
|
||||||
|
let (decoded, _) = Frame::deserialize(&mut buf).unwrap();
|
||||||
|
assert_eq!(decoded.frame_type, FRAME_CLOSE);
|
||||||
|
assert_eq!(decoded.stream_id, 7);
|
||||||
|
assert!(decoded.payload.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn frame_roundtrip_error() {
|
||||||
|
let frame = Frame::error(9, "connection refused");
|
||||||
|
let serialized = frame.serialize();
|
||||||
|
|
||||||
|
let mut buf = BytesMut::from(&serialized[..]);
|
||||||
|
let (decoded, _) = Frame::deserialize(&mut buf).unwrap();
|
||||||
|
assert_eq!(decoded.frame_type, FRAME_ERROR);
|
||||||
|
assert_eq!(
|
||||||
|
String::from_utf8_lossy(&decoded.payload),
|
||||||
|
"connection refused"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn frame_roundtrip_connect_reply() {
|
||||||
|
let frame = Frame::connect_reply(1, CONNECT_OK);
|
||||||
|
let serialized = frame.serialize();
|
||||||
|
|
||||||
|
let mut buf = BytesMut::from(&serialized[..]);
|
||||||
|
let (decoded, _) = Frame::deserialize(&mut buf).unwrap();
|
||||||
|
assert_eq!(decoded.frame_type, FRAME_CONNECT_REPLY);
|
||||||
|
assert_eq!(decoded.payload[0], CONNECT_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn frame_partial_read_returns_error() {
|
||||||
|
// Header is 14 bytes; with only 10 bytes, deserialization should fail
|
||||||
|
let mut buf = BytesMut::from(&[0u8; 10][..]);
|
||||||
|
let result = Frame::deserialize(&mut buf);
|
||||||
|
assert!(matches!(result, Err(FrameError::FrameTooShort(_, _))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn frame_multiple_frames_in_buffer() {
|
||||||
|
let frame1 = Frame::data(1, b"first");
|
||||||
|
let frame2 = Frame::data(3, b"second");
|
||||||
|
|
||||||
|
let mut buf = BytesMut::new();
|
||||||
|
buf.extend_from_slice(&frame1.serialize());
|
||||||
|
buf.extend_from_slice(&frame2.serialize());
|
||||||
|
|
||||||
|
let (d1, _) = Frame::deserialize(&mut buf).unwrap();
|
||||||
|
assert_eq!(d1.stream_id, 1);
|
||||||
|
assert_eq!(&d1.payload[..], b"first");
|
||||||
|
|
||||||
|
let (d2, _) = Frame::deserialize(&mut buf).unwrap();
|
||||||
|
assert_eq!(d2.stream_id, 3);
|
||||||
|
assert_eq!(&d2.payload[..], b"second");
|
||||||
|
|
||||||
|
assert!(buf.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn encode_decode_target_ipv4() {
|
||||||
|
let target = crate::socks5::Socks5Target::Ipv4(std::net::Ipv4Addr::new(10, 0, 0, 1), 8080);
|
||||||
|
let encoded = encode_target(&target);
|
||||||
|
let decoded = decode_target(&encoded).unwrap();
|
||||||
|
assert_eq!(decoded, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn encode_decode_target_domain() {
|
||||||
|
let target = crate::socks5::Socks5Target::Domain {
|
||||||
|
domain: "example.com".to_string(),
|
||||||
|
port: 443,
|
||||||
|
};
|
||||||
|
let encoded = encode_target(&target);
|
||||||
|
let decoded = decode_target(&encoded).unwrap();
|
||||||
|
assert_eq!(decoded, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decode_target_empty_fails() {
|
||||||
|
assert!(decode_target(&[]).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decode_target_truncated_fails() {
|
||||||
|
assert!(decode_target(&[crate::socks5::ATYP_IPV4, 127]).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decode_target_unsupported_atyp_fails() {
|
||||||
|
assert!(decode_target(&[0x04, 0, 0, 0, 0, 0, 0]).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn frame_reader_writes_all_then_reads() {
|
||||||
|
let target = crate::socks5::Socks5Target::Ipv4(std::net::Ipv4Addr::new(127, 0, 0, 1), 8080);
|
||||||
|
let frame = Frame::connect(1, encode_target(&target));
|
||||||
|
let serialized = frame.serialize();
|
||||||
|
|
||||||
|
let mut reader = FrameReader::new();
|
||||||
|
let mut cursor = Cursor::new(&serialized[..]);
|
||||||
|
let result = reader.read_frame(&mut cursor).await.unwrap().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result.frame_type, FRAME_CONNECT);
|
||||||
|
assert_eq!(result.stream_id, 1);
|
||||||
|
let decoded_target = decode_target(&result.payload).unwrap();
|
||||||
|
assert_eq!(decoded_target, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn frame_reader_eof_returns_none() {
|
||||||
|
let mut reader = FrameReader::new();
|
||||||
|
let mut cursor = Cursor::new(&[] as &[u8]);
|
||||||
|
let result = reader.read_frame(&mut cursor).await.unwrap();
|
||||||
|
assert!(result.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn frame_writer_roundtrip() {
|
||||||
|
let frame = Frame::data(5, b"test data here");
|
||||||
|
let serialized = frame.serialize();
|
||||||
|
|
||||||
|
let mut cursor = Vec::new();
|
||||||
|
FrameWriter::write_frame(&mut cursor, &frame).await.unwrap();
|
||||||
|
assert_eq!(cursor, serialized.as_ref());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn frame_header_size() {
|
||||||
|
assert_eq!(FRAME_HEADER_LEN, 14);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
mod cli;
|
mod cli;
|
||||||
mod config;
|
mod config;
|
||||||
mod errors;
|
mod errors;
|
||||||
|
mod framing;
|
||||||
mod generate;
|
mod generate;
|
||||||
mod redact;
|
mod redact;
|
||||||
mod socks5;
|
mod socks5;
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ pub enum Socks5Error {
|
|||||||
AuthFailed,
|
AuthFailed,
|
||||||
|
|
||||||
#[error("connection not allowed")]
|
#[error("connection not allowed")]
|
||||||
|
#[allow(dead_code)]
|
||||||
ConnectionNotAllowed,
|
ConnectionNotAllowed,
|
||||||
|
|
||||||
#[error("network unreachable")]
|
#[error("network unreachable")]
|
||||||
@@ -52,9 +53,11 @@ pub enum Socks5Error {
|
|||||||
NetworkUnreachable,
|
NetworkUnreachable,
|
||||||
|
|
||||||
#[error("host unreachable")]
|
#[error("host unreachable")]
|
||||||
|
#[allow(dead_code)]
|
||||||
HostUnreachable,
|
HostUnreachable,
|
||||||
|
|
||||||
#[error("connection refused")]
|
#[error("connection refused")]
|
||||||
|
#[allow(dead_code)]
|
||||||
ConnectionRefused,
|
ConnectionRefused,
|
||||||
|
|
||||||
#[error("protocol error: {0}")]
|
#[error("protocol error: {0}")]
|
||||||
|
|||||||
+1685
-1971
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user