use std::net::IpAddr;
use super::{
ProtocolError, ProtocolVersion, ReplyKind, SocksMethod, UsernamePasswordSubnegotiationVersion,
common::{authority_length, read_authority, write_authority_to_buf},
};
use rama_core::bytes::{BufMut, BytesMut};
use rama_core::telemetry::tracing;
use rama_net::address::HostWithPort;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Header {
pub version: ProtocolVersion,
pub method: SocksMethod,
}
impl Header {
#[must_use]
pub fn new(method: SocksMethod) -> Self {
Self {
version: ProtocolVersion::Socks5,
method,
}
}
pub async fn read_from<R>(r: &mut R) -> Result<Self, ProtocolError>
where
R: AsyncRead + Unpin,
{
let version: ProtocolVersion = r.read_u8().await?.into();
match version {
ProtocolVersion::Socks5 => (),
ProtocolVersion::Unknown(version) => {
return Err(ProtocolError::UnexpectedByte {
pos: 0,
byte: version,
});
}
}
let method: SocksMethod = r.read_u8().await?.into();
Ok(Self { version, method })
}
pub async fn write_to<W>(&self, w: &mut W) -> Result<(), std::io::Error>
where
W: AsyncWrite + Unpin,
{
tracing::trace!("write socks5 server header: on stack (w=2)");
let mut buf = [0u8; 2];
self.write_to_buf(&mut buf.as_mut_slice());
w.write_all(&buf[..]).await
}
pub fn write_to_buf<B: BufMut>(&self, buf: &mut B) {
buf.put_u8(self.version.into());
buf.put_u8(self.method.into());
}
#[expect(unused)]
#[expect(clippy::unused_self)]
const fn serialized_len(&self) -> usize {
1 + 1
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Reply {
pub version: ProtocolVersion,
pub reply: ReplyKind,
pub bind_address: HostWithPort,
}
impl Reply {
pub fn new(addr: impl Into<HostWithPort>) -> Self {
Self {
version: ProtocolVersion::Socks5,
reply: ReplyKind::Succeeded,
bind_address: addr.into(),
}
}
#[must_use]
pub fn error_reply(kind: ReplyKind) -> Self {
Self {
version: ProtocolVersion::Socks5,
reply: kind,
bind_address: HostWithPort::default_ipv4(0),
}
}
pub async fn read_from<R>(r: &mut R) -> Result<Self, ProtocolError>
where
R: AsyncRead + Unpin,
{
let version: ProtocolVersion = r.read_u8().await?.into();
match version {
ProtocolVersion::Socks5 => (),
ProtocolVersion::Unknown(version) => {
return Err(ProtocolError::unexpected_byte(0, version));
}
}
let reply: ReplyKind = r.read_u8().await?.into();
let rsv = r.read_u8().await?;
if rsv != 0 {
return Err(ProtocolError::unexpected_byte(2, rsv));
}
let bind_address = read_authority(r).await?;
Ok(Self {
version,
reply,
bind_address,
})
}
pub async fn write_to<W>(&self, w: &mut W) -> Result<(), std::io::Error>
where
W: AsyncWrite + Unpin,
{
let n = self.serialized_len();
if let Ok(ip) = self.bind_address.host.try_as_ip() {
return match ip {
IpAddr::V4(_) => {
tracing::trace!("write socks5 server reply w/ Ipv4 addr: on stack (w={n})");
debug_assert_eq!(4 + 4 + 2, n);
let mut buf = [0u8; 10];
self.write_to_buf(&mut buf.as_mut_slice())?;
w.write_all(&buf[..]).await
}
IpAddr::V6(_) => {
tracing::trace!("write socks5 server reply w/ Ipv6 addr: on stack (w={n})");
debug_assert_eq!(4 + 16 + 2, n);
let mut buf = [0u8; 22];
self.write_to_buf(&mut buf.as_mut_slice())?;
w.write_all(&buf[..]).await
}
};
}
const SMALL_LEN: usize = 32 + 1 + 6;
const MED_LEN: usize = 64 + 1 + 6;
if n <= SMALL_LEN {
tracing::trace!("write socks5 server reply w/ (small) domain name: on stack (w={n})");
let mut buf = [0u8; SMALL_LEN];
self.write_to_buf(&mut buf.as_mut_slice())?;
w.write_all(&buf[..n]).await
} else if n <= MED_LEN {
tracing::trace!("write socks5 server reply w/ (medium) domain name: on stack (w={n})");
let mut buf = [0u8; MED_LEN];
self.write_to_buf(&mut buf.as_mut_slice())?;
w.write_all(&buf[..n]).await
} else {
tracing::trace!("write socks5 server reply w/ (large) domain name: on heap (w={n})");
let mut buf = BytesMut::with_capacity(n);
self.write_to_buf(&mut buf)?;
w.write_all(&buf).await
}
}
pub fn write_to_buf<B: BufMut>(&self, buf: &mut B) -> Result<(), std::io::Error> {
buf.put_u8(self.version.into());
buf.put_u8(self.reply.into());
buf.put_u8(0 );
write_authority_to_buf(&self.bind_address, buf)
}
fn serialized_len(&self) -> usize {
4 + authority_length(&self.bind_address)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UsernamePasswordResponse {
pub version: UsernamePasswordSubnegotiationVersion,
pub status: u8,
}
impl UsernamePasswordResponse {
#[must_use]
pub fn new_success() -> Self {
Self {
version: UsernamePasswordSubnegotiationVersion::One,
status: 0,
}
}
#[must_use]
pub fn new_invalid_credentails() -> Self {
Self {
version: UsernamePasswordSubnegotiationVersion::One,
status: 1,
}
}
#[must_use]
pub fn new_user_not_found() -> Self {
Self {
version: UsernamePasswordSubnegotiationVersion::One,
status: 2,
}
}
#[must_use]
pub fn new_auth_system_unavailable() -> Self {
Self {
version: UsernamePasswordSubnegotiationVersion::One,
status: 4,
}
}
#[must_use]
pub fn success(&self) -> bool {
self.status == 0
}
}
impl UsernamePasswordResponse {
pub async fn read_from<R>(r: &mut R) -> Result<Self, ProtocolError>
where
R: AsyncRead + Unpin,
{
let version: UsernamePasswordSubnegotiationVersion = r.read_u8().await?.into();
match version {
UsernamePasswordSubnegotiationVersion::One => (),
UsernamePasswordSubnegotiationVersion::Unknown(version) => {
return Err(ProtocolError::unexpected_byte(0, version));
}
}
let status = r.read_u8().await?;
Ok(Self { version, status })
}
pub async fn write_to<W>(&self, w: &mut W) -> Result<(), std::io::Error>
where
W: AsyncWrite + Unpin,
{
tracing::trace!("write socks5 server header: on stack (w=2)");
let mut buf = [0u8; 2];
self.write_to_buf(&mut buf.as_mut_slice());
w.write_all(&buf[..]).await
}
pub fn write_to_buf<B: BufMut>(&self, buf: &mut B) {
buf.put_u8(self.version.into());
buf.put_u8(self.status);
}
#[expect(unused)]
#[expect(clippy::unused_self)]
fn serialized_len(&self) -> usize {
1 + 1
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::proto::test_write_read_eq;
#[tokio::test]
async fn test_header_write_read_eq() {
test_write_read_eq!(Header::new(SocksMethod::JSONParameterBlock), Header,);
}
#[tokio::test]
async fn test_reply_write_read_eq() {
test_write_read_eq!(
Reply {
version: ProtocolVersion::Socks5,
reply: ReplyKind::Succeeded,
bind_address: HostWithPort::default_ipv4(4128)
},
Reply,
);
test_write_read_eq!(Reply::error_reply(ReplyKind::ConnectionNotAllowed), Reply,);
}
#[tokio::test]
async fn test_username_password_response_write_read_eq() {
test_write_read_eq!(
UsernamePasswordResponse::new_success(),
UsernamePasswordResponse,
);
test_write_read_eq!(
UsernamePasswordResponse::new_invalid_credentails(),
UsernamePasswordResponse,
);
}
}