init commit from github.com/dizda/fast-socks5

This commit is contained in:
2022-06-08 00:40:54 +08:00
parent c7c77aaec0
commit c6b81bd13b
14 changed files with 3119 additions and 6 deletions

363
src/socks4/client.rs Normal file
View File

@@ -0,0 +1,363 @@
#[forbid(unsafe_code)]
use crate::read_exact;
use crate::socks4::{consts, ReplyError, Socks4Command};
use crate::util::target_addr::{TargetAddr, ToTargetAddr};
use crate::{Result, SocksError, SocksError::ReplySocks4Error};
use anyhow::Context;
use std::io;
use std::net::SocketAddr;
use std::net::ToSocketAddrs;
use std::pin::Pin;
use std::task::Poll;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use tokio::net::TcpStream;
const MAX_ADDR_LEN: usize = 260;
/// A SOCKS4 client.
/// `Socks4Stream` implements [`AsyncRead`] and [`AsyncWrite`].
#[derive(Debug)]
pub struct Socks4Stream<S: AsyncRead + AsyncWrite + Unpin> {
socket: S,
target_addr: Option<TargetAddr>,
}
impl<S> Socks4Stream<S>
where
S: AsyncRead + AsyncWrite + Unpin,
{
/// Possibility to use a stream already created rather than
/// creating a whole new `TcpStream::connect()`.
pub fn use_stream(socket: S) -> Result<Self> {
let stream = Socks4Stream {
socket,
target_addr: None,
};
Ok(stream)
}
/// https://www.openssh.com/txt/socks4.protocol
/// https://www.openssh.com/txt/socks4a.protocol
///
/// 1) CONNECT
///
/// +----+----+----+----+----+----+----+----+----+----+....+----+
/// | VN | CD | DSTPORT | DSTIP | USERID |NULL|
/// +----+----+----+----+----+----+----+----+----+----+....+----+
/// #of bytes 1 1 2 4 variable 1
///
/// VN is the SOCKS protocol version number and should be 4. CD is the
/// SOCKS command code and should be 1 for CONNECT request. NULL is a byte
/// of all zero bits.
///
/// The SOCKS server checks to see whether such a request should be granted
/// based on any combination of source IP address, destination IP address,
/// destination port number, the userid, and information it may obtain by
/// consulting IDENT, cf. RFC 1413. If the request is granted, the SOCKS
/// server makes a connection to the specified port of the destination host.
/// A reply packet is sent to the client when this connection is established,
/// or when the request is rejected or the operation fails.
///
/// Response:
///
/// +----+----+----+----+----+----+----+----+
/// | VN | CD | DSTPORT | DSTIP |
/// +----+----+----+----+----+----+----+----+
/// #of bytes 1 1 2 4
///
/// VN is the version of the reply code and should be 0. CD is the result
/// code with one of the following values:
///
/// 90: request granted
/// 91: request rejected or failed
/// 92: request rejected because SOCKS server cannot connect to
/// identd on the client
/// 93: request rejected because the client program and identd
/// report different user-ids
///
pub async fn request(
&mut self,
cmd: Socks4Command,
target_addr: TargetAddr,
resolve_locally: bool,
) -> Result<()> {
let resolved = if target_addr.is_domain() && resolve_locally {
target_addr.resolve_dns().await?
} else {
target_addr
};
self.target_addr = Some(resolved);
self.send_command_request(&cmd).await?;
self.read_command_request().await?;
Ok(())
}
async fn send_command_request(&mut self, cmd: &Socks4Command) -> Result<()> {
let mut packet = [0u8; MAX_ADDR_LEN];
packet[0] = consts::SOCKS4_VERSION;
packet[1] = cmd.as_u8();
match &self.target_addr {
Some(TargetAddr::Ip(SocketAddr::V4(addr))) => {
packet[2] = (addr.port() >> 8) as u8;
packet[3] = addr.port() as u8;
packet[4..8].copy_from_slice(&(addr.ip()).octets());
Ok(())
}
Some(TargetAddr::Ip(SocketAddr::V6(addr))) => {
error!("IPv6 are not supported: {:?}", addr);
Err(ReplySocks4Error(ReplyError::AddressTypeNotSupported))
}
Some(TargetAddr::Domain(domain, port)) => {
packet[2] = (port >> 8) as u8;
packet[3] = *port as u8;
packet[4..8].copy_from_slice(&[0, 0, 0, 1]);
let domain_bytes = domain.as_bytes();
let offset = 8 + domain_bytes.len();
packet[8..offset].copy_from_slice(domain_bytes);
Ok(())
}
_ => {
panic!("Unreachable case");
}
}?;
self.socket.write_all(&packet).await?;
Ok(())
}
#[rustfmt::skip]
async fn read_command_request(&mut self) -> Result<()> {
let [_, cd] = read_exact!(self.socket, [0u8; 2])?;
let reply = ReplyError::from_u8(cd);
match reply {
ReplyError::Succeeded => Ok(()),
_ => Err(SocksError::ReplySocks4Error(reply))
}
}
pub fn get_socket(self) -> S {
self.socket
}
pub fn get_socket_ref(&self) -> &S {
&self.socket
}
pub fn get_socket_mut(&mut self) -> &mut S {
&mut self.socket
}
}
/// Api if you want to use TcpStream to create a new connection to the SOCKS4 server.
impl Socks4Stream<TcpStream> {
/// Connects to a target server through a SOCKS4 proxy.
pub async fn connect<T>(
socks_server: T,
target_addr: String,
target_port: u16,
resolve_locally: bool,
) -> Result<Self>
where
T: ToSocketAddrs,
{
Self::connect_raw(
Socks4Command::Connect,
socks_server,
target_addr,
target_port,
resolve_locally,
)
.await
}
/// Process clients SOCKS requests
/// This is the entry point where a whole request is processed.
pub async fn connect_raw<T>(
cmd: Socks4Command,
socks_server: T,
target_addr: String,
target_port: u16,
resolve_locally: bool,
) -> Result<Self>
where
T: ToSocketAddrs,
{
let socket = TcpStream::connect(
socks_server
.to_socket_addrs()?
.next()
.context("unreachable")?,
)
.await?;
info!("Connected @ {}", &socket.peer_addr()?);
// Specify the target, here domain name, dns will be resolved on the server side
let target_addr = (target_addr.as_str(), target_port)
.to_target_addr()
.context("Can't convert address to TargetAddr format")?;
// upgrade the TcpStream to Socks4Stream
let mut socks_stream = Self::use_stream(socket)?;
socks_stream
.request(cmd, target_addr, resolve_locally)
.await?;
Ok(socks_stream)
}
}
/// Allow us to read directly from the struct
impl<S> AsyncRead for Socks4Stream<S>
where
S: AsyncRead + AsyncWrite + Unpin,
{
fn poll_read(
mut self: Pin<&mut Self>,
context: &mut std::task::Context,
buf: &mut tokio::io::ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
Pin::new(&mut self.socket).poll_read(context, buf)
}
}
/// Allow us to write directly into the struct
impl<S> AsyncWrite for Socks4Stream<S>
where
S: AsyncRead + AsyncWrite + Unpin,
{
fn poll_write(
mut self: Pin<&mut Self>,
context: &mut std::task::Context,
buf: &[u8],
) -> Poll<io::Result<usize>> {
Pin::new(&mut self.socket).poll_write(context, buf)
}
fn poll_flush(
mut self: Pin<&mut Self>,
context: &mut std::task::Context,
) -> Poll<io::Result<()>> {
Pin::new(&mut self.socket).poll_flush(context)
}
fn poll_shutdown(
mut self: Pin<&mut Self>,
context: &mut std::task::Context,
) -> Poll<io::Result<()>> {
Pin::new(&mut self.socket).poll_shutdown(context)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn get_domain() -> String {
"www.google.com".to_string()
}
async fn get_humans_txt(socks: &mut Socks4Stream<TcpStream>) -> Option<String> {
let headers = format!(
"GET /humans.txt HTTP/1.1\r\n\
Host: {}\r\n\
User-Agent: fast-socks5/0.1.0\r\n\
Accept: */*\r\n\r\n",
get_domain()
);
socks
.write_all(headers.as_bytes())
.await
.expect("should successfully write");
let response = &mut [0u8; 2048];
socks
.read(response)
.await
.expect("should successfully read");
// sometimes google returns body on second request
if response[0] == 0 {
response.copy_from_slice(&[0u8; 2048]);
socks
.read(response)
.await
.expect("should successfully read");
}
let response_str = String::from_utf8_lossy(response);
let response_body = response_str
.split("\n")
.into_iter()
.filter(|x| x.starts_with("Google"))
.last()
.map(|x| x.to_string());
response_body
}
fn assert_response_body(response_body: &String) {
let expected =
"Google is built by a large team of engineers, designers, researchers, robots, \
and others in many different sites across the globe. It is updated continuously, \
and built with more tools and technologies than we can shake a stick at. If you'd \
like to help us out, see careers.google.com.";
assert_eq!(expected, response_body);
}
#[tokio::test]
pub async fn test_use_stream() {
// TODO: replace with local socks4 server
// it requires implementation
let tcp = TcpStream::connect("217.17.56.160:4145")
.await
.expect("should connect to remote");
let mut socks = Socks4Stream::use_stream(tcp).expect("should wrap to socks stream");
socks
.request(
Socks4Command::Connect,
TargetAddr::Domain(get_domain(), 80),
true,
)
.await
.expect("should send connect successfully");
let response_body = get_humans_txt(&mut socks)
.await
.expect("should have response_body");
assert_response_body(&response_body);
}
#[tokio::test]
pub async fn test_use_stream_local_resolve() {
let mut socks = Socks4Stream::connect("217.17.56.160:4145", get_domain(), 80, true)
.await
.expect("should connect successfully to socks4 server");
let response_body = get_humans_txt(&mut socks)
.await
.expect("should have response_body");
assert_response_body(&response_body);
}
// Need to find socks4a supporting proxy or implement
// custom server and test using it
//
// #[tokio::test]
// pub async fn test_use_stream_remote_resolve() {
// let mut socks = Socks4Stream::connect("217.17.56.160:4145", get_domain(), 80, false)
// .await
// .expect("should connect successfully to socks4 server");
//
// let response_body = get_humans_txt(&mut socks)
// .await
// .expect("should have response_body");
//
// assert_response_body(&response_body);
// }
}

88
src/socks4/mod.rs Normal file
View File

@@ -0,0 +1,88 @@
pub mod client;
use thiserror::Error;
#[rustfmt::skip]
pub mod consts {
pub const SOCKS4_VERSION: u8 = 0x04;
pub const SOCKS4_CMD_CONNECT: u8 = 0x01;
pub const SOCKS4_CMD_BIND: u8 = 0x02;
pub const SOCKS4_REPLY_SUCCEEDED: u8 = 0x5a;
pub const SOCKS4_REPLY_FAILED: u8 = 0x5b;
pub const SOCKS4_REPLY_HOST_UNREACHABLE: u8 = 0x5c;
pub const SOCKS4_REPLY_INVALID_USER: u8 = 0x5d;
}
/// SOCKS4 reply code
#[derive(Error, Debug, Copy, Clone)]
pub enum ReplyError {
#[error("Succeeded")]
Succeeded,
#[error("General failure")]
GeneralFailure,
#[error("Host unreachable")]
HostUnreachable,
#[error("Address type not supported")]
AddressTypeNotSupported,
#[error("Invalid user")]
InvalidUser,
#[error("Unknown response")]
UnknownResponse(u8),
}
#[derive(Debug, PartialEq)]
pub enum Socks4Command {
Connect,
Bind,
}
#[allow(dead_code)]
impl Socks4Command {
#[inline]
#[rustfmt::skip]
pub fn as_u8(&self) -> u8 {
match self {
Socks4Command::Connect => consts::SOCKS4_CMD_CONNECT,
Socks4Command::Bind => consts::SOCKS4_CMD_BIND,
}
}
#[inline]
#[rustfmt::skip]
pub fn from_u8(code: u8) -> Option<Socks4Command> {
match code {
consts::SOCKS4_CMD_CONNECT => Some(Socks4Command::Connect),
consts::SOCKS4_CMD_BIND => Some(Socks4Command::Bind),
_ => None,
}
}
}
impl ReplyError {
#[inline]
#[rustfmt::skip]
pub fn as_u8(self) -> u8 {
match self {
ReplyError::Succeeded => consts::SOCKS4_REPLY_SUCCEEDED,
ReplyError::GeneralFailure => consts::SOCKS4_REPLY_FAILED,
ReplyError::HostUnreachable => consts::SOCKS4_REPLY_HOST_UNREACHABLE,
ReplyError::InvalidUser => consts::SOCKS4_REPLY_INVALID_USER,
reply => panic!("Unsupported ReplyStatus: {:?}", reply)
}
}
#[inline]
#[rustfmt::skip]
pub fn from_u8(code: u8) -> ReplyError {
match code {
consts::SOCKS4_REPLY_SUCCEEDED => ReplyError::Succeeded,
consts::SOCKS4_REPLY_FAILED => ReplyError::GeneralFailure,
consts::SOCKS4_REPLY_HOST_UNREACHABLE => ReplyError::HostUnreachable,
consts::SOCKS4_REPLY_INVALID_USER => ReplyError::InvalidUser,
_ => ReplyError::UnknownResponse(code),
}
}
}