use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::Arc;
use std::time::Duration;
use alkcall::channels::client::ChannelClient;
use alkcall::core::auth::Identity;
use tokio::io::AsyncWriteExt as _;
use tokio::net::{TcpListener, TcpStream, UdpSocket};
use tokio::sync::Semaphore;
use crate::consumer::{AssociateSession, TakenInner, TokenClient};
use crate::error::SocksSessionError;
use crate::rfc1928::consts;
use crate::rfc1928::server::{new_reply, states, Socks5ServerProtocol, SocksServerError};
use crate::rfc1928::util::target_addr::TargetAddr;
use crate::rfc1928::{ReplyError, Socks5Command, SocksError};
use crate::wire::{DatagramReader, WireError};
use bytes::Bytes;
#[derive(Clone, Debug)]
pub struct DoorCredential {
pub username: String,
pub password: String,
pub identity: Identity,
}
#[derive(Clone, Debug, Default)]
pub struct CredentialTable {
entries: HashMap<(String, String), Identity>,
}
impl CredentialTable {
pub fn new(entries: impl IntoIterator<Item = DoorCredential>) -> Self {
Self {
entries: entries
.into_iter()
.map(|c| ((c.username, c.password), c.identity))
.collect(),
}
}
pub fn lookup(&self, username: &str, password: &str) -> Option<&Identity> {
self.entries
.get(&(username.to_string(), password.to_string()))
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
#[derive(Debug)]
enum DoorExit {
ClientGone,
PlaneEnded,
LocalRfc(SocksError),
Open(String),
Io(std::io::Error),
}
impl std::fmt::Display for DoorExit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DoorExit::ClientGone => f.write_str("client gone"),
DoorExit::PlaneEnded => f.write_str("data plane ended"),
DoorExit::LocalRfc(err) => write!(f, "local rfc: {err}"),
DoorExit::Open(msg) => write!(f, "open failed: {msg}"),
DoorExit::Io(err) => write!(f, "io: {err}"),
}
}
}
impl From<SocksServerError> for DoorExit {
fn from(err: SocksServerError) -> Self {
DoorExit::LocalRfc(SocksError::ServerError(err))
}
}
pub struct FrontDoor {
bind_addr: SocketAddr,
credentials: Option<CredentialTable>,
idle_first_byte: Duration,
max_sessions: usize,
}
impl FrontDoor {
pub fn builder() -> FrontDoorBuilder {
FrontDoorBuilder::default()
}
}
#[derive(Default)]
pub struct FrontDoorBuilder {
bind_addr: Option<SocketAddr>,
credentials: Option<CredentialTable>,
idle_first_byte: Option<Duration>,
max_sessions: Option<usize>,
}
impl FrontDoorBuilder {
pub fn credentials(mut self, table: CredentialTable) -> Self {
self.credentials = Some(table);
self
}
pub fn bind(mut self, addr: impl Into<SocketAddr>) -> Self {
self.bind_addr = Some(addr.into());
self
}
pub fn idle_first_byte(mut self, window: Duration) -> Self {
self.idle_first_byte = Some(window);
self
}
pub fn max_sessions(mut self, cap: usize) -> Self {
self.max_sessions = Some(cap);
self
}
pub fn build(self) -> Result<FrontDoor, DoorBuildError> {
let bind_addr = self.bind_addr.ok_or(DoorBuildError::MissingBindAddr)?;
Ok(FrontDoor {
bind_addr,
credentials: self.credentials,
idle_first_byte: self.idle_first_byte.unwrap_or(Duration::from_secs(30)),
max_sessions: self.max_sessions.unwrap_or(256),
})
}
}
#[derive(Debug, thiserror::Error)]
pub enum DoorBuildError {
#[error("the door requires a bind address (.bind); the bind decision is never implied")]
MissingBindAddr,
}
impl FrontDoor {
pub async fn serve(self, client: Arc<ChannelClient>) -> std::io::Result<()> {
let listener = TcpListener::bind(self.bind_addr).await?;
tracing::info!(bind = %self.bind_addr, "front door: listening");
self.serve_on(listener, client).await
}
pub async fn serve_on(
self,
listener: TcpListener,
client: Arc<ChannelClient>,
) -> std::io::Result<()> {
let credentials = self.credentials;
let idle = self.idle_first_byte;
let live = Arc::new(Semaphore::new(self.max_sessions));
let token_client = match credentials {
Some(_) => Some(Arc::new(
TokenClient::wrap(&client)
.await
.map_err(|err| std::io::Error::other(err.to_string()))?,
)),
None => None,
};
loop {
let (stream, peer) = match listener.accept().await {
Ok(accepted) => accepted,
Err(err) => {
tracing::warn!(error = %err, "front door: accept failed");
continue;
}
};
let Ok(permit) = live.clone().try_acquire_owned() else {
tracing::warn!(peer = %peer, "front door: session cap reached; refusing");
drop(stream);
continue;
};
let client = Arc::clone(&client);
let per_accept_credentials = credentials.clone();
let per_accept_tokens = token_client.clone();
tokio::spawn(async move {
let _permit = permit;
let exit = serve_connection(
stream,
peer,
&client,
per_accept_credentials.as_ref(),
per_accept_tokens.as_ref(),
idle,
)
.await;
tracing::debug!(peer = %peer, "front door: connection ended: {exit}");
});
}
}
}
async fn serve_connection(
stream: TcpStream,
peer: SocketAddr,
client: &Arc<ChannelClient>,
credentials: Option<&CredentialTable>,
token_client: Option<&Arc<TokenClient>>,
idle: Duration,
) -> DoorExit {
let mut first = [0u8; 1];
match tokio::time::timeout(idle, stream.peek(&mut first)).await {
Ok(Ok(_)) => {}
Ok(Err(err)) => return DoorExit::Io(err),
Err(_) => {
tracing::debug!(peer = %peer, "front door: idle-first-byte timeout");
return DoorExit::ClientGone;
}
}
match (credentials, token_client) {
(None, _) => raw_pipe(stream, client).await,
(Some(table), Some(tokens)) => cred_bearing(stream, client, table, tokens).await,
(Some(_), None) => {
DoorExit::Open("the cred-bearing door's token surface was not built".to_string())
}
}
}
async fn raw_pipe(mut stream: TcpStream, client: &Arc<ChannelClient>) -> DoorExit {
let (_channel_id, send, recv) = match client
.open_channel(
crate::params::OP_SOCKS5_OPEN,
crate::params::Socks5Params::as_json(),
crate::params::SOCKS5_ALPN,
)
.await
{
Ok(open) => open,
Err(err) => return DoorExit::Open(err.to_string()),
};
let mut bidi = alkcall::core::types::BiStream::from_joined(recv, send);
let _ = tokio::io::copy_bidirectional(&mut stream, &mut bidi).await;
DoorExit::PlaneEnded
}
async fn cred_bearing(
mut stream: TcpStream,
_client: &Arc<ChannelClient>,
table: &CredentialTable,
token_client: &Arc<TokenClient>,
) -> DoorExit {
let outcome = local_rfc1929_half(&mut stream, table).await;
let (cmd, target, identity) = match outcome {
Ok(triple) => triple,
Err(exit) => return exit,
};
let token = identity.id.as_bytes().to_vec();
match cmd {
Socks5Command::TCPConnect => {
let session = match crate::consumer::Socks5ChannelSession::connect_with_token(
token_client,
target,
token,
)
.await
{
Ok(session) => session,
Err(err) => {
write_refusal_reply(&mut stream, &err).await;
return DoorExit::Open(err.to_string());
}
};
pipe_connect_plane(stream, session).await
}
Socks5Command::TCPBind => {
let session =
match crate::consumer::BindSession::bind_with_token(token_client, target, token)
.await
{
Ok(session) => session,
Err(err) => {
write_refusal_reply(&mut stream, &err).await;
return DoorExit::Open(err.to_string());
}
};
pipe_bind_plane(stream, session).await
}
Socks5Command::UDPAssociate => {
let reply = new_reply(
&ReplyError::CommandNotSupported,
SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0),
);
let _ = stream.write_all(&reply).await;
let _ = stream.flush().await;
DoorExit::LocalRfc(SocksError::ReplyError(ReplyError::CommandNotSupported))
}
}
}
async fn local_rfc1929_half(
stream: &mut TcpStream,
table: &CredentialTable,
) -> Result<(Socks5Command, TargetAddr, Identity), DoorExit> {
let found: Arc<std::sync::Mutex<Option<Identity>>> = Arc::new(std::sync::Mutex::new(None));
let found_for_check = Arc::clone(&found);
let (proto, _ok) = Socks5ServerProtocol::<_, states::Authenticated>::accept_password_auth(
stream,
|user, pass| {
let mapped = table.lookup(&user, &pass).cloned();
let mut slot = found_for_check.lock().unwrap_or_else(|e| e.into_inner());
let accepted = mapped.is_some();
*slot = mapped;
accepted
},
)
.await
.map_err(DoorExit::from)?;
let identity = found
.lock()
.unwrap_or_else(|e| e.into_inner())
.take()
.ok_or_else(|| DoorExit::from(SocksServerError::AuthenticationRejected))?;
let (_proto, cmd, target) = proto.read_command().await.map_err(DoorExit::from)?;
Ok((cmd, target, identity))
}
async fn pipe_connect_plane(
mut stream: TcpStream,
session: crate::consumer::Socks5ChannelSession,
) -> DoorExit {
let reply = match session.reply_addr() {
Some(TargetAddr::Ip(sa)) => new_reply(&ReplyError::Succeeded, sa),
Some(TargetAddr::Domain(_, port)) => new_reply(
&ReplyError::Succeeded,
SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port),
),
None => {
session.close().await;
return DoorExit::PlaneEnded;
}
};
if stream.write_all(&reply).await.is_err() {
session.close().await;
return DoorExit::ClientGone;
}
let _ = stream.flush().await;
let Ok(TakenInner { session, inner }) = session.into_inner() else {
return DoorExit::PlaneEnded;
};
let _ = tokio::io::copy_bidirectional(&mut stream, &mut { inner }).await;
let _ = session;
DoorExit::PlaneEnded
}
async fn pipe_bind_plane(
mut stream: TcpStream,
mut session: crate::consumer::BindSession,
) -> DoorExit {
let bind_addr = session.reply1().map(|reply1| reply1.bind_addr.clone());
let Some(bind_addr) = bind_addr else {
session.close().await;
return DoorExit::PlaneEnded;
};
let wire = bind_reply1_wire(&bind_addr);
if write_vanilla_reply(&mut stream, &wire).await.is_err() {
session.close().await;
return DoorExit::ClientGone;
}
bind_quiet_and_pipe(stream, session).await
}
fn bind_reply1_wire(bind_addr: &TargetAddr) -> Vec<u8> {
match bind_addr {
TargetAddr::Ip(sa) => new_reply(&ReplyError::Succeeded, *sa),
TargetAddr::Domain(name, port) => {
let mut wire = vec![
consts::SOCKS5_VERSION,
ReplyError::Succeeded.as_u8(),
0x00,
consts::SOCKS5_ADDR_TYPE_DOMAIN_NAME,
];
wire.push(name.len() as u8);
wire.extend_from_slice(name.as_bytes());
wire.extend_from_slice(&port.to_be_bytes());
wire
}
}
}
async fn bind_quiet_and_pipe(
mut stream: TcpStream,
mut session: crate::consumer::BindSession,
) -> DoorExit {
let plane = match session.await_inbound().await {
Ok(Some(plane)) => plane,
Ok(None) => return DoorExit::PlaneEnded,
Err(err) => {
write_refusal_reply(&mut stream, &err).await;
session.close().await;
return DoorExit::PlaneEnded;
}
};
let reply = match plane.peer {
TargetAddr::Ip(sa) => new_reply(&ReplyError::Succeeded, sa),
TargetAddr::Domain(_, port) => new_reply(
&ReplyError::Succeeded,
SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port),
),
};
if write_vanilla_reply(&mut stream, &reply).await.is_err() {
session.close().await;
return DoorExit::ClientGone;
}
let _ = tokio::io::copy_bidirectional(&mut stream, &mut { plane }).await;
session.close().await;
DoorExit::PlaneEnded
}
async fn write_vanilla_reply(stream: &mut TcpStream, reply: &[u8]) -> std::io::Result<()> {
stream.write_all(reply).await?;
stream.flush().await
}
async fn write_refusal_reply(stream: &mut TcpStream, err: &SocksSessionError) {
let code = match err {
SocksSessionError::Rfc(SocksError::ReplyError(code)) => *code,
_ => return,
};
let reply = new_reply(&code, SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0));
let _ = write_vanilla_reply(stream, &reply).await;
}
pub struct UdpFrontDoor {
bind_addr: SocketAddr,
idle: Duration,
}
#[derive(Debug)]
enum UdpDoorExit {
AssociationEnded,
Idle,
Session(String),
Io(std::io::Error),
}
impl std::fmt::Display for UdpDoorExit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UdpDoorExit::AssociationEnded => f.write_str("association ended"),
UdpDoorExit::Idle => f.write_str("client idle bound fired"),
UdpDoorExit::Session(msg) => write!(f, "in-band session: {msg}"),
UdpDoorExit::Io(err) => write!(f, "io: {err}"),
}
}
}
impl UdpFrontDoor {
pub fn builder() -> UdpFrontDoorBuilder {
UdpFrontDoorBuilder::default()
}
}
#[derive(Default)]
pub struct UdpFrontDoorBuilder {
bind_addr: Option<SocketAddr>,
idle: Option<Duration>,
}
impl UdpFrontDoorBuilder {
pub fn bind(mut self, addr: impl Into<SocketAddr>) -> Self {
self.bind_addr = Some(addr.into());
self
}
pub fn idle(mut self, window: Duration) -> Self {
self.idle = Some(window);
self
}
pub fn build(self) -> Result<UdpFrontDoor, DoorBuildError> {
let bind_addr = self.bind_addr.ok_or(DoorBuildError::MissingBindAddr)?;
Ok(UdpFrontDoor {
bind_addr,
idle: self.idle.unwrap_or(Duration::from_secs(30)),
})
}
}
impl UdpFrontDoor {
pub async fn serve(self, client: Arc<ChannelClient>) -> std::io::Result<()> {
let socket = UdpSocket::bind(self.bind_addr).await?;
tracing::info!(bind = %self.bind_addr, "udp front door: relay bound");
self.serve_on(socket, client).await
}
pub async fn serve_on(
self,
socket: UdpSocket,
client: Arc<ChannelClient>,
) -> std::io::Result<()> {
let mut association = AssociateSession::associate(client.as_ref())
.await
.map_err(|err| std::io::Error::other(err.to_string()))?;
let _ = association.relay_addr();
let Some((mut channel_read, mut channel_write)) = association.split() else {
return Ok(());
};
let socket = Arc::new(socket);
let client_peer: Arc<std::sync::RwLock<Option<std::net::IpAddr>>> =
Arc::new(std::sync::RwLock::new(None));
let reply_port: Arc<std::sync::RwLock<Option<u16>>> =
Arc::new(std::sync::RwLock::new(None));
let s2c = {
let socket = Arc::clone(&socket);
let client_peer = Arc::clone(&client_peer);
let reply_port = Arc::clone(&reply_port);
async move {
let mut scratch = vec![0u8; 65535];
loop {
let read =
tokio::time::timeout(self.idle, socket.recv_from(&mut scratch)).await;
let (n, from) = match read {
Ok(Ok(pair)) => pair,
Ok(Err(err)) => return UdpDoorExit::Io(err),
Err(_) => return UdpDoorExit::Idle,
};
let (frag, addr, payload) =
match crate::rfc1928::parse_udp_request(&scratch[..n]).await {
Ok(parsed) => parsed,
Err(_) => continue,
};
if frag != 0 {
tracing::debug!(
peer = %from,
"udp front door: FRAG != 0 refused (dropped)"
);
continue;
}
{
let mut known = client_peer.write().unwrap_or_else(|e| e.into_inner());
match *known {
None => *known = Some(from.ip()),
Some(ip) if ip != from.ip() => {
tracing::debug!(
peer = %from,
pinned = %ip,
"udp front door: datagram from a non-expected \
source IP dropped (RFC 1928 peer posture)"
);
continue;
}
Some(_) => {}
}
}
*reply_port.write().unwrap_or_else(|e| e.into_inner()) = Some(from.port());
if let Err(err) = send_inband(&mut channel_write, &addr, payload).await {
return UdpDoorExit::Session(err.to_string());
}
}
}
};
let c2s = {
let socket = Arc::clone(&socket);
let client_peer = Arc::clone(&client_peer);
let reply_port = Arc::clone(&reply_port);
async move {
let mut bridge = InbandBridge::new();
loop {
let (addr, payload) = match recv_inband(&mut bridge, &mut channel_read).await {
Ok(Some(pair)) => pair,
Ok(None) => return UdpDoorExit::AssociationEnded,
Err(err) => return UdpDoorExit::Session(err.to_string()),
};
let mut header = match crate::rfc1928::new_udp_header(addr) {
Ok(header) => header,
Err(err) => return UdpDoorExit::Session(err.to_string()),
};
header.extend_from_slice(&payload);
let ip = *client_peer.read().unwrap_or_else(|e| e.into_inner());
let port = *reply_port.read().unwrap_or_else(|e| e.into_inner());
let (Some(ip), Some(port)) = (ip, port) else {
continue;
};
let to = SocketAddr::new(ip, port);
if let Err(err) = socket.send_to(&header, to).await {
return UdpDoorExit::Io(err);
}
}
}
};
let exit = tokio::select! {
exit = s2c => exit,
exit = c2s => exit,
};
tracing::debug!("udp front door: association ended: {exit}");
let _ = association.shutdown().await;
match exit {
UdpDoorExit::Io(err) => Err(err),
_ => Ok(()),
}
}
}
async fn send_inband(
write: &mut tokio::io::WriteHalf<&mut AssociationStream>,
addr: &TargetAddr,
payload: &[u8],
) -> Result<(), SocksSessionError> {
let framed = crate::wire::frame(addr, payload).map_err(wire_error)?;
write.write_all(&framed).await?;
write.flush().await?;
Ok(())
}
struct InbandBridge {
reader: DatagramReader,
pending: std::collections::VecDeque<(TargetAddr, Bytes)>,
}
impl InbandBridge {
fn new() -> Self {
Self {
reader: DatagramReader::new(),
pending: std::collections::VecDeque::new(),
}
}
async fn recv<R>(
&mut self,
read: &mut R,
) -> Result<Option<(TargetAddr, Bytes)>, SocksSessionError>
where
R: tokio::io::AsyncRead + Unpin,
{
if let Some(dg) = self.pending.pop_front() {
return Ok(Some(dg));
}
let mut chunk = vec![0u8; 16 * 1024];
loop {
let n = tokio::io::AsyncReadExt::read(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)),
}
}
}
}
}
async fn recv_inband<R>(
bridge: &mut InbandBridge,
read: &mut R,
) -> Result<Option<(TargetAddr, Bytes)>, SocksSessionError>
where
R: tokio::io::AsyncRead + Unpin,
{
bridge.recv(read).await
}
fn wire_error(err: WireError) -> SocksSessionError {
match err {
WireError::Address(addr) => SocksError::AddrError(addr).into(),
other => SocksError::UdpHeaderError(crate::rfc1928::UdpHeaderError::ReadingError(
std::io::Error::other(other.to_string()),
))
.into(),
}
}
type AssociationStream = crate::rfc1928::client::Socks5Stream<alkcall::core::types::BiStream>;
#[cfg(test)]
mod tests {
use super::*;
use crate::wire::frame as wire_frame;
use tokio::io::AsyncWriteExt;
use tokio::io::{AsyncRead, DuplexStream};
async fn feed_then_recv<R>(
bridge: &mut InbandBridge,
read: &mut R,
writer: &mut DuplexStream,
bytes: &[u8],
) -> Result<Option<(TargetAddr, Bytes)>, SocksSessionError>
where
R: AsyncRead + Unpin,
{
if !bytes.is_empty() {
writer.write_all(bytes).await.expect("feed write");
writer.flush().await.expect("feed flush");
}
bridge.recv(read).await
}
#[tokio::test]
async fn batched_frames_queue_not_drop() {
let (mut writer, reader) = tokio::io::duplex(1024);
let (mut channel_read, _channel_write) = tokio::io::split(reader);
let addr = TargetAddr::Ip("10.0.0.2:53".parse().expect("addr"));
let mut wire = wire_frame(&addr, b"first").unwrap().to_vec();
wire.extend_from_slice(&wire_frame(&addr, b"second").unwrap());
let mut bridge = InbandBridge::new();
let first = feed_then_recv(&mut bridge, &mut channel_read, &mut writer, &wire).await;
match first {
Ok(Some((_src, payload))) => assert_eq!(&payload[..], b"first"),
other => panic!("the first datagram returned: {other:?}"),
}
let second = bridge.recv(&mut channel_read).await;
match second {
Ok(Some((_src, payload))) => assert_eq!(&payload[..], b"second"),
other => panic!("the batched datagram queued, not lost: {other:?}"),
}
}
#[tokio::test]
async fn eof_mid_frame_fails_loud_not_silent() {
let (mut writer, reader) = tokio::io::duplex(1024);
let (mut channel_read, _channel_write) = tokio::io::split(reader);
let addr = TargetAddr::Ip("10.0.0.3:53".parse().expect("addr"));
let framed = wire_frame(&addr, b"truncated").expect("frame");
let mut bridge = InbandBridge::new();
let feeder = tokio::spawn(async move {
writer
.write_all(&framed[..3])
.await
.expect("the partial frame");
writer.flush().await.expect("flush");
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
let _ = writer.shutdown().await;
});
let ended = bridge.recv(&mut channel_read).await;
assert!(
ended.is_err(),
"EOF mid-frame is the fail-loud truncated-datagram condition"
);
feeder.await.expect("feeder");
}
#[tokio::test]
async fn partial_frame_state_persists_across_calls() {
let (mut writer, reader) = tokio::io::duplex(1024);
let (mut channel_read, _channel_write) = tokio::io::split(reader);
let addr = TargetAddr::Ip("10.0.0.4:53".parse().expect("addr"));
let mut wire = wire_frame(&addr, b"whole").unwrap().to_vec();
wire.extend_from_slice(&wire_frame(&addr, b"partial").unwrap()[..3]);
let mut bridge = InbandBridge::new();
let feeder = tokio::spawn(async move {
writer.write_all(&wire).await.expect("the mixed chunk");
writer.flush().await.expect("flush");
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
let _ = writer.shutdown().await;
});
let first = bridge.recv(&mut channel_read).await;
match first {
Ok(Some((_src, payload))) => assert_eq!(&payload[..], b"whole"),
other => panic!("frame 1 returned: {other:?}"),
}
let second = bridge.recv(&mut channel_read).await;
assert!(
second.is_err(),
"frame 2's partial bytes survived the call boundary (fail-loud at EOF)"
);
feeder.await.expect("feeder");
}
}
#[cfg(test)]
mod refusal_and_wire_unit_tests {
use super::*;
use crate::SocksOpenError;
use alkcall::channels::client::ChannelOpenError;
use alkcall::protocol::wire::CallError;
use tokio::io::AsyncReadExt;
async fn socket_pair() -> (tokio::net::TcpStream, tokio::net::TcpStream) {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind");
let addr = listener.local_addr().expect("addr");
let client = tokio::net::TcpStream::connect(addr).await.expect("connect");
let (server, _peer) = listener.accept().await.expect("accept");
(client, server)
}
#[test]
fn bind_reply1_wire_forms() {
let ip = std::net::SocketAddr::from(([127, 0, 0, 1], 2121));
let ip_wire = bind_reply1_wire(&TargetAddr::Ip(ip));
assert_eq!(ip_wire[0], consts::SOCKS5_VERSION);
assert_eq!(ip_wire[1], consts::SOCKS5_REPLY_SUCCEEDED);
assert_eq!(ip_wire[3], consts::SOCKS5_ADDR_TYPE_IPV4);
assert_eq!(&ip_wire[4..8], &[127, 0, 0, 1]);
assert_eq!(&ip_wire[8..10], &2121u16.to_be_bytes());
let domain_wire = bind_reply1_wire(&TargetAddr::Domain("bind.test".into(), 2121));
let expected = {
let mut v = vec![
consts::SOCKS5_VERSION,
consts::SOCKS5_REPLY_SUCCEEDED,
0x00,
consts::SOCKS5_ADDR_TYPE_DOMAIN_NAME,
9,
];
v.extend_from_slice(b"bind.test");
v.extend_from_slice(&2121u16.to_be_bytes());
v
};
assert_eq!(domain_wire, expected, "the domain BND.ADDR frame");
}
#[tokio::test]
async fn write_refusal_reply_skips_non_reply_code_shapes() {
let (mut client, mut server) = socket_pair().await;
let open_err =
SocksSessionError::Open(SocksOpenError::Open(ChannelOpenError::CallFailed {
error: CallError::new("channel:open_failed", "refused", false),
}));
write_refusal_reply(&mut server, &open_err).await;
let mut probe = [0u8; 16];
let n = tokio::time::timeout(
std::time::Duration::from_millis(200),
client.read(&mut probe),
)
.await;
assert!(
matches!(n, Err(_) | Ok(Ok(0))),
"no reply bytes for a non-reply-code error: {n:?}"
);
}
#[tokio::test]
async fn write_refusal_reply_encodes_the_mapped_code() {
let (mut client, mut server) = socket_pair().await;
let refused =
SocksSessionError::Rfc(SocksError::ReplyError(ReplyError::ConnectionNotAllowed));
write_refusal_reply(&mut server, &refused).await;
let mut wire = [0u8; 10];
tokio::time::timeout(
std::time::Duration::from_secs(5),
client.read_exact(&mut wire),
)
.await
.expect("the reply arrives")
.expect("read");
assert_eq!(wire[0], consts::SOCKS5_VERSION);
assert_eq!(wire[1], consts::SOCKS5_REPLY_CONNECTION_NOT_ALLOWED);
assert_eq!(&wire[4..8], &[0, 0, 0, 0], "the placeholder BND.ADDR");
assert_eq!(&wire[8..10], &0u16.to_be_bytes());
}
}
#[cfg(test)]
mod bridge_later_batch_tests {
use super::*;
use tokio::io::AsyncWriteExt;
#[tokio::test]
async fn garbage_in_a_later_batch_frame_fails_loud() {
let (mut writer, reader) = tokio::io::duplex(1024);
let (mut channel_read, _channel_write) = tokio::io::split(reader);
let addr = TargetAddr::Ip("10.0.0.7:53".parse().expect("addr"));
let mut wire = crate::wire::frame(&addr, b"good").unwrap().to_vec();
wire.extend_from_slice(&[0xff, 0xff]);
wire.extend_from_slice(b"short");
let mut bridge = InbandBridge::new();
writer.write_all(&wire).await.expect("feed");
writer.flush().await.expect("flush");
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
let _ = writer.shutdown().await;
let first = bridge.recv(&mut channel_read).await;
assert!(
first.is_err() || matches!(&first, Ok(Some((src, payload))) if &payload[..] == b"good"),
"the first frame or the loud error: {first:?}"
);
if matches!(first, Ok(Some(_))) {
let second = bridge.recv(&mut channel_read).await;
assert!(
second.is_err(),
"the truncated later batch frame fails loud: {second:?}"
);
}
}
}