mod harness;
use std::sync::Arc;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use alksocks::consumer::{Socks5ChannelSession, TakenInner};
use alksocks::egress::SocksDialError;
use alksocks::error::SocksSessionError;
use alksocks::rfc1928::consts;
use alksocks::rfc1928::util::target_addr::{TargetAddr, ToTargetAddr};
use alksocks::rfc1928::SocksError as SocksErrorKind;
use harness::{
echo_dial, refusing_dial, wire_consumer_side, wire_consumer_side_with, ConsumerSideTopology,
};
fn expect_forbidden(err: &SocksSessionError) -> bool {
match err {
SocksSessionError::Open(open) => match open.open_ref() {
alkcall::channels::client::ChannelOpenError::CallFailed { error } => {
error.code == "FORBIDDEN"
}
_ => false,
},
_ => false,
}
}
fn refusing_session_dial() -> alksocks::egress::DialFn {
refusing_dial(SocksDialError::Refused("target down".into()))
}
async fn no_leaked_channels(topo: &ConsumerSideTopology) {
let deadline = std::time::Duration::from_secs(5);
let consumer_ids = topo.consumer.manager().channel_ids();
assert!(
consumer_ids.iter().all(|&id| id == 0),
"consumer leaked channel entries: {consumer_ids:?}"
);
tokio::time::timeout(deadline, async {
loop {
let producer_ids = topo.producer_manager.channel_ids();
if producer_ids.iter().all(|&id| id == 0) {
return;
}
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
})
.await
.expect("the producer's handler-exit teardown reaps the entry");
}
#[tokio::test]
async fn connect_echo_round_trip_and_dial_witness() {
let topo = wire_consumer_side(echo_dial()).await;
let mut session = Socks5ChannelSession::connect(
topo.consumer.as_ref(),
("echo.test", 443u16).to_target_addr().unwrap(),
)
.await
.expect("the session connects");
session
.split()
.expect("post-reply pipe")
.1
.write_all(b"ping")
.await
.expect("write the probe");
let (mut read, _) = session.split().expect("post-reply pipe");
let mut buf = [0u8; 4];
tokio::time::timeout(
std::time::Duration::from_secs(10),
read.read_exact(&mut buf),
)
.await
.expect("echo timed out")
.expect("read the echo");
assert_eq!(&buf, b"ping");
assert_eq!(
topo.identity_witness.lock().await.as_deref(),
Some("consumer")
);
session.close().await;
no_leaked_channels(&topo).await;
}
#[tokio::test]
async fn target_construction_ip_strings_parse_domains_pass_through() {
let targets = Arc::new(tokio::sync::Mutex::new(Vec::new()));
let topo = wire_consumer_side(harness::witnessing_echo_dial(Arc::clone(&targets))).await;
let session = Socks5ChannelSession::connect(
topo.consumer.as_ref(),
("echo.test", 443u16).to_target_addr().unwrap(),
)
.await
.expect("domain connect");
session.close().await;
let session = Socks5ChannelSession::connect(
topo.consumer.as_ref(),
("10.0.0.1", 8443u16).to_target_addr().unwrap(),
)
.await
.expect("ip-string connect");
session.close().await;
let dialed = targets.lock().await.clone();
assert_eq!(
dialed,
vec![
TargetAddr::Domain("echo.test".to_owned(), 443),
TargetAddr::Ip(std::net::SocketAddr::from(([10, 0, 0, 1], 8443)))
],
"the dial seam received the parsed target forms (ADR-012)"
);
no_leaked_channels(&topo).await;
}
#[tokio::test]
async fn refused_dial_surfaces_rfc_reply_code_no_phantom_channel() {
let topo = wire_consumer_side(refusing_session_dial()).await;
let err = Socks5ChannelSession::connect(
topo.consumer.as_ref(),
("down.test", 80u16).to_target_addr().unwrap(),
)
.await
.expect_err("the dial refused; the reply code rides the session error");
match &err {
SocksSessionError::Rfc(SocksErrorKind::ReplyError(code)) => {
assert_eq!(code.as_u8(), consts::SOCKS5_REPLY_CONNECTION_REFUSED);
}
other => panic!("expected an Rfc reply-code refusal, got {other:?}"),
}
no_leaked_channels(&topo).await;
let topo2 = wire_consumer_side(echo_dial()).await;
let session = Socks5ChannelSession::connect(
topo2.consumer.as_ref(),
("echo.test", 443u16).to_target_addr().unwrap(),
)
.await
.expect("fresh connect after the refusal");
session.close().await;
}
#[tokio::test]
async fn forbidden_open_is_a_typed_open_error_with_no_session() {
let bare = alkcall::core::auth::Identity {
id: "scopeless".to_string(),
scopes: vec![],
resources: Default::default(),
};
let topo = wire_consumer_side_with(echo_dial(), None, None, Some(bare)).await;
let err = Socks5ChannelSession::connect(
topo.consumer.as_ref(),
("gated.test", 443u16).to_target_addr().unwrap(),
)
.await
.expect_err("a scope-less identity fails closed");
assert!(
expect_forbidden(&err),
"FORBIDDEN is the open error: {err:?}"
);
no_leaked_channels(&topo).await;
}
#[tokio::test]
async fn drop_teardown_reaps_and_connection_stays_healthy() {
let topo = wire_consumer_side(echo_dial()).await;
let mut session = Socks5ChannelSession::connect(
topo.consumer.as_ref(),
("dropped.test", 443u16).to_target_addr().unwrap(),
)
.await
.expect("the session connects");
session
.split()
.expect("pipe")
.1
.write_all(b"mid")
.await
.expect("write before the drop");
let (mut read, _) = session.split().expect("pipe");
let mut buf = [0u8; 3];
read.read_exact(&mut buf).await.expect("echo before drop");
assert_eq!(&buf, b"mid");
drop(session);
no_leaked_channels(&topo).await;
let mut fresh = Socks5ChannelSession::connect(
topo.consumer.as_ref(),
("fresh.test", 443u16).to_target_addr().unwrap(),
)
.await
.expect("fresh connect after the drop");
fresh
.split()
.expect("pipe")
.1
.write_all(b"again")
.await
.expect("write");
let (mut read, _) = fresh.split().expect("pipe");
let mut buf = [0u8; 5];
tokio::time::timeout(
std::time::Duration::from_secs(10),
read.read_exact(&mut buf),
)
.await
.expect("echo timed out")
.expect("read the fresh echo");
assert_eq!(&buf, b"again");
fresh.close().await;
no_leaked_channels(&topo).await;
}
#[tokio::test]
async fn half_close_write_eof_ends_the_producer_pump() {
let topo = wire_consumer_side(echo_dial()).await;
let session = Socks5ChannelSession::connect(
topo.consumer.as_ref(),
("halfclose.test", 443u16).to_target_addr().unwrap(),
)
.await
.expect("the session connects");
let channel_id = session.channel_id();
assert!(channel_id >= 1, "a producer-allocated id");
assert!(topo.consumer.manager().has_channel(channel_id));
let TakenInner {
session: _owner,
inner,
} = session.into_inner().expect("the session is live");
let (mut read, mut write) = tokio::io::split(inner);
write.write_all(b"pre").await.expect("write");
let mut buf = [0u8; 3];
read.read_exact(&mut buf).await.expect("echo");
assert_eq!(&buf, b"pre");
write.shutdown().await.expect("half-close the pipe");
drop(write);
let mut eof = [0u8; 1];
tokio::time::timeout(std::time::Duration::from_secs(10), async {
loop {
match read.read(&mut eof).await {
Ok(0) | Err(_) => break,
Ok(_) => continue,
}
}
})
.await
.expect("the read resolves after the half-close");
drop(read);
drop(_owner);
no_leaked_channels(&topo).await;
}
#[tokio::test]
async fn into_inner_pairs_the_pipe_with_the_teardown_owner() {
let topo = wire_consumer_side(echo_dial()).await;
let session = Socks5ChannelSession::connect(
topo.consumer.as_ref(),
("inner.test", 443u16).to_target_addr().unwrap(),
)
.await
.expect("the session connects");
let channel_id = session.channel_id();
let TakenInner { session, inner } = session.into_inner().expect("the session is live");
assert!(topo.consumer.manager().has_channel(channel_id));
let (mut read, write) = tokio::io::split(inner);
let mut buf = [0u8; 1];
let _ = tokio::time::timeout(std::time::Duration::from_millis(50), read.read(&mut buf)).await;
drop(write);
drop(read);
let reaped = session.close().await;
assert!(reaped, "the adopted channel entry existed");
no_leaked_channels(&topo).await;
}
#[test]
fn session_type_is_not_clone() {
fn assert_send<T: Send>() {}
assert_send::<Socks5ChannelSession>();
}
#[tokio::test]
async fn both_directions_survive_across_halves() {
let topo = wire_consumer_side(echo_dial()).await;
let mut session = Socks5ChannelSession::connect(
topo.consumer.as_ref(),
("halves.test", 443u16).to_target_addr().unwrap(),
)
.await
.expect("the session connects");
let (_, mut write) = session.split().expect("pipe");
write.write_all(b"one").await.expect("write one");
write.flush().await.expect("flush");
write.write_all(b"two").await.expect("write two");
write.flush().await.expect("flush");
drop(write);
let (mut read, _) = session.split().expect("pipe");
let mut buf = [0u8; 6];
tokio::time::timeout(
std::time::Duration::from_secs(10),
read.read_exact(&mut buf),
)
.await
.expect("echo timed out")
.expect("read the echo");
assert_eq!(&buf, b"onetwo");
session.close().await;
no_leaked_channels(&topo).await;
}
#[tokio::test]
async fn close_after_into_inner_reaps() {
let topo = wire_consumer_side(echo_dial()).await;
let session = Socks5ChannelSession::connect(
topo.consumer.as_ref(),
("spent.test", 443u16).to_target_addr().unwrap(),
)
.await
.expect("the session connects");
let channel_id = session.channel_id();
let TakenInner { session, .. } = session.into_inner().expect("the session is live");
assert!(session.close().await, "the entry existed before the close");
no_leaked_channels(&topo).await;
assert!(channel_id >= 1);
}