use std::io::{Read, Write};
use std::net::{IpAddr, SocketAddr, ToSocketAddrs};
use crate::error::{Error, Result};
fn resolve_ipv4(host: &str, port: u16) -> Result<[u8; 4]> {
for addr in (host, port).to_socket_addrs()? {
if let IpAddr::V4(v4) = addr.ip() {
return Ok(v4.octets());
}
}
Err(Error::BadResponse(format!(
"socks4: {host} has no IPv4 address (SOCKS4 is IPv4-only; use socks5)"
)))
}
pub(crate) fn socks4_connect<S: Read + Write>(
stream: &mut S,
host: &str,
port: u16,
user: &str,
remote_dns: bool,
) -> Result<()> {
let mut req = Vec::with_capacity(16 + host.len());
req.push(0x04); req.push(0x01); req.extend_from_slice(&port.to_be_bytes());
if remote_dns {
req.extend_from_slice(&[0, 0, 0, 1]);
} else {
req.extend_from_slice(&resolve_ipv4(host, port)?);
}
req.extend_from_slice(user.as_bytes());
req.push(0x00); if remote_dns {
req.extend_from_slice(host.as_bytes());
req.push(0x00); }
stream.write_all(&req)?;
stream.flush()?;
let mut reply = [0u8; 8];
stream.read_exact(&mut reply)?;
if reply[0] != 0x00 {
return Err(Error::BadResponse(format!(
"socks4: unexpected reply version byte {:#04x}",
reply[0]
)));
}
match reply[1] {
0x5A => Ok(()), 0x5B => Err(Error::BadResponse(
"socks4: request rejected or failed".into(),
)),
0x5C => Err(Error::BadResponse(
"socks4: rejected (proxy could not reach client identd)".into(),
)),
0x5D => Err(Error::BadResponse(
"socks4: rejected (identd authentication failed)".into(),
)),
other => Err(Error::BadResponse(format!(
"socks4: unknown reply code {other:#04x}"
))),
}
}
pub(crate) fn socks5_negotiate<S: Read + Write>(
stream: &mut S,
auth: Option<(&str, &str)>,
) -> Result<()> {
let greeting: &[u8] = if auth.is_some() {
&[0x05, 0x02, 0x00, 0x02]
} else {
&[0x05, 0x01, 0x00]
};
stream.write_all(greeting)?;
stream.flush()?;
let mut sel = [0u8; 2];
stream.read_exact(&mut sel)?;
if sel[0] != 0x05 {
return Err(Error::BadResponse(format!(
"socks5: bad version {:#04x} in method selection",
sel[0]
)));
}
match sel[1] {
0x00 => Ok(()), 0x02 => {
let (user, pass) = auth.ok_or_else(|| {
Error::BadResponse("socks5: proxy requires auth but none was provided".into())
})?;
if user.len() > 255 || pass.len() > 255 {
return Err(Error::BadResponse(
"socks5: username/password exceeds 255 bytes".into(),
));
}
let mut a = Vec::with_capacity(3 + user.len() + pass.len());
a.push(0x01); a.push(user.len() as u8);
a.extend_from_slice(user.as_bytes());
a.push(pass.len() as u8);
a.extend_from_slice(pass.as_bytes());
stream.write_all(&a)?;
stream.flush()?;
let mut ar = [0u8; 2];
stream.read_exact(&mut ar)?;
if ar[0] != 0x01 || ar[1] != 0x00 {
return Err(Error::BadResponse(
"socks5: username/password authentication failed".into(),
));
}
Ok(())
}
0xFF => Err(Error::BadResponse(
"socks5: proxy offered no acceptable authentication method".into(),
)),
other => Err(Error::BadResponse(format!(
"socks5: proxy selected unsupported method {other:#04x}"
))),
}
}
pub(crate) fn socks5_request<S: Read + Write>(
stream: &mut S,
cmd: u8,
host: &str,
port: u16,
remote_dns: bool,
) -> Result<SocketAddr> {
let mut req = vec![0x05u8, cmd, 0x00];
if remote_dns {
if host.len() > 255 {
return Err(Error::BadResponse(
"socks5h: hostname exceeds 255 bytes".into(),
));
}
req.push(0x03); req.push(host.len() as u8);
req.extend_from_slice(host.as_bytes());
} else {
let addr = (host, port)
.to_socket_addrs()?
.next()
.ok_or_else(|| Error::InvalidUrl(host.to_string()))?;
match addr.ip() {
IpAddr::V4(v4) => {
req.push(0x01);
req.extend_from_slice(&v4.octets());
}
IpAddr::V6(v6) => {
req.push(0x04);
req.extend_from_slice(&v6.octets());
}
}
}
req.extend_from_slice(&port.to_be_bytes());
stream.write_all(&req)?;
stream.flush()?;
let mut head = [0u8; 4];
stream.read_exact(&mut head)?;
if head[0] != 0x05 {
return Err(Error::BadResponse(format!(
"socks5: bad version {:#04x} in reply",
head[0]
)));
}
if head[1] != 0x00 {
return Err(Error::BadResponse(format!(
"socks5: request failed ({})",
socks5_reply_msg(head[1])
)));
}
let ip = match head[3] {
0x01 => {
let mut a = [0u8; 4];
stream.read_exact(&mut a)?;
IpAddr::V4(a.into())
}
0x04 => {
let mut a = [0u8; 16];
stream.read_exact(&mut a)?;
IpAddr::V6(a.into())
}
other => {
return Err(Error::BadResponse(format!(
"socks5: unexpected ATYP {other:#04x} in reply (domain not allowed)"
)))
}
};
let mut p = [0u8; 2];
stream.read_exact(&mut p)?;
Ok(SocketAddr::new(ip, u16::from_be_bytes(p)))
}
pub(crate) fn socks5_connect<S: Read + Write>(
stream: &mut S,
host: &str,
port: u16,
auth: Option<(&str, &str)>,
remote_dns: bool,
) -> Result<()> {
socks5_negotiate(stream, auth)?;
socks5_request(stream, 0x01, host, port, remote_dns)?;
Ok(())
}
pub(crate) fn socks5_reply_msg(code: u8) -> &'static str {
match code {
0x01 => "general SOCKS server failure",
0x02 => "connection not allowed by ruleset",
0x03 => "network unreachable",
0x04 => "host unreachable",
0x05 => "connection refused",
0x06 => "TTL expired",
0x07 => "command not supported",
0x08 => "address type not supported",
_ => "unknown error",
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
struct Mock {
reply: Cursor<Vec<u8>>,
written: Vec<u8>,
}
impl Mock {
fn new(reply: Vec<u8>) -> Self {
Mock {
reply: Cursor::new(reply),
written: Vec::new(),
}
}
}
impl Read for Mock {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.reply.read(buf)
}
}
impl Write for Mock {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.written.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
#[test]
fn socks4a_request_bytes_and_grant() {
let mut m = Mock::new(vec![0x00, 0x5A, 0, 0, 0, 0, 0, 0]);
socks4_connect(&mut m, "example.com", 443, "bob", true).unwrap();
let w = &m.written;
assert_eq!(w[0], 0x04);
assert_eq!(w[1], 0x01);
assert_eq!(&w[2..4], &443u16.to_be_bytes());
assert_eq!(&w[4..8], &[0, 0, 0, 1]); assert_eq!(&w[8..11], b"bob");
assert_eq!(w[11], 0x00);
assert_eq!(&w[12..23], b"example.com");
assert_eq!(w[23], 0x00);
}
#[test]
fn socks4_rejected_is_error() {
let mut m = Mock::new(vec![0x00, 0x5B, 0, 0, 0, 0, 0, 0]);
let err = socks4_connect(&mut m, "127.0.0.1", 80, "", false).unwrap_err();
assert!(matches!(err, Error::BadResponse(_)));
}
#[test]
fn socks5_no_auth_domain_connect() {
let mut reply = vec![0x05, 0x00];
reply.extend_from_slice(&[0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0]);
let mut m = Mock::new(reply);
socks5_connect(&mut m, "example.com", 80, None, true).unwrap();
let w = &m.written;
assert_eq!(&w[0..3], &[0x05, 0x01, 0x00]); assert_eq!(&w[3..6], &[0x05, 0x01, 0x00]);
assert_eq!(w[6], 0x03); assert_eq!(w[7], 11); assert_eq!(&w[8..19], b"example.com");
assert_eq!(&w[19..21], &80u16.to_be_bytes());
}
#[test]
fn socks5_userpass_success_then_ipv6_bnd() {
let mut reply = vec![0x05, 0x02, 0x01, 0x00];
reply.extend_from_slice(&[0x05, 0x00, 0x00, 0x04]);
reply.extend_from_slice(&[0u8; 16 + 2]);
let mut m = Mock::new(reply);
socks5_connect(&mut m, "1.2.3.4", 8080, Some(("u", "p")), false).unwrap();
let w = &m.written;
assert_eq!(&w[0..4], &[0x05, 0x02, 0x00, 0x02]);
assert_eq!(&w[4..9], &[0x01, 0x01, b'u', 0x01, b'p']);
assert_eq!(w[9], 0x05);
assert_eq!(w[12], 0x01); assert_eq!(&w[13..17], &[1, 2, 3, 4]);
}
#[test]
fn socks5_auth_failure_is_error() {
let reply = vec![0x05, 0x02, 0x01, 0x01]; let mut m = Mock::new(reply);
let err = socks5_connect(&mut m, "h", 1, Some(("u", "p")), true).unwrap_err();
assert!(matches!(err, Error::BadResponse(_)));
}
#[test]
fn socks5_connect_refused_is_error() {
let mut reply = vec![0x05, 0x00];
reply.extend_from_slice(&[0x05, 0x05, 0x00, 0x01, 0, 0, 0, 0, 0, 0]); let mut m = Mock::new(reply);
let err = socks5_connect(&mut m, "h", 1, None, true).unwrap_err();
match err {
Error::BadResponse(msg) => assert!(msg.contains("refused")),
_ => panic!("expected BadResponse"),
}
}
}