#![cfg(feature = "std")]
use alloc::format;
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use std::io::{self, Read, Write};
const SOCKS5: u8 = 0x05;
const SOCKS4: u8 = 0x04;
const METHOD_NO_AUTH: u8 = 0x00;
const METHOD_NONE: u8 = 0xFF;
const CMD_CONNECT: u8 = 0x01;
const ATYP_IPV4: u8 = 0x01;
const ATYP_DOMAIN: u8 = 0x03;
const ATYP_IPV6: u8 = 0x04;
const REP_SUCCESS: u8 = 0x00;
const REP_GENERAL_FAILURE: u8 = 0x01;
const REP_CMD_NOT_SUPPORTED: u8 = 0x07;
const SOCKS4_GRANTED: u8 = 0x5A;
const SOCKS4_REJECTED: u8 = 0x5B;
const MAX_HOST_LEN: usize = 255;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SocksTarget {
pub host: String,
pub port: u16,
pub version: SocksVersion,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SocksVersion {
V4,
V5,
}
#[derive(Debug)]
pub enum SocksError {
Io(io::Error),
Protocol(&'static str),
Unsupported(&'static str),
}
impl core::fmt::Display for SocksError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
SocksError::Io(e) => write!(f, "socks: io: {e}"),
SocksError::Protocol(m) => write!(f, "socks: protocol: {m}"),
SocksError::Unsupported(m) => write!(f, "socks: unsupported: {m}"),
}
}
}
impl std::error::Error for SocksError {}
impl From<io::Error> for SocksError {
fn from(e: io::Error) -> Self {
SocksError::Io(e)
}
}
pub fn handshake<S: Read + Write>(s: &mut S) -> Result<SocksTarget, SocksError> {
let mut ver = [0u8; 1];
s.read_exact(&mut ver)?;
match ver[0] {
SOCKS5 => handshake_v5(s),
SOCKS4 => handshake_v4(s),
_ => Err(SocksError::Protocol("unknown SOCKS version")),
}
}
fn handshake_v5<S: Read + Write>(s: &mut S) -> Result<SocksTarget, SocksError> {
let mut nmethods = [0u8; 1];
s.read_exact(&mut nmethods)?;
let mut methods = vec![0u8; nmethods[0] as usize];
s.read_exact(&mut methods)?;
if !methods.contains(&METHOD_NO_AUTH) {
let _ = s.write_all(&[SOCKS5, METHOD_NONE]);
return Err(SocksError::Unsupported(
"SOCKS5: only no-authentication is supported",
));
}
s.write_all(&[SOCKS5, METHOD_NO_AUTH])?;
let mut head = [0u8; 4];
s.read_exact(&mut head)?;
if head[0] != SOCKS5 {
return Err(SocksError::Protocol("SOCKS5: bad request version"));
}
if head[1] != CMD_CONNECT {
write_v5_reply(s, REP_CMD_NOT_SUPPORTED);
return Err(SocksError::Unsupported(
"SOCKS5: only CONNECT is supported (BIND/UDP refused)",
));
}
let atyp = head[3];
let host = match atyp {
ATYP_IPV4 => {
let mut a = [0u8; 4];
s.read_exact(&mut a)?;
format!("{}.{}.{}.{}", a[0], a[1], a[2], a[3])
}
ATYP_IPV6 => {
let mut a = [0u8; 16];
s.read_exact(&mut a)?;
format_ipv6(&a)
}
ATYP_DOMAIN => {
let mut len = [0u8; 1];
s.read_exact(&mut len)?;
let n = len[0] as usize;
if n == 0 {
return Err(SocksError::Protocol("SOCKS5: empty domain name"));
}
let mut name = vec![0u8; n];
s.read_exact(&mut name)?;
String::from_utf8(name)
.map_err(|_| SocksError::Protocol("SOCKS5: non-UTF8 domain name"))?
}
_ => {
write_v5_reply(s, REP_GENERAL_FAILURE);
return Err(SocksError::Protocol("SOCKS5: unknown address type"));
}
};
let mut port = [0u8; 2];
s.read_exact(&mut port)?;
let port = u16::from_be_bytes(port);
Ok(SocksTarget {
host,
port,
version: SocksVersion::V5,
})
}
fn handshake_v4<S: Read + Write>(s: &mut S) -> Result<SocksTarget, SocksError> {
let mut head = [0u8; 1 + 2 + 4];
s.read_exact(&mut head)?;
let cmd = head[0];
let port = u16::from_be_bytes([head[1], head[2]]);
let ip = [head[3], head[4], head[5], head[6]];
if cmd != CMD_CONNECT {
write_v4_reply(s, SOCKS4_REJECTED);
return Err(SocksError::Unsupported(
"SOCKS4: only CONNECT is supported (BIND refused)",
));
}
read_nul_terminated(s, MAX_HOST_LEN)?;
let is_socks4a = ip[0] == 0 && ip[1] == 0 && ip[2] == 0 && ip[3] != 0;
let host = if is_socks4a {
let name = read_nul_terminated(s, MAX_HOST_LEN)?;
if name.is_empty() {
return Err(SocksError::Protocol("SOCKS4a: empty domain name"));
}
String::from_utf8(name).map_err(|_| SocksError::Protocol("SOCKS4a: non-UTF8 domain"))?
} else {
format!("{}.{}.{}.{}", ip[0], ip[1], ip[2], ip[3])
};
Ok(SocksTarget {
host,
port,
version: SocksVersion::V4,
})
}
pub fn write_reply<S: Write>(s: &mut S, version: SocksVersion, ok: bool) -> io::Result<()> {
match version {
SocksVersion::V5 => {
let rep = if ok { REP_SUCCESS } else { REP_GENERAL_FAILURE };
s.write_all(&[SOCKS5, rep, 0x00, ATYP_IPV4, 0, 0, 0, 0, 0, 0])
}
SocksVersion::V4 => {
let code = if ok { SOCKS4_GRANTED } else { SOCKS4_REJECTED };
s.write_all(&[0x00, code, 0, 0, 0, 0, 0, 0])
}
}
}
fn write_v5_reply<S: Write>(s: &mut S, rep: u8) {
let _ = s.write_all(&[SOCKS5, rep, 0x00, ATYP_IPV4, 0, 0, 0, 0, 0, 0]);
}
fn write_v4_reply<S: Write>(s: &mut S, code: u8) {
let _ = s.write_all(&[0x00, code, 0, 0, 0, 0, 0, 0]);
}
fn read_nul_terminated<S: Read>(s: &mut S, max: usize) -> Result<Vec<u8>, SocksError> {
let mut out = Vec::new();
let mut byte = [0u8; 1];
loop {
s.read_exact(&mut byte)?;
if byte[0] == 0 {
return Ok(out);
}
if out.len() >= max {
return Err(SocksError::Protocol("SOCKS: NUL-terminated field too long"));
}
out.push(byte[0]);
}
}
fn format_ipv6(a: &[u8; 16]) -> String {
let addr = std::net::Ipv6Addr::from(*a);
format!("{addr}")
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
struct MockStream {
input: Cursor<Vec<u8>>,
output: Vec<u8>,
}
impl MockStream {
fn new(input: Vec<u8>) -> Self {
Self {
input: Cursor::new(input),
output: Vec::new(),
}
}
}
impl Read for MockStream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.input.read(buf)
}
}
impl Write for MockStream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.output.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
#[test]
fn socks5_connect_ipv4() {
let req = vec![
SOCKS5,
1,
METHOD_NO_AUTH,
SOCKS5,
CMD_CONNECT,
0x00,
ATYP_IPV4,
1,
2,
3,
4,
0x00,
0x50,
];
let mut s = MockStream::new(req);
let t = handshake(&mut s).unwrap();
assert_eq!(t.host, "1.2.3.4");
assert_eq!(t.port, 80);
assert_eq!(t.version, SocksVersion::V5);
assert_eq!(&s.output[..2], &[SOCKS5, METHOD_NO_AUTH]);
}
#[test]
fn socks5_connect_domain() {
let host = b"example.com";
let mut req = vec![
SOCKS5,
1,
METHOD_NO_AUTH,
SOCKS5,
CMD_CONNECT,
0x00,
ATYP_DOMAIN,
];
req.push(host.len() as u8);
req.extend_from_slice(host);
req.extend_from_slice(&443u16.to_be_bytes());
let mut s = MockStream::new(req);
let t = handshake(&mut s).unwrap();
assert_eq!(t.host, "example.com");
assert_eq!(t.port, 443);
}
#[test]
fn socks5_connect_ipv6() {
let mut req = vec![
SOCKS5,
1,
METHOD_NO_AUTH,
SOCKS5,
CMD_CONNECT,
0x00,
ATYP_IPV6,
];
let addr = std::net::Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1);
req.extend_from_slice(&addr.octets());
req.extend_from_slice(&8080u16.to_be_bytes());
let mut s = MockStream::new(req);
let t = handshake(&mut s).unwrap();
assert_eq!(t.host, "2001:db8::1");
assert_eq!(t.port, 8080);
}
#[test]
fn socks5_rejects_non_no_auth() {
let req = vec![SOCKS5, 1, 0x02];
let mut s = MockStream::new(req);
let err = handshake(&mut s).unwrap_err();
assert!(matches!(err, SocksError::Unsupported(_)));
assert_eq!(&s.output[..2], &[SOCKS5, METHOD_NONE]);
}
#[test]
fn socks5_rejects_bind() {
let req = vec![
SOCKS5,
1,
METHOD_NO_AUTH,
SOCKS5,
0x02,
0x00,
ATYP_IPV4,
1,
2,
3,
4,
0,
80,
];
let mut s = MockStream::new(req);
let err = handshake(&mut s).unwrap_err();
assert!(matches!(err, SocksError::Unsupported(_)));
assert_eq!(s.output[1], METHOD_NO_AUTH);
assert_eq!(s.output[3], REP_CMD_NOT_SUPPORTED);
}
#[test]
fn socks5_rejects_udp() {
let req = vec![
SOCKS5,
1,
METHOD_NO_AUTH,
SOCKS5,
0x03,
0x00,
ATYP_IPV4,
0,
0,
0,
0,
0,
0,
];
let mut s = MockStream::new(req);
assert!(matches!(handshake(&mut s), Err(SocksError::Unsupported(_))));
}
#[test]
fn socks4_connect_ipv4() {
let mut req = vec![SOCKS4, CMD_CONNECT];
req.extend_from_slice(&80u16.to_be_bytes());
req.extend_from_slice(&[1, 2, 3, 4]);
req.extend_from_slice(b"me\0");
let mut s = MockStream::new(req);
let t = handshake(&mut s).unwrap();
assert_eq!(t.host, "1.2.3.4");
assert_eq!(t.port, 80);
assert_eq!(t.version, SocksVersion::V4);
}
#[test]
fn socks4a_connect_domain() {
let mut req = vec![SOCKS4, CMD_CONNECT];
req.extend_from_slice(&443u16.to_be_bytes());
req.extend_from_slice(&[0, 0, 0, 1]);
req.extend_from_slice(b"\0"); req.extend_from_slice(b"example.org\0");
let mut s = MockStream::new(req);
let t = handshake(&mut s).unwrap();
assert_eq!(t.host, "example.org");
assert_eq!(t.port, 443);
}
#[test]
fn socks4_rejects_bind() {
let mut req = vec![SOCKS4, 0x02 ];
req.extend_from_slice(&80u16.to_be_bytes());
req.extend_from_slice(&[1, 2, 3, 4]);
req.extend_from_slice(b"\0");
let mut s = MockStream::new(req);
assert!(matches!(handshake(&mut s), Err(SocksError::Unsupported(_))));
assert_eq!(s.output[1], SOCKS4_REJECTED);
}
#[test]
fn unknown_version_rejected() {
let mut s = MockStream::new(vec![0x06, 0, 0]);
assert!(matches!(handshake(&mut s), Err(SocksError::Protocol(_))));
}
#[test]
fn reply_v5_success_shape() {
let mut out = Vec::new();
write_reply(&mut out, SocksVersion::V5, true).unwrap();
assert_eq!(
out,
vec![SOCKS5, REP_SUCCESS, 0, ATYP_IPV4, 0, 0, 0, 0, 0, 0]
);
}
#[test]
fn reply_v4_failure_shape() {
let mut out = Vec::new();
write_reply(&mut out, SocksVersion::V4, false).unwrap();
assert_eq!(out, vec![0x00, SOCKS4_REJECTED, 0, 0, 0, 0, 0, 0]);
}
}