diff --git a/Dockerfile b/Dockerfile index 7364508..669bc7b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,5 @@ -FROM rust:1 as build +# This Dockerfile is used to build a socks proxy server. +FROM rust:1.72 as build RUN rustup component add rustfmt @@ -14,10 +15,10 @@ WORKDIR /socksx RUN cargo build --release # Define final image -FROM ubuntu:20.04 +FROM ubuntu:22.04 RUN apt-get update && apt-get install -y \ - libssl1.1 \ + libssl3 \ libuv1 \ && rm -rf /var/lib/apt/lists/* @@ -25,4 +26,4 @@ RUN apt-get update && apt-get install -y \ COPY --from=build /socksx/target/release/socksx . EXPOSE 1080 -ENTRYPOINT [ "./socksx" ] \ No newline at end of file +ENTRYPOINT [ "./socksx" ] diff --git a/Dockerfile.counter b/Dockerfile.counter new file mode 100644 index 0000000..a71b7f2 --- /dev/null +++ b/Dockerfile.counter @@ -0,0 +1,15 @@ +# This Dockerfile is supposed to create a socks proxy server that mimics a firewall. +# This won't build on apple silicon based macs. Try on Linux or Windows or intel based macs. +FROM ubuntu:22.04 + +RUN apt-get update && apt-get install -y \ + python3 \ + python3-pip \ + && rm -rf /var/lib/apt/lists/* + +RUN pip3 install click socksx + +COPY ./socksx-py/examples/functions.py /functions.py + +EXPOSE 1080 +ENTRYPOINT [ "./functions.py" ] diff --git a/Dockerfile.encrypt-decrypt b/Dockerfile.encrypt-decrypt new file mode 100644 index 0000000..ef41d76 --- /dev/null +++ b/Dockerfile.encrypt-decrypt @@ -0,0 +1,30 @@ +# This Dockerfile is used to build a socks proxy server that can be used to encrypt or decrypt the data +FROM rust:1.72 as build + +RUN rustup component add rustfmt + +RUN apt-get update && apt-get install -y \ + cmake \ + make \ + && rm -rf /var/lib/apt/lists/* + +# Copy over relevant crates +COPY ./socksx /socksx + +# Build an optimized binary +WORKDIR /socksx +RUN cargo build --example functions --release + +# Define final image +FROM ubuntu:22.04 + +RUN apt-get update && apt-get install -y \ + libssl3 \ + libuv1 \ + && rm -rf /var/lib/apt/lists/* + +# Copy `brane-log from the build stage +COPY --from=build /socksx/target/release/examples/functions . + +EXPOSE 1080 +ENTRYPOINT [ "./functions" ] diff --git a/README.md b/README.md index 6fe82d5..675629a 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,66 @@ -# SOCKS toolkit -[![Build Status](https://github.com/onnovalkering/socksx/workflows/CI/badge.svg)](https://github.com/onnovalkering/socksx/actions) +# SOCKS toolkit for Rust [![License: MIT](https://img.shields.io/github/license/onnovalkering/socksx.svg)](https://github.com/onnovalkering/socksx/blob/master/LICENSE) -[![Coverage Status](https://coveralls.io/repos/github/onnovalkering/socksx/badge.svg)](https://coveralls.io/github/onnovalkering/socksx?branch=master) -[![Crates.io](https://img.shields.io/crates/v/socksx)](https://crates.io/crates/socksx) A work-in-progress SOCKS toolkit for Rust. SOCKS5 ([rfc1928](https://tools.ietf.org/html/rfc1928)) and SOCKS6 ([draft-11](https://tools.ietf.org/html/draft-olteanu-intarea-socks-6-11)) are supported. [Documentation](https://docs.rs/socksx/latest) -## Python -The `socksx-py` crate is a [PyO3](https://github.com/PyO3/PyO3)-based Python interface to `socksx`. +## Client Usage +Example client usage can be found in ./socksx/examples/client.rs. To run the example, use the following command: +```bash +cargo run --example client -- --host 172.16.238.4 --port 1080 --dest_host 172.16.238.5 --dest_port 12345 --src_port 12346 +``` +Note: The ip addresses are just examples, you should use your own ip addresses. I created a docker network and assigned +ip addresses to the containers. -You can build and install this Python package locally (requires [`pipenv`](https://github.com/pypa/pipenv) and [`maturin`](https://github.com/PyO3/maturin)): +## Server Usage +### Building the binary +To build the binary, run the following command: +```bash +cargo build --release +``` -```shell -$ pipenv install && pipenv shell -$ maturin develop -m ./socksx-py/Cargo.toml +To run the binary, run the following command: +```bash +./target/release/socksx --host 0.0.0.0 --port 1080 --protocol socks5 ``` -To build a manylinux releases: +If you want to using the chaining feature, you can run the following command: +```bash +./target/release/socksx --host 0.0.0.0 --port 1080 --protocol socks6 --chain socks6://145.10.0.1:1080 +``` + +### Docker Image Build + +To build the Docker image for the proxy service, use the following command: + +(The Dockerfile is located at the root of the repository) +```bash +docker build -t proxy:latest -f Dockerfile . +``` + +Create a Docker network named `net` with a specified subnet. + +```bash +docker network create --subnet=172.16.238.0/24 net +``` + +To run the Docker container, use the following command: + +```bash +docker run --network=net --ip=172.16.238.2 -p 1080:1080 --name proxy proxy:latest --host 0.0.0.0 --port 1080 +``` + +Make sure to run these commands in the correct sequence: build the image, create the network, and then run the container. + +### Docker Compose +Check out the `docker-compose-proxy.yml` or `docker-compose-extensive.yml` file at the root of the repository for an example of how to use the proxy service with Docker Compose. + + -```shell -$ docker run --rm -v $(pwd):/io konstin2/maturin build --release -m ./socksx-py/Cargo.toml -``` \ No newline at end of file +## TODO +- [ ] support chaining in socks 5 +- [ ] add badge for coverage (coveralls) +- [ ] add badge for crates link +- [ ] add badge for CI status (github actions) +- [ ] add badge for docs.rs and documentation link \ No newline at end of file diff --git a/docker-compose-extensive.yml b/docker-compose-extensive.yml new file mode 100644 index 0000000..f9820e7 --- /dev/null +++ b/docker-compose-extensive.yml @@ -0,0 +1,96 @@ +# Example of two proxy chains simulating a sender node/domain and a receiver node/domains. +# On sender side we have socks proxy, which is connected to a counter (mimicing a firewall) and encrypt +# On receiver side we have socks proxy, which is connected to decrypt and counter +# Communication looks like this: +# sender(client) -> proxy (sender's side) -> counter -> encrypt -> proxy (destination's side) -> decrypt -> counter -> destination + +version: '3.8' + +services: + proxy-main: + build: + context: . + dockerfile: Dockerfile + ports: + - "1080:1080" + command: "--host 0.0.0.0 --port 1080 --chain socks6://counter-1:1080 --chain socks6://encrypt:1080 --chain socks6://proxy-other:1080" + depends_on: + - counter-1 + - encrypt + - proxy-other + networks: + net: + ipv4_address: 172.16.238.2 + + counter-1: + build: + context: . + dockerfile: Dockerfile.counter + command: "--host 0.0.0.0" + networks: + net: + ipv4_address: 172.16.238.3 + + encrypt: + build: + context: . + dockerfile: Dockerfile.encrypt-decrypt + command: "chacha20" + environment: + - CHACHA20_KEY="123456789012345678901234567890" + networks: + net: + ipv4_address: 172.16.238.4 + + proxy-other: + build: + context: . + dockerfile: Dockerfile + ports: + - "1081:1080" + command: "--host 0.0.0.0 --port 1080 --chain socks6://decrypt:1080 --chain socks6://counter-2:1080" + depends_on: + - counter-2 + - decrypt + networks: + net: + ipv4_address: 172.16.238.5 + + counter-2: + build: + context: . + dockerfile: Dockerfile.counter + command: "--host 0.0.0.0" + networks: + net: + ipv4_address: 172.16.238.6 + + decrypt: + build: + context: . + dockerfile: Dockerfile.encrypt-decrypt + command: "chacha20" + environment: + - CHACHA20_KEY="123456789012345678901234567890" + networks: + net: + ipv4_address: 172.16.238.7 + + netcat: + image: busybox + command: "nc -l -p 12345" + ports: + - "12345:12345" + restart: always + networks: + net: + ipv4_address: 172.16.238.8 + +# The reason to create a virtual network is to be able to assign static IP addresses to the destination container +# in this case, netcat. We can use this ip address from the client to connect to the destination. +networks: + net: + ipam: + driver: default + config: + - subnet: "172.16.238.0/24" diff --git a/docker-compose-proxy.yml b/docker-compose-proxy.yml new file mode 100644 index 0000000..d42d6d1 --- /dev/null +++ b/docker-compose-proxy.yml @@ -0,0 +1,33 @@ +# Example of using a standalone proxy to forward traffic to a destination + +version: '3.8' + +services: + proxy: + build: + context: . + dockerfile: Dockerfile + ports: + - "1080:1080" + command: "--host 0.0.0.0 --port 1080" + networks: + net: + ipv4_address: 172.16.238.2 + + # this will be the destination + netcat: + image: busybox + command: "nc -l -p 12345" + ports: + - "12345:12345" + restart: always + networks: + net: + ipv4_address: 172.16.238.3 + +networks: + net: + ipam: + driver: default + config: + - subnet: "172.16.238.0/24" diff --git a/socksx-py/README.md b/socksx-py/README.md new file mode 100644 index 0000000..04ea0da --- /dev/null +++ b/socksx-py/README.md @@ -0,0 +1,15 @@ +## socksx-py +The `socksx-py` crate is a [PyO3](https://github.com/PyO3/PyO3)-based Python interface to `socksx`. + +You can build and install this Python package locally (requires [`pipenv`](https://github.com/pypa/pipenv) and [`maturin`](https://github.com/PyO3/maturin)): + +```shell +$ pipenv install && pipenv shell +$ maturin develop -m ./Cargo.toml +``` + +To build a manylinux releases: + +```shell +$ docker run --rm -v $(pwd):/io konstin2/maturin build --release -m ./Cargo.toml +``` \ No newline at end of file diff --git a/socksx/examples/client.rs b/socksx/examples/client.rs index 41c7450..ca33d84 100644 --- a/socksx/examples/client.rs +++ b/socksx/examples/client.rs @@ -1,3 +1,6 @@ +/// A simple SOCKS client that connects to a destination server through a proxy. +/// This serves as an example of how to use the socksx crate. +/// This also serves as a test to ensure that the crate works as expected. use anyhow::Result; use clap::{App, Arg}; use socksx::{Socks5Client, Socks6Client}; @@ -5,6 +8,7 @@ use tokio::io::AsyncWriteExt; #[tokio::main] async fn main() -> Result<()> { + // Parse command-line arguments using the Clap library. let args = App::new("Client") .arg( Arg::new("VERSION") @@ -16,26 +20,31 @@ async fn main() -> Result<()> { ) .arg( Arg::new("PROXY_HOST") + .long("host") .help("The IP/hostname of the proxy") .default_value("127.0.0.1"), ) .arg( Arg::new("PROXY_PORT") + .long("port") .help("The port of the proxy server") .default_value("1080"), ) .arg( Arg::new("DEST_HOST") + .long("dest_host") .help("The IP/hostname of the destination") .default_value("127.0.0.1"), ) .arg( Arg::new("DEST_PORT") + .long("dest_port") .help("The port of the destination server") .default_value("12345"), ) .get_matches(); + // Extract values from command-line arguments. let proxy_host = args.value_of("PROXY_HOST").unwrap(); let proxy_port = args.value_of("PROXY_PORT").unwrap(); let proxy_addr = format!("{}:{}", proxy_host, proxy_port); @@ -44,6 +53,7 @@ async fn main() -> Result<()> { let dest_port = args.value_of("DEST_PORT").unwrap(); let dest_addr = format!("{}:{}", dest_host, dest_port); + // Determine the SOCKS version specified in the arguments. match args.value_of("VERSION") { Some("5") => connect_v5(proxy_addr, dest_addr).await, Some("6") => connect_v6(proxy_addr, dest_addr).await, @@ -52,34 +62,42 @@ async fn main() -> Result<()> { } } -/// -/// -/// +/// Connects to a destination through a proxy using SOCKS5 protocol, then sends an example message through the network tunnel. +/// +/// # Arguments +/// - `proxy_addr`: The address of the SOCKS5 proxy through which the traffic will be tunnelled. +/// - `dest_addr`: The address to which the traffic should be sent after the proxy. +/// +/// # Errors +/// This function can error if we failed to connect to the given proxy or failed to send it an example message. async fn connect_v5( proxy_addr: String, dest_addr: String, ) -> Result<()> { - println!("Creating client..."); + // Create a SOCKS5 client. let client = Socks5Client::new(proxy_addr, None).await?; - println!("Connecting..."); + + // Connect to the destination. let (mut outgoing, _) = client.connect(dest_addr).await?; - println!("Writing message!"); + // Write a message to the destination. outgoing.write(String::from("Hello, world!\n").as_bytes()).await?; Ok(()) } -/// -/// -/// +/// Connects to a destination using SOCKS6 protocol. async fn connect_v6( proxy_addr: String, dest_addr: String, ) -> Result<()> { + // Create a SOCKS6 client. let client = Socks6Client::new(proxy_addr, None).await?; + + // Connect to the destination. let (mut outgoing, _) = client.connect(dest_addr, None, None).await?; + // Write a message to the destination. outgoing.write(String::from("Hello, world!\n").as_bytes()).await?; Ok(()) diff --git a/socksx/examples/functions.rs b/socksx/examples/functions.rs index 9519c5c..6db7da6 100644 --- a/socksx/examples/functions.rs +++ b/socksx/examples/functions.rs @@ -1,18 +1,24 @@ +/// This example demonstrates how to apply a function to ingress traffic through the socks proxy. +/// This example uses ChaCha20 encryption/decryption as the function. +/// We can have other functions such as compression, decompression, firewall, VPN, annonimization, etc. +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll}; + use anyhow::Result; use bytes::BytesMut; -use chacha20::cipher::{NewCipher, StreamCipher}; use chacha20::{ChaCha20, Key, Nonce}; +use chacha20::cipher::{NewCipher, StreamCipher}; use clap::Parser; use dotenv::dotenv; use pin_project_lite::pin_project; -use socksx::{self, Socks5Handler, Socks6Handler, SocksHandler}; -use std::pin::Pin; -use std::sync::Arc; -use std::task::{Context, Poll}; use tokio::io::{self, AsyncBufRead, BufReader, BufWriter}; use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; use tokio::net::{TcpListener, TcpStream}; +use socksx::{self, Socks5Handler, Socks6Handler, SocksHandler}; + +// Define a trait alias for the SocksHandler to simplify code. type Handler = Arc; #[derive(Parser)] @@ -34,6 +40,7 @@ struct Args { function: Function, } +// Define subcommands for the program. #[derive(Parser, Clone)] enum Function { /// Apply ChaCha20 encryption/decryption to ingress traffic @@ -47,34 +54,39 @@ enum Function { #[tokio::main] async fn main() -> Result<()> { + // Load environment variables from a .env file. dotenv().ok(); + // Parse command-line arguments using Clap. let args = Args::parse(); + // Create a TCP listener bound to the specified host and port. let listener = TcpListener::bind(format!("{}:{}", args.host, args.port)).await?; + // Determine the appropriate SOCKS handler based on the specified version. let handler: Handler = match args.socks { 5 => Arc::new(Socks5Handler::default()), 6 => Arc::new(Socks6Handler::default()), _ => unreachable!(), }; + // Main loop for accepting incoming connections and processing them. loop { let (incoming, _) = listener.accept().await?; let handler = Arc::clone(&handler); let function = args.function.clone(); + // Spawn a new asynchronous task to process each connection. tokio::spawn(process(incoming, handler, function)); } } -/// -/// -/// +/// Process an incoming connection based on the specified function. async fn process( source: TcpStream, handler: Handler, function: Function, ) -> Result<()> { let mut source = source; + // Set up the destination connection using the SOCKS handler. let mut destination = handler.setup(&mut source).await?; // Apply a function to ingress traffic. @@ -82,6 +94,7 @@ async fn process( Function::ChaCha20 { key } => { let mut source = CryptStream::new(source, key); + // Bidirectional data transfer between source and destination. tokio::io::copy_bidirectional(&mut source, &mut destination).await?; } } @@ -89,6 +102,7 @@ async fn process( Ok(()) } +// Define a wrapper struct for encryption/decryption using ChaCha20. pin_project! { #[derive(Debug)] pub struct CryptStream { @@ -99,6 +113,7 @@ pin_project! { } impl CryptStream { + // Create a new CryptStream with encryption key. pub fn new( stream: RW, key: String, @@ -110,6 +125,7 @@ impl CryptStream { } } +// Implement the AsyncWrite trait for CryptStream. impl AsyncWrite for CryptStream { fn poll_write( self: Pin<&mut Self>, @@ -134,6 +150,7 @@ impl AsyncWrite for CryptStream { } } +// Implement the AsyncRead trait for CryptStream. impl AsyncRead for CryptStream { fn poll_read( mut self: Pin<&mut Self>, @@ -142,6 +159,7 @@ impl AsyncRead for CryptStream { ) -> Poll> { let reader = self.as_mut().project().inner; + // Poll to fill the buffer. let remaining = match reader.poll_fill_buf(cx) { std::task::Poll::Ready(t) => t, std::task::Poll::Pending => return std::task::Poll::Pending, diff --git a/socksx/examples/redirector.rs b/socksx/examples/redirector.rs index a850492..3629f94 100644 --- a/socksx/examples/redirector.rs +++ b/socksx/examples/redirector.rs @@ -1,8 +1,12 @@ +/// This is a simple redirector that redirects all incoming TCP connections through a SOCKS proxy to +/// a different destination. This is useful for redirecting traffic from a specific application +/// through a proxy. use anyhow::Result; use clap::{App, Arg}; -use socksx::{self, Socks5Client, Socks6Client}; use tokio::net::{TcpListener, TcpStream}; +use socksx::{self, Socks5Client, Socks6Client}; + // iptables -t nat -A OUTPUT ! -d $PROXY_HOST/32 -o eth0 -p tcp -m tcp -j REDIRECT --to-ports 42000 #[tokio::main] diff --git a/socksx/src/common/addresses.rs b/socksx/src/common/addresses.rs index 10f455c..910de43 100644 --- a/socksx/src/common/addresses.rs +++ b/socksx/src/common/addresses.rs @@ -1,19 +1,28 @@ -use crate::{constants::*, Credentials}; -use anyhow::Result; use std::convert::{TryFrom, TryInto}; use std::net::{IpAddr, SocketAddr}; + +use anyhow::Result; use tokio::io::{AsyncRead, AsyncReadExt}; use url::Url; -#[derive(Clone, Debug)] +use crate::{constants::*, Credentials}; + +/// Represents a SOCKS proxy address. +#[derive(Clone, Debug, PartialEq)] pub struct ProxyAddress { + /// The version of the SOCKS protocol. pub socks_version: u8, + /// The hostname or IP address of the proxy. pub host: String, + /// The port number of the proxy. pub port: u16, + /// Optional credentials for authentication. pub credentials: Option, } + impl ProxyAddress { + /// Creates a new `ProxyAddress` instance. pub fn new( socks_version: u8, host: String, @@ -28,12 +37,15 @@ impl ProxyAddress { } } + /// Creates a root `ProxyAddress` with predefined settings. pub fn root() -> Self { ProxyAddress::new(6, String::from("root"), 1080, None) } } + impl ToString for ProxyAddress { + // Converts the `ProxyAddress` to a string representation. fn to_string(&self) -> String { format!("socks{}://{}:{}", self.socks_version, self.host, self.port) } @@ -42,6 +54,7 @@ impl ToString for ProxyAddress { impl TryFrom for ProxyAddress { type Error = anyhow::Error; + // Converts a string to a `ProxyAddress`. fn try_from(proxy_addr: String) -> Result { let proxy_addr = Url::parse(&proxy_addr)?; @@ -74,16 +87,18 @@ impl TryFrom for ProxyAddress { } } -#[derive(Clone, Debug)] +/// Represents a network address, which could be either a domain name or an IP address. +#[derive(Clone, Debug, PartialEq)] pub enum Address { + /// An address represented by a domain name. Domainname { host: String, port: u16 }, + /// An address represented by an IP address. Ip(SocketAddr), } + impl Address { - /// - /// - /// + /// Creates a new `Address` instance. pub fn new>( host: S, port: u16, @@ -97,9 +112,7 @@ impl Address { } } - /// - /// - /// + /// Converts the `Address` into a byte sequence compatible with the SOCKS protocol. pub fn as_socks_bytes(&self) -> Vec { let mut bytes = vec![]; @@ -134,6 +147,7 @@ impl Address { } impl ToString for Address { + // Converts the `Address` to a string representation. fn to_string(&self) -> String { match self { Address::Domainname { host, port } => format!("{}:{}", host, port), @@ -142,6 +156,7 @@ impl ToString for Address { } } +/// Tries to convert a `SocketAddr` into an `Address`. impl TryFrom for Address { type Error = anyhow::Error; @@ -150,6 +165,7 @@ impl TryFrom for Address { } } +/// Tries to convert a `String` into an `Address`. impl TryFrom for Address { type Error = anyhow::Error; @@ -162,6 +178,7 @@ impl TryFrom for Address { } } +/// Tries to convert a `ProxyAddress` into an `Address`. impl TryFrom<&ProxyAddress> for Address { type Error = anyhow::Error; @@ -170,9 +187,7 @@ impl TryFrom<&ProxyAddress> for Address { } } -/// -/// -/// +/// Reads the destination address from a stream and returns it as an `Address`. pub async fn read_address(stream: &mut S) -> Result
where S: AsyncRead + Unpin, @@ -214,3 +229,122 @@ where Ok(Address::new(dst_addr, dst_port)) } + +#[cfg(test)] +mod tests { + use std::net::SocketAddr; + + use anyhow::Result; + + use super::*; + + #[test] + fn test_proxy_address_new() { + let proxy_address = ProxyAddress::new(5, "localhost".to_string(), 1080, None); + assert_eq!(proxy_address.socks_version, 5); + assert_eq!(proxy_address.host, "localhost"); + assert_eq!(proxy_address.port, 1080); + assert!(proxy_address.credentials.is_none()); + } + + #[test] + fn test_proxy_address_root() { + let root_address = ProxyAddress::root(); + assert_eq!(root_address.socks_version, 6); + assert_eq!(root_address.host, "root"); + assert_eq!(root_address.port, 1080); + assert!(root_address.credentials.is_none()); + } + + #[test] + fn test_address_new_domain() { + let address = Address::new("example.com", 80); + match address { + Address::Domainname { host, port } => { + assert_eq!(host, "example.com"); + assert_eq!(port, 80); + }, + _ => panic!("Expected a domain name address"), + } + } + + #[test] + fn test_address_new_ip() { + let address = Address::new("192.168.1.1", 22); + match address { + Address::Ip(socket_addr) => { + assert_eq!(socket_addr.ip().to_string(), "192.168.1.1"); + assert_eq!(socket_addr.port(), 22); + }, + _ => panic!("Expected an IP address"), + } + } + + #[test] + fn test_proxy_address_try_from_valid_string() -> Result<()> { + let proxy_str = "socks5://localhost:1080".to_string(); + let proxy_address: ProxyAddress = proxy_str.try_into()?; + assert_eq!(proxy_address.socks_version, SOCKS_VER_5); + assert_eq!(proxy_address.host, "localhost"); + assert_eq!(proxy_address.port, 1080); + Ok(()) + } + + #[test] + fn test_proxy_address_try_from_invalid_string() { + let proxy_str = "invalid://localhost:1080".to_string(); + let result: Result = proxy_str.try_into(); + assert!(result.is_err()); + } + + #[test] + fn test_address_try_from_valid_string() -> Result<()> { + let addr_str = "localhost:8000".to_string(); + let address: Address = addr_str.try_into()?; + match address { + Address::Domainname { host, port } => { + assert_eq!(host, "localhost"); + assert_eq!(port, 8000); + }, + _ => panic!("Expected a domain name address"), + } + Ok(()) + } + + #[test] + fn test_address_try_from_invalid_string() { + let addr_str = "localhost&8000".to_string(); + let result: Result
= addr_str.try_into(); + assert!(result.is_err()); + } + + #[test] + fn test_address_try_from_socket_addr() -> Result<()> { + let socket_addr: SocketAddr = "192.168.1.1:22".parse()?; + let address: Address = socket_addr.try_into()?; + match address { + Address::Ip(addr) => { + assert_eq!(addr.ip().to_string(), "192.168.1.1"); + assert_eq!(addr.port(), 22); + }, + _ => panic!("Expected an IP address"), + } + Ok(()) + } + + #[test] + fn test_address_try_from_proxy_address() -> Result<()> { + let proxy_address = ProxyAddress::new(5, "localhost".to_string(), 1080, None); + let address: Address = (&proxy_address).try_into()?; + match address { + Address::Domainname { host, port } => { + assert_eq!(host, "localhost"); + assert_eq!(port, 1080); + }, + _ => panic!("Expected a domain name address"), + } + Ok(()) + } + + // TODO: Add tests for `read_address` function once we have a way to mock the `AsyncRead`. +} diff --git a/socksx/src/common/constants.rs b/socksx/src/common/constants.rs index bbba28c..5807c4d 100644 --- a/socksx/src/common/constants.rs +++ b/socksx/src/common/constants.rs @@ -1,28 +1,50 @@ +/// SOCKS protocol version 5 identifier. pub const SOCKS_VER_5: u8 = 0x05u8; +/// SOCKS protocol version 6 identifier. pub const SOCKS_VER_6: u8 = 0x06u8; +/// Version identifier for SOCKS authentication. pub const SOCKS_AUTH_VER: u8 = 0x01u8; +/// Code for no authentication required. pub const SOCKS_AUTH_NOT_REQUIRED: u8 = 0x00u8; +/// Code for username/password authentication. pub const SOCKS_AUTH_USERNAME_PASSWORD: u8 = 0x02u8; +/// Code for no acceptable authentication methods. pub const SOCKS_AUTH_NO_ACCEPTABLE_METHODS: u8 = 0xFFu8; +/// Code for successful authentication. pub const SOCKS_AUTH_SUCCESS: u8 = 0x00u8; +/// Code for failed authentication. pub const SOCKS_AUTH_FAILED: u8 = 0x01u8; +/// Option kind for stack in SOCKS protocol. pub const SOCKS_OKIND_STACK: u16 = 0x01u16; +/// Option kind for advertising authentication methods. pub const SOCKS_OKIND_AUTH_METH_ADV: u16 = 0x02u16; +/// Option kind for selecting authentication methods. pub const SOCKS_OKIND_AUTH_METH_SEL: u16 = 0x03u16; +/// Option kind for authentication data. pub const SOCKS_OKIND_AUTH_DATA: u16 = 0x04u16; +/// Command code for no operation. pub const SOCKS_CMD_NOOP: u8 = 0x00u8; +/// Command code for establishing a TCP/IP stream connection. pub const SOCKS_CMD_CONNECT: u8 = 0x01u8; +/// Command code for establishing a TCP/IP port binding. pub const SOCKS_CMD_BIND: u8 = 0x02u8; +/// Command code for associating a UDP port. pub const SOCKS_CMD_UDP_ASSOCIATE: u8 = 0x03u8; +/// Padding byte for SOCKS protocol. pub const SOCKS_PADDING: u8 = 0x00u8; +/// Reserved byte for SOCKS protocol. pub const SOCKS_RSV: u8 = 0x00u8; +/// Address type identifier for IPv4 addresses. pub const SOCKS_ATYP_IPV4: u8 = 0x01u8; +/// Address type identifier for domain names. pub const SOCKS_ATYP_DOMAINNAME: u8 = 0x03u8; +/// Address type identifier for IPv6 addresses. pub const SOCKS_ATYP_IPV6: u8 = 0x04u8; +/// Reply code for succeeded operation. pub const SOCKS_REP_SUCCEEDED: u8 = 0x00u8; diff --git a/socksx/src/common/credentials.rs b/socksx/src/common/credentials.rs index 6c1021f..9b84a64 100644 --- a/socksx/src/common/credentials.rs +++ b/socksx/src/common/credentials.rs @@ -1,13 +1,19 @@ -#[derive(Clone, Debug)] +/// Represents the username and password credentials for SOCKS authentication. +#[derive(Clone, Debug, PartialEq)] pub struct Credentials { + /// The username as a byte vector. pub username: Vec, + /// The password as a byte vector. pub password: Vec, } impl Credentials { + /// Creates a new `Credentials` instance. /// + /// # Parameters /// - /// + /// * `username`: The username as a byte vector or convertible to a byte vector. + /// * `password`: The password as a byte vector or convertible to a byte vector. pub fn new>>( username: S, password: S, @@ -18,9 +24,11 @@ impl Credentials { Credentials { username, password } } + /// Converts the `Credentials` into a byte sequence compatible with the SOCKS authentication protocol. /// + /// # Returns /// - /// + /// Returns a vector of bytes containing the username and password in SOCKS-compatible format. pub fn as_socks_bytes(&self) -> Vec { // Append username let mut bytes = vec![self.username.len() as u8]; @@ -33,3 +41,22 @@ impl Credentials { bytes } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_credentials_new() { + let credentials = Credentials::new("username".to_string().into_bytes(), "password".to_string().into_bytes()); + assert_eq!(credentials.username, b"username".to_vec()); + assert_eq!(credentials.password, b"password".to_vec()); + } + + #[test] + fn test_credentials_as_socks_bytes() { + let credentials = Credentials::new("username".to_string().into_bytes(), "password".to_string().into_bytes()); + let socks_bytes = credentials.as_socks_bytes(); + assert_eq!(socks_bytes, vec![8, 117, 115, 101, 114, 110, 97, 109, 101, 8, 112, 97, 115, 115, 119, 111, 114, 100]); + } +} diff --git a/socksx/src/common/interface.rs b/socksx/src/common/interface.rs index fb7bce5..e85efc6 100644 --- a/socksx/src/common/interface.rs +++ b/socksx/src/common/interface.rs @@ -2,18 +2,46 @@ use anyhow::Result; use async_trait::async_trait; use tokio::net::TcpStream; +/// An asynchronous trait defining the core functionalities required for handling SOCKS requests. #[async_trait] pub trait SocksHandler { + /// Accepts a SOCKS request from a client. + /// + /// # Parameters + /// + /// * `source`: A mutable reference to the source `TcpStream` from which the request originates. + /// + /// # Returns + /// + /// Returns `Result<()>` indicating the success or failure of the operation. async fn accept_request( &self, source: &mut TcpStream, ) -> Result<()>; + /// Refuses a SOCKS request from a client. + /// + /// # Parameters + /// + /// * `source`: A reference to the source `TcpStream` from which the request originates. + /// + /// # Returns + /// + /// Returns `Result<()>` indicating the success or failure of the operation. async fn refuse_request( &self, source: &mut TcpStream, ) -> Result<()>; + /// Sets up the SOCKS connection for a given source. + /// + /// # Parameters + /// + /// * `source`: A mutable reference to the source `TcpStream`. + /// + /// # Returns + /// + /// Returns a `Result` containing the prepared `TcpStream` or an error. async fn setup( &self, source: &mut TcpStream, diff --git a/socksx/src/common/util.rs b/socksx/src/common/util.rs index f631223..af6c81d 100644 --- a/socksx/src/common/util.rs +++ b/socksx/src/common/util.rs @@ -1,13 +1,20 @@ -use anyhow::Result; use std::net::SocketAddr; + +use anyhow::Result; use tokio::net::{self, TcpStream}; +/// Retrieves the original destination address from a socket on a Linux system. /// +/// # Parameters /// +/// * `socket`: A reference to a socket implementing `AsRawFd`. /// +/// # Returns +/// +/// Returns a `Result` containing the original `SocketAddr` or an error. #[cfg(target_os = "linux")] pub fn get_original_dst(socket: &S) -> Result { - use nix::sys::socket::{self, sockopt, InetAddr}; + use nix::sys::socket::{self, InetAddr, sockopt}; let original_dst = socket::getsockopt(socket.as_raw_fd(), sockopt::OriginalDst)?; let original_dst = InetAddr::V4(original_dst).to_std(); @@ -16,11 +23,20 @@ pub fn get_original_dst(socket: &S) -> Result(socket: &S) -> Result { use std::str::FromStr; use windows::core::PSTR; - use windows::Win32::Networking::WinSock::{SO_ORIGINAL_DST, SOCKET, SOL_SOCKET, getsockopt}; + use windows::Win32::Networking::WinSock::{getsockopt, SO_ORIGINAL_DST, SOCKET, SOL_SOCKET}; // Attempt to recover the original destination let original_dst: String = unsafe { @@ -41,13 +57,19 @@ pub fn get_original_dst(socket: &S) -> Res } #[cfg(not(any(target_os = "linux", target_os = "windows")))] -pub fn get_original_dst(socket: S) -> Result { +pub fn get_original_dst(_socket: S) -> Result { todo!(); } +/// Resolves a given address to a `SocketAddr`. +/// +/// # Parameters /// +/// * `addr`: The address, either as a domain name or IP address. /// +/// # Returns /// +/// Returns a `Result` containing the resolved `SocketAddr` or an error. pub async fn resolve_addr>(addr: S) -> Result { let addr: String = addr.into(); @@ -64,9 +86,15 @@ pub async fn resolve_addr>(addr: S) -> Result { } } +/// Attempts to read the initial data from a TCP stream. /// +/// # Parameters /// +/// * `stream`: A mutable reference to a `TcpStream`. /// +/// # Returns +/// +/// Returns a `Result` containing an `Option` with the read data as a `Vec` or an error. pub async fn try_read_initial_data(stream: &mut TcpStream) -> Result>> { let mut initial_data = Vec::with_capacity(2usize.pow(14)); // 16KB is the max @@ -79,3 +107,47 @@ pub async fn try_read_initial_data(stream: &mut TcpStream) -> Result Self { + Self { + addr: addr.to_string(), + } + } + } + + impl Into for MockSocketAddr { + fn into(self) -> String { + self.addr + } + } + + // Test resolve_addr function + #[tokio::test] + async fn test_resolve_addr() { + // Test with valid IP address + let mock_addr = MockSocketAddr::new("127.0.0.1:8080"); + let result = resolve_addr(mock_addr).await; + assert!(result.is_ok()); + assert_eq!(result.unwrap().to_string(), "127.0.0.1:8080"); + + // Test with invalid IP address + let mock_addr = MockSocketAddr::new("300.300.300.300:8080"); + let result = resolve_addr(mock_addr).await; + assert!(result.is_err()); + + // Test with domain name (this will fail if domain cannot be resolved) + let mock_addr = MockSocketAddr::new("localhost:8080"); + let result = resolve_addr(mock_addr).await; + assert!(result.is_ok()); + } +} diff --git a/socksx/src/lib.rs b/socksx/src/lib.rs index 2a1e311..47252e8 100644 --- a/socksx/src/lib.rs +++ b/socksx/src/lib.rs @@ -1,3 +1,18 @@ +//! This crate provides SOCKS proxy client and server implementations. It supports both SOCKS5 and SOCKS6 protocols. +//! +//! While the crate is still in development, it is already usable. +//! +//! ## Chaining Features +//! +//! For `SOCKS version 5`, chaining is not supported yet. It will be added in the future. +//! Hence, it works in the following way: Client -> Socks5 -> Destination +//! +//! For `SOCKS version 6`, chaining is supported. It means that you can chain multiple SOCKS6 proxies together. +//! Apart from working like version 5, it can also be used to do this - Eg. Client -> Socks6 -> Socks6 -> Destination + + + + #[macro_use] extern crate anyhow; #[macro_use] @@ -5,23 +20,43 @@ extern crate log; #[macro_use] extern crate num_derive; +pub use tokio::io::copy_bidirectional; + +/// Represents network addresses. +pub use addresses::{Address, ProxyAddress}; +/// Manages user credentials. +pub use credentials::Credentials; +/// Handles SOCKS protocol. +pub use interface::SocksHandler; +/// SOCKS5 client and handler. +pub use socks5::{Socks5Client, Socks5Handler}; +/// SOCKS6 client and handler. +pub use socks6::{Socks6Client, Socks6Handler}; +pub use util::{get_original_dst, resolve_addr, try_read_initial_data}; + +/// Common network address representations #[path = "./common/addresses.rs"] pub mod addresses; + +/// SOCKS protocol Constants used across the crate. #[path = "./common/constants.rs"] pub mod constants; + +/// Credential management for the SOCKS proxy. #[path = "./common/credentials.rs"] pub mod credentials; + +/// Main interface for handling SOCKS. #[path = "./common/interface.rs"] pub mod interface; + +/// SOCKS5-specific implementations. pub mod socks5; + +/// SOCKS6-specific implementations. pub mod socks6; + +/// Utility functions and helpers. #[path = "./common/util.rs"] pub mod util; -pub use addresses::{Address, ProxyAddress}; -pub use credentials::Credentials; -pub use interface::SocksHandler; -pub use socks5::{Socks5Client, Socks5Handler}; -pub use socks6::{Socks6Client, Socks6Handler}; -pub use tokio::io::copy_bidirectional; -pub use util::{get_original_dst, resolve_addr, try_read_initial_data}; diff --git a/socksx/src/main.rs b/socksx/src/main.rs index 16fd7fd..556e816 100644 --- a/socksx/src/main.rs +++ b/socksx/src/main.rs @@ -1,19 +1,30 @@ +/// This is the main entry point for the SOCKSX proxy server. +/// It is responsible for parsing CLI arguments, setting up logging, and +/// spawning the main event loop. +/// The main event loop is responsible for accepting incoming connections and +/// spawning a new task for each connection. +/// Each task is responsible for handling the SOCKS handshake and proxying +/// data between the client and the destination server. #[macro_use] extern crate human_panic; +use std::{convert::TryInto, sync::Arc}; + use anyhow::Result; use clap::Parser; use dotenv::dotenv; use itertools::Itertools; use log::LevelFilter; -use socksx::{self, Socks5Handler, Socks6Handler, SocksHandler}; -use std::{convert::TryInto, sync::Arc}; use tokio::net::{TcpListener, TcpStream}; use tokio::sync::Semaphore; use tokio::time::Instant; +use socksx::{self, Socks5Handler, Socks6Handler, SocksHandler}; + +// Alias for SOCKS handler with Arc and Sync/Send trait bounds type Handler = Arc; +/// CLI arguments structure #[derive(Parser)] #[clap(version = env!("CARGO_PKG_VERSION"))] struct Args { @@ -42,11 +53,14 @@ struct Args { socks: u8, } +/// Main asynchronous function #[tokio::main] async fn main() -> Result<()> { + // Load environment variables from `.env` file dotenv().ok(); let args = Args::parse(); + // Setup logger let mut logger = env_logger::builder(); logger.format_module_path(false); @@ -55,6 +69,7 @@ async fn main() -> Result<()> { } else { logger.filter_level(LevelFilter::Info).init(); + // Setup human-friendly panic messages setup_panic!(Metadata { name: "SOCKSX".into(), version: env!("CARGO_PKG_VERSION").into(), @@ -65,20 +80,17 @@ async fn main() -> Result<()> { // TODO: validate host - // - // + // Convert and collect chain arguments let chain = args.chain.iter().cloned().map(|c| c.try_into()).try_collect()?; - // - // + // Create a semaphore for connection limiting let semaphore = if args.limit > 0 { Some(Arc::new(Semaphore::new(args.limit))) } else { None }; - // - // + // Bind TCP listener to the specified host and port let listener = TcpListener::bind(format!("{}:{}", args.host, args.port)).await?; let handler: Handler = match args.socks { 5 => Arc::new(Socks5Handler::new(chain)), @@ -86,6 +98,7 @@ async fn main() -> Result<()> { _ => unreachable!(), }; + // Main event loop for accepting incoming connections loop { let (incoming, _) = listener.accept().await?; @@ -96,9 +109,17 @@ async fn main() -> Result<()> { } } +/// Asynchronously processes an incoming connection +/// +/// # Parameters /// +/// - `incoming`: The incoming `TcpStream`. +/// - `handler`: The SOCKS handler. +/// - `semaphore`: An optional semaphore for limiting concurrent connections. /// +/// # Returns /// +/// Returns a `Result` indicating the success or failure of the operation. async fn process( incoming: TcpStream, handler: Handler, @@ -107,6 +128,7 @@ async fn process( let mut incoming = incoming; let start_time = Instant::now(); + // Handle the incoming connection based on the availability of permits if let Some(semaphore) = semaphore { let permit = semaphore.try_acquire(); if permit.is_ok() { @@ -118,6 +140,7 @@ async fn process( handler.accept_request(&mut incoming).await?; } + // Log the time taken to process the request println!("{}ms", Instant::now().saturating_duration_since(start_time).as_millis()); Ok(()) diff --git a/socksx/src/socks5/mod.rs b/socksx/src/socks5/mod.rs index 2a6b9fc..92bf471 100644 --- a/socksx/src/socks5/mod.rs +++ b/socksx/src/socks5/mod.rs @@ -1,15 +1,17 @@ -use crate::addresses::{self, Address}; -use crate::constants::*; use anyhow::Result; use num_traits::FromPrimitive; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; -mod s5_client; -mod s5_handler; - pub use s5_client::Socks5Client; pub use s5_handler::Socks5Handler; +use crate::addresses::{self, Address}; +use crate::constants::*; + +mod s5_client; +mod s5_handler; + +/// Represents the different commands for SOCKS5 protocol. #[repr(u8)] #[derive(Clone, Debug, FromPrimitive, PartialEq)] pub enum Socks5Command { @@ -18,6 +20,7 @@ pub enum Socks5Command { UdpAssociate = 0x03, } +/// Represents a SOCKS5 request. #[derive(Clone, Debug)] pub struct Socks5Request { pub command: Socks5Command, @@ -25,9 +28,16 @@ pub struct Socks5Request { } impl Socks5Request { + /// Creates a new SOCKS5 request. + /// + /// # Arguments /// + /// * `command` - The command type (e.g., Connect). + /// * `destination` - The target address and port to connect to. /// + /// # Returns /// + /// A new `Socks5Request` instance. pub fn new( command: u8, destination: Address, @@ -38,9 +48,11 @@ impl Socks5Request { } } + /// Converts the request into bytes suitable for transmission over a SOCKS5 connection. /// + /// # Returns /// - /// + /// A vector of bytes representing the request. pub fn into_socks_bytes(self) -> Vec { let mut data = vec![SOCKS_VER_5, SOCKS_CMD_CONNECT, SOCKS_RSV]; data.extend(self.destination.as_socks_bytes()); @@ -49,6 +61,7 @@ impl Socks5Request { } } +/// Represents different reply codes for SOCKS5 protocol. #[repr(u8)] #[derive(Clone, Debug, FromPrimitive, PartialEq)] pub enum Socks5Reply { @@ -64,15 +77,22 @@ pub enum Socks5Reply { ConnectionAttemptTimeOut = 0x09, } +/// Writes a SOCKS5 reply to the provided stream. +/// +/// # Arguments /// +/// * `stream` - The output stream where the reply will be written. +/// * `reply` - The SOCKS5 reply code to be written. /// +/// # Returns /// +/// A `Result` indicating success or an error. pub async fn write_reply( stream: &mut S, reply: Socks5Reply, ) -> Result<()> -where - S: AsyncWrite + Unpin, + where + S: AsyncWrite + Unpin, { let reply = [ SOCKS_VER_5, @@ -92,12 +112,18 @@ where Ok(()) } +/// Reads a SOCKS5 reply from the provided stream and returns the associated address. +/// +/// # Arguments /// +/// * `stream` - The input stream where the reply will be read from. /// +/// # Returns /// +/// A `Result` containing the address associated with the reply if successful, or an error if the reply indicates failure. pub async fn read_reply(stream: &mut S) -> Result
-where - S: AsyncRead + Unpin, + where + S: AsyncRead + Unpin, { let mut operation_reply = [0; 3]; stream.read_exact(&mut operation_reply).await?; diff --git a/socksx/src/socks5/s5_client.rs b/socksx/src/socks5/s5_client.rs index bfe0bfc..24174d9 100644 --- a/socksx/src/socks5/s5_client.rs +++ b/socksx/src/socks5/s5_client.rs @@ -1,11 +1,14 @@ -use crate::socks5::{self, Socks5Request}; -use crate::{constants::*, Address, Credentials}; -use anyhow::Result; use std::convert::TryInto; use std::net::SocketAddr; + +use anyhow::Result; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpStream; +use crate::{Address, constants::*, Credentials}; +use crate::socks5::{self, Socks5Request}; + +/// Represents a SOCKS5 client for connecting to proxy servers. #[derive(Clone)] pub struct Socks5Client { proxy_addr: SocketAddr, @@ -13,9 +16,16 @@ pub struct Socks5Client { } impl Socks5Client { + /// Creates a new `Socks5Client`. /// + /// # Arguments /// + /// * `proxy_addr` - The address of the SOCKS5 proxy server. + /// * `credentials` - Optional SOCKS5 authentication credentials. /// + /// # Returns + /// + /// A `Result` containing the new `Socks5Client` instance. pub async fn new>( proxy_addr: A, credentials: Option, @@ -28,17 +38,21 @@ impl Socks5Client { }) } - /// ... - /// ... - /// ... + /// Establishes a SOCKS5 connection to the specified destination. + /// + /// # Arguments /// - /// [rfc1928] https://tools.ietf.org/html/rfc1928 + /// * `destination` - The target address and port to connect to. + /// + /// # Returns + /// + /// A `Result` containing a tuple with a `TcpStream` to the destination and the bound address. pub async fn connect( &self, destination: A, ) -> Result<(TcpStream, Address)> - where - A: TryInto, + where + A: TryInto, { if let Some(Credentials { username, password }) = &self.credentials { ensure!(username.len() > 255, "Username MUST NOT be larger than 255 bytes."); @@ -70,11 +84,15 @@ impl Socks5Client { Ok((stream, binding)) } - /// ... - /// ... - /// ... + /// Negotiates the SOCKS5 authentication method with the proxy server. + /// + /// # Arguments + /// + /// * `stream` - The TCP stream connected to the proxy server. /// - /// [rfc1928] https://tools.ietf.org/html/rfc1928 + /// # Returns + /// + /// A `Result` containing the selected authentication method. async fn negotiate_auth_method( &self, stream: &mut TcpStream, @@ -110,11 +128,16 @@ impl Socks5Client { } } - /// ... - /// ... - /// ... + /// Authenticates with the SOCKS5 proxy using the provided credentials. + /// + /// # Arguments + /// + /// * `stream` - The TCP stream connected to the proxy server. + /// * `credentials` - The authentication credentials. + /// + /// # Returns /// - /// [rfc1929] https://tools.ietf.org/html/rfc1929 + /// A `Result` indicating success or an error if authentication fails. async fn authenticate( &self, stream: &mut TcpStream, diff --git a/socksx/src/socks5/s5_handler.rs b/socksx/src/socks5/s5_handler.rs index 9506458..77cd7ed 100644 --- a/socksx/src/socks5/s5_handler.rs +++ b/socksx/src/socks5/s5_handler.rs @@ -1,16 +1,18 @@ -use crate::addresses::{self, ProxyAddress}; -use crate::socks5::{self, Socks5Reply}; -use crate::SocksHandler; -use crate::{constants::*, Credentials}; use anyhow::Result; use async_trait::async_trait; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpStream; +use crate::{constants::*, Credentials}; +use crate::addresses::{self, ProxyAddress}; +use crate::socks5::{self, Socks5Reply}; +use crate::SocksHandler; + +/// Represents a SOCKS5 handler for processing client requests. #[derive(Clone)] pub struct Socks5Handler { credentials: Option, - chain: Vec, + //chain: Vec, } impl Default for Socks5Handler { @@ -20,22 +22,34 @@ impl Default for Socks5Handler { } impl Socks5Handler { + /// Creates a new `Socks5Handler` with an optional list of proxy addresses. + /// + /// # Arguments /// + /// * `chain` - A vector of `ProxyAddress` instances representing proxy servers in a chain. /// + /// # Returns /// - pub fn new(chain: Vec) -> Self { + /// A new `Socks5Handler` instance. + pub fn new(_chain: Vec) -> Self { Socks5Handler { credentials: None, - chain, + //chain, } } } #[async_trait] impl SocksHandler for Socks5Handler { + /// Accepts a SOCKS5 client request and sets up a bidirectional connection. /// + /// # Arguments /// + /// * `source` - The TCP stream representing the client connection. /// + /// # Returns + /// + /// A `Result` indicating success or an error. async fn accept_request( &self, source: &mut TcpStream, @@ -48,9 +62,15 @@ impl SocksHandler for Socks5Handler { Ok(()) } + /// Refuses a SOCKS5 client request and notifies the client. + /// + /// # Arguments /// + /// * `source` - The TCP stream representing the client connection. /// + /// # Returns /// + /// A `Result` indicating success or an error. async fn refuse_request( &self, source: &mut TcpStream, @@ -61,9 +81,15 @@ impl SocksHandler for Socks5Handler { Ok(()) } + /// Sets up the SOCKS5 connection with a client. + /// + /// # Arguments /// + /// * `source` - The TCP stream representing the client connection. /// + /// # Returns /// + /// A `Result` containing a TCP stream representing the destination connection. async fn setup( &self, source: &mut TcpStream, diff --git a/socksx/src/socks6/chain.rs b/socksx/src/socks6/chain.rs index 8b3c67a..e4d11b6 100644 --- a/socksx/src/socks6/chain.rs +++ b/socksx/src/socks6/chain.rs @@ -1,19 +1,25 @@ use crate::addresses::ProxyAddress; use crate::socks6::options::{MetadataOption, SocksOption}; +/// The `SocksChain` struct is used for managing a chain of SOCKS proxy addresses. #[derive(Clone, Debug)] pub struct SocksChain { + /// The current index within the links vector. pub index: usize, + /// A vector containing the chain of proxy addresses. pub links: Vec, } +// Default implementation for `SocksChain`. impl Default for SocksChain { + // Creates a new `SocksChain` with index set to 0 and an empty links vector. fn default() -> Self { Self::new(0, vec![]) } } impl SocksChain { + /// Creates a new `SocksChain` with a given index and list of proxy addresses. pub fn new( index: usize, links: Vec, @@ -21,23 +27,20 @@ impl SocksChain { Self { index, links } } - /// - /// - /// + /// Returns a reference to the current `ProxyAddress` based on the index. + /// Panics if index is out of bounds. pub fn current_link(&self) -> &ProxyAddress { self.links.get(self.index).unwrap() } - /// - /// - /// + /// Checks if there is a next `ProxyAddress` in the chain. + /// Returns `true` if a next link exists, `false` otherwise. pub fn has_next(&self) -> bool { self.index + 1 < self.links.len() } - /// - /// - /// + /// Advances to the next `ProxyAddress` in the chain. + /// Returns an `Option` containing a reference to the next `ProxyAddress`, if it exists. pub fn next_link(&mut self) -> Option<&ProxyAddress> { let link = self.links.get(self.index + 1); if link.is_some() { @@ -47,9 +50,8 @@ impl SocksChain { link } - /// - /// - /// + /// Inserts additional `ProxyAddress`es into the chain at the current position. + /// If the chain is empty, appends the root and then the new links. pub fn detour( &mut self, links: &[ProxyAddress], @@ -67,9 +69,8 @@ impl SocksChain { } } - /// - /// - /// + /// Converts the `SocksChain` into a vector of `SocksOption`s. + /// Adds metadata options to indicate the current index and total length of the chain. pub fn as_options(&self) -> Vec { let mut chain_options: Vec = self .links @@ -86,12 +87,65 @@ impl SocksChain { } } +// Test cases for `SocksChain`. #[cfg(test)] mod tests { use super::*; + // Tests default constructor #[test] - pub fn abc() { + pub fn test_default_constructor() { + let chain = SocksChain::default(); + assert_eq!(chain.index, 0); + assert_eq!(chain.links, vec![]); + } + + // Tests custom constructor + #[test] + pub fn test_custom_constructor() { + let chain = SocksChain::new(1, vec![ProxyAddress::new(6, String::from("localhost"), 1, None)]); + assert_eq!(chain.index, 1); + assert_eq!(chain.links, vec![ProxyAddress::new(6, String::from("localhost"), 1, None)]); + } + + // Tests the `current_link` method + #[test] + #[should_panic] + pub fn test_current_link_method() { + let chain = SocksChain::new(1, vec![ProxyAddress::new(6, String::from("localhost"), 1, None)]); + assert_eq!(chain.current_link(), &ProxyAddress::new(6, String::from("localhost"), 1, None)); + } + + // Test `has_next` method + #[test] + pub fn test_has_next() { + let chain = SocksChain::new(0, vec![ProxyAddress::new(6, String::from("localhost"), 1, None)]); + assert!(!chain.has_next()); + } + + // Test `next_link` method + #[test] + pub fn test_next_link() { + let mut chain = SocksChain::new(0, vec![ + ProxyAddress::new(6, String::from("localhost"), 1, None), + ProxyAddress::new(6, String::from("localhost"), 2, None), + ]); + assert_eq!(chain.next_link().unwrap().port, 2); + } + + // Test `detour` method with empty chain + #[test] + pub fn test_detour_empty_chain() { + let mut chain = SocksChain::default(); + chain.detour(&[ProxyAddress::new(6, String::from("localhost"), 1, None)]); + assert_eq!(chain.index, 0); + assert_eq!(chain.links[0], ProxyAddress::root()); + assert_eq!(chain.links[1], ProxyAddress::new(6, String::from("localhost"), 1, None)); + } + + // Tests the `detour` method by checking the order of ports after insertion. + #[test] + pub fn test_detour_method() { let mut chain = SocksChain::new( 1, vec![ diff --git a/socksx/src/socks6/mod.rs b/socksx/src/socks6/mod.rs index 0b8583f..d0c1b5d 100644 --- a/socksx/src/socks6/mod.rs +++ b/socksx/src/socks6/mod.rs @@ -1,23 +1,29 @@ +// General purpose SOCKS6 module. +use std::collections::HashMap; +use std::convert::TryInto; + +use anyhow::{ensure, Result}; +use num_traits::FromPrimitive; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; + +// Module imports +pub use chain::SocksChain; +pub use s6_client::Socks6Client; +pub use s6_handler::Socks6Handler; + +use crate::{constants::*, ProxyAddress}; use crate::addresses::{self, Address}; use crate::socks6::options::{ AuthMethodAdvertisementOption, AuthMethodSelectionOption, MetadataOption, SocksOption, UnrecognizedOption, }; -use crate::{constants::*, ProxyAddress}; -use anyhow::{ensure, Result}; -use num_traits::FromPrimitive; -use std::collections::HashMap; -use std::convert::TryInto; -use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; +// Sub-modules pub mod chain; pub mod options; mod s6_client; mod s6_handler; -pub use chain::SocksChain; -pub use s6_client::Socks6Client; -pub use s6_handler::Socks6Handler; - +/// Authentication methods supported. #[repr(u8)] #[derive(Clone, Debug, FromPrimitive)] pub enum AuthMethod { @@ -27,6 +33,7 @@ pub enum AuthMethod { NoAcceptableMethods = 0xFF, } +/// Command types in SOCKS6. #[repr(u8)] #[derive(Clone, Debug, FromPrimitive, PartialEq)] pub enum Socks6Command { @@ -36,6 +43,7 @@ pub enum Socks6Command { UdpAssociate = 0x03, } +/// Represents a SOCKS6 request. #[derive(Clone, Debug)] pub struct Socks6Request { pub command: Socks6Command, @@ -46,9 +54,7 @@ pub struct Socks6Request { } impl Socks6Request { - /// - /// - /// + /// Constructor for Socks6Request pub fn new( command: u8, destination: Address, @@ -65,9 +71,7 @@ impl Socks6Request { } } - /// - /// - /// + /// Chain function to link multiple proxies. pub fn chain( &self, static_links: &[ProxyAddress], @@ -100,9 +104,7 @@ impl Socks6Request { } } - /// - /// - /// + /// Convert the request into a byte sequence for SOCKS6. pub fn into_socks_bytes(self) -> Vec { let mut data = vec![SOCKS_VER_6, SOCKS_CMD_CONNECT]; data.extend(self.destination.as_socks_bytes()); @@ -118,9 +120,7 @@ impl Socks6Request { } } -/// -/// -/// +/// Reads a SOCKS6 request from the provided stream. pub async fn read_request(stream: &mut S) -> Result where S: AsyncRead + Unpin, @@ -147,7 +147,7 @@ where for option in &options { match option { SocksOption::AuthMethodAdvertisement(advertisement) => { - // Make note of initial data length for convience. + // Make note of initial data length for convenience. initial_data_length = advertisement.initial_data_length; } SocksOption::Metadata(key_value) => { @@ -170,9 +170,7 @@ where )) } -/// -/// -/// +/// Reads the SOCKS6 options from the stream. pub async fn read_options(stream: &mut S) -> Result> where S: AsyncRead + Unpin, @@ -211,6 +209,7 @@ where Ok(options) } +/// Reads the authentication response. pub async fn read_no_authentication(stream: &mut S) -> Result> where S: AsyncRead + Unpin, @@ -241,6 +240,7 @@ where Ok(options) } +/// Writes a reply to indicate no authentication is needed. pub async fn write_no_authentication(stream: &mut S) -> Result<()> where S: AsyncWrite + Unpin, @@ -252,6 +252,7 @@ where Ok(()) } +/// Writes the initial data for the SOCKS6 request. pub async fn write_initial_data( _stream: &mut S, _request: &Socks6Request, @@ -263,6 +264,7 @@ where Ok(()) } +/// Represents SOCKS6 replies. #[repr(u8)] #[derive(Clone, Debug, FromPrimitive, PartialEq)] pub enum Socks6Reply { @@ -278,9 +280,7 @@ pub enum Socks6Reply { ConnectionAttemptTimeOut = 0x09, } -/// -/// -/// +/// Writes a SOCKS6 reply to the stream. pub async fn write_reply( stream: &mut S, reply: Socks6Reply, @@ -308,9 +308,7 @@ where Ok(()) } -/// -/// -/// +/// Reads a SOCKS6 reply from the stream. pub async fn read_reply(stream: &mut S) -> Result<(Address, Vec)> where S: AsyncRead + Unpin, @@ -330,3 +328,45 @@ where Ok((binding, options)) } + +#[cfg(test)] +mod tests { + use super::*; + + // Test creation of a new Socks6Request. + #[test] + fn test_new_socks6_request() { + let request = Socks6Request::new( + Socks6Command::Connect as u8, + Address::new("192.168.1.1", 80), + 0, + vec![], + None, + ); + + // Ensure the fields are correctly set. + assert_eq!(request.command, Socks6Command::Connect); + assert_eq!( + request.destination, + Address::new("192.168.1.1", 80), + ); + assert_eq!(request.initial_data_length, 0); + assert_eq!(request.options.len(), 0); + assert_eq!(request.metadata.len(), 0); + } + + // Test conversion of Socks6Request into a byte sequence. + #[test] + fn test_into_socks_bytes() { + let request = Socks6Request::new( + Socks6Command::Connect as u8, + Address::new("192.168.1.1", 80), + 0, + vec![], + None, + ); + let result = request.into_socks_bytes(); + let expected_result: Vec = vec![6, 1, 1, 192, 168, 1, 1, 0, 80, 0, 0, 0]; + assert_eq!(result, expected_result); + } +} \ No newline at end of file diff --git a/socksx/src/socks6/options.rs b/socksx/src/socks6/options.rs index 6e4adc2..180dc19 100644 --- a/socksx/src/socks6/options.rs +++ b/socksx/src/socks6/options.rs @@ -1,8 +1,9 @@ use anyhow::Result; use num_traits::FromPrimitive; +/// Represents SOCKS authentication methods. #[repr(u8)] -#[derive(Clone, Debug, FromPrimitive)] +#[derive(Clone, Debug, FromPrimitive, PartialEq)] pub enum AuthMethod { NoAuthentication = 0x00, Gssapi = 0x01, @@ -10,6 +11,7 @@ pub enum AuthMethod { NoAcceptableMethods = 0xFF, } +/// Enumerates the types of SOCKS options. #[derive(Clone, Debug)] pub enum SocksOption { AuthMethodAdvertisement(AuthMethodAdvertisementOption), @@ -19,6 +21,7 @@ pub enum SocksOption { } impl SocksOption { + /// Converts the SOCKS option to a vector of bytes. pub fn as_socks_bytes(&self) -> Vec { use SocksOption::*; @@ -31,6 +34,7 @@ impl SocksOption { } } +/// Represents the authentication methods supported by the server. #[derive(Clone, Debug)] pub struct AuthMethodAdvertisementOption { pub initial_data_length: u16, @@ -38,6 +42,7 @@ pub struct AuthMethodAdvertisementOption { } impl AuthMethodAdvertisementOption { + /// Constructs a new `AuthMethodAdvertisementOption`. pub fn new( initial_data_length: u16, methods: Vec, @@ -48,13 +53,12 @@ impl AuthMethodAdvertisementOption { } } + /// Wraps the instance into a `SocksOption`. pub fn wrap(self) -> SocksOption { SocksOption::AuthMethodAdvertisement(self) } - /// - /// - /// + /// Deserializes the option from bytes. pub fn from_socks_bytes(bytes: Vec) -> Result { ensure!(bytes.len() >= 2, "Expected at least two bytes, got: {}", bytes.len()); let initial_data_length = ((bytes[0] as u16) << 8) | bytes[1] as u16; @@ -73,9 +77,7 @@ impl AuthMethodAdvertisementOption { Ok(Self::new(initial_data_length, methods).wrap()) } - /// - /// - /// + /// Serializes the option into bytes. pub fn into_socks_bytes(self) -> Vec { let mut data = self.initial_data_length.to_be_bytes().to_vec(); data.extend(self.methods.iter().cloned().map(|m| m as u8)); @@ -84,20 +86,24 @@ impl AuthMethodAdvertisementOption { } } +/// Represents the authentication methods selected by the client. #[derive(Clone, Debug)] pub struct AuthMethodSelectionOption { pub method: AuthMethod, } impl AuthMethodSelectionOption { + /// Constructs a new `AuthMethodSelectionOption`. pub fn new(method: AuthMethod) -> Self { Self { method } } + /// Wraps the instance into a `SocksOption`. pub fn wrap(self) -> SocksOption { SocksOption::AuthMethodSelection(self) } + /// Deserializes the option from bytes. pub fn from_socks_bytes(bytes: Vec) -> Result { ensure!(bytes.len() == 4, "Expected exactly four bytes, got: {}", bytes.len()); @@ -109,6 +115,7 @@ impl AuthMethodSelectionOption { } } + /// Serializes the option into bytes. pub fn into_socks_bytes(self) -> Vec { let data = vec![self.method as u8]; @@ -116,6 +123,7 @@ impl AuthMethodSelectionOption { } } +/// Represents a metadata option. #[derive(Clone, Debug)] pub struct MetadataOption { pub key: u16, @@ -123,6 +131,7 @@ pub struct MetadataOption { } impl MetadataOption { + /// Constructs a new `MetadataOption`. pub fn new( key: u16, value: String, @@ -130,10 +139,12 @@ impl MetadataOption { Self { key, value } } + /// Wraps the instance into a `SocksOption`. pub fn wrap(self) -> SocksOption { SocksOption::Metadata(self) } + /// Deserializes the option from bytes. pub fn from_socks_bytes(bytes: Vec) -> Result { ensure!(bytes.len() >= 4, "Expected at least four bytes, got: {}", bytes.len()); let key = ((bytes[0] as u16) << 8) | bytes[1] as u16; @@ -147,6 +158,7 @@ impl MetadataOption { } } + /// Serializes the option into bytes. pub fn into_socks_bytes(self) -> Vec { let mut data = self.key.to_be_bytes().to_vec(); data.extend((self.value.len() as u16).to_be_bytes().iter()); @@ -157,6 +169,7 @@ impl MetadataOption { } } +/// Represents an unrecognized option. #[derive(Clone, Debug)] pub struct UnrecognizedOption { kind: u16, @@ -164,6 +177,7 @@ pub struct UnrecognizedOption { } impl UnrecognizedOption { + /// Constructs a new `UnrecognizedOption`. pub fn new( kind: u16, data: Vec, @@ -171,18 +185,27 @@ impl UnrecognizedOption { Self { kind, data } } + /// Wraps the instance into a `SocksOption`. pub fn wrap(self) -> SocksOption { SocksOption::Unrecognized(self) } + /// Deserializes the option from bytes. pub fn into_socks_bytes(self) -> Vec { combine_and_pad(self.kind, self.data) } } +/// Combines and pads the SOCKS option bytes. /// +/// # Parameters /// +/// - `kind`: The kind of the SOCKS option. +/// - `data`: The data associated with the SOCKS option. /// +/// # Returns +/// +/// A vector of bytes representing the padded SOCKS option. fn combine_and_pad( kind: u16, data: Vec, @@ -201,3 +224,44 @@ fn combine_and_pad( bytes } + +#[cfg(test)] +mod tests { + use super::*; + + // Test the AuthMethod enum conversion from primitive types + #[test] + fn test_auth_method_from_primitive() { + let method = AuthMethod::from_u8(0x00); + assert_eq!(method, Some(AuthMethod::NoAuthentication)); + } + + // Test the AuthMethodAdvertisementOption constructor + #[test] + fn test_auth_method_advertisement_option_new() { + let option = AuthMethodAdvertisementOption::new(0, vec![AuthMethod::NoAuthentication]); + assert_eq!(option.initial_data_length, 0); + assert_eq!(option.methods, vec![AuthMethod::NoAuthentication]); + } + + // Test wrapping an AuthMethodAdvertisementOption into a SocksOption + #[test] + fn test_auth_method_advertisement_option_wrap() { + let option = AuthMethodAdvertisementOption::new(0, vec![]); + let wrapped = option.wrap(); + if let SocksOption::AuthMethodAdvertisement(_) = wrapped { + assert!(true); + } else { + assert!(false, "Expected AuthMethodAdvertisement variant"); + } + } + + // Test the from_socks_bytes function for AuthMethodAdvertisementOption + #[test] + fn test_from_socks_bytes_auth_method_advertisement() { + let bytes = vec![0x00, 0x02, 0x00, 0x01, 0x02]; + let result = AuthMethodAdvertisementOption::from_socks_bytes(bytes); + // Verify the result according to your expectations + assert!(result.is_ok()); + } +} \ No newline at end of file diff --git a/socksx/src/socks6/s6_client.rs b/socksx/src/socks6/s6_client.rs index 8f92676..018dfca 100644 --- a/socksx/src/socks6/s6_client.rs +++ b/socksx/src/socks6/s6_client.rs @@ -1,14 +1,17 @@ +use std::{convert::TryInto, net::SocketAddr}; + +use anyhow::{ensure, Result}; +use tokio::io::AsyncWriteExt; +use tokio::net::TcpStream; + +use crate::{Address, constants::*, Credentials}; use crate::socks6::{self, Socks6Request}; use crate::socks6::{ - options::{AuthMethodAdvertisementOption, SocksOption}, AuthMethod, + options::{AuthMethodAdvertisementOption, SocksOption}, }; -use crate::{constants::*, Address, Credentials}; -use anyhow::{ensure, Result}; -use std::{convert::TryInto, net::SocketAddr}; -use tokio::io::AsyncWriteExt; -use tokio::net::TcpStream; +/// Represents a SOCKS6 client. #[derive(Clone)] pub struct Socks6Client { proxy_addr: SocketAddr, @@ -16,9 +19,14 @@ pub struct Socks6Client { } impl Socks6Client { + /// Creates a new Socks6Client. /// + /// # Parameters + /// - `proxy_addr`: The address of the SOCKS6 proxy. + /// - `credentials`: Optional credentials for authentication. /// - /// + /// # Returns + /// A `Result` containing a new `Socks6Client` or an error. pub async fn new>( proxy_addr: A, credentials: Option, @@ -31,9 +39,15 @@ impl Socks6Client { }) } + /// Connects to a given destination through the SOCKS6 proxy. /// + /// # Parameters + /// - `destination`: The destination to connect to. + /// - `initial_data`: Optional initial data to send. + /// - `options`: Optional SOCKS options. /// - /// + /// # Returns + /// A `Result` containing a tuple of the `TcpStream` and the bound `Address`, or an error. pub async fn connect( &self, destination: A, @@ -49,10 +63,19 @@ impl Socks6Client { Ok((stream, binding)) } - /// ... - /// ... - /// ... - /// [socks6-draft11] https://tools.ietf.org/html/draft-olteanu-intarea-socks-6-11 + /// Conducts the handshake process with the SOCKS6 proxy. + /// + /// This method implements the handshake protocol as per [socks6-draft11]. + /// [socks6-draft11]: https://tools.ietf.org/html/draft-olteanu-intarea-socks-6-11 + /// + /// # Parameters + /// - `destination`: The destination to connect to. + /// - `initial_data`: Optional initial data to send. + /// - `options`: Optional SOCKS options. + /// - `stream`: The mutable reference to the `TcpStream`. + /// + /// # Returns + /// A `Result` containing the bound `Address` or an error. pub async fn handshake( &self, destination: A, diff --git a/socksx/src/socks6/s6_handler.rs b/socksx/src/socks6/s6_handler.rs index bf3bdff..22b0584 100644 --- a/socksx/src/socks6/s6_handler.rs +++ b/socksx/src/socks6/s6_handler.rs @@ -1,26 +1,33 @@ -use crate::addresses::ProxyAddress; -use crate::socks6::{self, Socks6Reply}; -use crate::{Socks6Client, SocksHandler}; use anyhow::Result; use async_trait::async_trait; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpStream; +use crate::{Socks6Client, SocksHandler}; +use crate::addresses::ProxyAddress; +use crate::socks6::{self, Socks6Reply}; + +/// Implements a SOCKS6 handler. #[derive(Clone)] pub struct Socks6Handler { static_links: Vec, } impl Default for Socks6Handler { + /// Default constructor for `Socks6Handler`. fn default() -> Self { Self::new(vec![]) } } impl Socks6Handler { + /// Constructs a new `Socks6Handler`. /// + /// # Parameters + /// - `static_links`: A list of static proxy addresses. /// - /// + /// # Returns + /// A new `Socks6Handler`. pub fn new(static_links: Vec) -> Self { Socks6Handler { static_links } } @@ -28,9 +35,13 @@ impl Socks6Handler { #[async_trait] impl SocksHandler for Socks6Handler { + /// Accepts a request from the source and sets up a tunnel to the destination. /// + /// # Parameters + /// - `source`: A mutable reference to the source TCP stream. /// - /// + /// # Returns + /// An `Ok(())` if the tunnel is successfully set up, otherwise an error. async fn accept_request( &self, source: &mut TcpStream, @@ -43,9 +54,13 @@ impl SocksHandler for Socks6Handler { Ok(()) } + /// Refuses a request from the source. /// + /// # Parameters + /// - `source`: A mutable reference to the source TCP stream. /// - /// + /// # Returns + /// An `Ok(())` if the source is successfully notified of the refusal, otherwise an error. async fn refuse_request( &self, source: &mut TcpStream, @@ -56,9 +71,13 @@ impl SocksHandler for Socks6Handler { Ok(()) } + /// Sets up the connection to the destination. /// + /// # Parameters + /// - `source`: A mutable reference to the source TCP stream. /// - /// + /// # Returns + /// A `Result` containing the destination `TcpStream` if successful, otherwise an error. async fn setup( &self, source: &mut TcpStream,