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 config;
|
||||
mod errors;
|
||||
mod framing;
|
||||
mod generate;
|
||||
mod redact;
|
||||
mod socks5;
|
||||
|
||||
@@ -45,6 +45,7 @@ pub enum Socks5Error {
|
||||
AuthFailed,
|
||||
|
||||
#[error("connection not allowed")]
|
||||
#[allow(dead_code)]
|
||||
ConnectionNotAllowed,
|
||||
|
||||
#[error("network unreachable")]
|
||||
@@ -52,9 +53,11 @@ pub enum Socks5Error {
|
||||
NetworkUnreachable,
|
||||
|
||||
#[error("host unreachable")]
|
||||
#[allow(dead_code)]
|
||||
HostUnreachable,
|
||||
|
||||
#[error("connection refused")]
|
||||
#[allow(dead_code)]
|
||||
ConnectionRefused,
|
||||
|
||||
#[error("protocol error: {0}")]
|
||||
|
||||
+1685
-1971
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user