pub mod client;
pub mod server;
pub mod util;
use std::fmt;
use std::io;
use thiserror::Error;
use util::stream::read_exact;
use util::target_addr::read_address;
use util::target_addr::AddrError;
use util::target_addr::TargetAddr;
use util::target_addr::ToTargetAddr;
use tokio::io::AsyncReadExt;
#[rustfmt::skip]
pub mod consts {
pub const SOCKS5_VERSION: u8 = 0x05;
pub const SOCKS5_AUTH_METHOD_NONE: u8 = 0x00;
pub const SOCKS5_AUTH_METHOD_GSSAPI: u8 = 0x01;
pub const SOCKS5_AUTH_METHOD_PASSWORD: u8 = 0x02;
pub const SOCKS5_AUTH_METHOD_NOT_ACCEPTABLE: u8 = 0xff;
pub const SOCKS5_CMD_TCP_CONNECT: u8 = 0x01;
pub const SOCKS5_CMD_TCP_BIND: u8 = 0x02;
pub const SOCKS5_CMD_UDP_ASSOCIATE: u8 = 0x03;
pub const SOCKS5_ADDR_TYPE_IPV4: u8 = 0x01;
pub const SOCKS5_ADDR_TYPE_DOMAIN_NAME: u8 = 0x03;
pub const SOCKS5_ADDR_TYPE_IPV6: u8 = 0x04;
pub const SOCKS5_REPLY_SUCCEEDED: u8 = 0x00;
pub const SOCKS5_REPLY_GENERAL_FAILURE: u8 = 0x01;
pub const SOCKS5_REPLY_CONNECTION_NOT_ALLOWED: u8 = 0x02;
pub const SOCKS5_REPLY_NETWORK_UNREACHABLE: u8 = 0x03;
pub const SOCKS5_REPLY_HOST_UNREACHABLE: u8 = 0x04;
pub const SOCKS5_REPLY_CONNECTION_REFUSED: u8 = 0x05;
pub const SOCKS5_REPLY_TTL_EXPIRED: u8 = 0x06;
pub const SOCKS5_REPLY_COMMAND_NOT_SUPPORTED: u8 = 0x07;
pub const SOCKS5_REPLY_ADDRESS_TYPE_NOT_SUPPORTED: u8 = 0x08;
}
#[derive(Debug, PartialEq)]
pub enum Socks5Command {
TCPConnect,
TCPBind,
UDPAssociate,
}
impl Socks5Command {
#[inline]
#[rustfmt::skip]
pub fn as_u8(&self) -> u8 {
match self {
Socks5Command::TCPConnect => consts::SOCKS5_CMD_TCP_CONNECT,
Socks5Command::TCPBind => consts::SOCKS5_CMD_TCP_BIND,
Socks5Command::UDPAssociate => consts::SOCKS5_CMD_UDP_ASSOCIATE,
}
}
#[inline]
#[rustfmt::skip]
pub fn from_u8(code: u8) -> Option<Socks5Command> {
match code {
consts::SOCKS5_CMD_TCP_CONNECT => Some(Socks5Command::TCPConnect),
consts::SOCKS5_CMD_TCP_BIND => Some(Socks5Command::TCPBind),
consts::SOCKS5_CMD_UDP_ASSOCIATE => Some(Socks5Command::UDPAssociate),
_ => None,
}
}
}
#[derive(Debug, PartialEq)]
pub enum AuthenticationMethod {
None,
Password { username: String, password: String },
}
impl AuthenticationMethod {
#[inline]
#[rustfmt::skip]
fn as_u8(&self) -> u8 {
match self {
AuthenticationMethod::None => consts::SOCKS5_AUTH_METHOD_NONE,
AuthenticationMethod::Password {..} =>
consts::SOCKS5_AUTH_METHOD_PASSWORD
}
}
}
impl fmt::Display for AuthenticationMethod {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
AuthenticationMethod::None => f.write_str("AuthenticationMethod::None"),
AuthenticationMethod::Password { .. } => f.write_str("AuthenticationMethod::Password"),
}
}
}
#[derive(Error, Debug)]
pub enum SocksError {
#[error("i/o error: {0}")]
Io(#[from] io::Error),
#[error("Auth method unacceptable `{0:?}`.")]
AuthMethodUnacceptable(Vec<u8>),
#[error("Unsupported SOCKS version `{0}`.")]
UnsupportedSocksVersion(u8),
#[error("Domain exceeded max sequence length")]
ExceededMaxDomainLen(usize),
#[error("Authentication rejected `{0}`")]
AuthenticationRejected(String),
#[error("Target addr should be present")]
MissingTargetAddr,
#[error(transparent)]
ServerError(#[from] server::SocksServerError),
#[error(transparent)]
UdpHeaderError(#[from] UdpHeaderError),
#[error(transparent)]
AddrError(#[from] AddrError),
#[error("Error with reply: {0}.")]
ReplyError(#[from] ReplyError),
}
pub type Result<T, E = SocksError> = core::result::Result<T, E>;
#[derive(Error, Debug, Copy, Clone)]
pub enum ReplyError {
#[error("Succeeded")]
Succeeded,
#[error("General failure")]
GeneralFailure,
#[error("Connection not allowed by ruleset")]
ConnectionNotAllowed,
#[error("Network unreachable")]
NetworkUnreachable,
#[error("Host unreachable")]
HostUnreachable,
#[error("Connection refused")]
ConnectionRefused,
#[error("Connection timeout")]
ConnectionTimeout,
#[error("TTL expired")]
TtlExpired,
#[error("Command not supported")]
CommandNotSupported,
#[error("Address type not supported")]
AddressTypeNotSupported,
}
impl ReplyError {
#[inline]
#[rustfmt::skip]
pub fn as_u8(self) -> u8 {
match self {
ReplyError::Succeeded => consts::SOCKS5_REPLY_SUCCEEDED,
ReplyError::GeneralFailure => consts::SOCKS5_REPLY_GENERAL_FAILURE,
ReplyError::ConnectionNotAllowed => consts::SOCKS5_REPLY_CONNECTION_NOT_ALLOWED,
ReplyError::NetworkUnreachable => consts::SOCKS5_REPLY_NETWORK_UNREACHABLE,
ReplyError::HostUnreachable => consts::SOCKS5_REPLY_HOST_UNREACHABLE,
ReplyError::ConnectionRefused => consts::SOCKS5_REPLY_CONNECTION_REFUSED,
ReplyError::ConnectionTimeout => consts::SOCKS5_REPLY_TTL_EXPIRED,
ReplyError::TtlExpired => consts::SOCKS5_REPLY_TTL_EXPIRED,
ReplyError::CommandNotSupported => consts::SOCKS5_REPLY_COMMAND_NOT_SUPPORTED,
ReplyError::AddressTypeNotSupported => consts::SOCKS5_REPLY_ADDRESS_TYPE_NOT_SUPPORTED,
}
}
#[inline]
#[rustfmt::skip]
pub fn from_u8(code: u8) -> ReplyError {
match code {
consts::SOCKS5_REPLY_SUCCEEDED => ReplyError::Succeeded,
consts::SOCKS5_REPLY_GENERAL_FAILURE => ReplyError::GeneralFailure,
consts::SOCKS5_REPLY_CONNECTION_NOT_ALLOWED => ReplyError::ConnectionNotAllowed,
consts::SOCKS5_REPLY_NETWORK_UNREACHABLE => ReplyError::NetworkUnreachable,
consts::SOCKS5_REPLY_HOST_UNREACHABLE => ReplyError::HostUnreachable,
consts::SOCKS5_REPLY_CONNECTION_REFUSED => ReplyError::ConnectionRefused,
consts::SOCKS5_REPLY_TTL_EXPIRED => ReplyError::TtlExpired,
consts::SOCKS5_REPLY_COMMAND_NOT_SUPPORTED => ReplyError::CommandNotSupported,
consts::SOCKS5_REPLY_ADDRESS_TYPE_NOT_SUPPORTED => ReplyError::AddressTypeNotSupported,
_ => ReplyError::GeneralFailure,
}
}
}
#[derive(thiserror::Error, Debug)]
pub enum UdpHeaderError {
#[error(transparent)]
AddrError(#[from] AddrError),
#[error("could not convert target addr: {0}")]
ToTargetAddr(#[source] io::Error),
#[error("could not read UDP header: {0}")]
ReadingError(#[source] io::Error),
#[error("does not match the expected reserved field")]
GarbageInReserved,
}
pub fn new_udp_header<T: ToTargetAddr>(target_addr: T) -> Result<Vec<u8>, UdpHeaderError> {
let mut header = vec![
0, 0, 0, ];
header.append(
&mut target_addr
.to_target_addr()
.map_err(UdpHeaderError::ToTargetAddr)?
.to_be_bytes()?,
);
Ok(header)
}
pub async fn parse_udp_request(mut req: &[u8]) -> Result<(u8, TargetAddr, &[u8]), UdpHeaderError> {
let rsv = read_exact!(req, [0u8; 2]).map_err(UdpHeaderError::ReadingError)?;
if !rsv.eq(&[0u8; 2]) {
return Err(UdpHeaderError::GarbageInReserved);
}
let [frag, atyp] = read_exact!(req, [0u8; 2]).map_err(UdpHeaderError::ReadingError)?;
let target_addr = read_address(&mut req, atyp).await?;
Ok((frag, target_addr, req))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn socks5_command_mapping() {
assert_eq!(
Socks5Command::TCPConnect.as_u8(),
consts::SOCKS5_CMD_TCP_CONNECT
);
assert_eq!(Socks5Command::TCPBind.as_u8(), consts::SOCKS5_CMD_TCP_BIND);
assert_eq!(
Socks5Command::UDPAssociate.as_u8(),
consts::SOCKS5_CMD_UDP_ASSOCIATE
);
assert_eq!(
Socks5Command::from_u8(consts::SOCKS5_CMD_TCP_CONNECT),
Some(Socks5Command::TCPConnect)
);
assert_eq!(
Socks5Command::from_u8(consts::SOCKS5_CMD_TCP_BIND),
Some(Socks5Command::TCPBind)
);
assert_eq!(
Socks5Command::from_u8(consts::SOCKS5_CMD_UDP_ASSOCIATE),
Some(Socks5Command::UDPAssociate)
);
assert_eq!(Socks5Command::from_u8(0x00), None);
assert_eq!(Socks5Command::from_u8(0x04), None);
}
#[test]
fn reply_error_mapping_table() {
let table = [
(ReplyError::Succeeded, consts::SOCKS5_REPLY_SUCCEEDED),
(
ReplyError::GeneralFailure,
consts::SOCKS5_REPLY_GENERAL_FAILURE,
),
(
ReplyError::ConnectionNotAllowed,
consts::SOCKS5_REPLY_CONNECTION_NOT_ALLOWED,
),
(
ReplyError::NetworkUnreachable,
consts::SOCKS5_REPLY_NETWORK_UNREACHABLE,
),
(
ReplyError::HostUnreachable,
consts::SOCKS5_REPLY_HOST_UNREACHABLE,
),
(
ReplyError::ConnectionRefused,
consts::SOCKS5_REPLY_CONNECTION_REFUSED,
),
(
ReplyError::ConnectionTimeout,
consts::SOCKS5_REPLY_TTL_EXPIRED,
),
(ReplyError::TtlExpired, consts::SOCKS5_REPLY_TTL_EXPIRED),
(
ReplyError::CommandNotSupported,
consts::SOCKS5_REPLY_COMMAND_NOT_SUPPORTED,
),
(
ReplyError::AddressTypeNotSupported,
consts::SOCKS5_REPLY_ADDRESS_TYPE_NOT_SUPPORTED,
),
];
for (reply, code) in table {
assert_eq!(reply.as_u8(), code);
assert!(matches!(ReplyError::from_u8(code), r if r.as_u8() == code));
}
}
#[test]
fn reply_error_unassigned_codes_map_to_general_failure() {
for code in [0x09u8, 0x0a, 0x7f, 0xff] {
assert_eq!(ReplyError::from_u8(code).as_u8(), 0x01);
}
}
#[tokio::test]
async fn udp_header_round_trip_ip() {
let target = TargetAddr::Ip("192.168.0.1:80".parse().unwrap());
let header = new_udp_header(target.clone()).unwrap();
assert_eq!(header, vec![0, 0, 0, 1, 192, 168, 0, 1, 0, 80]);
let (frag, parsed_target, payload) = parse_udp_request(&header).await.unwrap();
assert_eq!(frag, 0);
assert_eq!(parsed_target, target);
assert!(payload.is_empty());
}
#[tokio::test]
async fn udp_header_round_trip_domain() {
let target = TargetAddr::Domain("example.com".to_owned(), 53);
let header = new_udp_header(target.clone()).unwrap();
assert_eq!(
header,
vec![
0,
0,
0,
consts::SOCKS5_ADDR_TYPE_DOMAIN_NAME,
11,
b'e',
b'x',
b'a',
b'm',
b'p',
b'l',
b'e',
b'.',
b'c',
b'o',
b'm',
0,
53,
]
);
let (frag, parsed_target, payload) = parse_udp_request(&header).await.unwrap();
assert_eq!(frag, 0);
assert_eq!(parsed_target, target);
assert!(payload.is_empty());
}
#[tokio::test]
async fn udp_header_rejects_garbage_reserved() {
let mut header = new_udp_header(TargetAddr::Domain("example.com".to_owned(), 53)).unwrap();
header[1] = 0xff;
let err = parse_udp_request(&header).await.unwrap_err();
assert!(matches!(err, UdpHeaderError::GarbageInReserved));
}
#[tokio::test]
async fn udp_header_carries_fragment_and_payload() {
let mut frame = new_udp_header(TargetAddr::Ip("10.0.0.1:53".parse().unwrap())).unwrap();
frame[2] = 3;
frame.extend_from_slice(b"hello");
let (frag, target, payload) = parse_udp_request(&frame).await.unwrap();
assert_eq!(frag, 3);
assert_eq!(target, TargetAddr::Ip("10.0.0.1:53".parse().unwrap()));
assert_eq!(payload, b"hello");
}
#[tokio::test]
async fn udp_header_rejects_unknown_address_type() {
let frame = vec![0, 0, 0, 0x02, 1, 2, 3, 4, 0, 0];
let err = parse_udp_request(&frame).await.unwrap_err();
assert!(matches!(err, UdpHeaderError::AddrError(_)));
}
#[test]
fn socks_error_from_variants() {
let err: SocksError = UdpHeaderError::GarbageInReserved.into();
assert!(matches!(err, SocksError::UdpHeaderError(_)));
let err: SocksError = AddrError::IncorrectAddressType.into();
assert!(matches!(err, SocksError::AddrError(_)));
let err: SocksError = ReplyError::CommandNotSupported.into();
assert!(matches!(err, SocksError::ReplyError(_)));
}
}