Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New explicit and extensible server API #59

Merged
merged 23 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9606450
refactor: extract the auth state machine as Socks5ServerProtocol
valpackett Jan 10, 2025
656589a
refactor: handle the command read & error reply in Socks5ServerProtocol
valpackett Jan 10, 2025
3bd6f02
refactor: inline command handling into upgrade_to_socks5, add reply_s…
valpackett Jan 10, 2025
6af67c2
refactor: fully use the type-safe proto start to finish
valpackett Jan 10, 2025
7bbfab2
refactor: make public the API we're already mostly confident in
valpackett Jan 10, 2025
d5655e4
refactor: merge the impl blocks for Socks5Socket
valpackett Jan 10, 2025
8295a44
refactor: introduce new authentication method API
valpackett Jan 11, 2025
af025ab
refactor: extract the meat from upgrade_to_socks5
valpackett Jan 27, 2025
1eb8b35
refactor: update the server example to use the new API
valpackett Jan 27, 2025
007b3d9
refactor: mark old server API as deprecated
valpackett Jan 27, 2025
d509ab2
refactor: remove simple_tcp_server example
valpackett Jan 27, 2025
deb0d61
refactor: rewrite tests to use the new explicit API
valpackett Jan 27, 2025
2b5884b
refactor: update README description to reflect new API's nature
valpackett Jan 27, 2025
3fc0f61
refactor: add custom_auth_server example
valpackett Jan 27, 2025
092ffa6
refactor: accept_password_auth: return the value
valpackett Jan 31, 2025
636a68b
refactor: add docstrings to new public functions
valpackett Jan 31, 2025
c664e88
refactor: add router example
valpackett Feb 1, 2025
a59c47c
fix(server): prioritize authentication methods
dizda Jan 31, 2025
e5b34cc
refactor: target_addr: define AddrError
valpackett Feb 1, 2025
6b819de
refactor: stream: define ConnectError
valpackett Feb 1, 2025
3e1d804
refactor: lib: define UdpHeaderError
valpackett Feb 1, 2025
e9918fc
refactor: define SocksServerError, restore replying with errors
valpackett Feb 1, 2025
4c8985c
refactor: add a helper for DNS resolution
valpackett Feb 1, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,7 @@ name = "server"
name = "client"

[[example]]
name = "simple_tcp_server"
name = "custom_auth_server"

[[example]]
name = "router"
22 changes: 10 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,23 @@ This library is maintained by [anyip.io](https://anyip.io/) a residential and mo
- An `async`/`.await` [SOCKS4 Client](https://www.openssh.com/txt/socks4.protocol) implementation.
- An `async`/`.await` [SOCKS4a Client](https://www.openssh.com/txt/socks4a.protocol) implementation.
- No **unsafe** code
- Built on-top of `tokio` library
- Built on top of the [Tokio](https://tokio.rs/) runtime
- Ultra lightweight and scalable
- No system dependencies
- Cross-platform
- Infinitely extensible, explicit server API based on typestates for safety
- You control the request handling, the library only ensures you follow the proper protocol flow
- Can skip DNS resolution
- Can skip the authentication/handshake process (not RFC-compliant, for private use, to save on useless round-trips)
- Instead of proxying in-process, swap out `run_tcp_proxy` for custom handling to build a router or to use a custom accelerated proxying method
- Authentication methods:
- No-Auth method
- Username/Password auth method
- Custom auth methods can be implemented via the Authentication Trait
- Credentials returned on authentication success
- No-Auth method (`0x00`)
- Username/Password auth method (`0x02`)
- Custom auth methods can be implemented on the server side via the `AuthMethod` Trait
- Multiple auth methods with runtime negotiation can be supported, with fast *static* dispatch (enums can be generated with the `auth_method_enums` macro)
- UDP is supported
- All SOCKS5 RFC errors (replies) should be mapped
- `AsyncRead + AsyncWrite` traits are implemented on Socks5Stream & Socks5Socket
- `IPv4`, `IPv6`, and `Domains` types are supported
- Config helper for Socks5Server
- Helpers to run a Socks5Server à la *"std's TcpStream"* via `incoming.next().await`
- Examples come with real cases commands scenarios
- Can disable `DNS resolving`
- Can skip the authentication/handshake process, which will directly handle command's request (useful to save useless round-trips in a current authenticated environment)
- Can disable command execution (useful if you just want to forward the request to a different server)


## Install
Expand Down
179 changes: 179 additions & 0 deletions examples/custom_auth_server.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
#[forbid(unsafe_code)]
#[macro_use]
extern crate log;

use fast_socks5::{
auth_method_enums,
server::{
run_tcp_proxy, AuthMethod, AuthMethodSuccessState, DnsResolveHelper as _,
PasswordAuthentication, PasswordAuthenticationStarted, Socks5ServerProtocol,
},
ReplyError, Result, Socks5Command, SocksError,
};
use std::{future::Future, time::Duration};
use structopt::StructOpt;
use tokio::task;
use tokio::{
io::{AsyncRead, AsyncReadExt},
net::TcpListener,
};

/// # How to use it:
///
/// Listen on a local address:
/// `$ RUST_LOG=debug cargo run --example custom_auth_server -- --listen-addr 127.0.0.1:1337`
///
/// then try a client to connect to this server:
/// `$ RUST_LOG=debug cargo run --example client -- --socks-server 127.0.0.1:1337 --username user --password "correct_horse_battery_staple" -a perdu.com -p 80`
///
/// or via a cURL command
/// `curl -v -s --proxy "socks5://user:[email protected]:1337" "https://httpbin.org/get"`
///
#[derive(Debug, StructOpt)]
#[structopt(
name = "socks5-server-custom-auth",
about = "A socks5 server with a curious secret."
)]
struct Opt {
/// Bind on address address. eg. `127.0.0.1:1080`
#[structopt(short, long)]
pub listen_addr: String,
}

#[tokio::main]
async fn main() -> Result<()> {
env_logger::init();

spawn_socks_server().await
}

async fn spawn_socks_server() -> Result<()> {
let opt: Opt = Opt::from_args();

let listener = TcpListener::bind(&opt.listen_addr).await?;

info!("Listen for socks connections @ {}", &opt.listen_addr);

// Standard TCP loop
loop {
match listener.accept().await {
Ok((socket, _client_addr)) => {
spawn_and_log_error(serve_socks5(socket));
}
Err(err) => {
error!("accept error = {:?}", err);
}
}
}
}

pub struct BackdoorAuthenticationStarted<T>(T);
pub struct BackdoorAuthenticationSuccess<T>(T);

impl<T: AsyncRead + Unpin> BackdoorAuthenticationStarted<T> {
pub async fn verify_timing(self) -> Result<BackdoorAuthenticationSuccess<T>> {
let mut socket = self.0;
let mut buf = vec![0u8; 2];
if tokio::time::timeout(Duration::from_millis(500), socket.read_exact(&mut buf))
.await
.is_ok()
{
debug!("too early!");
return Err(SocksError::AuthenticationRejected("nope".to_owned()));
}
if tokio::time::timeout(Duration::from_millis(500), socket.read_exact(&mut buf))
.await
.is_err()
{
debug!("too late!");
return Err(SocksError::AuthenticationRejected("nope".to_owned()));
}
if buf[0] == 0x13 && buf[1] == 0x37 {
Ok(BackdoorAuthenticationSuccess(socket))
} else {
debug!("wrong contents!");
Err(SocksError::AuthenticationRejected("nope".to_owned()))
}
}
}

impl<T> AuthMethodSuccessState<T> for BackdoorAuthenticationSuccess<T> {
fn into_inner(self) -> T {
self.0
}
}

/// A silly example of a custom authentication method.
#[derive(Debug, Clone, Copy)]
pub struct BackdoorAuthentication;

impl<T> AuthMethod<T> for BackdoorAuthentication {
type StartingState = BackdoorAuthenticationStarted<T>;

fn method_id(self) -> u8 {
0xF0 // From the "RESERVED FOR PRIVATE METHODS" range
}

fn new(self, inner: T) -> Self::StartingState {
BackdoorAuthenticationStarted(inner)
}
}

auth_method_enums! {
pub enum Auth / AuthStarted<T> {
PasswordAuthentication(PasswordAuthenticationStarted<T>),
BackdoorAuthentication(BackdoorAuthenticationStarted<T>),
}
}

async fn serve_socks5(socket: tokio::net::TcpStream) -> Result<(), SocksError> {
let proto = match Socks5ServerProtocol::start(socket)
.negotiate_auth(&[
// The order of authentication methods can be tested by clients in sequence,
// so list more secure or preferred methods first
Auth::PasswordAuthentication(PasswordAuthentication),
Auth::BackdoorAuthentication(BackdoorAuthentication),
])
.await?
{
AuthStarted::PasswordAuthentication(auth) => {
let (user, pass, auth) = auth.read_username_password().await?;
if user == "user" && pass == "correct_horse_battery_staple" {
// better to not use spaces for trying out with cURL
auth.accept().await?.finish_auth()
} else {
auth.reject().await?;
return Err(SocksError::AuthenticationRejected(
"Wrong username/password".to_owned(),
));
}
}
AuthStarted::BackdoorAuthentication(auth) => auth.verify_timing().await?.finish_auth(),
};

let (proto, cmd, target_addr) = proto.read_command().await?.resolve_dns().await?;

const REQUEST_TIMEOUT: u64 = 10;
match cmd {
Socks5Command::TCPConnect => {
run_tcp_proxy(proto, &target_addr, REQUEST_TIMEOUT, false).await?;
}
_ => {
proto.reply_error(&ReplyError::CommandNotSupported).await?;
return Err(ReplyError::CommandNotSupported.into());
}
};
Ok(())
}

fn spawn_and_log_error<F>(fut: F) -> task::JoinHandle<()>
where
F: Future<Output = Result<()>> + Send + 'static,
{
task::spawn(async move {
match fut.await {
Ok(()) => {}
Err(err) => error!("{:#}", &err),
}
})
}
Loading