From 96064505048eb07faf2d294d943660c78a3bf914 Mon Sep 17 00:00:00 2001 From: Val Packett Date: Fri, 10 Jan 2025 03:43:32 -0300 Subject: [PATCH 01/23] refactor: extract the auth state machine as Socks5ServerProtocol Start working on a new, safer, more extensible, lower-level-ish API. First step, extract the methods handling the authentication, for now without touching the actual code inside of them. Don't make the methods public yet as this API isn't great yet. --- src/server.rs | 78 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 66 insertions(+), 12 deletions(-) diff --git a/src/server.rs b/src/server.rs index f762f61..fefb199 100644 --- a/src/server.rs +++ b/src/server.rs @@ -9,6 +9,7 @@ use crate::{consts, AuthenticationMethod, ReplyError, Result, SocksError}; use anyhow::Context; use std::future::Future; use std::io; +use std::marker::PhantomData; use std::net::IpAddr; use std::net::Ipv4Addr; use std::net::{SocketAddr, ToSocketAddrs as StdToSocketAddrs}; @@ -272,6 +273,32 @@ pub struct Socks5Socket { credentials: Option, } +pub mod states { + pub struct Opened; + pub struct AuthMethodsRead; + pub struct AuthMethodChosen; +} + +pub struct Socks5ServerProtocol { + inner: T, + _state: PhantomData, +} + +impl Socks5ServerProtocol { + fn new(inner: T) -> Self { + Socks5ServerProtocol { + inner, + _state: PhantomData, + } + } +} + +impl Socks5ServerProtocol { + pub fn start(inner: T) -> Self { + Self::new(inner) + } +} + impl Socks5Socket { pub fn new(socket: T, config: Arc>) -> Self { Socks5Socket { @@ -307,13 +334,22 @@ impl Socks5Socket { // Handshake if !self.config.skip_auth { - let methods = self.get_methods().await?; + let (proto, methods) = Socks5ServerProtocol::start(self.inner) + .get_methods() + .await?; - let auth_method = self.can_accept_method(methods).await?; + let (proto, auth_method) = proto + .can_accept_method(methods, self.config.as_ref()) + .await?; if self.config.auth.is_some() { - let credentials = self.authenticate(auth_method).await?; + let (inner, credentials) = proto + .authenticate(auth_method, self.config.as_ref()) + .await?; + self.inner = inner; self.credentials = Some(credentials); + } else { + self.inner = proto.inner; } } else { debug!("skipping auth"); @@ -337,7 +373,9 @@ impl Socks5Socket { pub fn into_inner(self) -> T { self.inner } +} +impl Socks5ServerProtocol { /// Read the authentication method provided by the client. /// A client send a list of methods that he supports, he could send /// @@ -354,7 +392,9 @@ impl Socks5Socket { /// eg. (auth) {5, 3} /// ``` /// - async fn get_methods(&mut self) -> Result> { + async fn get_methods( + mut self, + ) -> Result<(Socks5ServerProtocol, Vec)> { trace!("Socks5Socket: get_methods()"); // read the first 2 bytes which contains the SOCKS version and the methods len() let [version, methods_len] = @@ -377,9 +417,11 @@ impl Socks5Socket { debug!("methods supported sent by the client: {:?}", &methods); // Return methods available - Ok(methods) + Ok((Socks5ServerProtocol::new(self.inner), methods)) } +} +impl Socks5ServerProtocol { /// Decide to whether or not, accept the authentication method. /// Don't forget that the methods list sent by the client, contains one or more methods. /// @@ -399,16 +441,20 @@ impl Socks5Socket { /// eg. (non-acceptable) {5, 0xff} /// ``` /// - async fn can_accept_method(&mut self, client_methods: Vec) -> Result { + async fn can_accept_method( + mut self, + client_methods: Vec, + config: &Config, + ) -> Result<(Socks5ServerProtocol, u8)> { let method_supported; - if let Some(_auth) = self.config.auth.as_ref() { + if let Some(_auth) = config.auth.as_ref() { if client_methods.contains(&consts::SOCKS5_AUTH_METHOD_PASSWORD) { // can auth with password method_supported = consts::SOCKS5_AUTH_METHOD_PASSWORD; } else { // client hasn't provided a password - if self.config.allow_no_auth { + if config.allow_no_auth { // but we allow no auth, for ip whitelisting method_supported = consts::SOCKS5_AUTH_METHOD_NONE; } else { @@ -438,9 +484,11 @@ impl Socks5Socket { .write(&[consts::SOCKS5_VERSION, method_supported]) .await .context("Can't reply with method auth-none")?; - Ok(method_supported) + Ok((Socks5ServerProtocol::new(self.inner), method_supported)) } +} +impl Socks5ServerProtocol { async fn read_username_password(socket: &mut T) -> Result<(String, String)> { trace!("Socks5Socket: authenticate()"); let [version, user_len] = read_exact!(socket, [0u8; 2]).context("Can't read user len")?; @@ -485,7 +533,11 @@ impl Socks5Socket { /// - this server has `Authentication` trait implemented. /// - and the client supports authentication via username/password /// - or the client doesn't send authentication, but we let the trait decides if the `allow_no_auth()` set as `true` - async fn authenticate(&mut self, auth_method: u8) -> Result { + async fn authenticate( + mut self, + auth_method: u8, + config: &Config, + ) -> Result<(T, A::Item)> { let credentials = if auth_method == consts::SOCKS5_AUTH_METHOD_PASSWORD { let credentials = Self::read_username_password(&mut self.inner).await?; Some(credentials) @@ -496,7 +548,7 @@ impl Socks5Socket { None }; - let auth = self.config.auth.as_ref().context("No auth module")?; + let auth = config.auth.as_ref().context("No auth module")?; if let Some(credentials) = auth.authenticate(credentials).await { if auth_method == consts::SOCKS5_AUTH_METHOD_PASSWORD { @@ -509,7 +561,7 @@ impl Socks5Socket { info!("User logged successfully."); - return Ok(credentials); + return Ok((self.inner, credentials)); } else { self.inner .write_all(&[1, consts::SOCKS5_AUTH_METHOD_NOT_ACCEPTABLE]) @@ -521,7 +573,9 @@ impl Socks5Socket { ))); } } +} +impl Socks5Socket { /// Wrapper to principally cover ReplyError types for both functions read & execute request. async fn request(&mut self) -> Result<()> { self.read_command().await?; From 656589ab6c823a4c4de02d325ee9993cd2e57fc1 Mon Sep 17 00:00:00 2001 From: Val Packett Date: Fri, 10 Jan 2025 04:32:34 -0300 Subject: [PATCH 02/23] refactor: handle the command read & error reply in Socks5ServerProtocol Extend the type-level state machine further. Missing the success reply for now as that requires refactoring the actual proxying functions, which will be done next. --- src/server.rs | 115 ++++++++++++++++++++++++++------------------------ 1 file changed, 60 insertions(+), 55 deletions(-) diff --git a/src/server.rs b/src/server.rs index fefb199..eb0bf0c 100644 --- a/src/server.rs +++ b/src/server.rs @@ -277,6 +277,8 @@ pub mod states { pub struct Opened; pub struct AuthMethodsRead; pub struct AuthMethodChosen; + pub struct Authenticated; + pub struct CommandRead; } pub struct Socks5ServerProtocol { @@ -333,7 +335,7 @@ impl Socks5Socket { trace!("upgrading to socks5..."); // Handshake - if !self.config.skip_auth { + let proto = if !self.config.skip_auth { let (proto, methods) = Socks5ServerProtocol::start(self.inner) .get_methods() .await?; @@ -343,28 +345,54 @@ impl Socks5Socket { .await?; if self.config.auth.is_some() { - let (inner, credentials) = proto + let (proto, credentials) = proto .authenticate(auth_method, self.config.as_ref()) .await?; - self.inner = inner; self.credentials = Some(credentials); + proto } else { - self.inner = proto.inner; + Socks5ServerProtocol::new(proto.inner) } } else { debug!("skipping auth"); - } + Socks5ServerProtocol::new(self.inner) + }; - match self.request().await { - Ok(_) => {} - Err(SocksError::ReplyError(e)) => { - // If a reply error has been returned, we send it to the client - self.reply_error(&e).await?; - return Err(e.into()); // propagate the error to end this connection's task + let (proto, cmd, target_addr) = proto.read_command().await?; + self.cmd = Some(match cmd { + Socks5Command::UDPAssociate if !self.config.allow_udp => { + proto.reply_error(&ReplyError::CommandNotSupported).await?; + return Err(ReplyError::CommandNotSupported.into()); } - // if any other errors has been detected, we simply end connection's task - Err(d) => return Err(d), - }; + Socks5Command::TCPBind => { + proto.reply_error(&ReplyError::CommandNotSupported).await?; + return Err(ReplyError::CommandNotSupported.into()); + } + c => c, + }); + self.target_addr = Some(target_addr); + self.inner = proto.inner; + + if self.config.dns_resolve { + self.resolve_dns().await?; + } else { + debug!("Domain won't be resolved because `dns_resolve`'s config has been turned off.") + } + + if self.config.execute_command { + match self.execute_command().await { + Ok(_) => {} + Err(SocksError::ReplyError(e)) => { + // If a reply error has been returned, we send it to the client + Socks5ServerProtocol::::new(self.inner) + .reply_error(&e) + .await?; + return Err(e.into()); // propagate the error to end this connection's task + } + // if any other errors has been detected, we simply end connection's task + Err(d) => return Err(d), + }; + } Ok(self) } @@ -537,7 +565,7 @@ impl Socks5ServerProtocol, - ) -> Result<(T, A::Item)> { + ) -> Result<(Socks5ServerProtocol, A::Item)> { let credentials = if auth_method == consts::SOCKS5_AUTH_METHOD_PASSWORD { let credentials = Self::read_username_password(&mut self.inner).await?; Some(credentials) @@ -561,7 +589,7 @@ impl Socks5ServerProtocol Socks5ServerProtocol Socks5Socket { - /// Wrapper to principally cover ReplyError types for both functions read & execute request. - async fn request(&mut self) -> Result<()> { - self.read_command().await?; - - if self.config.dns_resolve { - self.resolve_dns().await?; - } else { - debug!("Domain won't be resolved because `dns_resolve`'s config has been turned off.") - } - - if self.config.execute_command { - self.execute_command().await?; - } - - Ok(()) - } - +impl Socks5ServerProtocol { /// Reply error to the client with the reply code according to the RFC. - async fn reply_error(&mut self, error: &ReplyError) -> Result<()> { + async fn reply_error(mut self, error: &ReplyError) -> Result<()> { let reply = new_reply(error, "0.0.0.0:0".parse().unwrap()); debug!("reply error to be written: {:?}", &reply); @@ -607,7 +618,9 @@ impl Socks5Socket { Ok(()) } +} +impl Socks5ServerProtocol { /// Decide to whether or not, accept the authentication method. /// Don't forget that the methods list sent by the client, contains one or more methods. /// @@ -622,7 +635,13 @@ impl Socks5Socket { /// /// It the request is correct, it should returns a ['SocketAddr']. /// - async fn read_command(&mut self) -> Result<()> { + async fn read_command( + mut self, + ) -> Result<( + Socks5ServerProtocol, + Socks5Command, + TargetAddr, + )> { let [version, cmd, rsv, address_type] = read_exact!(self.inner, [0u8; 4]).context("Malformed request")?; debug!( @@ -637,21 +656,7 @@ impl Socks5Socket { return Err(SocksError::UnsupportedSocksVersion(version)); } - match Socks5Command::from_u8(cmd) { - None => return Err(ReplyError::CommandNotSupported.into()), - Some(cmd) => match cmd { - Socks5Command::TCPConnect => { - self.cmd = Some(cmd); - } - Socks5Command::UDPAssociate => { - if !self.config.allow_udp { - return Err(ReplyError::CommandNotSupported.into()); - } - self.cmd = Some(cmd); - } - Socks5Command::TCPBind => return Err(ReplyError::CommandNotSupported.into()), - }, - } + let cmd = Socks5Command::from_u8(cmd).ok_or(ReplyError::CommandNotSupported)?; // Guess address type let target_addr = read_address(&mut self.inner, address_type) @@ -663,13 +668,13 @@ impl Socks5Socket { ReplyError::AddressTypeNotSupported })?; - self.target_addr = Some(target_addr); - - debug!("Request target is {}", self.target_addr.as_ref().unwrap()); + debug!("Request target is {}", target_addr); - Ok(()) + Ok((Socks5ServerProtocol::new(self.inner), cmd, target_addr)) } +} +impl Socks5Socket { /// This function is public, it can be call manually on your own-willing /// if config flag has been turned off: `Config::dns_resolve == false`. pub async fn resolve_dns(&mut self) -> Result<()> { From 3bd6f02634b0bf8892f877c38b0c13f0ff189fb4 Mon Sep 17 00:00:00 2001 From: Val Packett Date: Fri, 10 Jan 2025 04:55:26 -0300 Subject: [PATCH 03/23] refactor: inline command handling into upgrade_to_socks5, add reply_success Now almost the entire process goes through the type-level state machine, with only a break for the DNS resolution left, which will be dealt with next. --- src/server.rs | 175 ++++++++++++++++++++++---------------------------- 1 file changed, 77 insertions(+), 98 deletions(-) diff --git a/src/server.rs b/src/server.rs index eb0bf0c..715d88c 100644 --- a/src/server.rs +++ b/src/server.rs @@ -360,6 +360,7 @@ impl Socks5Socket { let (proto, cmd, target_addr) = proto.read_command().await?; self.cmd = Some(match cmd { + /* XXX: this is redundant, just to do it early before dns resolve? */ Socks5Command::UDPAssociate if !self.config.allow_udp => { proto.reply_error(&ReplyError::CommandNotSupported).await?; return Err(ReplyError::CommandNotSupported.into()); @@ -380,17 +381,70 @@ impl Socks5Socket { } if self.config.execute_command { - match self.execute_command().await { - Ok(_) => {} - Err(SocksError::ReplyError(e)) => { - // If a reply error has been returned, we send it to the client - Socks5ServerProtocol::::new(self.inner) - .reply_error(&e) + /* we've just set it to Some above. + * also, not gonna be used externally since we execute it here */ + let cmd = self.cmd.take().unwrap(); + let proto = Socks5ServerProtocol::::new(self.inner); + + match cmd { + Socks5Command::TCPBind => { + proto.reply_error(&ReplyError::CommandNotSupported).await?; + return Err(ReplyError::CommandNotSupported.into()); + } + Socks5Command::TCPConnect => { + let addr = self + .target_addr + .as_ref() + .context("target_addr empty")? + .to_socket_addrs()? + .next() + .context("unreachable")?; + + // TCP connect with timeout, to avoid memory leak for connection that takes forever + let outbound = + tcp_connect_with_timeout(addr, self.config.request_timeout).await?; + + // Disable Nagle's algorithm if config specifies to do so. + outbound.set_nodelay(self.config.nodelay)?; + + debug!("Connected to remote destination"); + + let mut inner = proto + .reply_success(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0)) .await?; - return Err(e.into()); // propagate the error to end this connection's task + + transfer(&mut inner, outbound).await?; + self.inner = inner; + } + Socks5Command::UDPAssociate => { + if self.config.allow_udp { + // The DST.ADDR and DST.PORT fields contain the address and port that + // the client expects to use to send UDP datagrams on for the + // association. The server MAY use this information to limit access + // to the association. + // @see Page 6, https://datatracker.ietf.org/doc/html/rfc1928. + // + // We do NOT limit the access from the client currently in this implementation. + let _not_used = self.target_addr.as_ref(); + + // Listen with UDP6 socket, so the client can connect to it with either + // IPv4 or IPv6. + let peer_sock = UdpSocket::bind("[::]:0").await?; + + // Respect the pre-populated reply IP address. + self.inner = proto + .reply_success(SocketAddr::new( + self.reply_ip.context("invalid reply ip")?, + peer_sock.local_addr()?.port(), + )) + .await?; + + transfer_udp(peer_sock).await?; + } else { + proto.reply_error(&ReplyError::CommandNotSupported).await?; + return Err(ReplyError::CommandNotSupported.into()); + } } - // if any other errors has been detected, we simply end connection's task - Err(d) => return Err(d), }; } @@ -604,6 +658,20 @@ impl Socks5ServerProtocol Socks5ServerProtocol { + /// Reply success to the client according to the RFC. + /// This consumes the wrapper as after this message actual proxying should begin. + async fn reply_success(mut self, sock_addr: SocketAddr) -> Result { + self.inner + .write(&new_reply(&ReplyError::Succeeded, sock_addr)) + .await + .context("Can't write successful reply")?; + + self.inner.flush().await.context("Can't flush the reply!")?; + + debug!("Wrote success"); + Ok(self.inner) + } + /// Reply error to the client with the reply code according to the RFC. async fn reply_error(mut self, error: &ReplyError) -> Result<()> { let reply = new_reply(error, "0.0.0.0:0".parse().unwrap()); @@ -690,95 +758,6 @@ impl Socks5Socket { Ok(()) } - /// Execute the socks5 command that the client wants. - async fn execute_command(&mut self) -> Result<()> { - match &self.cmd { - None => Err(ReplyError::CommandNotSupported.into()), - Some(cmd) => match cmd { - Socks5Command::TCPBind => Err(ReplyError::CommandNotSupported.into()), - Socks5Command::TCPConnect => return self.execute_command_connect().await, - Socks5Command::UDPAssociate => { - if self.config.allow_udp { - return self.execute_command_udp_assoc().await; - } else { - Err(ReplyError::CommandNotSupported.into()) - } - } - }, - } - } - - /// Connect to the target address that the client wants, - /// then forward the data between them (client <=> target address). - async fn execute_command_connect(&mut self) -> Result<()> { - // async-std's ToSocketAddrs doesn't supports external trait implementation - // @see https://github.com/async-rs/async-std/issues/539 - let addr = self - .target_addr - .as_ref() - .context("target_addr empty")? - .to_socket_addrs()? - .next() - .context("unreachable")?; - - // TCP connect with timeout, to avoid memory leak for connection that takes forever - let outbound = tcp_connect_with_timeout(addr, self.config.request_timeout).await?; - - // Disable Nagle's algorithm if config specifies to do so. - outbound.set_nodelay(self.config.nodelay)?; - - debug!("Connected to remote destination"); - - self.inner - .write(&new_reply( - &ReplyError::Succeeded, - SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0), - )) - .await - .context("Can't write successful reply")?; - - self.inner.flush().await.context("Can't flush the reply!")?; - - debug!("Wrote success"); - - transfer(&mut self.inner, outbound).await - } - - /// Bind to a random UDP port, wait for the traffic from - /// the client, and then forward the data to the remote addr. - async fn execute_command_udp_assoc(&mut self) -> Result<()> { - // The DST.ADDR and DST.PORT fields contain the address and port that - // the client expects to use to send UDP datagrams on for the - // association. The server MAY use this information to limit access - // to the association. - // @see Page 6, https://datatracker.ietf.org/doc/html/rfc1928. - // - // We do NOT limit the access from the client currently in this implementation. - let _not_used = self.target_addr.as_ref(); - - // Listen with UDP6 socket, so the client can connect to it with either - // IPv4 or IPv6. - let peer_sock = UdpSocket::bind("[::]:0").await?; - - // Respect the pre-populated reply IP address. - self.inner - .write(&new_reply( - &ReplyError::Succeeded, - SocketAddr::new( - self.reply_ip.context("invalid reply ip")?, - peer_sock.local_addr()?.port(), - ), - )) - .await - .context("Can't write successful reply")?; - - debug!("Wrote success"); - - transfer_udp(peer_sock).await?; - - Ok(()) - } - pub fn target_addr(&self) -> Option<&TargetAddr> { self.target_addr.as_ref() } From 6af67c28880b3fc6677e07437548ed9b87576bbb Mon Sep 17 00:00:00 2001 From: Val Packett Date: Fri, 10 Jan 2025 05:17:51 -0300 Subject: [PATCH 04/23] refactor: fully use the type-safe proto start to finish --- src/server.rs | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/src/server.rs b/src/server.rs index 715d88c..535d84c 100644 --- a/src/server.rs +++ b/src/server.rs @@ -359,23 +359,13 @@ impl Socks5Socket { }; let (proto, cmd, target_addr) = proto.read_command().await?; - self.cmd = Some(match cmd { - /* XXX: this is redundant, just to do it early before dns resolve? */ - Socks5Command::UDPAssociate if !self.config.allow_udp => { - proto.reply_error(&ReplyError::CommandNotSupported).await?; - return Err(ReplyError::CommandNotSupported.into()); - } - Socks5Command::TCPBind => { - proto.reply_error(&ReplyError::CommandNotSupported).await?; - return Err(ReplyError::CommandNotSupported.into()); - } - c => c, - }); + self.cmd = Some(cmd); self.target_addr = Some(target_addr); - self.inner = proto.inner; if self.config.dns_resolve { - self.resolve_dns().await?; + if let Some(addr) = self.target_addr { + self.target_addr = Some(addr.resolve_dns().await?); + } } else { debug!("Domain won't be resolved because `dns_resolve`'s config has been turned off.") } @@ -384,7 +374,6 @@ impl Socks5Socket { /* we've just set it to Some above. * also, not gonna be used externally since we execute it here */ let cmd = self.cmd.take().unwrap(); - let proto = Socks5ServerProtocol::::new(self.inner); match cmd { Socks5Command::TCPBind => { @@ -446,6 +435,8 @@ impl Socks5Socket { } } }; + } else { + self.inner = proto.inner; } Ok(self) From 7bbfab25fddae8d5a3962a3b53c3d4072585746b Mon Sep 17 00:00:00 2001 From: Val Packett Date: Fri, 10 Jan 2025 05:31:08 -0300 Subject: [PATCH 05/23] refactor: make public the API we're already mostly confident in --- src/server.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/server.rs b/src/server.rs index 535d84c..feba7bb 100644 --- a/src/server.rs +++ b/src/server.rs @@ -651,7 +651,7 @@ impl Socks5ServerProtocol Socks5ServerProtocol { /// Reply success to the client according to the RFC. /// This consumes the wrapper as after this message actual proxying should begin. - async fn reply_success(mut self, sock_addr: SocketAddr) -> Result { + pub async fn reply_success(mut self, sock_addr: SocketAddr) -> Result { self.inner .write(&new_reply(&ReplyError::Succeeded, sock_addr)) .await @@ -664,7 +664,7 @@ impl Socks5ServerProtocol Result<()> { + pub async fn reply_error(mut self, error: &ReplyError) -> Result<()> { let reply = new_reply(error, "0.0.0.0:0".parse().unwrap()); debug!("reply error to be written: {:?}", &reply); @@ -694,7 +694,7 @@ impl Socks5ServerProtocol Result<( Socks5ServerProtocol, @@ -775,9 +775,9 @@ impl Socks5Socket { } } -/// Copy data between two peers +/// Run a bidirectional proxy between two streams. /// Using 2 different generators, because they could be different structs with same traits. -async fn transfer(mut inbound: I, mut outbound: O) -> Result<()> +pub async fn transfer(mut inbound: I, mut outbound: O) -> Result<()> where I: AsyncRead + AsyncWrite + Unpin, O: AsyncRead + AsyncWrite + Unpin, @@ -832,7 +832,8 @@ async fn handle_udp_response(inbound: &UdpSocket, outbound: &UdpSocket) -> Resul } } -async fn transfer_udp(inbound: UdpSocket) -> Result<()> { +/// Run a bidirectional UDP SOCKS proxy for a bound port. +pub async fn transfer_udp(inbound: UdpSocket) -> Result<()> { let outbound = UdpSocket::bind("[::]:0").await?; let req_fut = handle_udp_request(&inbound, &outbound); From d5655e44238f5f34adff7ae62997cbeefc07f0e9 Mon Sep 17 00:00:00 2001 From: Val Packett Date: Fri, 10 Jan 2025 05:33:24 -0300 Subject: [PATCH 06/23] refactor: merge the impl blocks for Socks5Socket Now that almost everything in the middle belongs to Socks5ServerProtocol, reunite what's left in the main Socks5Socket impl into one syntactic block. --- src/server.rs | 82 +++++++++++++++++++++++++-------------------------- 1 file changed, 40 insertions(+), 42 deletions(-) diff --git a/src/server.rs b/src/server.rs index feba7bb..e6df912 100644 --- a/src/server.rs +++ b/src/server.rs @@ -446,6 +446,46 @@ impl Socks5Socket { pub fn into_inner(self) -> T { self.inner } + + /// This function is public, it can be call manually on your own-willing + /// if config flag has been turned off: `Config::dns_resolve == false`. + pub async fn resolve_dns(&mut self) -> Result<()> { + trace!("resolving dns"); + if let Some(target_addr) = self.target_addr.take() { + // decide whether we have to resolve DNS or not + self.target_addr = match target_addr { + TargetAddr::Domain(_, _) => Some(target_addr.resolve_dns().await?), + TargetAddr::Ip(_) => Some(target_addr), + }; + } + + Ok(()) + } + + pub fn target_addr(&self) -> Option<&TargetAddr> { + self.target_addr.as_ref() + } + + pub fn auth(&self) -> &AuthenticationMethod { + &self.auth + } + + pub fn cmd(&self) -> &Option { + &self.cmd + } + + /// Borrow the credentials of the user has authenticated with + pub fn get_credentials(&self) -> Option<&<::Item as Deref>::Target> + where + ::Item: Deref, + { + self.credentials.as_deref() + } + + /// Get the credentials of the user has authenticated with + pub fn take_credentials(&mut self) -> Option { + self.credentials.take() + } } impl Socks5ServerProtocol { @@ -733,48 +773,6 @@ impl Socks5ServerProtocol Socks5Socket { - /// This function is public, it can be call manually on your own-willing - /// if config flag has been turned off: `Config::dns_resolve == false`. - pub async fn resolve_dns(&mut self) -> Result<()> { - trace!("resolving dns"); - if let Some(target_addr) = self.target_addr.take() { - // decide whether we have to resolve DNS or not - self.target_addr = match target_addr { - TargetAddr::Domain(_, _) => Some(target_addr.resolve_dns().await?), - TargetAddr::Ip(_) => Some(target_addr), - }; - } - - Ok(()) - } - - pub fn target_addr(&self) -> Option<&TargetAddr> { - self.target_addr.as_ref() - } - - pub fn auth(&self) -> &AuthenticationMethod { - &self.auth - } - - pub fn cmd(&self) -> &Option { - &self.cmd - } - - /// Borrow the credentials of the user has authenticated with - pub fn get_credentials(&self) -> Option<&<::Item as Deref>::Target> - where - ::Item: Deref, - { - self.credentials.as_deref() - } - - /// Get the credentials of the user has authenticated with - pub fn take_credentials(&mut self) -> Option { - self.credentials.take() - } -} - /// Run a bidirectional proxy between two streams. /// Using 2 different generators, because they could be different structs with same traits. pub async fn transfer(mut inbound: I, mut outbound: O) -> Result<()> From 8295a44b5c04d9119affef68f88ab29d836a6b9c Mon Sep 17 00:00:00 2001 From: Val Packett Date: Sat, 11 Jan 2025 05:06:27 -0300 Subject: [PATCH 07/23] refactor: introduce new authentication method API This is the new typestate-based auth method API that provides open-ended extensibilty (i.e. a library user can add their own methods, using for example the 80h-FEh private ID range specified by the RFC). It provides a lot of safety, except for the possibility for two methods to declare the same ID. Sadly, functions in traits cannot be const, or I would've written a static assert at least in the enum macro.. --- src/server.rs | 481 +++++++++++++++++++++++++++++++------------------- 1 file changed, 298 insertions(+), 183 deletions(-) diff --git a/src/server.rs b/src/server.rs index e6df912..f3d3d10 100644 --- a/src/server.rs +++ b/src/server.rs @@ -275,8 +275,6 @@ pub struct Socks5Socket { pub mod states { pub struct Opened; - pub struct AuthMethodsRead; - pub struct AuthMethodChosen; pub struct Authenticated; pub struct CommandRead; } @@ -301,6 +299,237 @@ impl Socks5ServerProtocol { } } +impl Socks5ServerProtocol { + pub fn finish_auth>(auth: A) -> Self { + Self::new(auth.into_inner()) + } + + pub fn skip_auth_this_is_not_rfc_compliant(inner: T) -> Self { + Self::new(inner) + } +} + +/// A trait for the final successful state of an authentication method's implementation. +/// +/// This allows `Socks5ServerProtocol::finish_authentication` to +/// let the user continue with the protocol after the socket has been handed off to the +/// authentication method. +pub trait AuthMethodSuccessState { + fn into_inner(self) -> T; + + fn finish_auth(self) -> Socks5ServerProtocol + where + Self: Sized, + { + Socks5ServerProtocol::finish_auth(self) + } +} + +/// A metadata trait for authentication methods, essentially binding an ID value +/// (as used in the method negotiation) to an actual implementation of the method. +/// +/// Use blank structs for individual protocol implementations and +/// enums for sets of supported protocols (you'll need a matching enum for the `Impl`). +pub trait AuthMethod: Copy { + type StartingState; + fn method_id(self) -> u8; + fn new(self, inner: T) -> Self::StartingState; +} + +pub struct NoAuthenticationImpl(T); + +impl AuthMethodSuccessState for NoAuthenticationImpl { + fn into_inner(self) -> T { + self.0 + } +} + +/// The "NO AUTHENTICATION REQUIRED" auth method, ID 00h as specifed by RFC 1928. +/// +/// As the dummy no-auth method, it only has one state. Once it's been negotiated, +/// you can immediately continue with `finish_authentication`. +/// +/// Or not so immediately: if you want to use no-authentication with e.g. IP address +/// allowlisting or TLS client certificate auth for TLS-wrapped SOCKS5, this is your +/// opportunity to reject the no-authentication by dropping the connection! +#[derive(Debug, Clone, Copy)] +pub struct NoAuthentication; + +impl AuthMethod for NoAuthentication { + type StartingState = NoAuthenticationImpl; + + fn method_id(self) -> u8 { + 0x00 + } + + fn new(self, inner: T) -> Self::StartingState { + NoAuthenticationImpl(inner) + } +} + +mod password_states { + pub struct Started; + pub struct Received; + pub struct Finished; +} + +pub struct PasswordAuthenticationImpl { + inner: T, + _state: PhantomData, +} + +impl PasswordAuthenticationImpl { + fn new(inner: T) -> Self { + PasswordAuthenticationImpl { + inner, + _state: PhantomData, + } + } +} + +impl PasswordAuthenticationImpl { + pub async fn read_username_password( + self, + ) -> Result<( + String, + String, + PasswordAuthenticationImpl, + )> { + let mut socket = self.inner; + trace!("PasswordAuthenticationStarted: read_username_password()"); + let [version, user_len] = read_exact!(socket, [0u8; 2]).context("Can't read user len")?; + debug!( + "Auth: [version: {version}, user len: {len}]", + version = version, + len = user_len, + ); + + if user_len < 1 { + return Err(SocksError::AuthenticationFailed(format!( + "Username malformed ({} chars)", + user_len + ))); + } + + let username = + read_exact!(socket, vec![0u8; user_len as usize]).context("Can't get username.")?; + debug!("username bytes: {:?}", &username); + + let [pass_len] = read_exact!(socket, [0u8; 1]).context("Can't read pass len")?; + debug!("Auth: [pass len: {len}]", len = pass_len,); + + if pass_len < 1 { + return Err(SocksError::AuthenticationFailed(format!( + "Password malformed ({} chars)", + pass_len + ))); + } + + let password = + read_exact!(socket, vec![0u8; pass_len as usize]).context("Can't get password.")?; + debug!("password bytes: {:?}", &password); + + let username = String::from_utf8(username).context("Failed to convert username")?; + let password = String::from_utf8(password).context("Failed to convert password")?; + + Ok((username, password, PasswordAuthenticationImpl::new(socket))) + } +} + +impl PasswordAuthenticationImpl { + pub async fn accept( + mut self, + ) -> Result> { + self.inner + .write_all(&[1, consts::SOCKS5_REPLY_SUCCEEDED]) + .await + .context("Can't reply auth success")?; + + info!("Password authentication accepted."); + Ok(PasswordAuthenticationImpl::new(self.inner)) + } + + pub async fn reject(mut self) -> Result<()> { + self.inner + .write_all(&[1, consts::SOCKS5_AUTH_METHOD_NOT_ACCEPTABLE]) + .await + .context("Can't reply with auth method not acceptable.")?; + + info!("Password authentication rejected."); + Ok(()) + } +} + +impl AuthMethodSuccessState for PasswordAuthenticationImpl { + fn into_inner(self) -> T { + self.inner + } +} + +/// The "USERNAME/PASSWORD" auth method, ID 02h as specified by RFC 1928. +#[derive(Debug, Clone, Copy)] +pub struct PasswordAuthentication; + +impl AuthMethod for PasswordAuthentication { + type StartingState = PasswordAuthenticationImpl; + + fn method_id(self) -> u8 { + 0x02 + } + + fn new(self, inner: T) -> Self::StartingState { + PasswordAuthenticationImpl::new(inner) + } +} + +#[macro_export] +macro_rules! auth_method_enums { + ( + $(#[$enum_meta:meta])* + $vis:vis enum $enum:ident / $(#[$state_enum_meta:meta])* $state_enum:ident<$state_enum_par:ident> { + $($method:ident($state:ty)),+ $(,)? + } + ) => { + $(#[$state_enum_meta])* + $vis enum $state_enum<$state_enum_par> { + $($method($state)),+ + } + + #[derive(Clone, Copy)] + $(#[$enum_meta])* + $vis enum $enum { + $($method($method)),+ + } + + impl AuthMethod for $enum { + type StartingState = $state_enum; + + fn method_id(self) -> u8 { + match self { + $($enum::$method(auth) => AuthMethod::::method_id(auth)),+ + } + } + + fn new(self, inner: T) -> Self::StartingState { + match self { + $($enum::$method(auth) => $state_enum::$method(auth.new(inner))),+ + } + } + } + }; +} + +auth_method_enums! { + /// The combination of all authentication methods supported by this crate out of the box, + /// as an enum appropriate for static dispatch. + /// + /// If you want to add your own custom methods, you can generate a similar enum using the `auth_method_enums` macro. + pub enum StandardAuthentication / StandardAuthenticationStarted { + NoAuthentication(NoAuthenticationImpl), + PasswordAuthentication(PasswordAuthenticationImpl), + } +} + impl Socks5Socket { pub fn new(socket: T, config: Arc>) -> Self { Socks5Socket { @@ -334,28 +563,57 @@ impl Socks5Socket { pub async fn upgrade_to_socks5(mut self) -> Result> { trace!("upgrading to socks5..."); + // NOTE: this cannot be split into smaller methods without making self.inner an Option + // Handshake let proto = if !self.config.skip_auth { - let (proto, methods) = Socks5ServerProtocol::start(self.inner) - .get_methods() - .await?; - - let (proto, auth_method) = proto - .can_accept_method(methods, self.config.as_ref()) - .await?; - - if self.config.auth.is_some() { - let (proto, credentials) = proto - .authenticate(auth_method, self.config.as_ref()) + if let Some(auth_callback) = self.config.auth.as_ref() { + let auth = Socks5ServerProtocol::start(self.inner) + .negotiate_auth(if self.config.allow_no_auth { + &[ + StandardAuthentication::PasswordAuthentication(PasswordAuthentication), + StandardAuthentication::NoAuthentication(NoAuthentication), + ] + } else { + &[StandardAuthentication::PasswordAuthentication( + PasswordAuthentication, + )] + }) .await?; - self.credentials = Some(credentials); - proto + match auth { + StandardAuthenticationStarted::NoAuthentication(auth) => { + self.credentials = auth_callback.authenticate(None).await; + if self.credentials.is_some() { + auth.finish_auth() + } else { + return Err(SocksError::AuthenticationRejected(format!( + "Authentication, rejected." + ))); + } + } + StandardAuthenticationStarted::PasswordAuthentication(auth) => { + let (username, password, auth) = auth.read_username_password().await?; + self.credentials = + auth_callback.authenticate(Some((username, password))).await; + if self.credentials.is_some() { + auth.accept().await?.finish_auth() + } else { + auth.reject().await?; + return Err(SocksError::AuthenticationRejected(format!( + "Authentication, rejected." + ))); + } + } + } } else { - Socks5ServerProtocol::new(proto.inner) + Socks5ServerProtocol::start(self.inner) + .negotiate_auth(&[NoAuthentication]) + .await? + .finish_auth() } } else { debug!("skipping auth"); - Socks5ServerProtocol::new(self.inner) + Socks5ServerProtocol::skip_auth_this_is_not_rfc_compliant(self.inner) }; let (proto, cmd, target_addr) = proto.read_command().await?; @@ -489,27 +747,18 @@ impl Socks5Socket { } impl Socks5ServerProtocol { - /// Read the authentication method provided by the client. - /// A client send a list of methods that he supports, he could send + /// Negotiate an authentication method from a list of supported ones and initialize it. /// - /// - 0: Non auth - /// - 2: Auth with username/password + /// Internally, this reads the list of authentication methods provided by the client, and + /// picks the first one for which there exists an implementation in `server_methods`. /// - /// Altogether, then the server choose to use of of these, - /// or deny the handshake (thus the connection). - /// - /// # Examples - /// ```text - /// {SOCKS Version, methods-length} - /// eg. (non-auth) {5, 2} - /// eg. (auth) {5, 3} - /// ``` - /// - async fn get_methods( + /// If none of the auth methods requested by the client are in `server_methods`, + /// returns a `SocksError::AuthMethodUnacceptable`. + pub async fn negotiate_auth>( mut self, - ) -> Result<(Socks5ServerProtocol, Vec)> { - trace!("Socks5Socket: get_methods()"); - // read the first 2 bytes which contains the SOCKS version and the methods len() + server_methods: &[M], + ) -> Result { + trace!("Socks5ServerProtocol: negotiate_auth()"); let [version, methods_len] = read_exact!(self.inner, [0u8; 2]).context("Can't read methods")?; debug!( @@ -529,162 +778,28 @@ impl Socks5ServerProtocol .context("Can't get methods.")?; debug!("methods supported sent by the client: {:?}", &methods); - // Return methods available - Ok((Socks5ServerProtocol::new(self.inner), methods)) - } -} - -impl Socks5ServerProtocol { - /// Decide to whether or not, accept the authentication method. - /// Don't forget that the methods list sent by the client, contains one or more methods. - /// - /// # Request - /// - /// Client send an array of 3 entries: [0, 1, 2] - /// ```text - /// {SOCKS Version, Authentication chosen} - /// eg. (non-auth) {5, 0} - /// eg. (GSSAPI) {5, 1} - /// eg. (auth) {5, 2} - /// ``` - /// - /// # Response - /// ```text - /// eg. (accept non-auth) {5, 0x00} - /// eg. (non-acceptable) {5, 0xff} - /// ``` - /// - async fn can_accept_method( - mut self, - client_methods: Vec, - config: &Config, - ) -> Result<(Socks5ServerProtocol, u8)> { - let method_supported; - - if let Some(_auth) = config.auth.as_ref() { - if client_methods.contains(&consts::SOCKS5_AUTH_METHOD_PASSWORD) { - // can auth with password - method_supported = consts::SOCKS5_AUTH_METHOD_PASSWORD; - } else { - // client hasn't provided a password - if config.allow_no_auth { - // but we allow no auth, for ip whitelisting - method_supported = consts::SOCKS5_AUTH_METHOD_NONE; - } else { - // we don't allow no auth, so we deny the entry - debug!("Don't support this auth method, reply with (0xff)"); + for client_method_id in methods.iter() { + for server_method in server_methods { + if server_method.method_id() == *client_method_id { + debug!("Reply with method {}", *client_method_id); self.inner - .write_all(&[ - consts::SOCKS5_VERSION, - consts::SOCKS5_AUTH_METHOD_NOT_ACCEPTABLE, - ]) + .write_all(&[consts::SOCKS5_VERSION, *client_method_id]) .await - .context("Can't reply with method not acceptable.")?; - - return Err(SocksError::AuthMethodUnacceptable(client_methods)); + .context("Can't reply with auth method")?; + return Ok(server_method.new(self.inner)); } } - } else { - method_supported = consts::SOCKS5_AUTH_METHOD_NONE; } - debug!( - "Reply with method {} ({})", - AuthenticationMethod::from_u8(method_supported).context("Method not supported")?, - method_supported - ); + debug!("No auth method supported by both client and server, reply with (0xff)"); self.inner - .write(&[consts::SOCKS5_VERSION, method_supported]) + .write_all(&[ + consts::SOCKS5_VERSION, + consts::SOCKS5_AUTH_METHOD_NOT_ACCEPTABLE, + ]) .await - .context("Can't reply with method auth-none")?; - Ok((Socks5ServerProtocol::new(self.inner), method_supported)) - } -} - -impl Socks5ServerProtocol { - async fn read_username_password(socket: &mut T) -> Result<(String, String)> { - trace!("Socks5Socket: authenticate()"); - let [version, user_len] = read_exact!(socket, [0u8; 2]).context("Can't read user len")?; - debug!( - "Auth: [version: {version}, user len: {len}]", - version = version, - len = user_len, - ); - - if user_len < 1 { - return Err(SocksError::AuthenticationFailed(format!( - "Username malformed ({} chars)", - user_len - ))); - } - - let username = - read_exact!(socket, vec![0u8; user_len as usize]).context("Can't get username.")?; - debug!("username bytes: {:?}", &username); - - let [pass_len] = read_exact!(socket, [0u8; 1]).context("Can't read pass len")?; - debug!("Auth: [pass len: {len}]", len = pass_len,); - - if pass_len < 1 { - return Err(SocksError::AuthenticationFailed(format!( - "Password malformed ({} chars)", - pass_len - ))); - } - - let password = - read_exact!(socket, vec![0u8; pass_len as usize]).context("Can't get password.")?; - debug!("password bytes: {:?}", &password); - - let username = String::from_utf8(username).context("Failed to convert username")?; - let password = String::from_utf8(password).context("Failed to convert password")?; - - Ok((username, password)) - } - - /// Only called if - /// - this server has `Authentication` trait implemented. - /// - and the client supports authentication via username/password - /// - or the client doesn't send authentication, but we let the trait decides if the `allow_no_auth()` set as `true` - async fn authenticate( - mut self, - auth_method: u8, - config: &Config, - ) -> Result<(Socks5ServerProtocol, A::Item)> { - let credentials = if auth_method == consts::SOCKS5_AUTH_METHOD_PASSWORD { - let credentials = Self::read_username_password(&mut self.inner).await?; - Some(credentials) - } else { - // the client hasn't provided any credentials, the function auth.authenticate() - // will then check None, according to other parameters provided by the trait - // such as IP, etc. - None - }; - - let auth = config.auth.as_ref().context("No auth module")?; - - if let Some(credentials) = auth.authenticate(credentials).await { - if auth_method == consts::SOCKS5_AUTH_METHOD_PASSWORD { - // only the password way expect to write a response at this moment - self.inner - .write_all(&[1, consts::SOCKS5_REPLY_SUCCEEDED]) - .await - .context("Can't reply auth success")?; - } - - info!("User logged successfully."); - - return Ok((Socks5ServerProtocol::new(self.inner), credentials)); - } else { - self.inner - .write_all(&[1, consts::SOCKS5_AUTH_METHOD_NOT_ACCEPTABLE]) - .await - .context("Can't reply with auth method not acceptable.")?; - - return Err(SocksError::AuthenticationRejected(format!( - "Authentication, rejected." - ))); - } + .context("Can't reply with method not acceptable.")?; + Err(SocksError::AuthMethodUnacceptable(methods)) } } From af025ab7061f296f885a7faf752ee3d4b9cf02a1 Mon Sep 17 00:00:00 2001 From: Val Packett Date: Mon, 27 Jan 2025 01:41:39 -0300 Subject: [PATCH 08/23] refactor: extract the meat from upgrade_to_socks5 Now with run_tcp_proxy and run_udp_proxy it's very clear that upgrade_to_socks5 only translates from old API to new. --- src/server.rs | 262 +++++++++++++++++++++++++++----------------------- 1 file changed, 142 insertions(+), 120 deletions(-) diff --git a/src/server.rs b/src/server.rs index f3d3d10..11ff20b 100644 --- a/src/server.rs +++ b/src/server.rs @@ -67,6 +67,35 @@ pub trait Authentication: Send + Sync { async fn authenticate(&self, credentials: Option<(String, String)>) -> Option; } +async fn authenticate_callback( + auth_callback: &A, + auth: StandardAuthenticationStarted, +) -> Result<(Socks5ServerProtocol, A::Item)> { + match auth { + StandardAuthenticationStarted::NoAuthentication(auth) => { + if let Some(credentials) = auth_callback.authenticate(None).await { + Ok((auth.finish_auth(), credentials)) + } else { + Err(SocksError::AuthenticationRejected(format!( + "Authentication, rejected." + ))) + } + } + StandardAuthenticationStarted::PasswordAuthentication(auth) => { + let (username, password, auth) = auth.read_username_password().await?; + if let Some(credentials) = auth_callback.authenticate(Some((username, password))).await + { + Ok((auth.accept().await?.finish_auth(), credentials)) + } else { + auth.reject().await?; + Err(SocksError::AuthenticationRejected(format!( + "Authentication, rejected." + ))) + } + } + } +} + /// Basic user/pass auth method provided. pub struct SimpleUserPassword { pub username: String, @@ -530,6 +559,21 @@ auth_method_enums! { } } +impl StandardAuthentication { + pub fn allow_no_auth(allow: bool) -> &'static [StandardAuthentication] { + if allow { + &[ + StandardAuthentication::PasswordAuthentication(PasswordAuthentication), + StandardAuthentication::NoAuthentication(NoAuthentication), + ] + } else { + &[StandardAuthentication::PasswordAuthentication( + PasswordAuthentication, + )] + } + } +} + impl Socks5Socket { pub fn new(socket: T, config: Arc>) -> Self { Socks5Socket { @@ -563,140 +607,66 @@ impl Socks5Socket { pub async fn upgrade_to_socks5(mut self) -> Result> { trace!("upgrading to socks5..."); - // NOTE: this cannot be split into smaller methods without making self.inner an Option + // NOTE: this cannot be split in two without making self.inner an Option // Handshake - let proto = if !self.config.skip_auth { - if let Some(auth_callback) = self.config.auth.as_ref() { + let proto = match self.config.auth.as_ref() { + _ if self.config.skip_auth => { + debug!("skipping auth"); + Socks5ServerProtocol::skip_auth_this_is_not_rfc_compliant(self.inner) + } + None => Socks5ServerProtocol::start(self.inner) + .negotiate_auth(&[NoAuthentication]) + .await? + .finish_auth(), + Some(auth_callback) => { + let methods = StandardAuthentication::allow_no_auth(self.config.allow_no_auth); let auth = Socks5ServerProtocol::start(self.inner) - .negotiate_auth(if self.config.allow_no_auth { - &[ - StandardAuthentication::PasswordAuthentication(PasswordAuthentication), - StandardAuthentication::NoAuthentication(NoAuthentication), - ] - } else { - &[StandardAuthentication::PasswordAuthentication( - PasswordAuthentication, - )] - }) + .negotiate_auth(methods) .await?; - match auth { - StandardAuthenticationStarted::NoAuthentication(auth) => { - self.credentials = auth_callback.authenticate(None).await; - if self.credentials.is_some() { - auth.finish_auth() - } else { - return Err(SocksError::AuthenticationRejected(format!( - "Authentication, rejected." - ))); - } - } - StandardAuthenticationStarted::PasswordAuthentication(auth) => { - let (username, password, auth) = auth.read_username_password().await?; - self.credentials = - auth_callback.authenticate(Some((username, password))).await; - if self.credentials.is_some() { - auth.accept().await?.finish_auth() - } else { - auth.reject().await?; - return Err(SocksError::AuthenticationRejected(format!( - "Authentication, rejected." - ))); - } - } - } - } else { - Socks5ServerProtocol::start(self.inner) - .negotiate_auth(&[NoAuthentication]) - .await? - .finish_auth() + let (proto, creds) = authenticate_callback(auth_callback.as_ref(), auth).await?; + self.credentials = Some(creds); + proto } - } else { - debug!("skipping auth"); - Socks5ServerProtocol::skip_auth_this_is_not_rfc_compliant(self.inner) }; - let (proto, cmd, target_addr) = proto.read_command().await?; - self.cmd = Some(cmd); - self.target_addr = Some(target_addr); + let (proto, cmd, mut target_addr) = proto.read_command().await?; if self.config.dns_resolve { - if let Some(addr) = self.target_addr { - self.target_addr = Some(addr.resolve_dns().await?); - } + target_addr = target_addr.resolve_dns().await?; } else { debug!("Domain won't be resolved because `dns_resolve`'s config has been turned off.") } - if self.config.execute_command { - /* we've just set it to Some above. - * also, not gonna be used externally since we execute it here */ - let cmd = self.cmd.take().unwrap(); - - match cmd { - Socks5Command::TCPBind => { - proto.reply_error(&ReplyError::CommandNotSupported).await?; - return Err(ReplyError::CommandNotSupported.into()); - } - Socks5Command::TCPConnect => { - let addr = self - .target_addr - .as_ref() - .context("target_addr empty")? - .to_socket_addrs()? - .next() - .context("unreachable")?; - - // TCP connect with timeout, to avoid memory leak for connection that takes forever - let outbound = - tcp_connect_with_timeout(addr, self.config.request_timeout).await?; - - // Disable Nagle's algorithm if config specifies to do so. - outbound.set_nodelay(self.config.nodelay)?; - - debug!("Connected to remote destination"); - - let mut inner = proto - .reply_success(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0)) - .await?; - - transfer(&mut inner, outbound).await?; - self.inner = inner; - } - Socks5Command::UDPAssociate => { - if self.config.allow_udp { - // The DST.ADDR and DST.PORT fields contain the address and port that - // the client expects to use to send UDP datagrams on for the - // association. The server MAY use this information to limit access - // to the association. - // @see Page 6, https://datatracker.ietf.org/doc/html/rfc1928. - // - // We do NOT limit the access from the client currently in this implementation. - let _not_used = self.target_addr.as_ref(); - - // Listen with UDP6 socket, so the client can connect to it with either - // IPv4 or IPv6. - let peer_sock = UdpSocket::bind("[::]:0").await?; - - // Respect the pre-populated reply IP address. - self.inner = proto - .reply_success(SocketAddr::new( - self.reply_ip.context("invalid reply ip")?, - peer_sock.local_addr()?.port(), - )) - .await?; - - transfer_udp(peer_sock).await?; - } else { - proto.reply_error(&ReplyError::CommandNotSupported).await?; - return Err(ReplyError::CommandNotSupported.into()); - } - } - }; - } else { - self.inner = proto.inner; - } + match cmd { + cmd if !self.config.execute_command => { + self.cmd = Some(cmd); + self.inner = proto.inner; + } + Socks5Command::TCPConnect => { + self.inner = run_tcp_proxy( + proto, + &target_addr, + self.config.request_timeout, + self.config.nodelay, + ) + .await?; + } + Socks5Command::UDPAssociate if self.config.allow_udp => { + self.inner = run_udp_proxy( + proto, + &target_addr, + self.reply_ip.context("invalid reply ip")?, + ) + .await?; + } + _ => { + proto.reply_error(&ReplyError::CommandNotSupported).await?; + return Err(ReplyError::CommandNotSupported.into()); + } + }; + self.target_addr = Some(target_addr); /* legacy API leaves it exported */ Ok(self) } @@ -888,6 +858,58 @@ impl Socks5ServerProtocol( + proto: Socks5ServerProtocol, + addr: &TargetAddr, + request_timeout_s: u64, + nodelay: bool, +) -> Result { + let addr = addr.to_socket_addrs()?.next().context("unreachable")?; + + // TCP connect with timeout, to avoid memory leak for connection that takes forever + let outbound = tcp_connect_with_timeout(addr, request_timeout_s).await?; + + // Disable Nagle's algorithm if config specifies to do so. + outbound.set_nodelay(nodelay)?; + + debug!("Connected to remote destination"); + + let mut inner = proto + .reply_success(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0)) + .await?; + + transfer(&mut inner, outbound).await?; + Ok(inner) +} + +/// Handle the associate command by running a UDP proxy until the connection is done. +pub async fn run_udp_proxy( + proto: Socks5ServerProtocol, + _addr: &TargetAddr, + reply_ip: IpAddr, +) -> Result { + // The DST.ADDR and DST.PORT fields contain the address and port that + // the client expects to use to send UDP datagrams on for the + // association. The server MAY use this information to limit access + // to the association. + // @see Page 6, https://datatracker.ietf.org/doc/html/rfc1928. + // + // We do NOT limit the access from the client currently in this implementation. + + // Listen with UDP6 socket, so the client can connect to it with either + // IPv4 or IPv6. + let peer_sock = UdpSocket::bind("[::]:0").await?; + + // Respect the pre-populated reply IP address. + let inner = proto + .reply_success(SocketAddr::new(reply_ip, peer_sock.local_addr()?.port())) + .await?; + + transfer_udp(peer_sock).await?; + Ok(inner) +} + /// Run a bidirectional proxy between two streams. /// Using 2 different generators, because they could be different structs with same traits. pub async fn transfer(mut inbound: I, mut outbound: O) -> Result<()> From 1eb8b351d8c4e7b5752ce7c106e73f0087993bb3 Mon Sep 17 00:00:00 2001 From: Val Packett Date: Mon, 27 Jan 2025 03:54:50 -0300 Subject: [PATCH 09/23] refactor: update the server example to use the new API Now we can finally see it in all its glory! Simple, yet explicit and therefore extensible. Helpers have been added for auth to make it look nicer. --- examples/server.rs | 98 ++++++++++++++++++++++++---------------------- src/server.rs | 30 ++++++++++++++ 2 files changed, 81 insertions(+), 47 deletions(-) diff --git a/examples/server.rs b/examples/server.rs index 00f4524..16b2b5a 100644 --- a/examples/server.rs +++ b/examples/server.rs @@ -2,15 +2,15 @@ #[macro_use] extern crate log; +use anyhow::Context; use fast_socks5::{ - server::{Config, SimpleUserPassword, Socks5Server, Socks5Socket}, - Result, SocksError, + server::{run_tcp_proxy, run_udp_proxy, Socks5ServerProtocol}, + ReplyError, Result, Socks5Command, SocksError, }; use std::future::Future; use structopt::StructOpt; -use tokio::io::{AsyncRead, AsyncWrite}; +use tokio::net::TcpListener; use tokio::task; -use tokio_stream::StreamExt; /// # How to use it: /// @@ -52,7 +52,7 @@ struct Opt { } /// Choose the authentication type -#[derive(StructOpt, Debug)] +#[derive(StructOpt, Debug, PartialEq)] enum AuthMode { NoAuth, Password { @@ -79,72 +79,76 @@ async fn main() -> Result<()> { } async fn spawn_socks_server() -> Result<()> { - let opt: Opt = Opt::from_args(); + let opt: &'static Opt = Box::leak(Box::new(Opt::from_args())); if opt.allow_udp && opt.public_addr.is_none() { return Err(SocksError::ArgumentInputError( "Can't allow UDP if public-addr is not set", )); } + if opt.skip_auth && opt.auth != AuthMode::NoAuth { + return Err(SocksError::ArgumentInputError( + "Can't use skip-auth flag and authentication altogether.", + )); + } - let mut config = Config::default(); - config.set_request_timeout(opt.request_timeout); - config.set_skip_auth(opt.skip_auth); - config.set_udp_support(opt.allow_udp); - - let config = match opt.auth { - AuthMode::NoAuth => { - warn!("No authentication has been set!"); - config - } - AuthMode::Password { username, password } => { - if opt.skip_auth { - return Err(SocksError::ArgumentInputError( - "Can't use skip-auth flag and authentication altogether.", - )); - } - - info!("Simple auth system has been set."); - config.with_authentication(SimpleUserPassword { username, password }) - } - }; - - let listener = ::bind(&opt.listen_addr).await?; - let listener = listener.with_config(config); - - let mut incoming = listener.incoming(); + let listener = TcpListener::bind(&opt.listen_addr).await?; info!("Listen for socks connections @ {}", &opt.listen_addr); // Standard TCP loop - while let Some(socket_res) = incoming.next().await { - match socket_res { - Ok(mut socket) => { - if let Some(addr) = opt.public_addr { - socket.set_reply_ip(addr); - } - spawn_and_log_error(socket.upgrade_to_socks5()); + loop { + match listener.accept().await { + Ok((socket, _client_addr)) => { + spawn_and_log_error(serve_socks5(opt, socket)); } Err(err) => { error!("accept error = {:?}", err); } } } +} + +async fn serve_socks5(opt: &Opt, socket: tokio::net::TcpStream) -> Result<(), SocksError> { + let (proto, cmd, mut target_addr) = match &opt.auth { + AuthMode::NoAuth if opt.skip_auth => { + Socks5ServerProtocol::skip_auth_this_is_not_rfc_compliant(socket) + } + AuthMode::NoAuth => Socks5ServerProtocol::accept_no_auth(socket).await?, + AuthMode::Password { username, password } => { + Socks5ServerProtocol::accept_password_auth(socket, |user, pass| { + user == *username && pass == *password + }) + .await? + } + } + .read_command() + .await?; + target_addr = target_addr.resolve_dns().await?; + + match cmd { + Socks5Command::TCPConnect => { + run_tcp_proxy(proto, &target_addr, opt.request_timeout, false).await?; + } + Socks5Command::UDPAssociate if opt.allow_udp => { + let reply_ip = opt.public_addr.context("invalid reply ip")?; + run_udp_proxy(proto, &target_addr, reply_ip).await?; + } + _ => { + proto.reply_error(&ReplyError::CommandNotSupported).await?; + return Err(ReplyError::CommandNotSupported.into()); + } + }; Ok(()) } -fn spawn_and_log_error(fut: F) -> task::JoinHandle<()> +fn spawn_and_log_error(fut: F) -> task::JoinHandle<()> where - F: Future>> + Send + 'static, - T: AsyncRead + AsyncWrite + Unpin, + F: Future> + Send + 'static, { task::spawn(async move { match fut.await { - Ok(mut socket) => { - if let Some(user) = socket.take_credentials() { - info!("user logged in with `{}`", user.username); - } - } + Ok(()) => {} Err(err) => error!("{:#}", &err), } }) diff --git a/src/server.rs b/src/server.rs index 11ff20b..06c1b1d 100644 --- a/src/server.rs +++ b/src/server.rs @@ -336,6 +336,36 @@ impl Socks5ServerProtocol { pub fn skip_auth_this_is_not_rfc_compliant(inner: T) -> Self { Self::new(inner) } + + pub async fn accept_no_auth(inner: T) -> Result + where + T: AsyncWrite + AsyncRead + Unpin, + { + Ok(Socks5ServerProtocol::start(inner) + .negotiate_auth(&[NoAuthentication]) + .await? + .finish_auth()) + } + + pub async fn accept_password_auth(inner: T, mut check: F) -> Result + where + T: AsyncWrite + AsyncRead + Unpin, + F: FnMut(String, String) -> bool, + { + let (user, pass, auth) = Socks5ServerProtocol::start(inner) + .negotiate_auth(&[PasswordAuthentication]) + .await? + .read_username_password() + .await?; + if check(user, pass) { + Ok(auth.accept().await?.finish_auth()) + } else { + auth.reject().await?; + Err(SocksError::AuthenticationRejected( + "Wrong username/password".to_owned(), + )) + } + } } /// A trait for the final successful state of an authentication method's implementation. From 007b3d9caab91a6e724b45cf6093316ec85a15be Mon Sep 17 00:00:00 2001 From: Val Packett Date: Mon, 27 Jan 2025 04:11:14 -0300 Subject: [PATCH 10/23] refactor: mark old server API as deprecated --- src/server.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/server.rs b/src/server.rs index 06c1b1d..6ca1c55 100644 --- a/src/server.rs +++ b/src/server.rs @@ -217,11 +217,16 @@ impl Config { /// Wrapper of TcpListener /// Useful if you don't use any existing TcpListener's streams. +#[deprecated( + since = "0.11.0", + note = "Use the new explicit API instead, see examples/server.rs" +)] pub struct Socks5Server { listener: TcpListener, config: Arc>, } +#[allow(deprecated)] impl Socks5Server { pub async fn bind(addr: S) -> io::Result { let listener = TcpListener::bind(&addr).await?; @@ -231,6 +236,7 @@ impl Socks5Server { } } +#[allow(deprecated)] impl Socks5Server { /// Set a custom config pub fn with_config(self, config: Config) -> Socks5Server { @@ -249,6 +255,7 @@ impl Socks5Server { /// `Incoming` implements [`futures_core::stream::Stream`]. /// /// [`futures_core::stream::Stream`]: https://docs.rs/futures/0.3.30/futures/stream/trait.Stream.html +#[allow(deprecated)] pub struct Incoming<'a, A: Authentication>( &'a Socks5Server, Option> + Send + Sync + 'a>>>, @@ -256,6 +263,7 @@ pub struct Incoming<'a, A: Authentication>( /// Iterator for each incoming stream connection /// this wrapper will convert async_std TcpStream into Socks5Socket. +#[allow(deprecated)] impl<'a, A: Authentication> Stream for Incoming<'a, A> { type Item = Result>; @@ -289,6 +297,10 @@ impl<'a, A: Authentication> Stream for Incoming<'a, A> { } /// Wrap TcpStream and contains Socks5 protocol implementation. +#[deprecated( + since = "0.11.0", + note = "Use the new explicit API instead, see examples/server.rs" +)] pub struct Socks5Socket { inner: T, config: Arc>, @@ -604,6 +616,7 @@ impl StandardAuthentication { } } +#[allow(deprecated)] impl Socks5Socket { pub fn new(socket: T, config: Arc>) -> Self { Socks5Socket { @@ -1014,9 +1027,11 @@ pub async fn transfer_udp(inbound: UdpSocket) -> Result<()> { // Fixes the issue "cannot borrow data in dereference of `Pin<&mut >` as mutable" // // cf. https://users.rust-lang.org/t/take-in-impl-future-cannot-borrow-data-in-a-dereference-of-pin/52042 +#[allow(deprecated)] impl Unpin for Socks5Socket where T: AsyncRead + AsyncWrite + Unpin {} /// Allow us to read directly from the struct +#[allow(deprecated)] impl AsyncRead for Socks5Socket where T: AsyncRead + AsyncWrite + Unpin, @@ -1031,6 +1046,7 @@ where } /// Allow us to write directly into the struct +#[allow(deprecated)] impl AsyncWrite for Socks5Socket where T: AsyncRead + AsyncWrite + Unpin, @@ -1086,6 +1102,7 @@ fn new_reply(error: &ReplyError, sock_addr: SocketAddr) -> Vec { } #[cfg(test)] +#[allow(deprecated)] mod test { use crate::server::Socks5Server; use tokio_test::block_on; From d509ab237cb1a36f21c4844601ad4f25d8062869 Mon Sep 17 00:00:00 2001 From: Val Packett Date: Mon, 27 Jan 2025 04:11:41 -0300 Subject: [PATCH 11/23] refactor: remove simple_tcp_server example Not relevant anymore; with the new API, we don't do the Stream thing that wasn't really adding much at all. --- Cargo.toml | 3 - examples/simple_tcp_server.rs | 122 ---------------------------------- 2 files changed, 125 deletions(-) delete mode 100644 examples/simple_tcp_server.rs diff --git a/Cargo.toml b/Cargo.toml index 1224264..c50ba26 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,3 @@ name = "server" [[example]] name = "client" - -[[example]] -name = "simple_tcp_server" diff --git a/examples/simple_tcp_server.rs b/examples/simple_tcp_server.rs deleted file mode 100644 index 68f1d48..0000000 --- a/examples/simple_tcp_server.rs +++ /dev/null @@ -1,122 +0,0 @@ -#[forbid(unsafe_code)] -#[macro_use] -extern crate log; - -use fast_socks5::{ - server::{Authentication, Config, SimpleUserPassword, Socks5Socket}, - Result, -}; -use std::future::Future; -use std::sync::Arc; -use structopt::StructOpt; -use tokio::task; -use tokio::{ - io::{AsyncRead, AsyncWrite}, - net::TcpListener, -}; - -/// # How to use it: -/// -/// Listen on a local address, authentication-free: -/// `$ RUST_LOG=debug cargo run --example simple_tcp_server -- --listen-addr 127.0.0.1:1337 no-auth` -/// -/// Listen on a local address, with basic username/password requirement: -/// `$ RUST_LOG=debug cargo run --example simple_tcp_server -- --listen-addr 127.0.0.1:1337 password --username admin --password password` -/// -#[derive(Debug, StructOpt)] -#[structopt( - name = "socks5-server", - about = "A simple implementation of a socks5-server." -)] -struct Opt { - /// Bind on address address. eg. `127.0.0.1:1080` - #[structopt(short, long)] - pub listen_addr: String, - - /// Request timeout - #[structopt(short = "t", long, default_value = "10")] - pub request_timeout: u64, - - /// Choose authentication type - #[structopt(subcommand, name = "auth")] // Note that we mark a field as a subcommand - pub auth: AuthMode, -} - -/// Choose the authentication type -#[derive(StructOpt, Debug)] -enum AuthMode { - NoAuth, - Password { - #[structopt(short, long)] - username: String, - - #[structopt(short, long)] - password: String, - }, -} - -/// Useful read 1. https://blog.yoshuawuyts.com/rust-streams/ -/// Useful read 2. https://blog.yoshuawuyts.com/futures-concurrency/ -/// Useful read 3. https://blog.yoshuawuyts.com/streams-concurrency/ -/// error-libs benchmark: https://blog.yoshuawuyts.com/error-handling-survey/ -/// -/// TODO: Command to use the socks server with a simple user/password -/// TODO: Write functional tests: https://github.com/ark0f/async-socks5/blob/master/src/lib.rs#L762 -/// TODO: Write functional tests with cURL? -/// TODO: Move this to as a standalone library -#[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 mut config = Config::default(); - config.set_request_timeout(opt.request_timeout); - - let config = match opt.auth { - AuthMode::NoAuth => { - warn!("No authentication has been set!"); - config - } - AuthMode::Password { username, password } => { - info!("Simple auth system has been set."); - config.with_authentication(SimpleUserPassword { username, password }) - } - }; - - let config = Arc::new(config); - - let listener = TcpListener::bind(&opt.listen_addr).await?; - // listener.set_config(config); - - info!("Listen for socks connections @ {}", &opt.listen_addr); - - // Standard TCP loop - loop { - match listener.accept().await { - Ok((socket, _addr)) => { - info!("Connection from {}", socket.peer_addr()?); - let socket = Socks5Socket::new(socket, config.clone()); - - spawn_and_log_error(socket.upgrade_to_socks5()); - } - Err(err) => error!("accept error = {:?}", err), - } - } -} - -fn spawn_and_log_error(fut: F) -> task::JoinHandle<()> -where - F: Future>> + Send + 'static, - T: AsyncRead + AsyncWrite + Unpin, - A: Authentication, -{ - task::spawn(async move { - if let Err(e) = fut.await { - error!("{:#}", &e); - } - }) -} From deb0d6180610c6200d7317a4dd1b5c49f12ea583 Mon Sep 17 00:00:00 2001 From: Val Packett Date: Mon, 27 Jan 2025 04:19:08 -0300 Subject: [PATCH 12/23] refactor: rewrite tests to use the new explicit API --- src/lib.rs | 49 +++++++++++++++++++++++-------------------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index c9f0969..0fda2e9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -327,14 +327,10 @@ mod test { sync::oneshot::Sender, }; - use crate::{ - client, - server::{self, SimpleUserPassword}, - }; + use crate::{client, server, ReplyError, Socks5Command}; use std::{ net::{SocketAddr, ToSocketAddrs}, num::ParseIntError, - sync::Arc, }; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::sync::oneshot; @@ -344,27 +340,28 @@ mod test { let _ = env_logger::builder().is_test(true).try_init(); } - async fn setup_socks_server( - proxy_addr: &str, - auth: Option, - tx: Sender, - ) -> Result<()> { - let mut config = server::Config::default(); - config.set_udp_support(true); - let config = match auth { - None => config, - Some(up) => config.with_authentication(up), - }; - - let config = Arc::new(config); + async fn setup_socks_server(proxy_addr: &str, tx: Sender) -> Result<()> { + let reply_ip = proxy_addr.parse::().unwrap().ip(); + let listener = TcpListener::bind(proxy_addr).await?; tx.send(listener.local_addr()?).unwrap(); - loop { - let (stream, _) = listener.accept().await?; - let mut socks5_socket = server::Socks5Socket::new(stream, config.clone()); - socks5_socket.set_reply_ip(proxy_addr.parse::().unwrap().ip()); - socks5_socket.upgrade_to_socks5().await?; + loop { + let (stream, _) = listener.accept().await?; // NOTE: not spawning for test + let proto = server::Socks5ServerProtocol::accept_no_auth(stream).await?; + let (proto, cmd, mut target_addr) = proto.read_command().await?; + target_addr = target_addr.resolve_dns().await?; + match cmd { + Socks5Command::TCPConnect => { + server::run_tcp_proxy(proto, &target_addr, 10, false).await?; + } + Socks5Command::UDPAssociate => { + server::run_udp_proxy(proto, &target_addr, reply_ip).await?; + } + Socks5Command::TCPBind => { + proto.reply_error(&ReplyError::CommandNotSupported).await?; + } + } } } @@ -385,7 +382,7 @@ mod test { init(); block_on(async { let (tx, rx) = oneshot::channel(); - tokio::spawn(setup_socks_server("[::1]:0", None, tx)); + tokio::spawn(setup_socks_server("[::1]:0", tx)); let socket = client::Socks5Stream::connect( rx.await.unwrap(), @@ -406,7 +403,7 @@ mod test { const MOCK_ADDRESS: &str = "[::1]:40235"; let (tx, rx) = oneshot::channel(); - tokio::spawn(setup_socks_server("[::1]:0", None, tx)); + tokio::spawn(setup_socks_server("[::1]:0", tx)); let backing_socket = TcpStream::connect(rx.await.unwrap()).await.unwrap(); // Creates a UDP tunnel which can be used to forward UDP packets, "[::]:0" indicates the @@ -449,7 +446,7 @@ mod test { const DNS_SERVER: &str = "1.1.1.1:53"; let (tx, rx) = oneshot::channel(); - tokio::spawn(setup_socks_server("[::1]:0", None, tx)); + tokio::spawn(setup_socks_server("[::1]:0", tx)); let backing_socket = TcpStream::connect(rx.await.unwrap()).await.unwrap(); // Creates a UDP tunnel which can be used to forward UDP packets, "[::]:0" indicates the From 2b5884be8fa3aedba7ebe113269fcada6fa07d2c Mon Sep 17 00:00:00 2001 From: Val Packett Date: Mon, 27 Jan 2025 04:40:39 -0300 Subject: [PATCH 13/23] refactor: update README description to reflect new API's nature --- README.md | 22 ++++++++++------------ src/lib.rs | 24 +++++++++++------------- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index f59dc60..90864e0 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/lib.rs b/src/lib.rs index 0fda2e9..4bed896 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,25 +8,23 @@ //! - 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 //! From 3fc0f61a1c5bdc99964dc7f49c3775b34e08afb1 Mon Sep 17 00:00:00 2001 From: Val Packett Date: Mon, 27 Jan 2025 05:58:35 -0300 Subject: [PATCH 14/23] refactor: add custom_auth_server example --- Cargo.toml | 3 + examples/custom_auth_server.rs | 171 +++++++++++++++++++++++++++++++++ src/server.rs | 2 + 3 files changed, 176 insertions(+) create mode 100644 examples/custom_auth_server.rs diff --git a/Cargo.toml b/Cargo.toml index c50ba26..e87560e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,3 +44,6 @@ name = "server" [[example]] name = "client" + +[[example]] +name = "custom_auth_server" diff --git a/examples/custom_auth_server.rs b/examples/custom_auth_server.rs new file mode 100644 index 0000000..e888d06 --- /dev/null +++ b/examples/custom_auth_server.rs @@ -0,0 +1,171 @@ +#[forbid(unsafe_code)] +#[macro_use] +extern crate log; + +use fast_socks5::{ + auth_method_enums, + server::{ + run_tcp_proxy, AuthMethod, AuthMethodSuccessState, 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` +#[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); +pub struct BackdoorAuthenticationSuccess(T); + +impl BackdoorAuthenticationStarted { + pub async fn verify_timing(self) -> Result> { + 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 AuthMethodSuccessState for BackdoorAuthenticationSuccess { + fn into_inner(self) -> T { + self.0 + } +} + +/// A silly example of a custom authentication method. +#[derive(Debug, Clone, Copy)] +pub struct BackdoorAuthentication; + +impl AuthMethod for BackdoorAuthentication { + type StartingState = BackdoorAuthenticationStarted; + + 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 { + PasswordAuthentication(PasswordAuthenticationStarted), + BackdoorAuthentication(BackdoorAuthenticationStarted), + } +} + +async fn serve_socks5(socket: tokio::net::TcpStream) -> Result<(), SocksError> { + let proto = match Socks5ServerProtocol::start(socket) + .negotiate_auth(&[ + 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" { + 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, mut target_addr) = proto.read_command().await?; + + target_addr = target_addr.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(fut: F) -> task::JoinHandle<()> +where + F: Future> + Send + 'static, +{ + task::spawn(async move { + match fut.await { + Ok(()) => {} + Err(err) => error!("{:#}", &err), + } + }) +} diff --git a/src/server.rs b/src/server.rs index 6ca1c55..80f232b 100644 --- a/src/server.rs +++ b/src/server.rs @@ -449,6 +449,8 @@ pub struct PasswordAuthenticationImpl { _state: PhantomData, } +pub type PasswordAuthenticationStarted = PasswordAuthenticationImpl; + impl PasswordAuthenticationImpl { fn new(inner: T) -> Self { PasswordAuthenticationImpl { From 092ffa61d0a978998ef31b0744aff3d81f0216c9 Mon Sep 17 00:00:00 2001 From: Val Packett Date: Fri, 31 Jan 2025 04:15:54 -0300 Subject: [PATCH 15/23] refactor: accept_password_auth: return the value It might be necessary to return some password check result for further usage. Allow doing it in a convenient way. --- examples/server.rs | 1 + src/server.rs | 32 ++++++++++++++++++++++++++++---- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/examples/server.rs b/examples/server.rs index 16b2b5a..73eefba 100644 --- a/examples/server.rs +++ b/examples/server.rs @@ -119,6 +119,7 @@ async fn serve_socks5(opt: &Opt, socket: tokio::net::TcpStream) -> Result<(), So user == *username && pass == *password }) .await? + .0 } } .read_command() diff --git a/src/server.rs b/src/server.rs index 80f232b..10c8f19 100644 --- a/src/server.rs +++ b/src/server.rs @@ -340,6 +340,28 @@ impl Socks5ServerProtocol { } } +pub trait CheckResult { + fn is_good(&self) -> bool; +} + +impl CheckResult for bool { + fn is_good(&self) -> bool { + *self + } +} + +impl CheckResult for Option { + fn is_good(&self) -> bool { + self.is_some() + } +} + +impl CheckResult for Result { + fn is_good(&self) -> bool { + self.is_ok() + } +} + impl Socks5ServerProtocol { pub fn finish_auth>(auth: A) -> Self { Self::new(auth.into_inner()) @@ -359,18 +381,20 @@ impl Socks5ServerProtocol { .finish_auth()) } - pub async fn accept_password_auth(inner: T, mut check: F) -> Result + pub async fn accept_password_auth(inner: T, mut check: F) -> Result<(Self, R)> where T: AsyncWrite + AsyncRead + Unpin, - F: FnMut(String, String) -> bool, + F: FnMut(String, String) -> R, + R: CheckResult, { let (user, pass, auth) = Socks5ServerProtocol::start(inner) .negotiate_auth(&[PasswordAuthentication]) .await? .read_username_password() .await?; - if check(user, pass) { - Ok(auth.accept().await?.finish_auth()) + let check_result = check(user, pass); + if check_result.is_good() { + Ok((auth.accept().await?.finish_auth(), check_result)) } else { auth.reject().await?; Err(SocksError::AuthenticationRejected( From 636a68b1460fd1ed9df3f56a44f5245ad31d562d Mon Sep 17 00:00:00 2001 From: Val Packett Date: Fri, 31 Jan 2025 04:28:53 -0300 Subject: [PATCH 16/23] refactor: add docstrings to new public functions --- src/server.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/server.rs b/src/server.rs index 10c8f19..ff622c1 100644 --- a/src/server.rs +++ b/src/server.rs @@ -335,6 +335,7 @@ impl Socks5ServerProtocol { } impl Socks5ServerProtocol { + /// Start handling the SOCKS5 protocol flow, wrapping a client socket. pub fn start(inner: T) -> Self { Self::new(inner) } @@ -363,14 +364,21 @@ impl CheckResult for Result { } impl Socks5ServerProtocol { + /// Finish handling the authentication method-specific part of the protocol, + /// returning back to the overall SOCKS5 flow. pub fn finish_auth>(auth: A) -> Self { Self::new(auth.into_inner()) } + /// Wrap a socket in a SOCKS5 flow handler that's already marked as authenticated. + /// + /// This is not actually part of the official SOCKS5 protocol, but allows you to + /// only use the post-authentication subset of it. pub fn skip_auth_this_is_not_rfc_compliant(inner: T) -> Self { Self::new(inner) } + /// Handle the SOCKS5 auth negotiation supporting only the `NoAuthentication` method. pub async fn accept_no_auth(inner: T) -> Result where T: AsyncWrite + AsyncRead + Unpin, @@ -381,6 +389,10 @@ impl Socks5ServerProtocol { .finish_auth()) } + /// Handle the SOCKS5 auth negotiation supporting only the `PasswordAuthentication` method, + /// and verify the provided username and password using the provided closure. + /// + /// The closure can mutate state variables and/or return a result as `Option`/`Result`. pub async fn accept_password_auth(inner: T, mut check: F) -> Result<(Self, R)> where T: AsyncWrite + AsyncRead + Unpin, @@ -485,6 +497,7 @@ impl PasswordAuthenticationImpl { } impl PasswordAuthenticationImpl { + /// Handle the username and password sent by the client. pub async fn read_username_password( self, ) -> Result<( @@ -534,6 +547,7 @@ impl PasswordAuthenticationImpl PasswordAuthenticationImpl { + /// Notify the client with a "SUCCEEDED" reply and proceed to finish the authentication. pub async fn accept( mut self, ) -> Result> { @@ -546,6 +560,7 @@ impl PasswordAuthenticationImpl Result<()> { self.inner .write_all(&[1, consts::SOCKS5_AUTH_METHOD_NOT_ACCEPTABLE]) @@ -628,6 +643,7 @@ auth_method_enums! { } impl StandardAuthentication { + /// Return a slice containing either both supported methods or only `PasswordAuthentication`. pub fn allow_no_auth(allow: bool) -> &'static [StandardAuthentication] { if allow { &[ From c664e88dd0cc1606a6fc30811e951169b57ec210 Mon Sep 17 00:00:00 2001 From: Val Packett Date: Sat, 1 Feb 2025 02:38:40 -0300 Subject: [PATCH 17/23] refactor: add router example Just a fun demo showing how a router / frontend proxy could be built. --- Cargo.toml | 3 + examples/router.rs | 210 ++++++++++++++++++++++++++++++++++++++++ src/util/target_addr.rs | 7 ++ 3 files changed, 220 insertions(+) create mode 100644 examples/router.rs diff --git a/Cargo.toml b/Cargo.toml index e87560e..2092af9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,3 +47,6 @@ name = "client" [[example]] name = "custom_auth_server" + +[[example]] +name = "router" diff --git a/examples/router.rs b/examples/router.rs new file mode 100644 index 0000000..8bbd946 --- /dev/null +++ b/examples/router.rs @@ -0,0 +1,210 @@ +#[forbid(unsafe_code)] +#[macro_use] +extern crate log; + +use fast_socks5::{ + client, + server::{transfer, Socks5ServerProtocol}, + util::target_addr::TargetAddr, + ReplyError, Result, Socks5Command, SocksError, +}; +use std::{ + collections::HashSet, + future::Future, + net::{IpAddr, Ipv4Addr, SocketAddr}, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, +}; +use structopt::StructOpt; +use tokio::{ + io::{AsyncBufReadExt, AsyncWriteExt}, + net::TcpListener, + sync::RwLock, + task, +}; + +/// # How to use it: +/// +/// Listen on a local address, authentication-free: +/// `$ RUST_LOG=debug cargo run --example router -- --listen-addr 127.0.0.1:1080 no-auth` +/// +/// Listen on a local address, with basic username/password requirement: +/// `$ RUST_LOG=debug cargo run --example router -- --listen-addr 127.0.0.1:1080 password --username admin --password password` +/// +/// Now, connections will be refused since there are no backends. +/// +/// Run a backend proxy, with skipped authentication mode (-k): +/// `$ RUST_LOG=debug cargo run --example server -- --listen-addr 127.0.0.1:1337 --public-addr 127.0.0.1 -k no-auth` +/// +/// Connect to the secret admin console and add the backend: +/// `$ socat --experimental SOCKS5-CONNECT:127.0.0.1:admin.internal:1234 READLINE` +/// `ADD 127.0.0.1:1337` +/// +/// You can add more backends and they'll be used in a round-robin fashion. +/// +#[derive(Debug, StructOpt)] +#[structopt( + name = "socks5-router", + about = "A socks5 demo 'router' proxying requests to further downstream socks5 servers." +)] +struct Opt { + /// Bind on address address. eg. `127.0.0.1:1080` + #[structopt(short, long)] + pub listen_addr: String, + + /// Choose authentication type + #[structopt(subcommand, name = "auth")] // Note that we mark a field as a subcommand + pub auth: AuthMode, +} + +/// Choose the authentication type +#[derive(StructOpt, Debug, PartialEq)] +enum AuthMode { + NoAuth, + Password { + #[structopt(short, long)] + username: String, + + #[structopt(short, long)] + password: String, + }, +} + +#[tokio::main] +async fn main() -> Result<()> { + env_logger::init(); + + spawn_socks_server().await +} + +async fn spawn_socks_server() -> Result<()> { + let opt: &'static Opt = Box::leak(Box::new(Opt::from_args())); + + let backends = Arc::new(RwLock::new(HashSet::new())); + + 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(opt, backends.clone(), socket)); + } + Err(err) => { + error!("accept error = {:?}", err); + } + } + } +} + +static CONN_NUM: AtomicUsize = AtomicUsize::new(0); + +async fn serve_socks5( + opt: &Opt, + backends: Arc>>, + socket: tokio::net::TcpStream, +) -> Result<(), SocksError> { + let (proto, cmd, target_addr) = match &opt.auth { + AuthMode::NoAuth => Socks5ServerProtocol::accept_no_auth(socket).await?, + AuthMode::Password { username, password } => { + Socks5ServerProtocol::accept_password_auth(socket, |user, pass| { + user == *username && pass == *password + }) + .await? + .0 + } + } + .read_command() + .await?; + + if cmd != Socks5Command::TCPConnect { + proto.reply_error(&ReplyError::CommandNotSupported).await?; + return Err(ReplyError::CommandNotSupported.into()); + } + + // Not the most reasonable way to implement an admin interface, + // but rather an example of conditional interception (i.e. just + // not proxying at all and doing something else in-process). + if let TargetAddr::Domain(ref domain, _) = target_addr { + if domain == "admin.internal" { + let inner = proto + .reply_success(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0)) + .await?; + return serve_admin_console(backends, inner).await; + } + } + + let (target_addr, target_port) = target_addr.into_string_and_port(); + + let backends = backends.read().await; + let backends: Vec<_> = backends.iter().collect(); // not good but this is just a demo + if backends.is_empty() { + warn!("No backends! Go add one using the console"); + proto.reply_error(&ReplyError::NetworkUnreachable).await?; + return Ok(()); + } + let n = CONN_NUM.fetch_add(1, Ordering::SeqCst); + + let mut config = client::Config::default(); + config.set_skip_auth(true); + let client = client::Socks5Stream::connect( + backends[n % backends.len()], + target_addr, + target_port, + config, + ) + .await?; + drop(backends); + + let inner = proto + .reply_success(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0)) + .await?; + + transfer(inner, client).await +} + +async fn serve_admin_console( + backends: Arc>>, + socket: tokio::net::TcpStream, +) -> Result<(), SocksError> { + let mut stream = tokio::io::BufReader::new(socket); + stream.write_all(b"Welcome to the router admin console! Use LIST, ADD, or REMOVE commands to manage proxies.\n").await?; + let mut buf = String::with_capacity(128); + while let Ok(_) = stream.read_line(&mut buf).await { + if buf.starts_with("LIST") { + let backends = backends.read().await; + for addr in backends.iter() { + stream.write_all(addr.as_bytes()).await?; + stream.write_all(b"\n").await?; + } + } else if buf.starts_with("ADD ") { + let mut backends = backends.write().await; + if let Some(adr) = buf.strip_prefix("ADD ") { + backends.insert(adr.trim().to_owned()); + } + } else if buf.starts_with("REMOVE ") { + let mut backends = backends.write().await; + if let Some(adr) = buf.strip_prefix("REMOVE ") { + backends.remove(adr.trim()); + } + } + buf.clear(); + } + Ok(()) +} + +fn spawn_and_log_error(fut: F) -> task::JoinHandle<()> +where + F: Future> + Send + 'static, +{ + task::spawn(async move { + match fut.await { + Ok(()) => {} + Err(err) => error!("{:#}", &err), + } + }) +} diff --git a/src/util/target_addr.rs b/src/util/target_addr.rs index 038d41d..366e79b 100644 --- a/src/util/target_addr.rs +++ b/src/util/target_addr.rs @@ -112,6 +112,13 @@ impl TargetAddr { } Ok(buf) } + + pub fn into_string_and_port(self) -> (String, u16) { + match self { + TargetAddr::Ip(socket_addr) => (socket_addr.ip().to_string(), socket_addr.port()), + TargetAddr::Domain(domain, port) => (domain, port), + } + } } // async-std ToSocketAddrs doesn't supports external trait implementation From a59c47c27cd7da0e990cacc1e056dfab2bea8feb Mon Sep 17 00:00:00 2001 From: Jonathan Dizdarevic Date: Fri, 31 Jan 2025 14:39:13 +0700 Subject: [PATCH 18/23] fix(server): prioritize authentication methods --- examples/custom_auth_server.rs | 12 +++++++++++- src/server.rs | 8 ++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/examples/custom_auth_server.rs b/examples/custom_auth_server.rs index e888d06..94d7ff6 100644 --- a/examples/custom_auth_server.rs +++ b/examples/custom_auth_server.rs @@ -22,6 +22,13 @@ use tokio::{ /// /// 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:correct_horse_battery_staple@127.0.0.1:1337" "https://httpbin.org/get"` +/// #[derive(Debug, StructOpt)] #[structopt( name = "socks5-server-custom-auth", @@ -122,6 +129,8 @@ auth_method_enums! { 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), ]) @@ -129,7 +138,8 @@ async fn serve_socks5(socket: tokio::net::TcpStream) -> Result<(), SocksError> { { AuthStarted::PasswordAuthentication(auth) => { let (user, pass, auth) = auth.read_username_password().await?; - if user == "user" && pass == "correct horse battery staple" { + 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?; diff --git a/src/server.rs b/src/server.rs index ff622c1..2e57eea 100644 --- a/src/server.rs +++ b/src/server.rs @@ -647,6 +647,8 @@ impl StandardAuthentication { pub fn allow_no_auth(allow: bool) -> &'static [StandardAuthentication] { if allow { &[ + // The order of authentication methods can be tested by clients in sequence, + // so list more secure or preferred methods first StandardAuthentication::PasswordAuthentication(PasswordAuthentication), StandardAuthentication::NoAuthentication(NoAuthentication), ] @@ -833,8 +835,10 @@ impl Socks5ServerProtocol .context("Can't get methods.")?; debug!("methods supported sent by the client: {:?}", &methods); - for client_method_id in methods.iter() { - for server_method in server_methods { + // server_methods order matter! + // the server could choose to prioritize methods + for server_method in server_methods { + for client_method_id in methods.iter() { if server_method.method_id() == *client_method_id { debug!("Reply with method {}", *client_method_id); self.inner From e5b34cc3d5753fe222d7953b95048506c683565e Mon Sep 17 00:00:00 2001 From: Val Packett Date: Sat, 1 Feb 2025 03:49:35 -0300 Subject: [PATCH 19/23] refactor: target_addr: define AddrError Use the declared AddrError type as the actual return type in this module. anyhow should not be used in library code at all. --- src/lib.rs | 4 +++ src/util/target_addr.rs | 76 ++++++++++++++++++++++------------------- 2 files changed, 44 insertions(+), 36 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4bed896..737ef94 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,6 +51,7 @@ use std::fmt; use std::io; use thiserror::Error; use util::target_addr::read_address; +use util::target_addr::AddrError; use util::target_addr::TargetAddr; use util::target_addr::ToTargetAddr; @@ -184,6 +185,9 @@ pub enum SocksError { #[error("Authentication rejected `{0}`")] AuthenticationRejected(String), + #[error(transparent)] + AddrError(#[from] AddrError), + #[error("Error with reply: {0}.")] ReplyError(#[from] ReplyError), diff --git a/src/util/target_addr.rs b/src/util/target_addr.rs index 366e79b..310fdc1 100644 --- a/src/util/target_addr.rs +++ b/src/util/target_addr.rs @@ -1,37 +1,38 @@ use crate::consts; use crate::consts::SOCKS5_ADDR_TYPE_IPV4; use crate::read_exact; -use crate::SocksError; -use anyhow::Context; use std::fmt; use std::io; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}; use std::vec::IntoIter; -use thiserror::Error; use tokio::io::{AsyncRead, AsyncReadExt}; use tokio::net::lookup_host; /// SOCKS5 reply code -#[derive(Error, Debug)] +#[derive(thiserror::Error, Debug)] pub enum AddrError { - #[error("DNS Resolution failed")] - DNSResolutionFailed, - #[error("Can't read IPv4")] - IPv4Unreadable, - #[error("Can't read IPv6")] - IPv6Unreadable, - #[error("Can't read port number")] - PortNumberUnreadable, - #[error("Can't read domain len")] - DomainLenUnreadable, - #[error("Can't read Domain content")] - DomainContentUnreadable, + #[error("DNS Resolution failed: {0}")] + DNSResolutionFailed(#[source] io::Error), + #[error("DNS returned no appropriate records")] + NoDNSRecords, + #[error("Domain length {0} exceeded maximum")] + DomainLenTooLong(usize), + #[error("Can't read IPv4: {0}")] + IPv4Unreadable(#[source] io::Error), + #[error("Can't read IPv6: {0}")] + IPv6Unreadable(#[source] io::Error), + #[error("Can't read port number: {0}")] + PortNumberUnreadable(#[source] io::Error), + #[error("Can't read domain len: {0}")] + DomainLenUnreadable(#[source] io::Error), + #[error("Can't read domain content: {0}")] + DomainContentUnreadable(#[source] io::Error), + #[error("Can't convert address: {0}")] + AddrConversionFailed(#[source] io::Error), #[error("Malformed UTF-8")] - Utf8, + Utf8(#[source] std::string::FromUtf8Error), #[error("Unknown address type")] IncorrectAddressType, - #[error("{0}")] - Custom(String), } /// A description of a connection target. @@ -47,7 +48,7 @@ pub enum TargetAddr { } impl TargetAddr { - pub async fn resolve_dns(self) -> anyhow::Result { + pub async fn resolve_dns(self) -> Result { match self { TargetAddr::Ip(ip) => Ok(TargetAddr::Ip(ip)), TargetAddr::Domain(domain, port) => { @@ -55,11 +56,9 @@ impl TargetAddr { let socket_addr = lookup_host((&domain[..], port)) .await - .context(AddrError::DNSResolutionFailed)? + .map_err(|err| AddrError::DNSResolutionFailed(err))? .next() - .ok_or(AddrError::Custom( - "Can't fetch DNS to the domain.".to_string(), - ))?; + .ok_or(AddrError::NoDNSRecords)?; debug!("domain name resolved to {}", socket_addr); // has been converted to an ip @@ -79,7 +78,7 @@ impl TargetAddr { !self.is_ip() } - pub fn to_be_bytes(&self) -> anyhow::Result> { + pub fn to_be_bytes(&self) -> Result, AddrError> { let mut buf = vec![]; match self { TargetAddr::Ip(SocketAddr::V4(addr)) => { @@ -102,7 +101,7 @@ impl TargetAddr { TargetAddr::Domain(ref domain, port) => { debug!("TargetAddr::Domain"); if domain.len() > u8::max_value() as usize { - return Err(SocksError::ExceededMaxDomainLen(domain.len()).into()); + return Err(AddrError::DomainLenTooLong(domain.len())); } buf.extend_from_slice(&[consts::SOCKS5_ADDR_TYPE_DOMAIN_NAME, domain.len() as u8]); buf.extend_from_slice(domain.as_bytes()); // domain content @@ -218,38 +217,43 @@ pub enum Addr { pub async fn read_address( stream: &mut T, atyp: u8, -) -> anyhow::Result { +) -> Result { let addr = match atyp { consts::SOCKS5_ADDR_TYPE_IPV4 => { debug!("Address type `IPv4`"); - Addr::V4(read_exact!(stream, [0u8; 4]).context(AddrError::IPv4Unreadable)?) + Addr::V4(read_exact!(stream, [0u8; 4]).map_err(|err| AddrError::IPv4Unreadable(err))?) } consts::SOCKS5_ADDR_TYPE_IPV6 => { debug!("Address type `IPv6`"); - Addr::V6(read_exact!(stream, [0u8; 16]).context(AddrError::IPv6Unreadable)?) + Addr::V6(read_exact!(stream, [0u8; 16]).map_err(|err| AddrError::IPv6Unreadable(err))?) } consts::SOCKS5_ADDR_TYPE_DOMAIN_NAME => { debug!("Address type `domain`"); - let len = read_exact!(stream, [0]).context(AddrError::DomainLenUnreadable)?[0]; + let len = + read_exact!(stream, [0]).map_err(|err| AddrError::DomainLenUnreadable(err))?[0]; let domain = read_exact!(stream, vec![0u8; len as usize]) - .context(AddrError::DomainContentUnreadable)?; + .map_err(|err| AddrError::DomainContentUnreadable(err))?; // make sure the bytes are correct utf8 string - let domain = String::from_utf8(domain).context(AddrError::Utf8)?; + let domain = String::from_utf8(domain).map_err(|err| AddrError::Utf8(err))?; Addr::Domain(domain) } - _ => return Err(anyhow::anyhow!(AddrError::IncorrectAddressType)), + _ => return Err(AddrError::IncorrectAddressType), }; // Find port number - let port = read_exact!(stream, [0u8; 2]).context(AddrError::PortNumberUnreadable)?; + let port = read_exact!(stream, [0u8; 2]).map_err(|err| AddrError::PortNumberUnreadable(err))?; // Convert (u8 * 2) into u16 let port = (port[0] as u16) << 8 | port[1] as u16; // Merge ADDRESS + PORT into a TargetAddr let addr: TargetAddr = match addr { - Addr::V4([a, b, c, d]) => (Ipv4Addr::new(a, b, c, d), port).to_target_addr()?, - Addr::V6(x) => (Ipv6Addr::from(x), port).to_target_addr()?, + Addr::V4([a, b, c, d]) => (Ipv4Addr::new(a, b, c, d), port) + .to_target_addr() + .map_err(|err| AddrError::AddrConversionFailed(err))?, + Addr::V6(x) => (Ipv6Addr::from(x), port) + .to_target_addr() + .map_err(|err| AddrError::AddrConversionFailed(err))?, Addr::Domain(domain) => TargetAddr::Domain(domain, port), }; From 6b819deb33ab43f54d9f5140d0970e6cf91b7ae6 Mon Sep 17 00:00:00 2001 From: Val Packett Date: Sat, 1 Feb 2025 04:09:13 -0300 Subject: [PATCH 20/23] refactor: stream: define ConnectError Define a thiserror type for errors that occur when TCP connecting to targets. Prepare a ReplyError conversion for these, as the proper error replying was lost in the refactoring before. --- src/lib.rs | 4 +++ src/util/stream.rs | 73 +++++++++++++++++++++++++++++++--------------- 2 files changed, 54 insertions(+), 23 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 737ef94..79c1406 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -50,6 +50,7 @@ use anyhow::Context; use std::fmt; use std::io; use thiserror::Error; +use util::stream::ConnectError; use util::target_addr::read_address; use util::target_addr::AddrError; use util::target_addr::TargetAddr; @@ -188,6 +189,9 @@ pub enum SocksError { #[error(transparent)] AddrError(#[from] AddrError), + #[error(transparent)] + ConnectError(#[from] ConnectError), + #[error("Error with reply: {0}.")] ReplyError(#[from] ReplyError), diff --git a/src/util/stream.rs b/src/util/stream.rs index f656ec2..b6d5823 100644 --- a/src/util/stream.rs +++ b/src/util/stream.rs @@ -1,8 +1,9 @@ -use crate::{ReplyError, Result}; +use crate::ReplyError; +use std::io; +use std::time::Duration; use tokio::io::ErrorKind as IOErrorKind; -use tokio::time::timeout; use tokio::net::{TcpStream, ToSocketAddrs}; -use std::time::Duration; +use tokio::time::timeout; /// Easy to destructure bytes buffers by naming each fields: /// @@ -45,36 +46,62 @@ macro_rules! ready { }; } -pub async fn tcp_connect_with_timeout(addr: T, request_timeout_s: u64) -> Result - where T: ToSocketAddrs, +#[derive(thiserror::Error, Debug)] +pub enum ConnectError { + #[error("Connection timed out")] + ConnectionTimeout, + #[error("Connection refused: {0}")] + ConnectionRefused(#[source] io::Error), + #[error("Connection aborted: {0}")] + ConnectionAborted(#[source] io::Error), + #[error("Connection reset: {0}")] + ConnectionReset(#[source] io::Error), + #[error("Not connected: {0}")] + NotConnected(#[source] io::Error), + #[error("Other i/o error: {0}")] + Other(#[source] io::Error), +} + +impl ConnectError { + pub fn to_reply_error(&self) -> ReplyError { + match self { + ConnectError::ConnectionTimeout => ReplyError::ConnectionTimeout, + ConnectError::ConnectionRefused(_) => ReplyError::ConnectionRefused, + ConnectError::ConnectionAborted(_) | ConnectError::ConnectionReset(_) => { + ReplyError::ConnectionNotAllowed + } + ConnectError::NotConnected(_) => ReplyError::NetworkUnreachable, + ConnectError::Other(_) => ReplyError::GeneralFailure, + } + } +} + +pub async fn tcp_connect_with_timeout( + addr: T, + request_timeout_s: u64, +) -> Result +where + T: ToSocketAddrs, { let fut = tcp_connect(addr); match timeout(Duration::from_secs(request_timeout_s), fut).await { Ok(result) => result, - Err(_) => Err(ReplyError::ConnectionTimeout.into()), + Err(_) => Err(ConnectError::ConnectionTimeout), } } -pub async fn tcp_connect(addr: T) -> Result - where T: ToSocketAddrs, +pub async fn tcp_connect(addr: T) -> Result +where + T: ToSocketAddrs, { match TcpStream::connect(addr).await { Ok(o) => Ok(o), Err(e) => match e.kind() { - // Match other TCP errors with ReplyError - IOErrorKind::ConnectionRefused => { - Err(ReplyError::ConnectionRefused.into()) - } - IOErrorKind::ConnectionAborted => { - Err(ReplyError::ConnectionNotAllowed.into()) - } - IOErrorKind::ConnectionReset => { - Err(ReplyError::ConnectionNotAllowed.into()) - } - IOErrorKind::NotConnected => { - Err(ReplyError::NetworkUnreachable.into()) - } - _ => Err(e.into()), // #[error("General failure")] ? + IOErrorKind::ConnectionRefused => Err(ConnectError::ConnectionRefused(e)), + IOErrorKind::ConnectionAborted => Err(ConnectError::ConnectionAborted(e)), + IOErrorKind::ConnectionReset => Err(ConnectError::ConnectionReset(e)), + IOErrorKind::NotConnected => Err(ConnectError::NotConnected(e)), + _ => Err(ConnectError::Other(e)), }, } -} \ No newline at end of file +} From 3e1d80464baff1347dd1657b0dd11fa8f57dbed1 Mon Sep 17 00:00:00 2001 From: Val Packett Date: Sat, 1 Feb 2025 05:18:14 -0300 Subject: [PATCH 21/23] refactor: lib: define UdpHeaderError New small thiserror based type for UDP header related errors. Not sure why ReplyErrors were used here before: all of this happens way after we've had a chance to reply with either success or error in the TCP SOCKS5 flow. --- src/lib.rs | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 79c1406..d9fb119 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -186,6 +186,9 @@ pub enum SocksError { #[error("Authentication rejected `{0}`")] AuthenticationRejected(String), + #[error(transparent)] + UdpHeaderError(#[from] UdpHeaderError), + #[error(transparent)] AddrError(#[from] AddrError), @@ -273,6 +276,18 @@ impl ReplyError { } } +#[derive(thiserror::Error, Debug)] +pub enum UdpHeaderError { + #[error(transparent)] + AddrError(#[from] AddrError), + #[error("could not convert target addr: {0}")] + ToTargetAddr(#[source] io::Error), + #[error("could not read UDP header: {0}")] + ReadingError(#[source] io::Error), + #[error("does not match the expected reserved field")] + GarbageInReserved, +} + /// Generate UDP header /// /// # UDP Request header structure. @@ -295,32 +310,34 @@ impl ReplyError { /// o DST.PORT desired destination port /// o DATA user data /// ``` -pub fn new_udp_header(target_addr: T) -> Result> { +pub fn new_udp_header(target_addr: T) -> Result, UdpHeaderError> { let mut header = vec![ 0, 0, // RSV 0, // FRAG ]; - header.append(&mut target_addr.to_target_addr()?.to_be_bytes()?); + header.append( + &mut target_addr + .to_target_addr() + .map_err(UdpHeaderError::ToTargetAddr)? + .to_be_bytes()?, + ); Ok(header) } /// Parse data from UDP client on raw buffer, return (frag, target_addr, payload). -pub async fn parse_udp_request<'a>(mut req: &'a [u8]) -> Result<(u8, TargetAddr, &'a [u8])> { - let rsv = read_exact!(req, [0u8; 2]).context("Malformed request")?; +pub async fn parse_udp_request<'a>( + mut req: &'a [u8], +) -> Result<(u8, TargetAddr, &'a [u8]), UdpHeaderError> { + let rsv = read_exact!(req, [0u8; 2]).map_err(UdpHeaderError::ReadingError)?; if !rsv.eq(&[0u8; 2]) { - return Err(ReplyError::GeneralFailure.into()); + return Err(UdpHeaderError::GarbageInReserved); } - let [frag, atyp] = read_exact!(req, [0u8; 2]).context("Malformed request")?; + let [frag, atyp] = read_exact!(req, [0u8; 2]).map_err(UdpHeaderError::ReadingError)?; - let target_addr = read_address(&mut req, atyp).await.map_err(|e| { - // print explicit error - error!("{:#}", e); - // then convert it to a reply - ReplyError::AddressTypeNotSupported - })?; + let target_addr = read_address(&mut req, atyp).await?; Ok((frag, target_addr, req)) } From e9918fc02838a1c969690cd4ff796f67d2ea4c99 Mon Sep 17 00:00:00 2001 From: Val Packett Date: Sat, 1 Feb 2025 05:30:21 -0300 Subject: [PATCH 22/23] refactor: define SocksServerError, restore replying with errors Liberate the new server API from anyhow; restore the reporting of errors to the client as reply_error. --- examples/router.rs | 3 +- src/lib.rs | 6 +- src/server.rs | 310 +++++++++++++++++++++++++++++++-------------- 3 files changed, 222 insertions(+), 97 deletions(-) diff --git a/examples/router.rs b/examples/router.rs index 8bbd946..5621cf5 100644 --- a/examples/router.rs +++ b/examples/router.rs @@ -164,7 +164,8 @@ async fn serve_socks5( .reply_success(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0)) .await?; - transfer(inner, client).await + transfer(inner, client).await; + Ok(()) } async fn serve_admin_console( diff --git a/src/lib.rs b/src/lib.rs index d9fb119..c267a30 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -46,7 +46,6 @@ pub mod util; #[cfg(feature = "socks4")] pub mod socks4; -use anyhow::Context; use std::fmt; use std::io; use thiserror::Error; @@ -181,11 +180,12 @@ pub enum SocksError { UnsupportedSocksVersion(u8), #[error("Domain exceeded max sequence length")] ExceededMaxDomainLen(usize), - #[error("Authentication failed `{0}`")] - AuthenticationFailed(String), #[error("Authentication rejected `{0}`")] AuthenticationRejected(String), + #[error(transparent)] + ServerError(#[from] server::SocksServerError), + #[error(transparent)] UdpHeaderError(#[from] UdpHeaderError), diff --git a/src/server.rs b/src/server.rs index 2e57eea..548c86f 100644 --- a/src/server.rs +++ b/src/server.rs @@ -3,9 +3,12 @@ use crate::parse_udp_request; use crate::read_exact; use crate::ready; use crate::util::stream::tcp_connect_with_timeout; +use crate::util::stream::ConnectError; +use crate::util::target_addr::AddrError; use crate::util::target_addr::{read_address, TargetAddr}; use crate::Socks5Command; -use crate::{consts, AuthenticationMethod, ReplyError, Result, SocksError}; +use crate::UdpHeaderError; +use crate::{consts, AuthenticationMethod, ReplyError, SocksError}; use anyhow::Context; use std::future::Future; use std::io; @@ -15,6 +18,7 @@ use std::net::Ipv4Addr; use std::net::{SocketAddr, ToSocketAddrs as StdToSocketAddrs}; use std::ops::Deref; use std::pin::Pin; +use std::string::FromUtf8Error; use std::sync::Arc; use std::task::{Context as AsyncContext, Poll}; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; @@ -23,6 +27,66 @@ use tokio::net::{TcpListener, TcpStream, ToSocketAddrs as AsyncToSocketAddrs}; use tokio::try_join; use tokio_stream::Stream; +#[derive(thiserror::Error, Debug)] +pub enum SocksServerError { + #[error("i/o error when {context}: {source}")] + Io { + source: io::Error, + context: &'static str, + }, + #[error("string error when {context}: {source}")] + FromUtf8 { + source: FromUtf8Error, + context: &'static str, + }, + #[error(transparent)] + ConnectError(#[from] ConnectError), + #[error(transparent)] + UdpHeaderError(#[from] UdpHeaderError), + #[error(transparent)] + AddrError(#[from] AddrError), + #[error("BUG: {0}")] // should be unreachable + Bug(&'static str), + #[error("Auth method unacceptable `{0:?}`.")] + AuthMethodUnacceptable(Vec), + #[error("Unsupported SOCKS version `{0}`.")] + UnsupportedSocksVersion(u8), + #[error("Unsupported SOCKS command `{0}`.")] + UnknownCommand(u8), + #[error("Empty username received")] + EmptyUsername, + #[error("Empty password received")] + EmptyPassword, + #[error("Authentication rejected")] + AuthenticationRejected, +} + +impl SocksServerError { + pub fn to_reply_error(&self) -> ReplyError { + match self { + SocksServerError::UnknownCommand(_) => ReplyError::CommandNotSupported, + SocksServerError::AddrError(_) => ReplyError::AddressTypeNotSupported, + _ => ReplyError::GeneralFailure, + } + } +} + +trait ErrorContext { + fn err_when(self, context: &'static str) -> Result; +} + +impl ErrorContext for Result { + fn err_when(self, context: &'static str) -> Result { + self.map_err(|source| SocksServerError::Io { source, context }) + } +} + +impl ErrorContext for Result { + fn err_when(self, context: &'static str) -> Result { + self.map_err(|source| SocksServerError::FromUtf8 { source, context }) + } +} + #[derive(Clone)] pub struct Config { /// Timeout of the command request @@ -70,15 +134,13 @@ pub trait Authentication: Send + Sync { async fn authenticate_callback( auth_callback: &A, auth: StandardAuthenticationStarted, -) -> Result<(Socks5ServerProtocol, A::Item)> { +) -> Result<(Socks5ServerProtocol, A::Item), SocksServerError> { match auth { StandardAuthenticationStarted::NoAuthentication(auth) => { if let Some(credentials) = auth_callback.authenticate(None).await { Ok((auth.finish_auth(), credentials)) } else { - Err(SocksError::AuthenticationRejected(format!( - "Authentication, rejected." - ))) + Err(SocksServerError::AuthenticationRejected) } } StandardAuthenticationStarted::PasswordAuthentication(auth) => { @@ -88,9 +150,7 @@ async fn authenticate_callback( /// this wrapper will convert async_std TcpStream into Socks5Socket. #[allow(deprecated)] impl<'a, A: Authentication> Stream for Incoming<'a, A> { - type Item = Result>; + type Item = Result, SocksError>; /// this code is mainly borrowed from [`Incoming::poll_next()` of `TcpListener`][tcpListenerLink] /// @@ -379,7 +439,7 @@ impl Socks5ServerProtocol { } /// Handle the SOCKS5 auth negotiation supporting only the `NoAuthentication` method. - pub async fn accept_no_auth(inner: T) -> Result + pub async fn accept_no_auth(inner: T) -> Result where T: AsyncWrite + AsyncRead + Unpin, { @@ -393,7 +453,10 @@ impl Socks5ServerProtocol { /// and verify the provided username and password using the provided closure. /// /// The closure can mutate state variables and/or return a result as `Option`/`Result`. - pub async fn accept_password_auth(inner: T, mut check: F) -> Result<(Self, R)> + pub async fn accept_password_auth( + inner: T, + mut check: F, + ) -> Result<(Self, R), SocksServerError> where T: AsyncWrite + AsyncRead + Unpin, F: FnMut(String, String) -> R, @@ -409,9 +472,7 @@ impl Socks5ServerProtocol { Ok((auth.accept().await?.finish_auth(), check_result)) } else { auth.reject().await?; - Err(SocksError::AuthenticationRejected( - "Wrong username/password".to_owned(), - )) + Err(SocksServerError::AuthenticationRejected) } } } @@ -500,14 +561,17 @@ impl PasswordAuthenticationImpl Result<( - String, - String, - PasswordAuthenticationImpl, - )> { + ) -> Result< + ( + String, + String, + PasswordAuthenticationImpl, + ), + SocksServerError, + > { let mut socket = self.inner; trace!("PasswordAuthenticationStarted: read_username_password()"); - let [version, user_len] = read_exact!(socket, [0u8; 2]).context("Can't read user len")?; + let [version, user_len] = read_exact!(socket, [0u8; 2]).err_when("reading user len")?; debug!( "Auth: [version: {version}, user len: {len}]", version = version, @@ -515,32 +579,26 @@ impl PasswordAuthenticationImpl PasswordAuthenticationImpl Result> { + ) -> Result, SocksServerError> { self.inner .write_all(&[1, consts::SOCKS5_REPLY_SUCCEEDED]) .await - .context("Can't reply auth success")?; + .err_when("replying auth success")?; info!("Password authentication accepted."); Ok(PasswordAuthenticationImpl::new(self.inner)) } /// Notify the client with a "NOT_ACCEPTABLE" reply and drop the socket. - pub async fn reject(mut self) -> Result<()> { + pub async fn reject(mut self) -> Result<(), SocksServerError> { self.inner .write_all(&[1, consts::SOCKS5_AUTH_METHOD_NOT_ACCEPTABLE]) .await - .context("Can't reply with auth method not acceptable.")?; + .err_when("replying with auth method not acceptable")?; info!("Password authentication rejected."); Ok(()) @@ -691,7 +749,7 @@ impl Socks5Socket { /// Process clients SOCKS requests /// This is the entry point where a whole request is processed. - pub async fn upgrade_to_socks5(mut self) -> Result> { + pub async fn upgrade_to_socks5(mut self) -> Result, SocksError> { trace!("upgrading to socks5..."); // NOTE: this cannot be split in two without making self.inner an Option @@ -764,7 +822,7 @@ impl Socks5Socket { /// This function is public, it can be call manually on your own-willing /// if config flag has been turned off: `Config::dns_resolve == false`. - pub async fn resolve_dns(&mut self) -> Result<()> { + pub async fn resolve_dns(&mut self) -> Result<(), SocksError> { trace!("resolving dns"); if let Some(target_addr) = self.target_addr.take() { // decide whether we have to resolve DNS or not @@ -810,14 +868,14 @@ impl Socks5ServerProtocol /// picks the first one for which there exists an implementation in `server_methods`. /// /// If none of the auth methods requested by the client are in `server_methods`, - /// returns a `SocksError::AuthMethodUnacceptable`. + /// returns a `SocksServerError::AuthMethodUnacceptable`. pub async fn negotiate_auth>( mut self, server_methods: &[M], - ) -> Result { + ) -> Result { trace!("Socks5ServerProtocol: negotiate_auth()"); let [version, methods_len] = - read_exact!(self.inner, [0u8; 2]).context("Can't read methods")?; + read_exact!(self.inner, [0u8; 2]).err_when("reading methods")?; debug!( "Handshake headers: [version: {version}, methods len: {len}]", version = version, @@ -825,14 +883,14 @@ impl Socks5ServerProtocol ); if version != consts::SOCKS5_VERSION { - return Err(SocksError::UnsupportedSocksVersion(version)); + return Err(SocksServerError::UnsupportedSocksVersion(version)); } // {METHODS available from the client} // eg. (non-auth) {0, 1} // eg. (auth) {0, 1, 2} - let methods = read_exact!(self.inner, vec![0u8; methods_len as usize]) - .context("Can't get methods.")?; + let methods = + read_exact!(self.inner, vec![0u8; methods_len as usize]).err_when("reading methods")?; debug!("methods supported sent by the client: {:?}", &methods); // server_methods order matter! @@ -844,7 +902,7 @@ impl Socks5ServerProtocol self.inner .write_all(&[consts::SOCKS5_VERSION, *client_method_id]) .await - .context("Can't reply with auth method")?; + .err_when("replying with auth method")?; return Ok(server_method.new(self.inner)); } } @@ -857,42 +915,59 @@ impl Socks5ServerProtocol consts::SOCKS5_AUTH_METHOD_NOT_ACCEPTABLE, ]) .await - .context("Can't reply with method not acceptable.")?; - Err(SocksError::AuthMethodUnacceptable(methods)) + .err_when("replying with method not acceptable")?; + Err(SocksServerError::AuthMethodUnacceptable(methods)) } } impl Socks5ServerProtocol { /// Reply success to the client according to the RFC. /// This consumes the wrapper as after this message actual proxying should begin. - pub async fn reply_success(mut self, sock_addr: SocketAddr) -> Result { + pub async fn reply_success(mut self, sock_addr: SocketAddr) -> Result { self.inner .write(&new_reply(&ReplyError::Succeeded, sock_addr)) .await - .context("Can't write successful reply")?; + .err_when("writing successful reply")?; - self.inner.flush().await.context("Can't flush the reply!")?; + self.inner.flush().await.err_when("flushing auth reply")?; debug!("Wrote success"); Ok(self.inner) } /// Reply error to the client with the reply code according to the RFC. - pub async fn reply_error(mut self, error: &ReplyError) -> Result<()> { + pub async fn reply_error(mut self, error: &ReplyError) -> Result<(), SocksServerError> { let reply = new_reply(error, "0.0.0.0:0".parse().unwrap()); debug!("reply error to be written: {:?}", &reply); self.inner .write(&reply) .await - .context("Can't write the reply!")?; + .err_when("writing unsuccessful reply")?; - self.inner.flush().await.context("Can't flush the reply!")?; + self.inner.flush().await.err_when("flushing auth reply")?; Ok(()) } } +macro_rules! try_notify { + ($proto:expr, $e:expr) => { + match $e { + Ok(res) => res, + Err(err) => { + if let Err(rep_err) = $proto.reply_error(&err.to_reply_error()).await { + error!( + "extra error while reporting an error to the client: {}", + rep_err + ); + } + return Err(err); + } + } + }; +} + impl Socks5ServerProtocol { /// Decide to whether or not, accept the authentication method. /// Don't forget that the methods list sent by the client, contains one or more methods. @@ -910,13 +985,16 @@ impl Socks5ServerProtocol Result<( - Socks5ServerProtocol, - Socks5Command, - TargetAddr, - )> { + ) -> Result< + ( + Socks5ServerProtocol, + Socks5Command, + TargetAddr, + ), + SocksServerError, + > { let [version, cmd, rsv, address_type] = - read_exact!(self.inner, [0u8; 4]).context("Malformed request")?; + read_exact!(self.inner, [0u8; 4]).err_when("reading command")?; debug!( "Request: [version: {version}, command: {cmd}, rev: {rsv}, address_type: {address_type}]", version = version, @@ -926,24 +1004,27 @@ impl Socks5ServerProtocol( addr: &TargetAddr, request_timeout_s: u64, nodelay: bool, -) -> Result { - let addr = addr.to_socket_addrs()?.next().context("unreachable")?; +) -> Result { + let addr = try_notify!( + proto, + addr.to_socket_addrs() + .err_when("converting to socket addr") + .and_then(|mut addrs| addrs.next().ok_or(SocksServerError::Bug("no socket addrs"))) + ); // TCP connect with timeout, to avoid memory leak for connection that takes forever - let outbound = tcp_connect_with_timeout(addr, request_timeout_s).await?; + let outbound = match tcp_connect_with_timeout(addr, request_timeout_s).await { + Ok(stream) => stream, + Err(err) => { + proto.reply_error(&err.to_reply_error()).await?; + return Err(err.into()); + } + }; // Disable Nagle's algorithm if config specifies to do so. - outbound.set_nodelay(nodelay)?; + try_notify!( + proto, + outbound.set_nodelay(nodelay).err_when("setting nodelay") + ); debug!("Connected to remote destination"); @@ -968,7 +1063,7 @@ pub async fn run_tcp_proxy( .reply_success(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0)) .await?; - transfer(&mut inner, outbound).await?; + transfer(&mut inner, outbound).await; Ok(inner) } @@ -977,7 +1072,7 @@ pub async fn run_udp_proxy( proto: Socks5ServerProtocol, _addr: &TargetAddr, reply_ip: IpAddr, -) -> Result { +) -> Result { // The DST.ADDR and DST.PORT fields contain the address and port that // the client expects to use to send UDP datagrams on for the // association. The server MAY use this information to limit access @@ -988,11 +1083,21 @@ pub async fn run_udp_proxy( // Listen with UDP6 socket, so the client can connect to it with either // IPv4 or IPv6. - let peer_sock = UdpSocket::bind("[::]:0").await?; + let peer_sock = try_notify!( + proto, + UdpSocket::bind("[::]:0") + .await + .err_when("binding udp socket") + ); + + let peer_addr = try_notify!( + proto, + peer_sock.local_addr().err_when("getting peer's local addr") + ); // Respect the pre-populated reply IP address. let inner = proto - .reply_success(SocketAddr::new(reply_ip, peer_sock.local_addr()?.port())) + .reply_success(SocketAddr::new(reply_ip, peer_addr.port())) .await?; transfer_udp(peer_sock).await?; @@ -1001,7 +1106,7 @@ pub async fn run_udp_proxy( /// Run a bidirectional proxy between two streams. /// Using 2 different generators, because they could be different structs with same traits. -pub async fn transfer(mut inbound: I, mut outbound: O) -> Result<()> +pub async fn transfer(mut inbound: I, mut outbound: O) where I: AsyncRead + AsyncWrite + Unpin, O: AsyncRead + AsyncWrite + Unpin, @@ -1010,16 +1115,23 @@ where Ok(res) => info!("transfer closed ({}, {})", res.0, res.1), Err(err) => error!("transfer error: {:?}", err), }; - - Ok(()) } -async fn handle_udp_request(inbound: &UdpSocket, outbound: &UdpSocket) -> Result<()> { +async fn handle_udp_request( + inbound: &UdpSocket, + outbound: &UdpSocket, +) -> Result<(), SocksServerError> { let mut buf = vec![0u8; 0x10000]; loop { - let (size, client_addr) = inbound.recv_from(&mut buf).await?; + let (size, client_addr) = inbound + .recv_from(&mut buf) + .await + .err_when("udp receiving from")?; debug!("Server recieve udp from {}", client_addr); - inbound.connect(client_addr).await?; + inbound + .connect(client_addr) + .await + .err_when("connecting udp inbound")?; let (frag, target_addr, data) = parse_udp_request(&buf[..size]).await?; @@ -1032,33 +1144,45 @@ async fn handle_udp_request(inbound: &UdpSocket, outbound: &UdpSocket) -> Result let mut target_addr = target_addr .resolve_dns() .await? - .to_socket_addrs()? + .to_socket_addrs() + .err_when("udp target to socket addrs")? .next() - .context("unreachable")?; + .ok_or(SocksServerError::Bug("no socket addrs"))?; target_addr.set_ip(match target_addr.ip() { std::net::IpAddr::V4(v4) => std::net::IpAddr::V6(v4.to_ipv6_mapped()), v6 @ std::net::IpAddr::V6(_) => v6, }); - outbound.send_to(data, target_addr).await?; + outbound + .send_to(data, target_addr) + .await + .err_when("udp sending to")?; } } -async fn handle_udp_response(inbound: &UdpSocket, outbound: &UdpSocket) -> Result<()> { +async fn handle_udp_response( + inbound: &UdpSocket, + outbound: &UdpSocket, +) -> Result<(), SocksServerError> { let mut buf = vec![0u8; 0x10000]; loop { - let (size, remote_addr) = outbound.recv_from(&mut buf).await?; + let (size, remote_addr) = outbound + .recv_from(&mut buf) + .await + .err_when("udp receiving from")?; debug!("Recieve packet from {}", remote_addr); let mut data = new_udp_header(remote_addr)?; data.extend_from_slice(&buf[..size]); - inbound.send(&data).await?; + inbound.send(&data).await.err_when("udp sending")?; } } /// Run a bidirectional UDP SOCKS proxy for a bound port. -pub async fn transfer_udp(inbound: UdpSocket) -> Result<()> { - let outbound = UdpSocket::bind("[::]:0").await?; +pub async fn transfer_udp(inbound: UdpSocket) -> Result<(), SocksServerError> { + let outbound = UdpSocket::bind("[::]:0") + .await + .err_when("binding udp socket")?; let req_fut = handle_udp_request(&inbound, &outbound); let res_fut = handle_udp_response(&inbound, &outbound); From 4c8985cf686e7a86348f14c0892a17783dde878f Mon Sep 17 00:00:00 2001 From: Val Packett Date: Sat, 1 Feb 2025 06:38:00 -0300 Subject: [PATCH 23/23] refactor: add a helper for DNS resolution that reports errors to the client. --- examples/custom_auth_server.rs | 8 ++---- examples/server.rs | 8 +++--- src/server.rs | 52 +++++++++++++++++++++++++--------- src/util/target_addr.rs | 10 +++++++ 4 files changed, 55 insertions(+), 23 deletions(-) diff --git a/examples/custom_auth_server.rs b/examples/custom_auth_server.rs index 94d7ff6..f59f00d 100644 --- a/examples/custom_auth_server.rs +++ b/examples/custom_auth_server.rs @@ -5,8 +5,8 @@ extern crate log; use fast_socks5::{ auth_method_enums, server::{ - run_tcp_proxy, AuthMethod, AuthMethodSuccessState, PasswordAuthentication, - PasswordAuthenticationStarted, Socks5ServerProtocol, + run_tcp_proxy, AuthMethod, AuthMethodSuccessState, DnsResolveHelper as _, + PasswordAuthentication, PasswordAuthenticationStarted, Socks5ServerProtocol, }, ReplyError, Result, Socks5Command, SocksError, }; @@ -151,9 +151,7 @@ async fn serve_socks5(socket: tokio::net::TcpStream) -> Result<(), SocksError> { AuthStarted::BackdoorAuthentication(auth) => auth.verify_timing().await?.finish_auth(), }; - let (proto, cmd, mut target_addr) = proto.read_command().await?; - - target_addr = target_addr.resolve_dns().await?; + let (proto, cmd, target_addr) = proto.read_command().await?.resolve_dns().await?; const REQUEST_TIMEOUT: u64 = 10; match cmd { diff --git a/examples/server.rs b/examples/server.rs index 73eefba..8050b69 100644 --- a/examples/server.rs +++ b/examples/server.rs @@ -4,7 +4,7 @@ extern crate log; use anyhow::Context; use fast_socks5::{ - server::{run_tcp_proxy, run_udp_proxy, Socks5ServerProtocol}, + server::{run_tcp_proxy, run_udp_proxy, DnsResolveHelper as _, Socks5ServerProtocol}, ReplyError, Result, Socks5Command, SocksError, }; use std::future::Future; @@ -109,7 +109,7 @@ async fn spawn_socks_server() -> Result<()> { } async fn serve_socks5(opt: &Opt, socket: tokio::net::TcpStream) -> Result<(), SocksError> { - let (proto, cmd, mut target_addr) = match &opt.auth { + let (proto, cmd, target_addr) = match &opt.auth { AuthMode::NoAuth if opt.skip_auth => { Socks5ServerProtocol::skip_auth_this_is_not_rfc_compliant(socket) } @@ -123,10 +123,10 @@ async fn serve_socks5(opt: &Opt, socket: tokio::net::TcpStream) -> Result<(), So } } .read_command() + .await? + .resolve_dns() .await?; - target_addr = target_addr.resolve_dns().await?; - match cmd { Socks5Command::TCPConnect => { run_tcp_proxy(proto, &target_addr, opt.request_timeout, false).await?; diff --git a/src/server.rs b/src/server.rs index 548c86f..1d7cf52 100644 --- a/src/server.rs +++ b/src/server.rs @@ -65,7 +65,7 @@ impl SocksServerError { pub fn to_reply_error(&self) -> ReplyError { match self { SocksServerError::UnknownCommand(_) => ReplyError::CommandNotSupported, - SocksServerError::AddrError(_) => ReplyError::AddressTypeNotSupported, + SocksServerError::AddrError(err) => err.to_reply_error(), _ => ReplyError::GeneralFailure, } } @@ -775,13 +775,18 @@ impl Socks5Socket { } }; - let (proto, cmd, mut target_addr) = proto.read_command().await?; + let (proto, cmd, target_addr) = { + let triple = proto.read_command().await?; - if self.config.dns_resolve { - target_addr = target_addr.resolve_dns().await?; - } else { - debug!("Domain won't be resolved because `dns_resolve`'s config has been turned off.") - } + if self.config.dns_resolve { + triple.resolve_dns().await? + } else { + debug!( + "Domain won't be resolved because `dns_resolve`'s config has been turned off." + ); + triple + } + }; match cmd { cmd if !self.config.execute_command => { @@ -962,7 +967,7 @@ macro_rules! try_notify { rep_err ); } - return Err(err); + return Err(err.into()); } } }; @@ -1010,12 +1015,7 @@ impl Socks5ServerProtocol Socks5ServerProtocol Result; +} + +impl DnsResolveHelper + for ( + Socks5ServerProtocol, + Socks5Command, + TargetAddr, + ) +where + T: AsyncRead + AsyncWrite + Unpin, +{ + async fn resolve_dns(self) -> Result { + let (proto, cmd, target_addr) = self; + let resolved_addr = try_notify!(proto, target_addr.resolve_dns().await); + Ok((proto, cmd, resolved_addr)) + } +} + /// Handle the connect command by running a TCP proxy until the connection is done. pub async fn run_tcp_proxy( proto: Socks5ServerProtocol, diff --git a/src/util/target_addr.rs b/src/util/target_addr.rs index 310fdc1..d2ef63f 100644 --- a/src/util/target_addr.rs +++ b/src/util/target_addr.rs @@ -1,6 +1,7 @@ use crate::consts; use crate::consts::SOCKS5_ADDR_TYPE_IPV4; use crate::read_exact; +use crate::ReplyError; use std::fmt; use std::io; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}; @@ -35,6 +36,15 @@ pub enum AddrError { IncorrectAddressType, } +impl AddrError { + pub fn to_reply_error(&self) -> ReplyError { + match self { + AddrError::IncorrectAddressType => ReplyError::AddressTypeNotSupported, + _ => ReplyError::ConnectionRefused, + } + } +} + /// A description of a connection target. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum TargetAddr {