use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::Arc;
use alkcall::channels::operations::{ChannelCore, Establishment, OpenEstablisher, OpenHandler};
use alkcall::channels::pump::pump_bidi;
use alkcall::core::auth::AuthContext;
use alkcall::core::types::Connection;
use alkcall::registry::registration::OperationRegistry;
use serde_json::Value;
use tokio::io::{AsyncRead, AsyncWrite};
use crate::egress::{
BindAccept, BindAcceptFactory, BindTargetHandle, DialFn, Egress, EgressFactory,
};
use crate::params::{socks5_open_spec, SOCKS5_BIND_SCOPE};
use crate::rfc1928::server::{new_reply, Socks5ServerProtocol, SocksServerError};
use crate::rfc1928::util::target_addr::TargetAddr;
use crate::rfc1928::{ReplyError, Socks5Command, SocksError};
use crate::wire::{frame_inner, DatagramReader};
pub use crate::params::{OP_SOCKS5_OPEN, SOCKS5_ALPN, SOCKS5_OPEN_SCOPE};
pub(crate) fn sentinel_reply_addr() -> SocketAddr {
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0)
}
pub type IdentityWitness = Arc<tokio::sync::Mutex<Option<String>>>;
pub fn socks5_session_establisher(witness: Option<IdentityWitness>) -> OpenEstablisher {
Arc::new(move |_input: Value, auth: AuthContext| {
let witness = witness.clone();
Box::pin(async move {
if let Some(witness) = witness {
*witness.lock().await = auth.identity.as_ref().map(|i| i.id.clone());
}
Ok(Establishment::default())
})
})
}
pub fn make_socks5_pump_handler(
dial: Option<DialFn>,
egress: Option<EgressFactory>,
accept: Option<BindAcceptFactory>,
) -> OpenHandler {
Arc::new(
move |_input: Value,
_plan: Option<alkcall::channels::operations::ChannelPlan>,
conn: Connection,
auth: AuthContext| {
let dial = dial.clone();
let egress = egress.clone();
let accept = accept.clone();
let can_bind = auth
.identity
.as_ref()
.map(|i| i.scopes.iter().any(|s| s == SOCKS5_BIND_SCOPE))
.unwrap_or(false);
tokio::spawn(async move {
let Ok(bidi) = conn.accept_bi().await else {
tracing::error!("socks5 pump: accept_bi failed");
return;
};
if let Err(err) = serve_socks5_over(bidi, dial, egress, accept, can_bind).await {
tracing::warn!("socks5 session ended: {err:#}");
}
})
},
)
}
pub async fn serve_socks5_over<T>(
stream: T,
dial: Option<DialFn>,
egress: Option<EgressFactory>,
accept: Option<BindAcceptFactory>,
can_bind: bool,
) -> Result<(), SocksError>
where
T: AsyncRead + AsyncWrite + Unpin,
{
let (proto, cmd, target) = Socks5ServerProtocol::accept_no_auth(stream)
.await?
.read_command()
.await?;
match cmd {
Socks5Command::TCPConnect => serve_connect(proto, target, dial).await,
Socks5Command::UDPAssociate => serve_udp_associate(proto, egress).await,
Socks5Command::TCPBind => {
if !can_bind {
proto.reply_error(&ReplyError::ConnectionNotAllowed).await?;
return Err(SocksError::ServerError(SocksServerError::Refused(
"bind command gate: not allowed",
)));
}
serve_tcp_bind(proto, target, accept).await
}
}
}
async fn serve_connect<T>(
proto: Socks5ServerProtocol<T, crate::rfc1928::server::states::CommandRead>,
target: TargetAddr,
dial: Option<DialFn>,
) -> Result<(), SocksError>
where
T: AsyncRead + AsyncWrite + Unpin,
{
let Some(dial) = dial else {
proto.reply_error(&ReplyError::CommandNotSupported).await?;
return Err(SocksError::ServerError(SocksServerError::UnknownCommand(
Socks5Command::TCPConnect.as_u8(),
)));
};
let handle = match dial(target).await {
Ok(handle) => handle,
Err(err) => {
proto.reply_error(&err.to_reply_error()).await?;
return Err(SocksError::ServerError(SocksServerError::Refused(
"dial refused in-stream",
)));
}
};
let inner = proto.reply_success(sentinel_reply_addr()).await?;
pump_bidi(inner, handle.read, handle.write).await;
Ok(())
}
async fn serve_udp_associate<T>(
proto: Socks5ServerProtocol<T, crate::rfc1928::server::states::CommandRead>,
egress: Option<EgressFactory>,
) -> Result<(), SocksError>
where
T: AsyncRead + AsyncWrite + Unpin,
{
let Some(egress) = egress else {
proto.reply_error(&ReplyError::CommandNotSupported).await?;
return Err(SocksError::ServerError(SocksServerError::UnknownCommand(
Socks5Command::UDPAssociate.as_u8(),
)));
};
let mut egress = match egress().await {
Ok(egress) => egress,
Err(err) => {
proto.reply_error(&err.to_reply_error()).await?;
return Err(SocksError::ServerError(SocksServerError::Refused(
"egress refused in-stream",
)));
}
};
let inner = proto.reply_success(sentinel_reply_addr()).await?;
run_datagram_stage(inner, egress.as_mut()).await;
Ok(())
}
async fn run_datagram_stage<T>(mut inner: T, egress: &mut (dyn Egress + Send)) -> StageExit
where
T: AsyncRead + AsyncWrite + Unpin,
{
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let mut reader = DatagramReader::new();
let mut chunk = vec![0u8; 16 * 1024];
loop {
tokio::select! {
reply = egress.next_reply() => {
match reply {
Ok(Some(frame)) => {
match frame_inner(&frame) {
Ok(wire) => {
if inner.write_all(&wire).await.is_err() {
return StageExit::Io;
}
}
Err(err) => {
tracing::debug!("udp stage: reply frame rejected: {err}");
return StageExit::ReplyFrameRejected;
}
}
}
Ok(None) => tokio::task::yield_now().await,
Err(err) => {
tracing::debug!("udp egress relay ended: {err:#}");
return StageExit::EgressEnded;
}
}
}
read = inner.read(&mut chunk) => {
match read {
Ok(0) => return StageExit::ChannelEof,
Ok(n) => {
for dg in reader.feed(&chunk[..n]) {
match dg {
Err(err) => {
tracing::debug!("udp stage: undecodable frame: {err}");
return StageExit::UndecodableFrame;
}
Ok((addr, payload)) => {
if let Err(err) = egress.send(addr, payload).await {
tracing::debug!("udp egress send failed: {err:#}");
return StageExit::EgressEnded;
}
}
}
}
}
Err(err) => {
tracing::debug!("udp stage: channel read error: {err}");
return StageExit::Io;
}
}
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum StageExit {
ChannelEof,
UndecodableFrame,
EgressEnded,
ReplyFrameRejected,
Io,
}
async fn serve_tcp_bind<T>(
proto: Socks5ServerProtocol<T, crate::rfc1928::server::states::CommandRead>,
target: TargetAddr,
accept: Option<BindAcceptFactory>,
) -> Result<(), SocksError>
where
T: AsyncRead + AsyncWrite + Unpin,
{
let accept = match accept {
Some(factory) => match factory(target).await {
Ok(accept) => accept,
Err(err) => {
proto.reply_error(&err.to_reply_error()).await?;
return Err(SocksError::ServerError(SocksServerError::Refused(
"bind factory refused in-stream",
)));
}
},
None => in_band_bind_accept(),
};
let inner = proto.reply_success(accept.bind_addr()).await?;
bind_quiet_wait_and_serve(inner, accept).await
}
fn in_band_bind_accept() -> BindAccept {
BindAccept::new(sentinel_bind_peer_addr(), async {
Ok(BindTargetHandle {
peer: TargetAddr::Ip(sentinel_bind_peer_addr()),
read: Box::new(tokio::io::empty()) as Box<dyn AsyncRead + Send + Sync + Unpin>,
write: Box::new(tokio::io::sink()) as Box<dyn AsyncWrite + Send + Sync + Unpin>,
})
})
}
async fn bind_quiet_wait_and_serve<T>(mut inner: T, accept: BindAccept) -> Result<(), SocksError>
where
T: AsyncRead + AsyncWrite + Unpin,
{
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let mut chunk = [0u8; 1];
let wait = accept.accept();
tokio::pin!(wait);
let handle = tokio::select! {
handle = &mut wait => match handle {
Ok(h) => h,
Err(err) => {
let _ = inner
.write_all(&new_reply(&err.to_reply_error(), sentinel_bind_peer_addr()))
.await;
let _ = inner.flush().await;
return Err(SocksError::ServerError(SocksServerError::Refused(
"bind accept refused in-stream",
)));
}
},
read = inner.read(&mut chunk) => match read {
Ok(0) => return Ok(()),
Ok(_) => {
return Err(SocksError::ServerError(
SocksServerError::UnexpectedUdpControlGarbage(chunk[0]),
));
}
Err(_) => return Ok(()),
},
};
let peer = match handle.peer {
TargetAddr::Ip(addr) => addr,
TargetAddr::Domain(_, port) => SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port),
};
let mut inner = inner;
inner
.write_all(&new_reply(&ReplyError::Succeeded, peer))
.await?;
inner.flush().await?;
pump_bidi(inner, handle.read, handle.write).await;
Ok(())
}
fn sentinel_bind_peer_addr() -> SocketAddr {
SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0)
}
pub fn register_socks5_openable(
core: &ChannelCore,
on_registry: &Arc<OperationRegistry>,
auth: AuthContext,
dial: Option<DialFn>,
egress: Option<EgressFactory>,
accept: Option<BindAcceptFactory>,
witness: Option<IdentityWitness>,
) -> Result<(), String> {
core.register_openable_with_establisher(
socks5_open_spec(),
Some(socks5_session_establisher(witness)),
make_socks5_pump_handler(dial, egress, accept),
on_registry,
auth,
None,
)
}
#[derive(Clone, Default)]
pub struct Socks5Producer {
dial: Option<DialFn>,
egress: Option<EgressFactory>,
accept: Option<BindAcceptFactory>,
bind_policy: bool,
}
impl Socks5Producer {
pub fn builder() -> Socks5ProducerBuilder {
Socks5ProducerBuilder::default()
}
pub async fn serve_socks5_over<T>(&self, stream: T) -> Result<(), SocksError>
where
T: AsyncRead + AsyncWrite + Unpin,
{
serve_socks5_over(
stream,
self.dial.clone(),
self.egress.clone(),
self.accept.clone(),
self.bind_policy,
)
.await
}
pub fn register(
&self,
core: &ChannelCore,
on_registry: &Arc<OperationRegistry>,
auth: AuthContext,
witness: Option<IdentityWitness>,
) -> Result<(), String> {
register_socks5_openable(
core,
on_registry,
auth,
self.dial.clone(),
self.egress.clone(),
self.accept.clone(),
witness,
)
}
}
#[derive(Default)]
pub struct Socks5ProducerBuilder {
dial: Option<DialFn>,
egress: Option<EgressFactory>,
accept: Option<BindAcceptFactory>,
bind_policy: Option<bool>,
}
impl Socks5ProducerBuilder {
pub fn dial(mut self, dial: DialFn) -> Self {
self.dial = Some(dial);
self
}
pub fn egress(mut self, egress: EgressFactory) -> Self {
self.egress = Some(egress);
self
}
pub fn accept(mut self, accept: BindAcceptFactory) -> Self {
self.accept = Some(accept);
self
}
pub fn bind_policy(mut self, can_bind: bool) -> Self {
self.bind_policy = Some(can_bind);
self
}
pub fn build(self) -> Socks5Producer {
Socks5Producer {
bind_policy: self.bind_policy.unwrap_or(self.accept.is_some()),
dial: self.dial,
egress: self.egress,
accept: self.accept,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_common as common;
use crate::egress::{BindAcceptError, BindAcceptFactoryError, SocksDialError};
use bytes::Bytes;
use common::{
datagram_frame, decode_datagram, echo_dial_witnessed, read_reply_v4, refusing_dial,
write_request,
};
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::io::{AsyncReadExt, AsyncWriteExt, DuplexStream};
fn echo_dial(targets: Arc<tokio::sync::Mutex<Vec<TargetAddr>>>) -> DialFn {
echo_dial_witnessed(targets)
}
async fn raw_connect(
dial: DialFn,
) -> (
tokio::task::JoinHandle<Result<(), SocksError>>,
DuplexStream,
) {
let (client_end, server_end) = tokio::io::duplex(64 * 1024);
let handle = tokio::spawn(serve_socks5_over(server_end, Some(dial), None, None, false));
(handle, client_end)
}
async fn raw_connect_with(
dial: Option<DialFn>,
accept: Option<BindAcceptFactory>,
can_bind: bool,
) -> (
tokio::task::JoinHandle<Result<(), SocksError>>,
DuplexStream,
) {
let (client_end, server_end) = tokio::io::duplex(64 * 1024);
let handle = tokio::spawn(serve_socks5_over(server_end, dial, None, accept, can_bind));
(handle, client_end)
}
async fn greet(client: &mut DuplexStream) {
common::greet(client).await
}
async fn read_reply(client: &mut DuplexStream) -> (u8, [u8; 6]) {
read_reply_v4(client).await
}
#[tokio::test]
async fn connect_round_trip_through_pump_bidi() {
let targets = Arc::new(tokio::sync::Mutex::new(Vec::new()));
let (handle, mut client) = raw_connect(echo_dial(Arc::clone(&targets))).await;
greet(&mut client).await;
write_request(
&mut client,
&Socks5Command::TCPConnect,
&TargetAddr::Domain("echo.test".into(), 443),
)
.await;
let (code, _addr) = read_reply(&mut client).await;
assert_eq!(code, 0x00, "the dial resolved");
client.write_all(b"ping").await.unwrap();
let mut buf = [0u8; 4];
client.read_exact(&mut buf).await.unwrap();
assert_eq!(&buf, b"ping");
assert_eq!(
targets.lock().await.last(),
Some(&TargetAddr::Domain("echo.test".into(), 443))
);
drop(client);
let result = tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("session ends on client EOF")
.expect("join");
result.expect("clean session");
}
#[tokio::test]
async fn dial_refusal_maps_to_reply_code_with_no_data_plane() {
let dial = refusing_dial(SocksDialError::Refused("target down".into()));
let (handle, mut client) = raw_connect(dial).await;
greet(&mut client).await;
write_request(
&mut client,
&Socks5Command::TCPConnect,
&TargetAddr::Domain("down.test".into(), 80),
)
.await;
let (code, _addr) = read_reply(&mut client).await;
assert_eq!(code, 0x05, "dial refusal → ConnectionRefused");
drop(client);
let err = tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("session ends")
.expect("join")
.expect_err("the session surfaces the refusal in-stream");
assert!(matches!(
err,
SocksError::ServerError(SocksServerError::Refused("dial refused in-stream"))
));
}
#[tokio::test]
async fn dial_error_variants_map_to_their_reply_codes() {
for (err, code) in [
(SocksDialError::Refused("x".into()), 0x05),
(SocksDialError::NetworkUnreachable("x".into()), 0x03),
(SocksDialError::HostUnreachable("x".into()), 0x04),
(SocksDialError::NotAllowed("x".into()), 0x02),
(SocksDialError::General("x".into()), 0x01),
] {
let (handle, mut client) = raw_connect(refusing_dial(err)).await;
greet(&mut client).await;
write_request(
&mut client,
&Socks5Command::TCPConnect,
&TargetAddr::Ip(SocketAddr::from(([10, 0, 0, 1], 80))),
)
.await;
let (got, _addr) = read_reply(&mut client).await;
assert_eq!(got, code, "the mapped code rides the reply");
drop(client);
let _ = tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("session ends")
.expect("join");
}
}
#[tokio::test]
async fn missing_dial_refuses_connect_command_not_supported() {
let (client_end, server_end) = tokio::io::duplex(64 * 1024);
let handle = tokio::spawn(serve_socks5_over(server_end, None, None, None, false));
let mut client = client_end;
greet(&mut client).await;
write_request(
&mut client,
&Socks5Command::TCPConnect,
&TargetAddr::Domain("no.dial".into(), 443),
)
.await;
let (code, _addr) = read_reply(&mut client).await;
assert_eq!(code, 0x07, "missing dial → CommandNotSupported");
drop(client);
let err = tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("session ends")
.expect("join")
.expect_err("the refusal is surfaced in-stream");
assert!(matches!(
err,
SocksError::ServerError(SocksServerError::UnknownCommand(_))
));
}
use crate::egress::EgressError;
async fn raw_associate(
egress: Option<EgressFactory>,
) -> (
tokio::task::JoinHandle<Result<(), SocksError>>,
tokio::io::DuplexStream,
) {
let (client_end, server_end) = tokio::io::duplex(64 * 1024);
let dial = echo_dial(Arc::new(tokio::sync::Mutex::new(Vec::new())));
let handle = tokio::spawn(serve_socks5_over(
server_end,
Some(dial),
egress,
None,
false,
));
let mut client = client_end;
greet(&mut client).await;
write_request(
&mut client,
&Socks5Command::UDPAssociate,
&TargetAddr::Domain("relay.test".into(), 1080),
)
.await;
let (code, addr) = read_reply(&mut client).await;
assert_eq!(code, 0x00, "the associate reply succeeds");
assert_eq!(
(addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]),
(127, 0, 0, 1, 0, 0),
"the reply carries the sentinel BND.ADDR 127.0.0.1:0"
);
(handle, client)
}
fn echo_egress_factory() -> EgressFactory {
common::echo_egress_factory()
}
#[tokio::test]
async fn udp_associate_round_trip_through_the_echo_egress() {
let (handle, mut client) = raw_associate(Some(echo_egress_factory())).await;
let target = TargetAddr::Ip(SocketAddr::from(([10, 0, 0, 1], 53)));
for i in 0..5u8 {
let payload = vec![i; 10];
client
.write_all(&datagram_frame(&target, &payload))
.await
.unwrap();
}
for i in 0..5u8 {
let mut buf = vec![0u8; 2 + 7 + 10];
client.read_exact(&mut buf).await.expect("reply frame");
let (src, echoed) = decode_datagram(&buf);
assert_eq!(src, target, "the echo reports the destination");
assert_eq!(&echoed[..], &vec![i; 10][..], "in-order echo");
}
client
.write_all(&datagram_frame(&target, b"alive"))
.await
.unwrap();
let mut buf = vec![0u8; 2 + 7 + 5];
client.read_exact(&mut buf).await.expect("reply frame");
let (_, echoed) = decode_datagram(&buf);
assert_eq!(&echoed[..], b"alive");
drop(client);
let result = tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("session ends on channel EOF")
.expect("join");
result.expect("clean session");
}
#[tokio::test]
async fn udp_empty_payload_datagram_is_legal_not_eof() {
let (handle, mut client) = raw_associate(Some(echo_egress_factory())).await;
let target = TargetAddr::Ip(SocketAddr::from(([10, 0, 0, 2], 53)));
client
.write_all(&datagram_frame(&target, b"pre"))
.await
.unwrap();
let empty = datagram_frame(&target, b"");
assert_eq!(
empty.len(),
2 + 7,
"the empty payload's frame is addr-only inner"
);
client.write_all(&empty).await.unwrap();
client
.write_all(&datagram_frame(&target, b"post"))
.await
.unwrap();
let mut buf = vec![0u8; 2 + 7 + 3];
client.read_exact(&mut buf).await.expect("pre echo");
let (_, echoed) = decode_datagram(&buf);
assert_eq!(&echoed[..], b"pre");
let mut buf = vec![0u8; 2 + 7];
client.read_exact(&mut buf).await.expect("empty echo");
let (src, echoed) = decode_datagram(&buf);
assert_eq!(src, target);
assert!(
echoed.is_empty(),
"the empty-payload datagram round-trips (never EOF)"
);
let mut buf = vec![0u8; 2 + 7 + 4];
client.read_exact(&mut buf).await.expect("post echo");
let (_, echoed) = decode_datagram(&buf);
assert_eq!(&echoed[..], b"post");
drop(client);
let result = tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("session ends")
.expect("join");
result.expect("clean session");
}
#[tokio::test]
async fn udp_undecodable_frame_ends_association() {
let (handle, mut client) = raw_associate(Some(echo_egress_factory())).await;
let inner = vec![0x7Fu8, 1, 2, 3];
let mut garbage_frame = (inner.len() as u16).to_be_bytes().to_vec();
garbage_frame.extend_from_slice(&inner);
client.write_all(&garbage_frame).await.unwrap();
let mut buf = [0u8; 16];
let outcome =
tokio::time::timeout(std::time::Duration::from_secs(5), client.read(&mut buf)).await;
match outcome {
Err(_) => panic!("the association must end after an undecodable frame"),
Ok(Ok(0)) | Ok(Err(_)) => {}
Ok(Ok(n)) => panic!("EOF expected after an undecodable frame — got {n} bytes"),
}
drop(client);
tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("session ends")
.expect("join")
.expect("the stage exited");
}
#[tokio::test]
async fn udp_channel_eof_ends_association_and_connection_stays_healthy() {
let egress = echo_egress_factory();
let (handle, mut client) = raw_associate(Some(Arc::clone(&egress))).await;
use tokio::io::AsyncWriteExt as _;
client.shutdown().await.unwrap();
let result = tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("session ends on channel EOF")
.expect("join");
result.expect("clean session (EOF is the clean exit)");
drop(client);
let (handle, mut client) = raw_associate(Some(Arc::clone(&egress))).await;
let target = TargetAddr::Ip(SocketAddr::from(([10, 0, 0, 3], 53)));
client
.write_all(&datagram_frame(&target, b"fresh"))
.await
.unwrap();
let mut buf = vec![0u8; 2 + 7 + 5];
client.read_exact(&mut buf).await.expect("fresh echo");
let (_, echoed) = decode_datagram(&buf);
assert_eq!(&echoed[..], b"fresh");
drop(client);
tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("session ends")
.expect("join")
.expect("clean");
}
#[tokio::test]
async fn udp_instant_ok_none_egress_does_not_starve_the_stage() {
struct HotNoneEgress {
inbound: tokio::sync::mpsc::UnboundedSender<Bytes>,
outbound: tokio::sync::Mutex<tokio::sync::mpsc::UnboundedReceiver<Bytes>>,
}
impl crate::egress::Egress for HotNoneEgress {
fn send(
&mut self,
addr: TargetAddr,
payload: Bytes,
) -> futures::future::BoxFuture<'_, Result<(), EgressError>> {
Box::pin(async move {
let mut inner = addr
.to_be_bytes()
.map_err(|e| EgressError::General(format!("addr encode: {e}")))?;
inner.extend_from_slice(&payload);
self.inbound
.send(Bytes::from(inner))
.map_err(|_| EgressError::General("egress closed".into()))
})
}
fn next_reply(
&mut self,
) -> futures::future::BoxFuture<'_, Result<Option<Bytes>, EgressError>> {
Box::pin(async move {
match self.outbound.lock().await.try_recv() {
Ok(inner) => Ok(Some(inner)),
Err(_) => Ok(None),
}
})
}
}
let factory: EgressFactory = Arc::new(|| {
Box::pin(async {
let (inbound, outbound) = tokio::sync::mpsc::unbounded_channel();
Ok(Box::new(HotNoneEgress {
inbound,
outbound: tokio::sync::Mutex::new(outbound),
}) as Box<dyn Egress + Send>)
})
});
let (handle, mut client) = raw_associate(Some(factory)).await;
let target = TargetAddr::Ip(SocketAddr::from(([10, 0, 0, 4], 53)));
client
.write_all(&datagram_frame(&target, b"hot"))
.await
.unwrap();
let mut buf = vec![0u8; 2 + 7 + 3];
tokio::time::timeout(std::time::Duration::from_secs(5), async {
client.read_exact(&mut buf).await
})
.await
.expect("the channel branch is not starved — the echo arrived")
.expect("read the echo");
let (_, echoed) = decode_datagram(&buf);
assert_eq!(&echoed[..], b"hot");
drop(client);
tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("session ends")
.expect("join")
.expect("clean");
}
#[tokio::test]
async fn udp_zero_yield_ok_none_egress_does_not_starve_the_stage() {
struct PureHotNoneEgress {
inbound: tokio::sync::mpsc::UnboundedSender<Bytes>,
outbound: tokio::sync::mpsc::UnboundedReceiver<Bytes>,
}
impl crate::egress::Egress for PureHotNoneEgress {
fn send(
&mut self,
addr: TargetAddr,
payload: Bytes,
) -> futures::future::BoxFuture<'_, Result<(), EgressError>> {
Box::pin(async move {
let mut inner = addr
.to_be_bytes()
.map_err(|e| EgressError::General(format!("addr encode: {e}")))?;
inner.extend_from_slice(&payload);
self.inbound
.send(Bytes::from(inner))
.map_err(|_| EgressError::General("egress closed".into()))
})
}
fn next_reply(
&mut self,
) -> futures::future::BoxFuture<'_, Result<Option<Bytes>, EgressError>> {
Box::pin(async {
match self.outbound.try_recv() {
Ok(inner) => Ok(Some(inner)),
Err(_) => Ok(None),
}
})
}
}
let factory: EgressFactory = Arc::new(|| {
Box::pin(async {
let (inbound, outbound) = tokio::sync::mpsc::unbounded_channel();
Ok(Box::new(PureHotNoneEgress { inbound, outbound }) as Box<dyn Egress + Send>)
})
});
let (client_end, server_end) = tokio::io::duplex(64 * 1024);
let handle = tokio::spawn(serve_socks5_over(
server_end,
None,
Some(factory),
None,
false,
));
let mut client = client_end;
greet(&mut client).await;
write_request(
&mut client,
&Socks5Command::UDPAssociate,
&TargetAddr::Domain("pure.hot".into(), 1080),
)
.await;
let (code, _addr) = read_reply(&mut client).await;
assert_eq!(code, 0x00);
let target = TargetAddr::Ip(SocketAddr::from(([10, 0, 0, 6], 53)));
client
.write_all(&datagram_frame(&target, b"pure"))
.await
.unwrap();
let mut buf = vec![0u8; 2 + 7 + 4];
tokio::time::timeout(std::time::Duration::from_secs(5), async {
client.read_exact(&mut buf).await
})
.await
.expect("the channel branch is not starved — the echo arrived")
.expect("read the echo");
let (_, echoed) = decode_datagram(&buf);
assert_eq!(&echoed[..], b"pure");
drop(client);
tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("session ends")
.expect("join")
.expect("clean");
}
#[tokio::test]
async fn missing_egress_refuses_associate_command_not_supported() {
let (client_end, server_end) = tokio::io::duplex(64 * 1024);
let dial = echo_dial(Arc::new(tokio::sync::Mutex::new(Vec::new())));
let handle = tokio::spawn(serve_socks5_over(server_end, Some(dial), None, None, false));
let mut client = client_end;
greet(&mut client).await;
write_request(
&mut client,
&Socks5Command::UDPAssociate,
&TargetAddr::Domain("no.egress".into(), 1080),
)
.await;
let (code, _addr) = read_reply(&mut client).await;
assert_eq!(code, 0x07, "missing egress → CommandNotSupported");
drop(client);
let err = tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("session ends")
.expect("join")
.expect_err("the refusal surfaces in-stream");
assert!(matches!(
err,
SocksError::ServerError(SocksServerError::UnknownCommand(_))
));
let (handle, mut client) =
raw_connect(echo_dial(Arc::new(tokio::sync::Mutex::new(Vec::new())))).await;
greet(&mut client).await;
let (code, _addr) = read_reply_after_connect(&mut client).await;
assert_eq!(code, 0x00, "CONNECT still serves");
drop(client);
tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("session ends")
.expect("join")
.expect("clean");
}
#[tokio::test]
async fn egress_factory_failure_maps_to_reply_code() {
let factory: EgressFactory =
Arc::new(|| Box::pin(async { Err(EgressError::Unreachable("no egress".into())) }));
let (client_end, server_end) = tokio::io::duplex(64 * 1024);
let dial = echo_dial(Arc::new(tokio::sync::Mutex::new(Vec::new())));
let handle = tokio::spawn(serve_socks5_over(
server_end,
Some(dial),
Some(factory),
None,
false,
));
let mut client = client_end;
greet(&mut client).await;
write_request(
&mut client,
&Socks5Command::UDPAssociate,
&TargetAddr::Domain("down.egress".into(), 1080),
)
.await;
let (code, _addr) = read_reply(&mut client).await;
assert_eq!(code, 0x03, "egress-factory refusal → NetworkUnreachable");
drop(client);
let err = tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("session ends")
.expect("join")
.expect_err("the refusal surfaces in-stream");
assert!(matches!(
err,
SocksError::ServerError(SocksServerError::Refused("egress refused in-stream"))
));
}
#[tokio::test]
async fn egress_factory_called_once_and_dropped_with_the_stage() {
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
static CALLS: AtomicUsize = AtomicUsize::new(0);
let dropped = Arc::new(AtomicBool::new(false));
let dropped_for_factory = Arc::clone(&dropped);
let factory: EgressFactory = Arc::new(move || {
CALLS.fetch_add(1, Ordering::SeqCst);
let dropped = Arc::clone(&dropped_for_factory);
Box::pin(async move {
let (inbound, outbound) = tokio::sync::mpsc::unbounded_channel();
struct Witness {
flag: Arc<AtomicBool>,
inbound: tokio::sync::mpsc::UnboundedSender<Bytes>,
outbound: tokio::sync::Mutex<tokio::sync::mpsc::UnboundedReceiver<Bytes>>,
}
impl crate::egress::Egress for Witness {
fn send(
&mut self,
addr: TargetAddr,
payload: Bytes,
) -> futures::future::BoxFuture<'_, Result<(), EgressError>>
{
let mut inner = addr.to_be_bytes().unwrap().to_vec();
inner.extend_from_slice(&payload);
Box::pin(async move {
self.inbound
.send(Bytes::from(inner))
.map_err(|_| EgressError::General("egress closed".into()))
})
}
fn next_reply(
&mut self,
) -> futures::future::BoxFuture<'_, Result<Option<Bytes>, EgressError>>
{
Box::pin(async move {
match self.outbound.lock().await.recv().await {
Some(inner) => Ok(Some(inner)),
None => Err(EgressError::General("egress closed".into())),
}
})
}
}
impl Drop for Witness {
fn drop(&mut self) {
self.flag.store(true, Ordering::SeqCst);
}
}
Ok(Box::new(Witness {
flag: dropped,
inbound,
outbound: tokio::sync::Mutex::new(outbound),
}) as Box<dyn Egress + Send>)
})
});
let (handle, mut client) = raw_associate(Some(factory)).await;
assert_eq!(CALLS.load(Ordering::SeqCst), 1, "the factory ran once");
assert!(
!dropped.load(Ordering::SeqCst),
"the egress lives while the stage runs"
);
let target = TargetAddr::Ip(SocketAddr::from(([10, 0, 0, 4], 53)));
client
.write_all(&datagram_frame(&target, b"witness"))
.await
.unwrap();
let mut buf = vec![0u8; 2 + 7 + 7];
client.read_exact(&mut buf).await.expect("echo");
let (_, echoed) = decode_datagram(&buf);
assert_eq!(&echoed[..], b"witness");
drop(client);
tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("session ends")
.expect("join")
.expect("clean");
assert!(
dropped.load(Ordering::SeqCst),
"stage exit drops the egress (teardown-at-birth)"
);
}
struct FailStream {
fail_read: bool,
fail_write: bool,
}
impl tokio::io::AsyncRead for FailStream {
fn poll_read(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
_buf: &mut tokio::io::ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
if self.fail_read {
Poll::Ready(Err(std::io::Error::other("stage read failed")))
} else {
Poll::Pending
}
}
}
impl tokio::io::AsyncWrite for FailStream {
fn poll_write(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<std::io::Result<usize>> {
if self.fail_write {
Poll::Ready(Err(std::io::Error::other("stage write failed")))
} else {
Poll::Ready(Ok(buf.len()))
}
}
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
Poll::Ready(Ok(()))
}
fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
Poll::Ready(Ok(()))
}
}
struct OneReplyEgress {
reply: Option<Bytes>,
}
impl crate::egress::Egress for OneReplyEgress {
fn send(
&mut self,
_addr: TargetAddr,
_payload: Bytes,
) -> futures::future::BoxFuture<'_, Result<(), EgressError>> {
Box::pin(async move { Err(EgressError::General("send refused".into())) })
}
fn next_reply(
&mut self,
) -> futures::future::BoxFuture<'_, Result<Option<Bytes>, EgressError>> {
let reply = self.reply.take();
Box::pin(async move {
match reply {
Some(inner) => Ok(Some(inner)),
None => Ok(std::future::pending::<Option<Bytes>>().await),
}
})
}
}
struct RelayEndedEgress;
impl crate::egress::Egress for RelayEndedEgress {
fn send(
&mut self,
_addr: TargetAddr,
_payload: Bytes,
) -> futures::future::BoxFuture<'_, Result<(), EgressError>> {
Box::pin(async move { Err(EgressError::General("send refused".into())) })
}
fn next_reply(
&mut self,
) -> futures::future::BoxFuture<'_, Result<Option<Bytes>, EgressError>> {
Box::pin(async move { Err(EgressError::General("relay ended".into())) })
}
}
struct ParkedEgress;
impl crate::egress::Egress for ParkedEgress {
fn send(
&mut self,
_addr: TargetAddr,
_payload: Bytes,
) -> futures::future::BoxFuture<'_, Result<(), EgressError>> {
Box::pin(async move { Err(EgressError::General("send refused".into())) })
}
fn next_reply(
&mut self,
) -> futures::future::BoxFuture<'_, Result<Option<Bytes>, EgressError>> {
Box::pin(async move { Ok(std::future::pending::<Option<Bytes>>().await) })
}
}
#[tokio::test]
async fn datagram_stage_reply_frame_rejected_exit() {
let mut egress = Box::new(OneReplyEgress {
reply: Some(Bytes::from(vec![0u8; crate::wire::MAX_INNER_LEN + 1])),
}) as Box<dyn Egress + Send>;
let exit = run_datagram_stage(
FailStream {
fail_read: false,
fail_write: true,
},
egress.as_mut(),
)
.await;
assert_eq!(exit, StageExit::ReplyFrameRejected);
}
#[tokio::test]
async fn datagram_stage_egress_relay_ended_exit_via_next_reply() {
let mut egress = Box::new(RelayEndedEgress) as Box<dyn Egress + Send>;
let exit = run_datagram_stage(
FailStream {
fail_read: false,
fail_write: false,
},
egress.as_mut(),
)
.await;
assert_eq!(exit, StageExit::EgressEnded);
}
#[tokio::test]
async fn datagram_stage_write_failure_is_the_io_exit() {
let inner = {
let mut v = TargetAddr::Ip(SocketAddr::from(([10, 0, 0, 1], 53)))
.to_be_bytes()
.unwrap()
.to_vec();
v.extend_from_slice(b"payload");
Bytes::from(v)
};
let mut egress = Box::new(OneReplyEgress { reply: Some(inner) }) as Box<dyn Egress + Send>;
let exit = run_datagram_stage(
FailStream {
fail_read: false,
fail_write: true,
},
egress.as_mut(),
)
.await;
assert_eq!(exit, StageExit::Io);
}
#[tokio::test]
async fn datagram_stage_read_failure_is_the_io_exit() {
let mut egress = Box::new(ParkedEgress) as Box<dyn Egress + Send>;
let exit = run_datagram_stage(
FailStream {
fail_read: true,
fail_write: false,
},
egress.as_mut(),
)
.await;
assert_eq!(exit, StageExit::Io);
}
#[tokio::test]
async fn udp_egress_send_failure_ends_the_association_and_drops_the_egress() {
use std::sync::atomic::{AtomicBool, Ordering};
let dropped = Arc::new(AtomicBool::new(false));
let dropped_for_factory = Arc::clone(&dropped);
let factory: EgressFactory = Arc::new(move || {
let dropped = Arc::clone(&dropped_for_factory);
Box::pin(async move {
struct FailingSendWitness {
flag: Arc<AtomicBool>,
}
impl crate::egress::Egress for FailingSendWitness {
fn send(
&mut self,
_addr: TargetAddr,
_payload: Bytes,
) -> futures::future::BoxFuture<'_, Result<(), EgressError>>
{
Box::pin(
async move { Err(EgressError::General("egress send failed".into())) },
)
}
fn next_reply(
&mut self,
) -> futures::future::BoxFuture<'_, Result<Option<Bytes>, EgressError>>
{
Box::pin(async move { Ok(std::future::pending::<Option<Bytes>>().await) })
}
}
impl Drop for FailingSendWitness {
fn drop(&mut self) {
self.flag.store(true, Ordering::SeqCst);
}
}
Ok(Box::new(FailingSendWitness { flag: dropped }) as Box<dyn Egress + Send>)
})
});
let (client_end, server_end) = tokio::io::duplex(64 * 1024);
let handle = tokio::spawn(serve_socks5_over(
server_end,
None,
Some(factory),
None,
false,
));
let mut client = client_end;
greet(&mut client).await;
write_request(
&mut client,
&Socks5Command::UDPAssociate,
&TargetAddr::Domain("send.fail".into(), 1080),
)
.await;
let (code, _addr) = read_reply(&mut client).await;
assert_eq!(code, 0x00, "the associate reply succeeds");
let target = TargetAddr::Ip(SocketAddr::from(([10, 0, 0, 4], 53)));
client
.write_all(&datagram_frame(&target, b"doomed"))
.await
.unwrap();
let mut buf = [0u8; 16];
let outcome =
tokio::time::timeout(std::time::Duration::from_secs(5), client.read(&mut buf))
.await
.expect("read resolves");
match outcome {
Ok(0) | Err(_) => {}
Ok(n) => panic!("EOF expected after the egress send failure — got {n} bytes"),
}
drop(client);
tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("session ends")
.expect("join")
.expect("the stage exited");
assert!(
dropped.load(Ordering::SeqCst),
"the stage exit drops the egress on the failure path too"
);
}
#[tokio::test]
async fn connect_and_associate_serve_on_the_same_registration() {
let targets = Arc::new(tokio::sync::Mutex::new(Vec::new()));
let dial = echo_dial(Arc::clone(&targets));
let (client_end, server_end) = tokio::io::duplex(64 * 1024);
let serve = tokio::spawn(serve_socks5_over(
server_end,
Some(Arc::clone(&dial)),
Some(echo_egress_factory()),
None,
false,
));
let mut client = client_end;
greet(&mut client).await;
write_request(
&mut client,
&Socks5Command::TCPConnect,
&TargetAddr::Domain("tcp.test".into(), 443),
)
.await;
let (code, _addr) = read_reply(&mut client).await;
assert_eq!(code, 0x00);
client.write_all(b"tcp").await.unwrap();
let mut buf = [0u8; 3];
client.read_exact(&mut buf).await.unwrap();
assert_eq!(&buf, b"tcp");
drop(client);
tokio::time::timeout(std::time::Duration::from_secs(5), serve)
.await
.expect("session ends")
.expect("join")
.expect("clean");
let (handle, mut client) = raw_associate(Some(echo_egress_factory())).await;
let target = TargetAddr::Ip(SocketAddr::from(([10, 0, 0, 5], 53)));
client
.write_all(&datagram_frame(&target, b"udp"))
.await
.unwrap();
let mut buf = vec![0u8; 2 + 7 + 3];
client.read_exact(&mut buf).await.expect("echo");
let (_, echoed) = decode_datagram(&buf);
assert_eq!(&echoed[..], b"udp");
drop(client);
tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("session ends")
.expect("join")
.expect("clean");
}
async fn read_reply_after_connect(client: &mut tokio::io::DuplexStream) -> (u8, [u8; 6]) {
write_request(
client,
&Socks5Command::TCPConnect,
&TargetAddr::Domain("echo.test".into(), 443),
)
.await;
read_reply(client).await
}
#[tokio::test]
async fn dispatch_guard_connect_serves_while_associate_bind_refuse() {
let dial = echo_dial(Arc::new(tokio::sync::Mutex::new(Vec::new())));
let (handle, mut client) = raw_connect(Arc::clone(&dial)).await;
greet(&mut client).await;
write_request(
&mut client,
&Socks5Command::TCPConnect,
&TargetAddr::Domain("ok.test".into(), 443),
)
.await;
let (code, _addr) = read_reply(&mut client).await;
assert_eq!(code, 0x00);
client.write_all(b"ok").await.unwrap();
let mut buf = [0u8; 2];
client.read_exact(&mut buf).await.unwrap();
assert_eq!(&buf, b"ok");
drop(client);
let _ = tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("session ends")
.expect("join");
}
fn bind_accept_factory(
requested: Arc<tokio::sync::Mutex<Option<TargetAddr>>>,
) -> BindAcceptFactory {
Arc::new(move |target: TargetAddr| {
let requested = Arc::clone(&requested);
Box::pin(async move {
*requested.lock().await = Some(target.clone());
let bind_addr = SocketAddr::from(([127, 0, 0, 1], 2121));
let accept_future = async {
let (mut echo_end, target_end) = tokio::io::duplex(64 * 1024);
tokio::spawn(async move {
loop {
let mut buf = vec![0u8; 4096];
match echo_end.read(&mut buf).await {
Ok(0) | Err(_) => return,
Ok(n) => {
if echo_end.write_all(&buf[..n]).await.is_err() {
return;
}
}
}
}
});
let (read_half, write_half) = tokio::io::split(target_end);
Ok(BindTargetHandle {
peer: TargetAddr::Ip(SocketAddr::from(([10, 0, 0, 7], 44444))),
read: Box::new(read_half) as Box<dyn AsyncRead + Send + Sync + Unpin>,
write: Box::new(write_half) as Box<dyn AsyncWrite + Send + Sync + Unpin>,
})
};
Ok(BindAccept::new(bind_addr, accept_future))
})
})
}
fn parked_accept_factory() -> BindAcceptFactory {
common::parked_accept_factory()
}
#[tokio::test]
async fn bind_round_trip_reply1_reply2_then_pass_through() {
let requested = Arc::new(tokio::sync::Mutex::new(None));
let accept = bind_accept_factory(Arc::clone(&requested));
let (handle, mut client) = raw_connect_with(None, Some(accept), true).await;
greet(&mut client).await;
write_request(
&mut client,
&Socks5Command::TCPBind,
&TargetAddr::Domain("bind.test".into(), 2121),
)
.await;
let (code, addr) = read_reply(&mut client).await;
assert_eq!(code, 0x00, "reply#1 success");
assert_eq!(
SocketAddr::from((
[addr[0], addr[1], addr[2], addr[3]],
u16::from_be_bytes([addr[4], addr[5]])
)),
SocketAddr::from(([127, 0, 0, 1], 2121)),
"reply#1 carries the factory's bind_addr"
);
assert_eq!(
requested.lock().await.clone(),
Some(TargetAddr::Domain("bind.test".into(), 2121))
);
let (code, addr) = read_reply(&mut client).await;
assert_eq!(code, 0x00, "reply#2 success");
assert_eq!(
SocketAddr::from((
[addr[0], addr[1], addr[2], addr[3]],
u16::from_be_bytes([addr[4], addr[5]])
)),
SocketAddr::from(([10, 0, 0, 7], 44444)),
"reply#2 carries the accepted peer"
);
client.write_all(b"ping").await.unwrap();
let mut buf = [0u8; 4];
client.read_exact(&mut buf).await.unwrap();
assert_eq!(&buf, b"ping");
client.write_all(b"pong").await.unwrap();
client.read_exact(&mut buf).await.unwrap();
assert_eq!(&buf, b"pong");
drop(client);
let result = tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("session ends on client EOF")
.expect("join");
result.expect("clean session");
}
#[tokio::test]
async fn bind_command_gate_refuses_0x02_while_connect_still_serves() {
let requested = Arc::new(tokio::sync::Mutex::new(None));
let accept = bind_accept_factory(Arc::clone(&requested));
let (handle, mut client) = raw_connect_with(None, Some(accept), false).await;
greet(&mut client).await;
write_request(
&mut client,
&Socks5Command::TCPBind,
&TargetAddr::Domain("gate.test".into(), 2121),
)
.await;
let (code, _addr) = read_reply(&mut client).await;
assert_eq!(code, 0x02, "the gate refuses ConnectionNotAllowed");
assert!(
requested.lock().await.is_none(),
"the gate precedes the accept seam — no phantom listener"
);
drop(client);
let err = tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("session ends")
.expect("join")
.expect_err("the refusal surfaces in-stream");
assert!(matches!(
err,
SocksError::ServerError(SocksServerError::Refused("bind command gate: not allowed"))
));
let dial = echo_dial(Arc::new(tokio::sync::Mutex::new(Vec::new())));
let (handle, mut client) = raw_connect_with(Some(dial), None, false).await;
greet(&mut client).await;
write_request(
&mut client,
&Socks5Command::TCPConnect,
&TargetAddr::Domain("still.ok".into(), 443),
)
.await;
let (code, _addr) = read_reply(&mut client).await;
assert_eq!(code, 0x00, "CONNECT is unaffected by the bind gate");
drop(client);
let _ = tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("session ends")
.expect("join");
}
#[tokio::test]
async fn bind_quiet_wait_garbage_byte_ends_the_session() {
let accept = parked_accept_factory();
let (handle, mut client) = raw_connect_with(None, Some(accept), true).await;
greet(&mut client).await;
write_request(
&mut client,
&Socks5Command::TCPBind,
&TargetAddr::Domain("quiet.test".into(), 2121),
)
.await;
let (code, _addr) = read_reply(&mut client).await;
assert_eq!(code, 0x00, "reply#1 succeeded");
client.write_all(&[0x05, 0x02]).await.unwrap();
let mut buf = [0u8; 10];
let outcome =
tokio::time::timeout(std::time::Duration::from_secs(5), client.read(&mut buf)).await;
match outcome {
Err(_) => panic!("the session must end on quiet-wait garbage"),
Ok(Err(_)) => {}
Ok(Ok(0)) => {}
Ok(Ok(n)) => panic!("no reply#2 after garbage — got {n} bytes"),
}
drop(client);
let err = tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("session ends")
.expect("join")
.expect_err("garbage surfaces as the control-garbage error");
assert!(matches!(
err,
SocksError::ServerError(SocksServerError::UnexpectedUdpControlGarbage(_))
));
}
#[tokio::test]
async fn bind_client_gone_teardown_drops_the_accept_future() {
use std::sync::atomic::{AtomicBool, Ordering};
let dropped = Arc::new(AtomicBool::new(false));
let factory: BindAcceptFactory = {
let dropped = Arc::clone(&dropped);
Arc::new(move |_target: TargetAddr| {
let dropped = Arc::clone(&dropped);
Box::pin(async move {
struct Owned(Arc<AtomicBool>);
impl Drop for Owned {
fn drop(&mut self) {
self.0.store(true, Ordering::SeqCst);
}
}
let owner = Owned(Arc::clone(&dropped));
Ok(BindAccept::new(
SocketAddr::from(([127, 0, 0, 1], 2121)),
async move {
std::future::pending::<()>().await;
drop(owner);
unreachable!("pending never resolves")
},
))
})
})
};
let (handle, mut client) = raw_connect_with(None, Some(factory), true).await;
greet(&mut client).await;
write_request(
&mut client,
&Socks5Command::TCPBind,
&TargetAddr::Domain("gone.test".into(), 2121),
)
.await;
let (code, _addr) = read_reply(&mut client).await;
assert_eq!(code, 0x00, "reply#1 succeeded");
drop(client);
tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("session ends on client EOF")
.expect("join")
.expect("clean session (EOF is not an error)");
assert!(
dropped.load(Ordering::SeqCst),
"the accept future's owned resource dropped with the session"
);
}
#[tokio::test]
async fn bind_factory_failure_maps_to_reply1() {
let factory: BindAcceptFactory = Arc::new(move |_target: TargetAddr| {
Box::pin(async move { Err(BindAcceptFactoryError::Refused("port held".into())) })
});
let (handle, mut client) = raw_connect_with(None, Some(factory), true).await;
greet(&mut client).await;
write_request(
&mut client,
&Socks5Command::TCPBind,
&TargetAddr::Domain("factory.fail".into(), 2121),
)
.await;
let (code, _addr) = read_reply(&mut client).await;
assert_eq!(code, 0x05, "factory refusal → reply#1 ConnectionRefused");
drop(client);
let err = tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("session ends")
.expect("join")
.expect_err("the refusal surfaces in-stream");
assert!(matches!(
err,
SocksError::ServerError(SocksServerError::Refused("bind factory refused in-stream"))
));
}
#[tokio::test]
async fn bind_accept_stage_failure_maps_to_reply2() {
let factory: BindAcceptFactory = Arc::new(move |_target: TargetAddr| {
Box::pin(async move {
Ok(BindAccept::new(
SocketAddr::from(([127, 0, 0, 1], 2121)),
async move { Err(BindAcceptError::NotAllowed("policy".into())) },
))
})
});
let (handle, mut client) = raw_connect_with(None, Some(factory), true).await;
greet(&mut client).await;
write_request(
&mut client,
&Socks5Command::TCPBind,
&TargetAddr::Domain("accept.fail".into(), 2121),
)
.await;
let (code1, _addr) = read_reply(&mut client).await;
assert_eq!(code1, 0x00, "reply#1 succeeded — the factory resolved");
let (code2, _addr) = read_reply(&mut client).await;
assert_eq!(
code2, 0x02,
"accept-stage refusal → reply#2 ConnectionNotAllowed"
);
drop(client);
let err = tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("session ends")
.expect("join")
.expect_err("the refusal surfaces in-stream");
assert!(matches!(
err,
SocksError::ServerError(SocksServerError::Refused("bind accept refused in-stream"))
));
}
#[tokio::test]
async fn bind_no_factory_serves_the_wrapper_aware_variant_3() {
let (handle, mut client) = raw_connect_with(None, None, true).await;
greet(&mut client).await;
write_request(
&mut client,
&Socks5Command::TCPBind,
&TargetAddr::Domain("variant3.test".into(), 2121),
)
.await;
let (code, addr) = read_reply(&mut client).await;
assert_eq!(code, 0x00, "variant 3 serves reply#1");
assert_eq!(
(addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]),
(0, 0, 0, 0, 0, 0),
"reply#1 carries the wrapper-aware sentinel"
);
let (code, addr) = read_reply(&mut client).await;
assert_eq!(code, 0x00, "variant 3 serves reply#2");
assert_eq!(
(addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]),
(0, 0, 0, 0, 0, 0),
"reply#2 carries the wrapper-aware sentinel peer"
);
client.write_all(b"hi").await.unwrap();
let mut eof = [0u8; 1];
let outcome =
tokio::time::timeout(std::time::Duration::from_secs(5), client.read(&mut eof))
.await
.expect("read resolves");
assert_eq!(outcome.unwrap_or(0), 0, "the empty half EOFs");
drop(client);
let result = tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("session ends")
.expect("join");
result.expect("clean session");
}
#[tokio::test]
async fn bind_quiet_wait_is_not_under_the_establishment_bound() {
let accept = parked_accept_factory();
let (handle, mut client) = raw_connect_with(None, Some(accept), true).await;
greet(&mut client).await;
write_request(
&mut client,
&Socks5Command::TCPBind,
&TargetAddr::Domain("ftp.wait".into(), 2121),
)
.await;
let (code, _addr) = read_reply(&mut client).await;
assert_eq!(code, 0x00, "reply#1 succeeded");
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
client.write_all(b"x").await.unwrap();
let mut buf = [0u8; 1];
let outcome =
tokio::time::timeout(std::time::Duration::from_secs(5), client.read(&mut buf)).await;
match outcome {
Err(_) => panic!("the session must end on quiet-wait garbage"),
Ok(Ok(0)) | Ok(Err(_)) => {}
Ok(Ok(n)) => panic!("no reply#2 after garbage — got {n} bytes"),
}
drop(client);
let _ = tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("session ends")
.expect("join");
}
#[tokio::test]
async fn bind_quiet_wait_read_error_ends_the_wait_cleanly() {
use std::cell::Cell;
use std::sync::atomic::{AtomicBool, Ordering};
let dropped = Arc::new(AtomicBool::new(false));
struct FailReadStream {
polled: Cell<bool>,
}
impl tokio::io::AsyncRead for FailReadStream {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
_buf: &mut tokio::io::ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
if self.polled.get() {
Poll::Ready(Err(std::io::Error::other("quiet-wait read failed")))
} else {
self.polled.set(true);
cx.waker().wake_by_ref();
Poll::Pending
}
}
}
impl tokio::io::AsyncWrite for FailReadStream {
fn poll_write(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<std::io::Result<usize>> {
Poll::Ready(Ok(buf.len()))
}
fn poll_flush(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<std::io::Result<()>> {
Poll::Ready(Ok(()))
}
fn poll_shutdown(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<std::io::Result<()>> {
Poll::Ready(Ok(()))
}
}
struct OwnedAccept(Arc<AtomicBool>);
impl Drop for OwnedAccept {
fn drop(&mut self) {
self.0.store(true, Ordering::SeqCst);
}
}
let accept = {
let dropped = Arc::clone(&dropped);
BindAccept::new(SocketAddr::from(([127, 0, 0, 1], 2121)), async move {
let owner = OwnedAccept(dropped);
std::future::pending::<()>().await;
drop(owner);
unreachable!("pending never resolves")
})
};
let outcome = bind_quiet_wait_and_serve(
FailReadStream {
polled: Cell::new(false),
},
accept,
)
.await;
outcome.expect("an error-shaped quiet wait is not an error exit");
assert!(
dropped.load(Ordering::SeqCst),
"the accept future dropped with the wait (the client-gone contract, error arm)"
);
}
#[tokio::test]
async fn bind_domain_peer_falls_back_to_the_unspecified_address() {
let factory: BindAcceptFactory = Arc::new(move |_target: TargetAddr| {
Box::pin(async move {
Ok(BindAccept::new(
SocketAddr::from(([127, 0, 0, 1], 2121)),
async {
let (mut echo_end, target_end) = tokio::io::duplex(64);
tokio::spawn(async move {
let mut buf = [0u8; 64];
loop {
match echo_end.read(&mut buf).await {
Ok(0) | Err(_) => return,
Ok(n) => {
if echo_end.write_all(&buf[..n]).await.is_err() {
return;
}
}
}
}
});
let (read_half, write_half) = tokio::io::split(target_end);
Ok(BindTargetHandle {
peer: TargetAddr::Domain("peer.example".into(), 44444),
read: Box::new(read_half) as Box<dyn AsyncRead + Send + Sync + Unpin>,
write: Box::new(write_half)
as Box<dyn AsyncWrite + Send + Sync + Unpin>,
})
},
))
})
});
let (client_end, server_end) = tokio::io::duplex(64 * 1024);
let handle = tokio::spawn(serve_socks5_over(
server_end,
None,
None,
Some(factory),
true,
));
let mut client = client_end;
greet(&mut client).await;
write_request(
&mut client,
&Socks5Command::TCPBind,
&TargetAddr::Domain("domain.peer".into(), 2121),
)
.await;
let (code1, _addr) = read_reply(&mut client).await;
assert_eq!(code1, 0x00, "reply#1 succeeded");
let (code2, addr) = read_reply(&mut client).await;
assert_eq!(code2, 0x00, "reply#2 succeeded");
assert_eq!(
SocketAddr::from((
[addr[0], addr[1], addr[2], addr[3]],
u16::from_be_bytes([addr[4], addr[5]])
)),
SocketAddr::from(([0, 0, 0, 0], 44444)),
"the Domain peer fell back to 0.0.0.0:port"
);
client.write_all(b"ping").await.unwrap();
let mut buf = [0u8; 4];
tokio::time::timeout(
std::time::Duration::from_secs(5),
client.read_exact(&mut buf),
)
.await
.expect("the data plane echoes")
.expect("read the echo");
assert_eq!(&buf, b"ping");
drop(client);
tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("session ends")
.expect("join")
.expect("clean");
}
#[tokio::test]
async fn pump_handler_tracks_the_data_plane_to_completion() {
let targets = Arc::new(tokio::sync::Mutex::new(Vec::new()));
let dial = echo_dial(Arc::clone(&targets));
let handler = make_socks5_pump_handler(Some(Arc::clone(&dial)), None, None);
let _ = &handler;
let (client_end, server_end) = tokio::io::duplex(64 * 1024);
let serve = tokio::spawn(serve_socks5_over(server_end, Some(dial), None, None, false));
let mut client = client_end;
greet(&mut client).await;
write_request(
&mut client,
&Socks5Command::TCPConnect,
&TargetAddr::Domain("inline.test".into(), 443),
)
.await;
let (code, _addr) = read_reply(&mut client).await;
assert_eq!(code, 0x00);
client.write_all(b"tick").await.unwrap();
let mut buf = [0u8; 4];
client.read_exact(&mut buf).await.unwrap();
assert_eq!(&buf, b"tick");
drop(client);
let result = tokio::time::timeout(std::time::Duration::from_secs(5), serve)
.await
.expect("the session ends on client EOF — the data plane is tracked")
.expect("join");
result.expect("clean session");
assert_eq!(
targets.lock().await.last(),
Some(&TargetAddr::Domain("inline.test".into(), 443))
);
}
#[tokio::test]
async fn establisher_records_identity_and_defaults_the_establishment() {
let witness: IdentityWitness = Arc::new(tokio::sync::Mutex::new(None));
let establisher = socks5_session_establisher(Some(Arc::clone(&witness)));
let auth = AuthContext {
identity: Some(alkcall::core::auth::Identity {
id: "opener".to_string(),
scopes: vec![SOCKS5_OPEN_SCOPE.to_string()],
resources: Default::default(),
}),
..AuthContext::anonymous(b"alk/socks5")
};
let establishment = establisher(serde_json::json!({}), auth)
.await
.expect("the session gate passes");
assert!(establishment.plan.is_none(), "no plan payload");
assert_eq!(
witness.lock().await.as_deref(),
Some("opener"),
"the per-call opener identity is recorded"
);
let establisher = socks5_session_establisher(None);
let auth = AuthContext::anonymous(b"alk/socks5");
let establishment = establisher(serde_json::json!({}), auth)
.await
.expect("the session gate passes without a witness");
assert!(establishment.plan.is_none(), "no plan payload");
}
#[test]
fn pump_handler_resolves_the_bind_gate_from_the_opener_identity() {
fn gate(scopes: &[&str]) -> bool {
let auth = AuthContext {
identity: Some(alkcall::core::auth::Identity {
id: "caller".to_string(),
scopes: scopes.iter().map(|s| s.to_string()).collect(),
resources: Default::default(),
}),
..AuthContext::anonymous(b"alk/socks5")
};
auth.identity
.as_ref()
.map(|i| i.scopes.iter().any(|s| s == SOCKS5_BIND_SCOPE))
.unwrap_or(false)
}
assert!(!gate(&[SOCKS5_OPEN_SCOPE]), "open does not imply bind");
assert!(gate(&[SOCKS5_BIND_SCOPE]));
assert!(gate(&[SOCKS5_OPEN_SCOPE, SOCKS5_BIND_SCOPE]));
assert!(!gate(&[]), "scope-less fails closed");
}
#[tokio::test]
async fn builder_shape_round_trips() {
let producer = Socks5Producer::builder()
.dial(echo_dial(Arc::new(tokio::sync::Mutex::new(Vec::new()))))
.build();
let (client_end, server_end) = tokio::io::duplex(64 * 1024);
let serve = tokio::spawn(async move { producer.serve_socks5_over(server_end).await });
let mut client = client_end;
greet(&mut client).await;
write_request(
&mut client,
&Socks5Command::TCPConnect,
&TargetAddr::Domain("builder.test".into(), 443),
)
.await;
let (code, _addr) = read_reply(&mut client).await;
assert_eq!(code, 0x00);
client.write_all(b"ping").await.unwrap();
let mut buf = [0u8; 4];
client.read_exact(&mut buf).await.unwrap();
assert_eq!(&buf, b"ping");
drop(client);
let result = tokio::time::timeout(std::time::Duration::from_secs(5), serve)
.await
.expect("session ends")
.expect("join");
result.expect("clean");
}
#[tokio::test]
async fn builder_bind_policy_false_refuses_bind_while_connect_associate_serve() {
let requested = Arc::new(tokio::sync::Mutex::new(None));
let producer = Socks5Producer::builder()
.dial(echo_dial(Arc::new(tokio::sync::Mutex::new(Vec::new()))))
.egress(echo_egress_factory())
.accept(bind_accept_factory(Arc::clone(&requested)))
.bind_policy(false)
.build();
let (client_end, server_end) = tokio::io::duplex(64 * 1024);
let serve = tokio::spawn(async move { producer.serve_socks5_over(server_end).await });
let mut client = client_end;
greet(&mut client).await;
write_request(
&mut client,
&Socks5Command::TCPBind,
&TargetAddr::Domain("policy.closed".into(), 2121),
)
.await;
let (code, _addr) = read_reply(&mut client).await;
assert_eq!(
code, 0x02,
"the explicit policy refuses ConnectionNotAllowed"
);
assert!(
requested.lock().await.is_none(),
"the closed gate precedes the accept seam — no phantom listener"
);
drop(client);
let err = tokio::time::timeout(std::time::Duration::from_secs(5), serve)
.await
.expect("session ends")
.expect("join")
.expect_err("the refusal surfaces in-stream");
assert!(matches!(
err,
SocksError::ServerError(SocksServerError::Refused("bind command gate: not allowed"))
));
let producer = Socks5Producer::builder()
.dial(echo_dial(Arc::new(tokio::sync::Mutex::new(Vec::new()))))
.egress(echo_egress_factory())
.accept(bind_accept_factory(Arc::new(tokio::sync::Mutex::new(None))))
.bind_policy(false)
.build();
let (client_end, server_end) = tokio::io::duplex(64 * 1024);
let serve = tokio::spawn(async move { producer.serve_socks5_over(server_end).await });
let mut client = client_end;
greet(&mut client).await;
write_request(
&mut client,
&Socks5Command::TCPConnect,
&TargetAddr::Domain("policy.connect".into(), 443),
)
.await;
let (code, _addr) = read_reply(&mut client).await;
assert_eq!(code, 0x00, "CONNECT is unaffected by the bind policy");
client.write_all(b"ping").await.unwrap();
let mut buf = [0u8; 4];
client.read_exact(&mut buf).await.unwrap();
assert_eq!(&buf, b"ping");
drop(client);
let result = tokio::time::timeout(std::time::Duration::from_secs(5), serve)
.await
.expect("session ends")
.expect("join");
result.expect("clean");
let producer = Socks5Producer::builder()
.dial(echo_dial(Arc::new(tokio::sync::Mutex::new(Vec::new()))))
.egress(echo_egress_factory())
.accept(bind_accept_factory(Arc::new(tokio::sync::Mutex::new(None))))
.bind_policy(false)
.build();
let (client_end, server_end) = tokio::io::duplex(64 * 1024);
let serve = tokio::spawn(async move { producer.serve_socks5_over(server_end).await });
let mut client = client_end;
greet(&mut client).await;
write_request(
&mut client,
&Socks5Command::UDPAssociate,
&TargetAddr::Domain("policy.udp".into(), 1080),
)
.await;
let (code, addr) = read_reply(&mut client).await;
assert_eq!(code, 0x00, "ASSOCIATE is unaffected by the bind policy");
assert_eq!(
(addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]),
(127, 0, 0, 1, 0, 0),
"the reply carries the sentinel BND.ADDR"
);
let target = TargetAddr::Ip(SocketAddr::from(([10, 0, 0, 8], 53)));
client
.write_all(&datagram_frame(&target, b"policy"))
.await
.unwrap();
let mut buf = vec![0u8; 2 + 7 + 6];
client.read_exact(&mut buf).await.expect("echo");
let (_, echoed) = decode_datagram(&buf);
assert_eq!(&echoed[..], b"policy");
drop(client);
let result = tokio::time::timeout(std::time::Duration::from_secs(5), serve)
.await
.expect("session ends")
.expect("join");
result.expect("clean");
}
#[tokio::test]
async fn builder_default_bind_policy_follows_accept_presence() {
let requested = Arc::new(tokio::sync::Mutex::new(None));
let producer = Socks5Producer::builder()
.dial(echo_dial(Arc::new(tokio::sync::Mutex::new(Vec::new()))))
.accept(bind_accept_factory(Arc::clone(&requested)))
.build();
let (client_end, server_end) = tokio::io::duplex(64 * 1024);
let serve = tokio::spawn(async move { producer.serve_socks5_over(server_end).await });
let mut client = client_end;
greet(&mut client).await;
write_request(
&mut client,
&Socks5Command::TCPBind,
&TargetAddr::Domain("default.open".into(), 2121),
)
.await;
let (code, _addr) = read_reply(&mut client).await;
assert_eq!(code, 0x00, "the presence default serves BIND");
assert!(
requested.lock().await.is_some(),
"the accept seam ran (the factory is wired and the gate open)"
);
drop(client);
let result = tokio::time::timeout(std::time::Duration::from_secs(5), serve)
.await
.expect("session ends")
.expect("join");
result.expect("clean");
let producer = Socks5Producer::builder()
.dial(echo_dial(Arc::new(tokio::sync::Mutex::new(Vec::new()))))
.build();
let (client_end, server_end) = tokio::io::duplex(64 * 1024);
let serve = tokio::spawn(async move { producer.serve_socks5_over(server_end).await });
let mut client = client_end;
greet(&mut client).await;
write_request(
&mut client,
&Socks5Command::TCPBind,
&TargetAddr::Domain("default.closed".into(), 2121),
)
.await;
let (code, _addr) = read_reply(&mut client).await;
assert_eq!(code, 0x02, "no factory + no policy = the closed default");
drop(client);
let err = tokio::time::timeout(std::time::Duration::from_secs(5), serve)
.await
.expect("session ends")
.expect("join")
.expect_err("the refusal surfaces in-stream");
assert!(matches!(
err,
SocksError::ServerError(SocksServerError::Refused("bind command gate: not allowed"))
));
}
#[test]
fn sentinel_reply_addr_shape() {
assert_eq!(sentinel_reply_addr(), SocketAddr::from(([127, 0, 0, 1], 0)));
}
#[test]
fn producer_builder_seams_survive_clone() {
let producer = Socks5Producer::builder()
.dial(echo_dial(Arc::new(tokio::sync::Mutex::new(Vec::new()))))
.build();
let cloned = producer.clone();
let (client_end, server_end) = tokio::io::duplex(64);
drop(client_end);
drop(cloned.serve_socks5_over(server_end));
}
}