use rama_core::error::BoxError;
use rama_core::extensions::ExtensionsRef;
use rama_core::io::Io;
use rama_core::telemetry::tracing;
use rama_net::address::{HostWithPort, SocketAddress};
use rama_net::client::ConnectionErrorKind;
use rama_net::extensions::StreamTransformed;
use rama_utils::collections::smallvec::smallvec;
use std::fmt;
use crate::{
Socks5Auth,
client::udp::UdpSocketRelayBinder,
proto::{
Command, ProtocolError, ProtocolVersion, ReplyKind, SocksMethod,
UsernamePasswordSubnegotiationVersion,
client::{Header, Request, RequestRef, UsernamePasswordRequestRef},
server::{self, Reply},
},
};
use super::bind::Binder;
#[derive(Debug, Clone, Default)]
pub struct Client {
auth: Option<Socks5Auth>,
}
impl Client {
#[inline]
#[must_use]
pub fn new() -> Self {
Self::default()
}
rama_utils::macros::generate_set_and_with! {
pub fn auth(mut self, auth: impl Into<Socks5Auth>) -> Self {
self.auth = Some(auth.into());
self
}
}
}
#[derive(Debug)]
pub struct HandshakeError {
kind: HandshakeErrorKind,
context: Option<&'static str>,
}
impl HandshakeError {
pub(crate) fn io(err: std::io::Error) -> Self {
Self {
kind: HandshakeErrorKind::IO(err),
context: None,
}
}
pub(crate) fn other(err: impl Into<BoxError>) -> Self {
Self {
kind: HandshakeErrorKind::Other(err.into()),
context: None,
}
}
pub(crate) fn protocol(value: ProtocolError) -> Self {
Self {
kind: HandshakeErrorKind::Protocol(value),
context: None,
}
}
pub(crate) fn reply_kind(kind: ReplyKind) -> Self {
Self {
kind: HandshakeErrorKind::Reply(kind),
context: None,
}
}
fn method_mismatch(method: SocksMethod) -> Self {
Self {
kind: HandshakeErrorKind::MethodMismatch(method),
context: None,
}
}
fn unauthorized(status: u8) -> Self {
Self {
kind: HandshakeErrorKind::Unauthorized(status),
context: None,
}
}
pub(crate) fn with_context(mut self, context: &'static str) -> Self {
self.context = Some(context);
self
}
}
impl HandshakeError {
pub fn reply(&self) -> ReplyKind {
match self.kind {
HandshakeErrorKind::IO(_)
| HandshakeErrorKind::Protocol(_)
| HandshakeErrorKind::MethodMismatch(_)
| HandshakeErrorKind::Other(_) => ReplyKind::GeneralServerFailure,
HandshakeErrorKind::Unauthorized(_) => ReplyKind::ConnectionNotAllowed,
HandshakeErrorKind::Reply(reply_kind) => reply_kind,
}
}
pub(crate) fn connection_error_kind(&self) -> ConnectionErrorKind {
match &self.kind {
HandshakeErrorKind::IO(error) => match error.kind() {
std::io::ErrorKind::TimedOut => ConnectionErrorKind::Timeout,
_ => ConnectionErrorKind::Unavailable,
},
HandshakeErrorKind::Protocol(_) => ConnectionErrorKind::Protocol,
HandshakeErrorKind::MethodMismatch(_) | HandshakeErrorKind::Unauthorized(_) => {
ConnectionErrorKind::Authentication
}
HandshakeErrorKind::Reply(reply) => match reply {
ReplyKind::GeneralServerFailure
| ReplyKind::NetworkUnreachable
| ReplyKind::HostUnreachable
| ReplyKind::ConnectionRefused => ConnectionErrorKind::Unavailable,
ReplyKind::ConnectionNotAllowed => ConnectionErrorKind::Rejected,
ReplyKind::TtlExpired => ConnectionErrorKind::Timeout,
ReplyKind::CommandNotSupported | ReplyKind::AddressTypeNotSupported => {
ConnectionErrorKind::Protocol
}
ReplyKind::Succeeded => ConnectionErrorKind::Other,
ReplyKind::Unknown(_) => ConnectionErrorKind::Protocol,
},
HandshakeErrorKind::Other(_) => ConnectionErrorKind::Other,
}
}
}
#[derive(Debug)]
enum HandshakeErrorKind {
IO(std::io::Error),
Protocol(ProtocolError),
MethodMismatch(SocksMethod),
Reply(ReplyKind),
Unauthorized(u8),
Other(BoxError),
}
impl fmt::Display for HandshakeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let context = self.context.unwrap_or("no context");
match &self.kind {
HandshakeErrorKind::IO(error) => {
write!(f, "client: handshake error: I/O: {error} ({context})")
}
HandshakeErrorKind::Protocol(error) => {
write!(
f,
"client: handshake error: protocol error: {error} ({context})"
)
}
HandshakeErrorKind::MethodMismatch(method) => {
write!(
f,
"client: handshake error: method mismatch: {method:?} ({context})"
)
}
HandshakeErrorKind::Reply(reply) => {
write!(
f,
"client: handshake error: error reply: {reply:?} ({context})"
)
}
HandshakeErrorKind::Unauthorized(status) => {
write!(
f,
"client: handshake error: unauthorized: {status} ({context})"
)
}
HandshakeErrorKind::Other(error) => {
write!(f, "client: handshake error: other: {error} ({context})")
}
}
}
}
impl std::error::Error for HandshakeError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match &self.kind {
HandshakeErrorKind::IO(err) => Some(err as &(dyn std::error::Error + 'static)),
HandshakeErrorKind::Protocol(err) => Some(
err.source()
.unwrap_or(err as &(dyn std::error::Error + 'static)),
),
HandshakeErrorKind::MethodMismatch(_)
| HandshakeErrorKind::Reply(_)
| HandshakeErrorKind::Unauthorized(_)
| HandshakeErrorKind::Other(_) => None,
}
}
}
impl Client {
pub async fn handshake_connect<S: Io + Unpin + ExtensionsRef>(
&self,
stream: &mut S,
destination: &HostWithPort,
) -> Result<HostWithPort, HandshakeError> {
let auth_outcome = match self.auth.as_ref() {
Some(auth) => self.handshake_headers_auth(stream, auth).await,
None => self.handshake_headers_no_auth(stream).await,
};
#[cfg(feature = "dial9")]
crate::dial9::record_handshake_auth(
match &auth_outcome {
Ok(m) => u8::from(*m),
Err(_) => 0xff,
},
auth_outcome.is_ok(),
);
let selected_method = auth_outcome?;
let request = RequestRef::new(Command::Connect, destination);
request
.write_to(stream)
.await
.map_err(|err| HandshakeError::io(err).with_context("write client request: connect"))?;
tracing::trace!(
"socks5 client: client request sent w/ {} towards {}",
selected_method,
destination,
);
let server_reply = server::Reply::read_from(stream)
.await
.map_err(|err| HandshakeError::protocol(err).with_context("read server reply"))?;
#[cfg(feature = "dial9")]
crate::dial9::record_handshake_connect(
destination.host.clone(),
destination.port,
server_reply.reply.into(),
);
if server_reply.reply != ReplyKind::Succeeded {
return Err(HandshakeError::reply_kind(server_reply.reply)
.with_context("server responded with non-success reply"));
}
tracing::trace!(
"socks5 client: connected w/ {} towards {}",
selected_method,
destination
);
stream.extensions().insert(StreamTransformed {
by: "rama-socks5::Socks5Client",
});
Ok(server_reply.bind_address)
}
pub async fn handshake_bind<S: Io + Unpin + ExtensionsRef>(
&self,
mut stream: S,
requested_bind_address: Option<SocketAddress>,
) -> Result<Binder<S>, HandshakeError> {
let selected_method = match self.auth.as_ref() {
Some(auth) => self.handshake_headers_auth(&mut stream, auth).await?,
None => self.handshake_headers_no_auth(&mut stream).await?,
};
let destination = requested_bind_address.unwrap_or_else(|| SocketAddress::local_ipv4(0));
let request = Request {
version: ProtocolVersion::Socks5,
command: Command::Bind,
destination: destination.into(),
};
request
.write_to(&mut stream)
.await
.map_err(|err| HandshakeError::io(err).with_context("write client request: bind"))?;
tracing::trace!(
"socks5 client: bind handshake initiated w/ method: {selected_method} for destination: {destination}"
);
let server_reply = server::Reply::read_from(&mut stream)
.await
.map_err(|err| HandshakeError::protocol(err).with_context("read server reply"))?;
if server_reply.reply != ReplyKind::Succeeded {
return Err(HandshakeError::reply_kind(server_reply.reply)
.with_context("server responded with non-success reply"));
}
let HostWithPort {
host: select_host,
port: selected_port,
} = server_reply.bind_address;
let Ok(selected_addr) = select_host.try_as_ip() else {
tracing::debug!(
"bind command response does not accept non-IP host {select_host} as bind address",
);
let reply_kind = ReplyKind::AddressTypeNotSupported;
Reply::error_reply(reply_kind)
.write_to(&mut stream)
.await
.map_err(|err| {
HandshakeError::io(err).with_context("read server response: bind failed")
})?;
return Err(
HandshakeError::reply_kind(ReplyKind::AddressTypeNotSupported)
.with_context("selected bind addr must be an IP"),
);
};
let selected_bind_address = SocketAddress::new(selected_addr, selected_port);
tracing::trace!(
"socks5 client: socks5 server ready to bind w/ method {selected_method} at requested destination: {destination}",
);
stream.extensions().insert(StreamTransformed {
by: "rama-socks5::Socks5Client",
});
Ok(Binder::new(
stream,
requested_bind_address,
selected_bind_address,
))
}
pub async fn handshake_udp<S: Io + Unpin + ExtensionsRef>(
&self,
mut stream: S,
) -> Result<UdpSocketRelayBinder<S>, HandshakeError> {
let selected_method = match self.auth.as_ref() {
Some(auth) => self.handshake_headers_auth(&mut stream, auth).await?,
None => self.handshake_headers_no_auth(&mut stream).await?,
};
tracing::trace!("socks5 client: ready for udp association w/ method: {selected_method}",);
stream.extensions().insert(StreamTransformed {
by: "rama-socks5::Socks5Client",
});
Ok(UdpSocketRelayBinder::new(stream))
}
async fn handshake_headers_auth<S: Io + Unpin>(
&self,
stream: &mut S,
auth: &Socks5Auth,
) -> Result<SocksMethod, HandshakeError> {
let auth_method = auth.socks5_method();
let header = Header::new([SocksMethod::NoAuthenticationRequired, auth_method]);
header.write_to(stream).await.map_err(|err| {
HandshakeError::io(err).with_context("write client header: with auth method")
})?;
let methods = header.methods;
tracing::trace!("socks5 client: header with auth written w/ methods: {methods:?}");
let server_header = server::Header::read_from(stream).await.map_err(|err| {
HandshakeError::protocol(err).with_context("read server header (auth?)")
})?;
tracing::trace!(
"socks5 client: headers exchanged with auth as a provided method {} (for methods: {methods:?})",
server_header.method,
);
if server_header.method == SocksMethod::NoAuthenticationRequired {
return Ok(SocksMethod::NoAuthenticationRequired);
}
if server_header.method != auth_method {
return Err(HandshakeError::method_mismatch(server_header.method)
.with_context("unsolicited auth method"));
}
tracing::trace!(
"socks5 client: auth sub-negotation started w/ selected method {:?} for methods {:?}",
server_header.method,
methods,
);
match auth {
Socks5Auth::UsernamePassword(basic) => {
UsernamePasswordRequestRef {
version: UsernamePasswordSubnegotiationVersion::One,
basic,
}
.write_to(stream)
.await
.map_err(|err| {
HandshakeError::io(err).with_context(
"write client sub-negotiation request: username-password auth",
)
})?;
tracing::trace!(
"socks5 client: username-password request sent w/ selected method {:?} for methods {:?}",
server_header.method,
methods,
);
let auth_reply = server::UsernamePasswordResponse::read_from(stream)
.await
.map_err(|err| {
HandshakeError::protocol(err).with_context(
"read server sub-negotiation response: username-password auth",
)
})?;
if !auth_reply.success() {
return Err(HandshakeError::unauthorized(auth_reply.status));
}
tracing::trace!(
"socks5 client: authorized using username-password w/ selected method {:?} for methods {:?}",
server_header.method,
methods,
);
}
}
Ok(auth_method)
}
async fn handshake_headers_no_auth<S: Io + Unpin>(
&self,
stream: &mut S,
) -> Result<SocksMethod, HandshakeError> {
let header = Header::new(smallvec![SocksMethod::NoAuthenticationRequired]);
header.write_to(stream).await.map_err(|err| {
HandshakeError::io(err).with_context("write client headers: no auth required")
})?;
let methods = header.methods;
tracing::trace!("socks5 client: header without auth written for methods: {methods:?}");
let server_header = server::Header::read_from(stream).await.map_err(|err| {
HandshakeError::protocol(err).with_context("read server headers: no auth required (?)")
})?;
tracing::trace!(
"socks5 client: headers exchanged without auth /w selected method {} for methods: {:?}",
server_header.method,
methods,
);
if server_header.method != SocksMethod::NoAuthenticationRequired {
return Err(HandshakeError::method_mismatch(server_header.method)
.with_context("expected 'no auth required' method"));
}
Ok(SocksMethod::NoAuthenticationRequired)
}
}
#[cfg(test)]
mod tests {
use super::*;
use rama_core::ServiceInput;
use rama_net::{address::Host, user};
use rama_utils::str::non_empty_str;
#[tokio::test]
async fn test_client_handshake_connect_no_auth_failure_command_not_supported() {
let mut stream = ServiceInput::new(
tokio_test::io::Builder::new()
.write(b"\x05\x01\x00")
.read(b"\x05\x00")
.write(b"\x05\x01\x00\x01\x00\x00\x00\x00\x00\x00")
.read(b"\x05\x07\x00\x01\x00\x00\x00\x00\x00\x00")
.build(),
);
let client = Client::new();
let err = client
.handshake_connect(&mut stream, &HostWithPort::default_ipv4(0))
.await
.unwrap_err();
assert_eq!(err.reply(), ReplyKind::CommandNotSupported);
assert_eq!(err.connection_error_kind(), ConnectionErrorKind::Protocol);
}
#[tokio::test]
async fn test_client_handshake_connect_auth_error_guest() {
let mut stream = ServiceInput::new(
tokio_test::io::Builder::new()
.write(b"\x05\x01\x00")
.read(b"\x05\xff")
.build(),
);
let client = Client::default();
let err = client
.handshake_connect(&mut stream, &HostWithPort::default_ipv4(0))
.await
.unwrap_err();
assert_eq!(err.reply(), ReplyKind::GeneralServerFailure);
}
#[tokio::test]
async fn test_client_handshake_connect_auth_not_used_by_server_failure_command_not_supported() {
let mut stream = ServiceInput::new(
tokio_test::io::Builder::new()
.write(b"\x05\x02\x00\x02")
.read(b"\x05\x00")
.write(b"\x05\x01\x00\x01\x00\x00\x00\x00\x00\x00")
.read(b"\x05\x07\x00\x01\x00\x00\x00\x00\x00\x00")
.build(),
);
let client = Client::default().with_auth(user::Basic::new(
non_empty_str!("john"),
non_empty_str!("secret"),
));
let err = client
.handshake_connect(&mut stream, &HostWithPort::default_ipv4(0))
.await
.unwrap_err();
assert_eq!(err.reply(), ReplyKind::CommandNotSupported);
}
#[tokio::test]
async fn test_client_handshake_connect_with_auth_flow_failure_command_not_supported() {
let mut stream = ServiceInput::new(
tokio_test::io::Builder::new()
.write(b"\x05\x02\x00\x02")
.read(b"\x05\x02")
.write("\x01\x04john\x06secret".as_bytes())
.read(b"\x01\x00")
.write(b"\x05\x01\x00\x01\x00\x00\x00\x00\x00\x00")
.read(b"\x05\x07\x00\x01\x00\x00\x00\x00\x00\x00")
.build(),
);
let client = Client::default().with_auth(user::Basic::new(
non_empty_str!("john"),
non_empty_str!("secret"),
));
let err = client
.handshake_connect(&mut stream, &HostWithPort::default_ipv4(0))
.await
.unwrap_err();
assert_eq!(err.reply(), ReplyKind::CommandNotSupported);
}
#[tokio::test]
async fn test_client_handshake_connect_with_auth_flow_failure_invalid_credentials() {
let mut stream = ServiceInput::new(
tokio_test::io::Builder::new()
.write(b"\x05\x02\x00\x02")
.read(b"\x05\x02")
.write("\x01\x04john\x06secret".as_bytes())
.read(b"\x01\x01")
.build(),
);
let client = Client::default().with_auth(user::Basic::new(
non_empty_str!("john"),
non_empty_str!("secret"),
));
let err = client
.handshake_connect(&mut stream, &HostWithPort::default_ipv4(0))
.await
.unwrap_err();
assert_eq!(err.reply(), ReplyKind::ConnectionNotAllowed);
assert_eq!(
err.connection_error_kind(),
ConnectionErrorKind::Authentication
);
}
#[tokio::test]
async fn test_client_handshake_connect_failure_method_mismatch() {
let mut stream = ServiceInput::new(
tokio_test::io::Builder::new()
.write(b"\x05\x01\x00")
.read(b"\x05\x02")
.build(),
);
let client = Client::default();
let err = client
.handshake_connect(&mut stream, &HostWithPort::default_ipv4(0))
.await
.unwrap_err();
assert_eq!(err.reply(), ReplyKind::GeneralServerFailure);
assert_eq!(
err.connection_error_kind(),
ConnectionErrorKind::Authentication
);
}
#[tokio::test]
async fn test_client_handshake_connect_guest_connect_established() {
let mut stream = ServiceInput::new(
tokio_test::io::Builder::new()
.write(b"\x05\x01\x00")
.read(b"\x05\x00")
.write(&[
b'\x05', b'\x01', b'\x00', b'\x04', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 0, 1,
])
.read(&[b'\x05', b'\x00', b'\x00', b'\x01', 127, 0, 0, 1, 0, 65])
.build(),
);
let client = Client::default();
let local_addr = client
.handshake_connect(&mut stream, &HostWithPort::local_ipv6(1))
.await
.unwrap();
assert_eq!(local_addr, HostWithPort::local_ipv4(65));
}
#[tokio::test]
async fn test_client_handshake_connect_guest_connect_established_domain() {
let mut stream = ServiceInput::new(
tokio_test::io::Builder::new()
.write(b"\x05\x01\x00")
.read(b"\x05\x00")
.write("\x05\x01\x00\x03\x0bexample.com\x00\x01".as_bytes())
.read(&[b'\x05', b'\x00', b'\x00', b'\x01', 127, 0, 0, 1, 0, 1])
.build(),
);
let client = Client::default();
let local_addr = client
.handshake_connect(&mut stream, &HostWithPort::new(Host::EXAMPLE_NAME, 1))
.await
.unwrap();
assert_eq!(local_addr, HostWithPort::local_ipv4(1));
}
#[tokio::test]
async fn test_client_handshake_connect_guest_connect_established_domain_with_auth_flow() {
let mut stream = ServiceInput::new(
tokio_test::io::Builder::new()
.write(b"\x05\x02\x00\x02")
.read(b"\x05\x02")
.write(b"\x01\x04john\x06secret")
.read(b"\x01\x00")
.write(b"\x05\x01\x00\x03\x0bexample.com\x00\x01")
.read(&[b'\x05', b'\x00', b'\x00', b'\x01', 127, 0, 0, 1, 0, 1])
.build(),
);
let client = Client::default().with_auth(user::Basic::new(
non_empty_str!("john"),
non_empty_str!("secret"),
));
let local_addr = client
.handshake_connect(&mut stream, &HostWithPort::new(Host::EXAMPLE_NAME, 1))
.await
.unwrap();
assert_eq!(local_addr, HostWithPort::local_ipv4(1));
}
#[tokio::test]
async fn test_client_handshake_connect_guest_connect_established_domain_with_auth_flow_username_only()
{
let mut stream = ServiceInput::new(
tokio_test::io::Builder::new()
.write(b"\x05\x02\x00\x02")
.read(b"\x05\x02")
.write(b"\x01\x04john\x00")
.read(b"\x01\x00")
.write(b"\x05\x01\x00\x03\x0bexample.com\x00\x01")
.read(&[b'\x05', b'\x00', b'\x00', b'\x01', 127, 0, 0, 1, 0, 1])
.build(),
);
let client = Client::default().with_auth(user::Basic::new_insecure(non_empty_str!("john")));
let local_addr = client
.handshake_connect(&mut stream, &HostWithPort::example_domain_with_port(1))
.await
.unwrap();
assert_eq!(local_addr, HostWithPort::local_ipv4(1));
}
}