use alkcall::channels::client::{ChannelClient, ChannelOpenError};
use alkcall::channels::manager::ChannelManager;
use alkcall::core::types::BiStream;
use alkcall::protocol::connection::CallConnection;
use bytes::Bytes;
use std::net::{IpAddr, Ipv6Addr, SocketAddr};
use std::sync::Arc;
use tokio::io::{AsyncReadExt, AsyncWriteExt, ReadHalf, WriteHalf};
use crate::error::{SocksOpenError, SocksSessionError};
use crate::params::{Socks5Params, OP_SOCKS5_OPEN, SOCKS5_ALPN};
use crate::rfc1928::client::{Config, Socks5Stream};
use crate::rfc1928::consts;
use crate::rfc1928::util::target_addr::{read_address, TargetAddr, ToTargetAddr};
use crate::rfc1928::{ReplyError, Socks5Command, SocksError, UdpHeaderError};
use crate::wire::{frame, DatagramReader, WireError};
async fn open_and_negotiate<T, F, Fut>(
client: &ChannelClient,
negotiate: F,
) -> Result<(u32, T, ChannelManager), SocksSessionError>
where
F: FnOnce(Socks5Stream<BiStream>) -> Fut,
Fut: std::future::Future<Output = Result<T, SocksSessionError>>,
{
let (channel_id, send, recv) = client
.open_channel(OP_SOCKS5_OPEN, Socks5Params::as_json(), SOCKS5_ALPN)
.await
.map_err(SocksOpenError::from)?;
let bidi = BiStream::from_joined(recv, send);
let negotiated = negotiate(Socks5Stream::use_stream(bidi, None, Config::default()).await?);
match negotiated.await {
Ok(inner) => Ok((channel_id, inner, client.manager().clone())),
Err(err) => {
let _ = client.manager().teardown_channel(channel_id);
Err(err)
}
}
}
async fn open_with_token_and_negotiate<T, F, Fut>(
client: &crate::consumer::TokenClient,
token: Vec<u8>,
negotiate: F,
) -> Result<(u32, T, ChannelManager), SocksSessionError>
where
F: FnOnce(Socks5Stream<BiStream>) -> Fut,
Fut: std::future::Future<Output = Result<T, SocksSessionError>>,
{
let channel_id = client.open_with_token(token).await?;
let (send, recv) = client
.manager()
.adopt_channel(channel_id, SOCKS5_ALPN, None)
.await
.map_err(|e| SocksOpenError::from(ChannelOpenError::AdoptFailed(e)))?;
let bidi = BiStream::from_joined(recv, send);
let negotiated = negotiate(Socks5Stream::use_stream(bidi, None, Config::default()).await?);
match negotiated.await {
Ok(inner) => Ok((channel_id, inner, client.manager().clone())),
Err(err) => {
let _ = client.manager().teardown_channel(channel_id);
Err(err)
}
}
}
#[derive(Debug, thiserror::Error)]
#[error("the session's pipe/stream was already taken (spent)")]
pub struct SpentSessionError;
pub struct Socks5ChannelSession {
channel_id: u32,
manager: Option<ChannelManager>,
inner: Option<Socks5Stream<BiStream>>,
reply_addr: Option<TargetAddr>,
}
impl std::fmt::Debug for Socks5ChannelSession {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Socks5ChannelSession")
.field("channel_id", &self.channel_id)
.field("connected", &self.inner.is_some())
.finish()
}
}
impl Socks5ChannelSession {
pub async fn connect(
client: &ChannelClient,
target: impl ToTargetAddr,
) -> Result<Self, SocksSessionError> {
let target: TargetAddr = target.to_target_addr()?;
let (channel_id, inner, manager) = open_and_negotiate(client, |mut inner| async move {
let reply_addr = inner.request(Socks5Command::TCPConnect, target).await?;
Ok((reply_addr, inner))
})
.await?;
let (reply_addr, inner) = inner;
Ok(Self {
channel_id,
manager: Some(manager),
inner: Some(inner),
reply_addr: Some(reply_addr),
})
}
pub async fn connect_with_token(
client: &TokenClient,
target: impl ToTargetAddr,
token: Vec<u8>,
) -> Result<Self, SocksSessionError> {
let target: TargetAddr = target.to_target_addr()?;
let (channel_id, inner, manager) =
open_with_token_and_negotiate(client, token, |mut inner| async move {
let reply_addr = inner.request(Socks5Command::TCPConnect, target).await?;
Ok((reply_addr, inner))
})
.await?;
let (reply_addr, inner) = inner;
Ok(Self {
channel_id,
manager: Some(manager),
inner: Some(inner),
reply_addr: Some(reply_addr),
})
}
pub fn channel_id(&self) -> u32 {
self.channel_id
}
pub fn reply_addr(&self) -> Option<TargetAddr> {
self.reply_addr.clone()
}
pub fn split(&mut self) -> Option<SplitPipe<'_>> {
let inner = self.inner.as_mut()?;
Some(tokio::io::split(inner))
}
pub fn into_inner(mut self) -> Result<TakenInner, SpentSessionError> {
match self.inner.take() {
Some(inner) => Ok(TakenInner {
session: self,
inner,
}),
None => Err(SpentSessionError),
}
}
pub async fn close(mut self) -> bool {
if let Some(inner) = self.inner.as_mut() {
let _ = inner.shutdown().await;
}
self.inner.take();
self.reap()
}
fn reap(&mut self) -> bool {
self.manager
.take()
.map(|m| m.teardown_channel(self.channel_id).is_ok())
.unwrap_or(false)
}
}
impl Drop for Socks5ChannelSession {
fn drop(&mut self) {
self.inner.take();
let _ = self.reap();
}
}
pub struct AssociateSession {
channel_id: u32,
manager: Option<ChannelManager>,
inner: Option<Socks5Stream<BiStream>>,
reader: DatagramReader,
pending: std::collections::VecDeque<(TargetAddr, Bytes)>,
relay_addr: Option<TargetAddr>,
}
impl std::fmt::Debug for AssociateSession {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AssociateSession")
.field("channel_id", &self.channel_id)
.field("associated", &self.inner.is_some())
.field("relay_addr", &self.relay_addr)
.field("pending_datagrams", &self.pending.len())
.finish()
}
}
impl AssociateSession {
pub async fn associate(client: &ChannelClient) -> Result<Self, SocksSessionError> {
let (channel_id, inner, manager) = open_and_negotiate(client, |mut inner| async move {
let relay_addr = inner
.request(
Socks5Command::UDPAssociate,
TargetAddr::Ip(SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0)),
)
.await?;
Ok((relay_addr, inner))
})
.await?;
let (relay_addr, inner) = inner;
Ok(Self {
channel_id,
manager: Some(manager),
inner: Some(inner),
reader: DatagramReader::new(),
pending: std::collections::VecDeque::new(),
relay_addr: Some(relay_addr),
})
}
pub async fn associate_with_token(
client: &TokenClient,
token: Vec<u8>,
) -> Result<Self, SocksSessionError> {
let (channel_id, inner, manager) =
open_with_token_and_negotiate(client, token, |mut inner| async move {
let relay_addr = inner
.request(
Socks5Command::UDPAssociate,
TargetAddr::Ip(SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0)),
)
.await?;
Ok((relay_addr, inner))
})
.await?;
let (relay_addr, inner) = inner;
Ok(Self {
channel_id,
manager: Some(manager),
inner: Some(inner),
reader: DatagramReader::new(),
pending: std::collections::VecDeque::new(),
relay_addr: Some(relay_addr),
})
}
pub fn channel_id(&self) -> u32 {
self.channel_id
}
pub fn relay_addr(&self) -> Option<TargetAddr> {
self.relay_addr.clone()
}
pub async fn send_datagram(
&mut self,
dst: impl ToTargetAddr,
payload: &[u8],
) -> Result<(), SocksSessionError> {
let dst: TargetAddr = dst.to_target_addr()?;
let framed = frame(&dst, payload).map_err(wire_error)?;
let Some(inner) = self.inner.as_mut() else {
return Err(SocksSessionError::Io(std::io::Error::new(
std::io::ErrorKind::NotConnected,
"the association's stream is spent",
)));
};
let socket = inner.get_socket_mut();
socket.write_all(&framed).await?;
socket.flush().await?;
Ok(())
}
pub async fn recv_datagram(
&mut self,
) -> Result<Option<(TargetAddr, Bytes)>, SocksSessionError> {
if let Some(dg) = self.pending.pop_front() {
return Ok(Some(dg));
}
let mut chunk = vec![0u8; 16 * 1024];
let Some(inner) = self.inner.as_mut() else {
return Ok(None);
};
let socket = inner.get_socket_mut();
loop {
let n = socket.read(&mut chunk).await?;
if n == 0 {
if self.reader.is_mid_frame() {
return Err(SocksSessionError::Rfc(SocksError::Io(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"association ended mid-datagram (fail-loud)",
))));
}
return Ok(None);
}
let mut dgs = self.reader.feed(&chunk[..n]).into_iter();
if let Some(first) = dgs.next() {
for res in dgs {
match res {
Ok(pair) => self.pending.push_back(pair),
Err(err) => return Err(wire_error(err)),
}
}
match first {
Ok(pair) => return Ok(Some(pair)),
Err(err) => return Err(wire_error(err)),
}
}
}
}
pub fn split(&mut self) -> Option<SplitPipe<'_>> {
let inner = self.inner.as_mut()?;
Some(tokio::io::split(inner))
}
pub fn into_inner(mut self) -> Result<TakenAssociateInner, SpentSessionError> {
match self.inner.take() {
Some(inner) => Ok(TakenAssociateInner {
session: self,
inner,
}),
None => Err(SpentSessionError),
}
}
pub async fn shutdown(mut self) -> Result<bool, SocksSessionError> {
let reaped = if let Some(inner) = self.inner.as_mut() {
let socket = inner.get_socket_mut();
socket.shutdown().await?;
self.inner.take();
self.reap()
} else {
self.reap()
};
Ok(reaped)
}
pub async fn close(mut self) -> bool {
if let Some(inner) = self.inner.as_mut() {
let _ = inner.shutdown().await;
}
self.inner.take();
self.reap()
}
fn reap(&mut self) -> bool {
self.manager
.take()
.map(|m| m.teardown_channel(self.channel_id).is_ok())
.unwrap_or(false)
}
}
impl Drop for AssociateSession {
fn drop(&mut self) {
self.inner.take();
let _ = self.reap();
}
}
pub struct TakenAssociateInner {
pub session: AssociateSession,
pub inner: Socks5Stream<BiStream>,
}
pub struct TakenInner {
pub session: Socks5ChannelSession,
pub inner: Socks5Stream<BiStream>,
}
pub type SplitPipe<'s> = (
ReadHalf<&'s mut Socks5Stream<BiStream>>,
WriteHalf<&'s mut Socks5Stream<BiStream>>,
);
pub struct BindReply1<'s> {
inner: &'s mut Socks5Stream<BiStream>,
pub bind_addr: TargetAddr,
}
impl BindReply1<'_> {
pub fn raw_mut(&mut self) -> &mut Socks5Stream<BiStream> {
self.inner
}
}
pub struct BindDataPlane {
inner: Socks5Stream<BiStream>,
pub peer: TargetAddr,
}
impl BindDataPlane {
pub fn into_inner(self) -> Socks5Stream<BiStream> {
self.inner
}
pub fn get_socket_mut(&mut self) -> &mut BiStream {
self.inner.get_socket_mut()
}
}
impl tokio::io::AsyncRead for BindDataPlane {
fn poll_read(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>,
) -> std::task::Poll<std::io::Result<()>> {
std::pin::Pin::new(&mut self.inner).poll_read(cx, buf)
}
}
impl tokio::io::AsyncWrite for BindDataPlane {
fn poll_write(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> std::task::Poll<std::io::Result<usize>> {
std::pin::Pin::new(&mut self.inner).poll_write(cx, buf)
}
fn poll_flush(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
std::pin::Pin::new(&mut self.inner).poll_flush(cx)
}
fn poll_shutdown(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
std::pin::Pin::new(&mut self.inner).poll_shutdown(cx)
}
}
pub struct BindSession {
channel_id: u32,
manager: Option<ChannelManager>,
inner: Option<Socks5Stream<BiStream>>,
bind_addr: Option<TargetAddr>,
}
impl std::fmt::Debug for BindSession {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BindSession")
.field("channel_id", &self.channel_id)
.field("awaiting_inbound", &self.inner.is_some())
.field("bind_addr", &self.bind_addr)
.finish()
}
}
impl BindSession {
pub async fn bind(
client: &ChannelClient,
target: impl ToTargetAddr,
) -> Result<Self, SocksSessionError> {
let target: TargetAddr = target.to_target_addr()?;
let (channel_id, inner, manager) = open_and_negotiate(client, |mut inner| async move {
let bind_addr = inner.request(Socks5Command::TCPBind, target).await?;
Ok((bind_addr, inner))
})
.await?;
let (bind_addr, inner) = inner;
Ok(Self {
channel_id,
manager: Some(manager),
inner: Some(inner),
bind_addr: Some(bind_addr),
})
}
pub async fn bind_with_token(
client: &TokenClient,
target: impl ToTargetAddr,
token: Vec<u8>,
) -> Result<Self, SocksSessionError> {
let target: TargetAddr = target.to_target_addr()?;
let (channel_id, inner, manager) =
open_with_token_and_negotiate(client, token, |mut inner| async move {
let bind_addr = inner.request(Socks5Command::TCPBind, target).await?;
Ok((bind_addr, inner))
})
.await?;
let (bind_addr, inner) = inner;
Ok(Self {
channel_id,
manager: Some(manager),
inner: Some(inner),
bind_addr: Some(bind_addr),
})
}
pub fn channel_id(&self) -> u32 {
self.channel_id
}
pub fn reply1(&mut self) -> Option<BindReply1<'_>> {
let inner = self.inner.as_mut()?;
Some(BindReply1 {
inner,
bind_addr: self.bind_addr.clone()?,
})
}
pub async fn await_inbound(&mut self) -> Result<Option<BindDataPlane>, SocksSessionError> {
let Some(mut inner) = self.inner.take() else {
return Ok(None);
};
match read_reply2(&mut inner).await {
Ok(peer) => Ok(Some(BindDataPlane { inner, peer })),
Err(err) => {
self.inner = Some(inner);
Err(err)
}
}
}
pub fn split(&mut self) -> Option<SplitPipe<'_>> {
let inner = self.inner.as_mut()?;
Some(tokio::io::split(inner))
}
pub async fn close(mut self) -> bool {
if let Some(inner) = self.inner.as_mut() {
let _ = inner.shutdown().await;
}
self.inner.take();
self.reap()
}
fn reap(&mut self) -> bool {
self.manager
.take()
.map(|m| m.teardown_channel(self.channel_id).is_ok())
.unwrap_or(false)
}
}
impl Drop for BindSession {
fn drop(&mut self) {
self.inner.take();
let _ = self.reap();
}
}
fn wire_error(err: WireError) -> SocksSessionError {
match err {
WireError::Address(addr) => SocksError::AddrError(addr).into(),
other => SocksError::UdpHeaderError(UdpHeaderError::ReadingError(std::io::Error::other(
other.to_string(),
)))
.into(),
}
}
async fn read_reply2<T>(inner: &mut Socks5Stream<T>) -> Result<TargetAddr, SocksSessionError>
where
T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
{
let socket = inner.get_socket_mut();
let mut header = [0u8; 4];
socket.read_exact(&mut header).await?;
let [version, reply, _rsv, atyp] = header;
if version != consts::SOCKS5_VERSION {
return Err(SocksError::UnsupportedSocksVersion(version).into());
}
if reply != consts::SOCKS5_REPLY_SUCCEEDED {
return Err(SocksError::ReplyError(ReplyError::from_u8(reply)).into());
}
let peer = read_address(socket, atyp)
.await
.map_err(SocksError::AddrError)?;
Ok(peer)
}
pub struct TokenClient {
call: Arc<CallConnection>,
manager: ChannelManager,
}
impl TokenClient {
pub async fn wrap(client: &ChannelClient) -> Result<Self, TokenClientError> {
let call = client
.take_call_connection()
.await
.ok_or(TokenClientError::CallConnectionTaken)?;
Ok(Self {
call,
manager: client.manager().clone(),
})
}
pub fn manager(&self) -> &ChannelManager {
&self.manager
}
pub async fn open_with_token(&self, token: Vec<u8>) -> Result<u32, SocksOpenError> {
let payload = serde_json::json!({
"operationId": OP_SOCKS5_OPEN,
"input": Socks5Params::as_json(),
"auth_token": String::from_utf8_lossy(&token).into_owned(),
});
let response = self.call.call_with_payload(payload).await;
let out = response
.result
.map_err(|error| ChannelOpenError::CallFailed { error })
.map_err(SocksOpenError::from)?;
let channel_id = out
.get("channel_id")
.and_then(|v: &serde_json::Value| v.as_u64())
.ok_or(ChannelOpenError::MissingChannelId)? as u32;
Ok(channel_id)
}
}
#[derive(Debug, thiserror::Error)]
pub enum TokenClientError {
#[error("the channel client's call connection was already taken (a client is one calling surface, the door-only contract)")]
CallConnectionTaken,
}
impl std::fmt::Debug for TokenClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TokenClient").finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
use alkcall::channels::client::ChannelOpenError;
use futures::future::FutureExt as _;
fn assert_send<T: Send>() {}
#[test]
fn session_shape_is_send_and_not_clone() {
assert_send::<Socks5ChannelSession>();
assert_send::<TakenInner>();
}
#[test]
fn bind_session_shape_is_send_and_not_clone() {
assert_send::<BindSession>();
assert_send::<BindDataPlane>();
}
#[tokio::test]
async fn spent_bind_await_inbound_returns_the_spent_shape() {
let mut session = BindSession {
channel_id: 9,
manager: None,
inner: None,
bind_addr: None,
};
let spent = session.await_inbound().await;
assert!(
matches!(spent, Ok(None)),
"the spent shape: {:?}",
spent.as_ref().err().map(|e| e.to_string())
);
assert!(session.reply1().is_none(), "no reply#1 view either");
assert!(session.split().is_none(), "no pipe after teardown");
}
#[test]
fn debug_impl_names_the_channel_and_liveness() {
let session = Socks5ChannelSession {
channel_id: 7,
manager: None,
inner: None,
reply_addr: None,
};
let rendered = format!("{session:?}");
assert!(rendered.contains("channel_id: 7"), "{rendered}");
assert!(rendered.contains("connected: false"), "{rendered}");
}
#[test]
fn spent_session_reap_is_a_no_op() {
let mut session = Socks5ChannelSession {
channel_id: 9,
manager: None,
inner: None,
reply_addr: None,
};
assert!(!session.reap(), "no manager, so nothing was reaped");
assert!(session.split().is_none(), "no pipe after teardown");
}
#[test]
fn spent_session_into_inner_is_an_err_not_a_panic() {
let session = Socks5ChannelSession {
channel_id: 9,
manager: None,
inner: None,
reply_addr: None,
};
assert!(
session.into_inner().is_err(),
"a spent session must return Err, not Ok"
);
}
#[test]
fn open_errors_carry_the_typed_surface_through_connects_mapping() {
let open = SocksOpenError::Open(ChannelOpenError::MissingChannelId);
let session: SocksSessionError = open.into();
assert!(matches!(session, SocksSessionError::Open(_)));
}
#[test]
fn associate_session_shape_is_send_and_not_clone() {
assert_send::<AssociateSession>();
assert_send::<TakenAssociateInner>();
}
#[test]
fn associate_debug_impl_names_the_channel_and_liveness() {
let session = AssociateSession {
channel_id: 7,
manager: None,
inner: None,
reader: DatagramReader::new(),
pending: Default::default(),
relay_addr: None,
};
let rendered = format!("{session:?}");
assert!(rendered.contains("channel_id: 7"), "{rendered}");
assert!(rendered.contains("associated: false"), "{rendered}");
}
#[test]
fn spent_associate_reap_is_a_no_op() {
let mut session = AssociateSession {
channel_id: 9,
manager: None,
inner: None,
reader: DatagramReader::new(),
pending: Default::default(),
relay_addr: None,
};
assert!(!session.reap(), "no manager, so nothing was reaped");
assert!(session.split().is_none(), "no stream after teardown");
assert!(session.relay_addr().is_none());
}
#[test]
fn spent_associate_into_inner_is_an_err_not_a_panic() {
let session = AssociateSession {
channel_id: 9,
manager: None,
inner: None,
reader: DatagramReader::new(),
pending: Default::default(),
relay_addr: None,
};
assert!(
session.into_inner().is_err(),
"a spent session must return Err, not Ok"
);
}
#[tokio::test]
async fn recv_datagram_persists_reader_state_across_calls() {
use crate::wire::frame as wire_frame;
use tokio::io::AsyncWriteExt as _;
let (mut writer, reader) = tokio::io::duplex(1024);
let addr = TargetAddr::Ip("10.0.0.1:53".parse().unwrap());
let wire = {
let mut v = wire_frame(&addr, b"0123456789").unwrap().to_vec();
v.extend_from_slice(&wire_frame(&addr, b"second").unwrap());
v
};
writer.write_all(&wire[..5]).await.unwrap();
writer.write_all(&wire[5..]).await.unwrap();
drop(writer);
let mut session = build_test_session(reader);
let (src, first) = session
.recv_datagram()
.await
.expect("recv")
.expect("a datagram");
assert_eq!(src, addr);
assert_eq!(&first[..], b"0123456789");
let (src, second) = session
.recv_datagram()
.await
.expect("recv")
.expect("a datagram");
assert_eq!(src, addr);
assert_eq!(&second[..], b"second");
assert!(session.recv_datagram().await.expect("eof").is_none());
}
#[tokio::test]
async fn recv_datagram_fails_loud_when_stream_ends_mid_frame() {
use tokio::io::AsyncWriteExt as _;
let (mut writer, reader) = tokio::io::duplex(1024);
let addr = TargetAddr::Ip("10.0.0.1:53".parse().unwrap());
let framed = crate::wire::frame(&addr, b"payload").unwrap();
writer.write_all(&framed[..4]).await.unwrap();
drop(writer);
let mut session = build_test_session(reader);
let err = session
.recv_datagram()
.await
.expect_err("a stream end mid-frame is the truncated-datagram condition");
assert!(
matches!(err, SocksSessionError::Rfc(SocksError::Io(_))),
"fail-loud, not a silent partial: {err:?}"
);
}
#[tokio::test]
async fn recv_datagram_fails_loud_on_an_undecodable_frame() {
use tokio::io::AsyncWriteExt as _;
let (mut writer, reader) = tokio::io::duplex(1024);
let inner_bytes = vec![0x7Fu8, 1, 2, 3];
let mut garbage = (inner_bytes.len() as u16).to_be_bytes().to_vec();
garbage.extend_from_slice(&inner_bytes);
writer.write_all(&garbage).await.unwrap();
drop(writer);
let mut session = build_test_session(reader);
let err = session
.recv_datagram()
.await
.expect_err("garbage ATYP is loud, never a silent drop");
assert!(matches!(err, SocksSessionError::Rfc(_)), "{err:?}");
}
#[test]
fn wire_error_maps_the_decode_garbage_shapes() {
let addr_err =
crate::wire::frame(&TargetAddr::Domain("a".repeat(256), 80), b"").unwrap_err();
let mapped = wire_error(addr_err);
assert!(matches!(
mapped,
SocksSessionError::Rfc(SocksError::AddrError(_))
));
let garbage = crate::wire::DatagramReader::new()
.feed(&[0x00, 0x04, 0x7f, 1, 2, 3])
.into_iter()
.next()
.unwrap()
.unwrap_err();
let mapped = wire_error(garbage);
assert!(matches!(
mapped,
SocksSessionError::Rfc(SocksError::UdpHeaderError(_))
));
}
#[tokio::test]
async fn send_datagram_on_a_spent_stream_is_the_io_arm() {
let mut session = AssociateSession {
channel_id: 9,
manager: None,
inner: None,
reader: DatagramReader::new(),
pending: Default::default(),
relay_addr: None,
};
let err = session
.send_datagram(TargetAddr::Ip("10.0.0.1:53".parse().unwrap()), b"payload")
.await
.expect_err("a spent stream must not send");
assert!(
matches!(err, SocksSessionError::Io(_)),
"the spent-stream send is the Io arm: {err:?}"
);
assert!(
session.recv_datagram().await.expect("spent recv").is_none(),
"a spent stream reads the spent shape, never an error"
);
let shutdown = session.shutdown().await.expect("spent shutdown");
assert!(!shutdown, "no manager, so nothing was reaped");
}
#[tokio::test]
async fn spent_associate_close_reaps_a_no_op() {
let session = AssociateSession {
channel_id: 9,
manager: None,
inner: None,
reader: DatagramReader::new(),
pending: Default::default(),
relay_addr: None,
};
assert!(!session.close().await, "no manager, so nothing was reaped");
}
#[tokio::test]
async fn recv_datagram_mid_batch_later_frame_err_drops_the_pending_oks() {
use tokio::io::AsyncWriteExt as _;
let (mut peer_writer, session_reader) = tokio::io::duplex(1024);
let addr = TargetAddr::Ip("10.0.0.1:53".parse().unwrap());
let mut wire = crate::wire::frame(&addr, b"first").unwrap().to_vec();
wire.extend_from_slice(&[0x00, 0x04, 0x7f, 1, 2, 3]);
peer_writer.write_all(&wire).await.unwrap();
drop(peer_writer);
let (write_peer, session_writer) = tokio::io::duplex(64);
let _ = write_peer;
let mut session = AssociateSession {
channel_id: 1,
manager: None,
inner: Some(build_skip_auth_bi_stream(session_reader, session_writer)),
reader: DatagramReader::new(),
pending: Default::default(),
relay_addr: None,
};
let err = session
.recv_datagram()
.await
.expect_err("the later frame's garbage fails loud");
assert!(
matches!(err, SocksSessionError::Rfc(SocksError::UdpHeaderError(_))),
"the decode garbage rides the UDP-header class: {err:?}"
);
assert!(session.pending.is_empty());
}
#[tokio::test]
async fn associate_into_inner_pairs_the_stream_with_the_teardown_owner() {
let (_write_peer, session_writer) = tokio::io::duplex(64);
let (read_peer, session_reader) = tokio::io::duplex(64);
let _ = read_peer;
let session = AssociateSession {
channel_id: 1,
manager: None,
inner: Some(build_skip_auth_bi_stream(session_reader, session_writer)),
reader: DatagramReader::new(),
pending: Default::default(),
relay_addr: Some(TargetAddr::Ip("127.0.0.1:0".parse().unwrap())),
};
let taken = session.into_inner().expect("the stream is live");
assert!(taken.session.relay_addr().is_some(), "the owner");
assert!(taken.session.channel_id() == 1);
drop(taken);
}
#[tokio::test]
async fn associate_debug_renders_channel_liveness_and_pending() {
let mut session = AssociateSession {
channel_id: 7,
manager: None,
inner: Some(build_skip_auth_bi_stream(
tokio::io::duplex(16).0,
tokio::io::duplex(16).0,
)),
reader: DatagramReader::new(),
pending: Default::default(),
relay_addr: Some(TargetAddr::Ip("127.0.0.1:0".parse().unwrap())),
};
session.pending.push_back((
TargetAddr::Ip("10.0.0.1:53".parse().unwrap()),
Bytes::from_static(b"x"),
));
let rendered = format!("{session:?}");
assert!(rendered.contains("channel_id: 7"), "{rendered}");
assert!(rendered.contains("associated: true"), "{rendered}");
assert!(rendered.contains("relay_addr"), "{rendered}");
assert!(rendered.contains("pending_datagrams: 1"), "{rendered}");
drop(session);
let bind = BindSession {
channel_id: 8,
manager: None,
inner: Some(build_skip_auth_bi_stream(
tokio::io::duplex(16).0,
tokio::io::duplex(16).0,
)),
bind_addr: Some(TargetAddr::Domain("bind.test".into(), 2121)),
};
let rendered = format!("{bind:?}");
assert!(rendered.contains("channel_id: 8"), "{rendered}");
assert!(rendered.contains("awaiting_inbound: true"), "{rendered}");
assert!(rendered.contains("bind_addr"), "{rendered}");
}
#[tokio::test]
async fn associate_split_yields_live_halves_and_survives_the_round_trip() {
use crate::wire::frame as wire_frame;
use tokio::io::AsyncWriteExt as _;
let addr = TargetAddr::Ip("10.0.0.1:53".parse().unwrap());
let framed = wire_frame(&addr, b"split").unwrap().to_vec();
let (mut peer_writer, session_reader) = tokio::io::duplex(1024);
peer_writer.write_all(&framed).await.unwrap();
drop(peer_writer);
let (mut peer_reader, session_writer) = tokio::io::duplex(1024);
let mut session = AssociateSession {
channel_id: 1,
manager: None,
inner: Some(build_skip_auth_bi_stream(session_reader, session_writer)),
reader: DatagramReader::new(),
pending: Default::default(),
relay_addr: None,
};
let (mut read, mut write) = session.split().expect("live halves");
let mut header = [0u8; 2];
read.read_exact(&mut header)
.await
.expect("the frame header");
let len = u16::from_be_bytes(header) as usize;
let mut rest = vec![0u8; len];
read.read_exact(&mut rest).await.expect("the frame body");
assert_eq!(len, 7 + 5, "one v4-address datagram frame");
drop(read);
write.write_all(&framed).await.unwrap();
write.flush().await.unwrap();
drop(write);
let mut got = vec![0u8; framed.len()];
peer_reader
.read_exact(&mut got)
.await
.expect("the peer read");
assert_eq!(got, framed, "the write half rode the raw pipe");
}
fn build_skip_auth_bi_stream(
session_end: tokio::io::DuplexStream,
writer_end: tokio::io::DuplexStream,
) -> Socks5Stream<BiStream> {
let mut config = Config::default();
config.set_skip_auth(true);
Socks5Stream::use_stream(BiStream::from_joined(session_end, writer_end), None, config)
.now_or_never()
.expect("skip_auth's use_stream is instantly ready")
.expect("skip_auth's use_stream succeeds")
}
#[tokio::test]
async fn bind_split_yields_live_halves_pre_reply2() {
use tokio::io::AsyncWriteExt as _;
let (mut peer_writer, session_reader) = tokio::io::duplex(1024);
peer_writer.write_all(b"raw").await.unwrap();
drop(peer_writer);
let (mut peer_reader, session_writer) = tokio::io::duplex(1024);
let mut session = BindSession {
channel_id: 1,
manager: None,
inner: Some(build_skip_auth_bi_stream(session_reader, session_writer)),
bind_addr: Some(TargetAddr::Ip("127.0.0.1:2121".parse().unwrap())),
};
{
let (mut read, mut write) = session.split().expect("live pre-reply2 halves");
write.write_all(b"ack").await.unwrap();
write.flush().await.expect("flush");
let mut buf = [0u8; 3];
read.read_exact(&mut buf).await.expect("the raw bytes");
assert_eq!(&buf, b"raw");
}
let mut buf = [0u8; 3];
peer_reader.read_exact(&mut buf).await.expect("the ack");
assert_eq!(&buf, b"ack");
drop(peer_reader);
let outcome = session.await_inbound().await;
assert!(
!matches!(outcome, Ok(None)),
"the reply#2 phase was not consumed by the split"
);
session.close().await;
}
#[tokio::test]
async fn bind_data_plane_poll_flush_and_poll_shutdown_pass_through() {
use tokio::io::AsyncWriteExt as _;
let (_session_writer_unused, session_reader) = tokio::io::duplex(64);
let (mut peer_reader, session_writer) = tokio::io::duplex(64);
let mut plane = BindDataPlane {
inner: build_skip_auth_bi_stream(session_reader, session_writer),
peer: TargetAddr::Ip("10.0.0.7:44444".parse().unwrap()),
};
plane.write_all(b"half").await.expect("write");
plane.flush().await.expect("the poll_flush passthrough");
plane
.shutdown()
.await
.expect("the poll_shutdown passthrough");
drop(plane);
let mut seen = Vec::new();
let mut buf = [0u8; 4];
loop {
match peer_reader.read(&mut buf).await {
Ok(0) | Err(_) => break,
Ok(n) => seen.extend_from_slice(&buf[..n]),
}
}
assert_eq!(&seen, b"half", "the write rode through, then EOF");
}
#[tokio::test]
async fn read_reply2_rejects_a_foreign_producers_version() {
use tokio::io::AsyncWriteExt as _;
let (mut peer_writer, session_reader) = tokio::io::duplex(64);
peer_writer
.write_all(&[0x04, 0x00, 0x00, 0x01, 10, 0, 0, 1, 0, 80])
.await
.unwrap();
drop(peer_writer);
let (_write_peer_unused, session_writer) = tokio::io::duplex(64);
let mut inner = build_skip_auth_bi_stream(session_reader, session_writer);
let err = read_reply2(&mut inner)
.await
.expect_err("a foreign version is refused");
assert!(
matches!(
err,
SocksSessionError::Rfc(SocksError::UnsupportedSocksVersion(0x04))
),
"the version pin is loud: {err:?}"
);
}
fn build_test_session(session_end: tokio::io::DuplexStream) -> AssociateSession {
let mut config = Config::default();
config.set_skip_auth(true);
let (read, write) = tokio::io::split(session_end);
let inner = Socks5Stream::use_stream(BiStream::from_joined(read, write), None, config)
.now_or_never()
.expect("skip_auth's use_stream is instantly ready")
.expect("skip_auth's use_stream succeeds");
AssociateSession {
channel_id: 1,
manager: None,
inner: Some(inner),
reader: DatagramReader::new(),
pending: Default::default(),
relay_addr: None,
}
}
}