#![cfg(feature = "composed")]
mod harness;
use std::net::SocketAddr;
use std::time::Duration;
use alksocks::composed::{bindfirst_accept, register_forwarded_serving};
use alksocks::consumer::BindSession;
use alksocks::rfc1928::util::target_addr::{TargetAddr, ToTargetAddr};
use alktunnels::params::Substrate;
use alktunnels::producer::ResourceRegistry;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use harness::{
assert_composed_channels, composed_fresh_queue, composed_opener_identity, composed_push_accept,
composed_queue_loop, composed_scopeless_opener_identity, composed_test_bind,
wire_bindfirst_composed, COMPOSED_BOUND_PORT, COMPOSED_PEER_PORT, COMPOSED_RESOURCE,
};
struct Suite {
topo: harness::BindFirstTopology,
queue: harness::ComposedAcceptQueue,
}
async fn wire_suite() -> Suite {
wire_suite_with_identity(composed_opener_identity()).await
}
async fn wire_suite_with_identity(identity: alkcall::core::auth::Identity) -> Suite {
let resource_registry = ResourceRegistry::new();
resource_registry
.register(COMPOSED_RESOURCE, Substrate::Tcp, "assembly-owned")
.await;
let queue = composed_fresh_queue();
let topo = wire_bindfirst_composed(
resource_registry,
composed_test_bind(),
composed_queue_loop(queue.clone()),
None, identity,
)
.await;
let registry = register_forwarded_serving(
&alkcall::channels::operations::ChannelCore::new(
topo.opener.manager().clone(),
alkcall::channels::policy::default_policy(),
),
&topo.opener_registry,
alkcall::core::auth::AuthContext::anonymous(b"alk/tunnel"),
)
.expect("register forwarded openable");
let factory = bindfirst_accept(topo.opener.clone(), registry, COMPOSED_RESOURCE);
let core = alkcall::channels::operations::ChannelCore::new(
topo.producer_manager.clone(),
alkcall::channels::policy::default_policy(),
);
alksocks::producer::Socks5Producer::builder()
.accept(factory)
.build()
.register(
&core,
&topo.producer_registry,
alkcall::core::auth::AuthContext::anonymous(b"alk/socks5"),
None,
)
.expect("register socks5 openable post-hoc (the composed accept factory)");
Suite { topo, queue }
}
async fn bind_session(suite: &Suite) -> BindSession {
BindSession::bind(
suite.topo.opener.as_ref(),
("bind.test", 2121).to_target_addr().unwrap(),
)
.await
.expect("the session binds through the composed accept")
}
#[tokio::test]
async fn composed_bind_full_story_bound_peer_and_round_trip() {
let _ = tracing_subscriber::fmt()
.with_max_level(tracing::level_filters::LevelFilter::TRACE)
.with_test_writer()
.try_init();
let suite = wire_suite().await;
let mut session = bind_session(&suite).await;
let channel_id = session.channel_id();
assert!(
suite.topo.producer_manager.has_channel(channel_id),
"the SOCKS5 channel is live on the producer side"
);
let bind_addr = session.reply1().expect("reply#1").bind_addr;
assert_eq!(
bind_addr,
TargetAddr::Ip(SocketAddr::from(([127, 0, 0, 1], COMPOSED_BOUND_PORT))),
"reply#1 advertises the real OS-chosen bound"
);
composed_push_accept(&suite.queue, ("10.0.0.1", COMPOSED_PEER_PORT)).await;
let mut plane = tokio::time::timeout(Duration::from_secs(10), session.await_inbound())
.await
.expect("reply#2 within timeout")
.expect("await_inbound ok")
.expect("the plane exists");
assert_eq!(
plane.peer,
TargetAddr::Ip(SocketAddr::from(([10, 0, 0, 1], COMPOSED_PEER_PORT))),
"reply#2 carries the forwarded op's real peer"
);
plane.write_all(b"bind-ping").await.expect("write");
plane.flush().await.expect("flush");
let mut buf = [0u8; 9];
tokio::time::timeout(Duration::from_secs(10), plane.read_exact(&mut buf))
.await
.expect("round trip")
.expect("read");
assert_eq!(&buf, b"bind-ping");
drop(plane);
assert!(session.close().await, "the SOCKS5 channel reaped");
for _ in 0..50 {
tokio::time::sleep(Duration::from_millis(100)).await;
let opener_ids = suite.topo.opener.manager().channel_ids();
if opener_ids.iter().all(|&id| id == 0) {
break;
}
}
assert_composed_channels(&suite.topo, &[0], &[channel_id]).await;
}
#[tokio::test]
async fn client_gone_teardown_releases_the_listener() {
let suite = wire_suite().await;
let mut session = bind_session(&suite).await;
let socks5_channel = session.channel_id();
let _ = session.reply1().expect("reply#1");
drop(session);
let mut socks5_gone = false;
let mut listen_gone = false;
for _ in 0..50 {
tokio::time::sleep(Duration::from_millis(200)).await;
socks5_gone = !suite.topo.producer_manager.has_channel(socks5_channel);
listen_gone = suite
.topo
.producer_manager
.channel_ids()
.iter()
.all(|&id| id == 0);
if socks5_gone && listen_gone {
break;
}
}
assert!(
socks5_gone,
"the SOCKS5 channel reaped after the client dropped"
);
assert!(
listen_gone,
"the listen channel reaped (no leaked listener): producer={:?}",
suite.topo.producer_manager.channel_ids()
);
assert!(
suite.queue.0.lock().await.is_empty(),
"the dropped listener consumed nothing"
);
assert_composed_channels(&suite.topo, &[0], &[0]).await;
}
#[tokio::test]
async fn concurrent_bind_sessions_get_distinct_tokens() {
let suite = wire_suite().await;
let mut s1 = bind_session(&suite).await;
let mut s2 = bind_session(&suite).await;
assert_ne!(s1.channel_id(), s2.channel_id(), "distinct SOCKS5 channels");
let _ = s1.reply1().expect("reply#1 s1");
let _ = s2.reply1().expect("reply#2 s2");
composed_push_accept(&suite.queue, ("10.0.0.1", COMPOSED_PEER_PORT)).await;
composed_push_accept(&suite.queue, ("10.0.0.2", COMPOSED_PEER_PORT + 1)).await;
let mut p1 = tokio::time::timeout(Duration::from_secs(10), s1.await_inbound())
.await
.expect("s1 reply#2")
.expect("s1 plane ok")
.expect("s1 plane");
let mut p2 = tokio::time::timeout(Duration::from_secs(10), s2.await_inbound())
.await
.expect("s2 reply#2")
.expect("s2 plane ok")
.expect("s2 plane");
let peers = [p1.peer.clone(), p2.peer.clone()];
assert_ne!(peers[0], peers[1], "the two accepts routed distinctly");
p1.write_all(b"one").await.expect("p1 write");
p2.write_all(b"two").await.expect("p2 write");
let mut b1 = [0u8; 3];
let mut b2 = [0u8; 3];
tokio::time::timeout(Duration::from_secs(10), p1.read_exact(&mut b1))
.await
.expect("p1 echo")
.expect("p1 read");
tokio::time::timeout(Duration::from_secs(10), p2.read_exact(&mut b2))
.await
.expect("p2 echo")
.expect("p2 read");
assert_eq!(&b1, b"one");
assert_eq!(&b2, b"two");
drop(p1);
drop(p2);
assert!(s1.close().await);
assert!(s2.close().await);
tokio::time::sleep(Duration::from_millis(300)).await;
assert_composed_channels(&suite.topo, &[0], &[0]).await;
}
#[tokio::test]
async fn forwarded_open_failure_is_per_accept_isolated() {
let suite = wire_suite_with_identity(composed_scopeless_opener_identity()).await;
let mut session = bind_session(&suite).await;
let _ = session.reply1().expect("reply#1");
composed_push_accept(&suite.queue, ("10.0.0.1", COMPOSED_PEER_PORT)).await;
composed_push_accept(&suite.queue, ("10.0.0.1", COMPOSED_PEER_PORT)).await;
tokio::time::sleep(Duration::from_millis(300)).await;
assert!(
suite.queue.0.lock().await.is_empty(),
"the accept loop consumed both accepts (alive throughout)"
);
drop(session);
tokio::time::sleep(Duration::from_millis(300)).await;
}
#[tokio::test]
async fn late_forwarded_open_after_client_gone_drops_the_channel() {
let suite = wire_suite().await;
let mut session = bind_session(&suite).await;
let _ = session.reply1().expect("reply#1");
drop(session);
composed_push_accept(&suite.queue, ("10.0.0.1", COMPOSED_PEER_PORT)).await;
tokio::time::sleep(Duration::from_millis(500)).await;
assert_composed_channels(&suite.topo, &[0], &[0]).await;
}