mod harness;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use alksocks::consumer::{AssociateSession, Socks5ChannelSession};
use alksocks::egress::EgressError;
use alksocks::error::SocksSessionError;
use alksocks::rfc1928::consts;
use alksocks::rfc1928::util::target_addr::{TargetAddr, ToTargetAddr};
use alksocks::SocksError as SocksErrorKind;
use harness::{echo_dial, echo_egress, refusing_egress, wire_consumer_side_with};
async fn no_leaked_channels(topo: &harness::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 associate_round_trips_multiple_datagrams_one_association() {
let topo = wire_consumer_side_with(
echo_dial(),
Some(echo_egress()),
None,
Some(harness::consumer_identity()),
)
.await;
let mut assoc = AssociateSession::associate(topo.consumer.as_ref())
.await
.expect("the association associates");
let relay = assoc
.relay_addr()
.expect("the reply carried BND.ADDR")
.into_string_and_port();
assert_eq!(relay.0, "127.0.0.1", "the in-band sentinel BND.ADDR");
assert_eq!(relay.1, 0);
let target = TargetAddr::Ip("10.0.0.1:53".parse().unwrap());
for i in 0..5u8 {
let payload = vec![i; 10];
assoc
.send_datagram(&target, &payload)
.await
.expect("send datagram");
}
for i in 0..5u8 {
let (src, payload) =
tokio::time::timeout(std::time::Duration::from_secs(10), assoc.recv_datagram())
.await
.expect("recv within timeout")
.expect("a datagram (not eof)")
.expect("a datagram");
assert_eq!(src, target, "the echo reports the destination");
assert_eq!(payload, bytes::Bytes::from(vec![i; 10]), "in-order echo");
}
assoc.send_datagram(&target, b"alive").await.expect("send");
let (_, payload) =
tokio::time::timeout(std::time::Duration::from_secs(10), assoc.recv_datagram())
.await
.expect("recv timed out")
.expect("a datagram (not eof)")
.expect("a datagram");
assert_eq!(&payload[..], b"alive");
assert!(
topo.consumer.manager().has_channel(assoc.channel_id()),
"the adopted channel entry exists while associated"
);
let reaped = assoc.shutdown().await.expect("half-close");
assert!(reaped, "the adopted channel entry existed");
no_leaked_channels(&topo).await;
}
#[tokio::test]
async fn recv_datagram_state_persists_across_calls() {
let topo = wire_consumer_side_with(echo_dial(), Some(echo_egress()), None, None).await;
let mut assoc = AssociateSession::associate(topo.consumer.as_ref())
.await
.expect("the association associates");
let target = TargetAddr::Ip("10.0.0.2:53".parse().unwrap());
assoc
.send_datagram(&target, b"first")
.await
.expect("send the first datagram");
let big = vec![7u8; 60 * 1024];
assoc
.send_datagram(&target, &big)
.await
.expect("send the big datagram");
assoc
.send_datagram(&target, b"third")
.await
.expect("send the third datagram");
let (src, first) =
tokio::time::timeout(std::time::Duration::from_secs(10), assoc.recv_datagram())
.await
.expect("recv timed out")
.expect("a datagram (not eof)")
.expect("a datagram");
assert_eq!(src, target);
assert_eq!(&first[..], b"first", "the first datagram, in order");
let (src, second) =
tokio::time::timeout(std::time::Duration::from_secs(10), assoc.recv_datagram())
.await
.expect("recv timed out")
.expect("a datagram (not eof)")
.expect("a datagram");
assert_eq!(src, target);
assert_eq!(&second[..], &big[..], "the split datagram reassembled");
let (_, third) =
tokio::time::timeout(std::time::Duration::from_secs(10), assoc.recv_datagram())
.await
.expect("recv timed out")
.expect("a datagram (not eof)")
.expect("a datagram");
assert_eq!(&third[..], b"third", "the batched datagram drained");
let reaped = assoc.shutdown().await.expect("half-close");
assert!(reaped);
no_leaked_channels(&topo).await;
}
#[tokio::test]
async fn empty_payload_datagram_round_trips_and_survives() {
let topo = wire_consumer_side_with(echo_dial(), Some(echo_egress()), None, None).await;
let mut assoc = AssociateSession::associate(topo.consumer.as_ref())
.await
.expect("the association associates");
let target = TargetAddr::Ip("10.0.0.3:53".parse().unwrap());
assoc
.send_datagram(&target, b"pre")
.await
.expect("send pre");
assoc
.send_datagram(&target, b"")
.await
.expect("an empty payload is a legal datagram");
assoc
.send_datagram(&target, b"post")
.await
.expect("send post");
let (_, pre) = tokio::time::timeout(std::time::Duration::from_secs(10), assoc.recv_datagram())
.await
.expect("recv timed out")
.expect("a datagram (not eof)")
.expect("a datagram");
assert_eq!(&pre[..], b"pre");
let (src, empty) =
tokio::time::timeout(std::time::Duration::from_secs(10), assoc.recv_datagram())
.await
.expect("recv timed out")
.expect("a datagram (not eof)")
.expect("a datagram");
assert_eq!(src, target);
assert!(
empty.is_empty(),
"the empty-payload datagram round-trips (never EOF)"
);
let (_, post) = tokio::time::timeout(std::time::Duration::from_secs(10), assoc.recv_datagram())
.await
.expect("recv timed out")
.expect("a datagram (not eof)")
.expect("a datagram");
assert_eq!(&post[..], b"post");
let reaped = assoc.shutdown().await.expect("half-close");
assert!(reaped);
no_leaked_channels(&topo).await;
}
#[tokio::test]
async fn shutdown_ends_the_association_and_a_fresh_associate_succeeds() {
let topo = wire_consumer_side_with(echo_dial(), Some(echo_egress()), None, None).await;
let mut assoc = AssociateSession::associate(topo.consumer.as_ref())
.await
.expect("the association associates");
let channel_id = assoc.channel_id();
let target = TargetAddr::Ip("10.0.0.4:53".parse().unwrap());
assoc
.send_datagram(&target, b"before")
.await
.expect("send before shutdown");
let (_, echoed) =
tokio::time::timeout(std::time::Duration::from_secs(10), assoc.recv_datagram())
.await
.expect("recv timed out")
.expect("a datagram (not eof)")
.expect("a datagram");
assert_eq!(&echoed[..], b"before");
assoc.shutdown().await.expect("shutdown");
assert!(
!topo.consumer.manager().has_channel(channel_id),
"the adopted channel entry was reaped"
);
let mut fresh = AssociateSession::associate(topo.consumer.as_ref())
.await
.expect("fresh associate after the shutdown");
fresh
.send_datagram(&target, b"fresh")
.await
.expect("send after fresh associate");
let (_, echoed) =
tokio::time::timeout(std::time::Duration::from_secs(10), fresh.recv_datagram())
.await
.expect("recv timed out")
.expect("a datagram (not eof)")
.expect("a datagram");
assert_eq!(&echoed[..], b"fresh");
let reaped = fresh.shutdown().await.expect("shutdown");
assert!(reaped);
no_leaked_channels(&topo).await;
}
#[tokio::test]
async fn drop_teardown_reaps_and_connection_stays_healthy() {
let topo = wire_consumer_side_with(echo_dial(), Some(echo_egress()), None, None).await;
let mut assoc = AssociateSession::associate(topo.consumer.as_ref())
.await
.expect("the association associates");
let channel_id = assoc.channel_id();
assoc
.send_datagram(TargetAddr::Ip("10.0.0.5:53".parse().unwrap()), b"mid")
.await
.expect("send before the drop");
let (_, echoed) =
tokio::time::timeout(std::time::Duration::from_secs(10), assoc.recv_datagram())
.await
.expect("recv timed out")
.expect("a datagram (not eof)")
.expect("a datagram");
assert_eq!(&echoed[..], b"mid");
drop(assoc);
assert!(
!topo.consumer.manager().has_channel(channel_id),
"the adopted channel entry was reaped on Drop"
);
no_leaked_channels(&topo).await;
let mut fresh = AssociateSession::associate(topo.consumer.as_ref())
.await
.expect("fresh associate after the drop");
let target = TargetAddr::Ip("10.0.0.5:53".parse().unwrap());
fresh
.send_datagram(&target, b"again")
.await
.expect("send after the drop");
let (_, echoed) =
tokio::time::timeout(std::time::Duration::from_secs(10), fresh.recv_datagram())
.await
.expect("recv timed out")
.expect("a datagram (not eof)")
.expect("a datagram");
assert_eq!(&echoed[..], b"again");
fresh.shutdown().await.expect("shutdown");
no_leaked_channels(&topo).await;
}
#[tokio::test]
async fn missing_egress_refusal_surfaces_rfc_reply_code_no_phantom_channel() {
let topo = wire_consumer_side_with(echo_dial(), None, None, None).await;
let err = AssociateSession::associate(topo.consumer.as_ref())
.await
.expect_err("no egress wired; the ASSOCIATE must refuse");
match &err {
SocksSessionError::Rfc(SocksErrorKind::ReplyError(code)) => {
assert_eq!(code.as_u8(), consts::SOCKS5_REPLY_COMMAND_NOT_SUPPORTED);
}
other => panic!("expected an Rfc reply-code refusal, got {other:?}"),
}
no_leaked_channels(&topo).await;
let topo2 = wire_consumer_side_with(echo_dial(), Some(echo_egress()), None, None).await;
let mut assoc = AssociateSession::associate(topo2.consumer.as_ref())
.await
.expect("fresh associate after the refusal");
assoc
.send_datagram(TargetAddr::Ip("10.0.0.6:53".parse().unwrap()), b"fresh")
.await
.expect("send");
let (_, echoed) =
tokio::time::timeout(std::time::Duration::from_secs(10), assoc.recv_datagram())
.await
.expect("recv timed out")
.expect("a datagram (not eof)")
.expect("a datagram");
assert_eq!(&echoed[..], b"fresh");
assoc.shutdown().await.expect("shutdown");
no_leaked_channels(&topo2).await;
}
#[tokio::test]
async fn egress_factory_refusal_surfaces_the_mapped_reply_code() {
let topo = wire_consumer_side_with(
echo_dial(),
Some(refusing_egress(EgressError::Unreachable(
"no egress".into(),
))),
None,
None,
)
.await;
let err = AssociateSession::associate(topo.consumer.as_ref())
.await
.expect_err("the egress factory refused");
match &err {
SocksSessionError::Rfc(SocksErrorKind::ReplyError(code)) => {
assert_eq!(code.as_u8(), consts::SOCKS5_REPLY_NETWORK_UNREACHABLE);
}
other => panic!("expected an Rfc reply-code refusal, got {other:?}"),
}
no_leaked_channels(&topo).await;
}
#[tokio::test]
async fn connect_and_associate_served_on_the_same_registration() {
let topo = wire_consumer_side_with(echo_dial(), Some(echo_egress()), None, None).await;
let mut assoc = AssociateSession::associate(topo.consumer.as_ref())
.await
.expect("the association associates");
let mut connect = Socks5ChannelSession::connect(
topo.consumer.as_ref(),
("echo.test", 443u16).to_target_addr().unwrap(),
)
.await
.expect("the CONNECT session connects");
let target = TargetAddr::Ip("10.0.0.7:53".parse().unwrap());
assoc
.send_datagram(&target, b"udp")
.await
.expect("send datagram");
let (_, echoed) =
tokio::time::timeout(std::time::Duration::from_secs(10), assoc.recv_datagram())
.await
.expect("recv timed out")
.expect("a datagram (not eof)")
.expect("a datagram");
assert_eq!(&echoed[..], b"udp");
connect
.split()
.expect("pipe")
.1
.write_all(b"tcp")
.await
.expect("write");
let (mut read, _) = connect.split().expect("pipe");
let mut buf = [0u8; 3];
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"tcp");
assoc.shutdown().await.expect("shutdown");
connect.close().await;
no_leaked_channels(&topo).await;
}