diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f3274b4e1..dc22196386 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added + +- socks5-client/network-requester: add support for socks4a protocol + + ## [v1.1.1](https://github.com/nymtech/nym/tree/v1.1.1) (2022-11-29) ### Added diff --git a/clients/socks5/src/socks/client.rs b/clients/socks5/src/socks/client.rs index 3e3c24347c..beb0e16c7f 100644 --- a/clients/socks5/src/socks/client.rs +++ b/clients/socks5/src/socks/client.rs @@ -2,8 +2,8 @@ use super::authentication::{AuthenticationMethods, Authenticator, User}; use super::request::{SocksCommand, SocksRequest}; -use super::types::{ResponseCode, SocksProxyError}; -use super::{RESERVED, SOCKS_VERSION}; +use super::types::{ResponseCodeV4, ResponseCodeV5, SocksProxyError}; +use super::{SocksVersion, RESERVED, SOCKS4_VERSION, SOCKS5_VERSION}; use client_connections::{LaneQueueLengths, TransmissionLane}; use client_core::client::inbound_messages::{InputMessage, InputMessageSender}; use futures::channel::mpsc; @@ -129,13 +129,13 @@ impl AsyncWrite for StreamState { /// A client connecting to the Socks proxy server, because /// it wants to make a Nym-protected outbound request. Typically, this is /// something like e.g. a wallet app running on your laptop connecting to -/// SphinxSocksServer. +/// `SphinxSocksServer`. pub(crate) struct SocksClient { controller_sender: ControllerSender, stream: StreamState, auth_nmethods: u8, authenticator: Authenticator, - socks_version: u8, + socks_version: Option, input_sender: InputMessageSender, connection_id: ConnectionId, service_provider: Recipient, @@ -158,15 +158,14 @@ impl Drop for SocksClient { } impl SocksClient { - /// Create a new SOCKClient #[allow(clippy::too_many_arguments)] pub fn new( stream: TcpStream, authenticator: Authenticator, input_sender: InputMessageSender, - service_provider: Recipient, + service_provider: &Recipient, controller_sender: ControllerSender, - self_address: Recipient, + self_address: &Recipient, lane_queue_lengths: LaneQueueLengths, shutdown_listener: ShutdownListener, ) -> Self { @@ -176,11 +175,11 @@ impl SocksClient { connection_id, stream: StreamState::Available(stream), auth_nmethods: 0, - socks_version: 0, + socks_version: None, authenticator, input_sender, - service_provider, - self_address, + service_provider: *service_provider, + self_address: *self_address, started_proxy: false, lane_queue_lengths, shutdown_listener, @@ -192,13 +191,45 @@ impl SocksClient { rng.next_u64() } + pub async fn send_error(&mut self, err: SocksProxyError) -> Result<(), SocksProxyError> { + let error_text = format!("{}", err); + let version = self + .socks_version + .as_ref() + .expect("Trying to send error without knowing the version"); + + match version { + SocksVersion::V4 => { + let response = ResponseCodeV4::RequestRejected; + self.send_error_v4(response).await + } + SocksVersion::V5 => { + let response = if error_text.contains("Host") { + ResponseCodeV5::HostUnreachable + } else if error_text.contains("Network") { + ResponseCodeV5::NetworkUnreachable + } else if error_text.contains("ttl") { + ResponseCodeV5::TtlExpired + } else { + ResponseCodeV5::Failure + }; + self.send_error_v5(response).await + } + } + } + // Send an error back to the client - pub async fn error(&mut self, r: ResponseCode) -> Result<(), SocksProxyError> { - self.stream.write_all(&[5, r as u8]).await?; + pub async fn send_error_v4(&mut self, r: ResponseCodeV4) -> Result<(), SocksProxyError> { + self.stream.write_all(&[SOCKS4_VERSION, r as u8]).await?; Ok(()) } - /// Shutdown the TcpStream to the client and end the session + pub async fn send_error_v5(&mut self, r: ResponseCodeV5) -> Result<(), SocksProxyError> { + self.stream.write_all(&[SOCKS5_VERSION, r as u8]).await?; + Ok(()) + } + + /// Shutdown the `TcpStream` to the client and end the session pub async fn shutdown(&mut self) -> Result<(), SocksProxyError> { info!("client is shutting down its TCP stream"); self.stream.shutdown().await?; @@ -210,25 +241,27 @@ impl SocksClient { /// is in use and that the client is authenticated, then runs the request. pub async fn run(&mut self) -> Result<(), SocksProxyError> { debug!("New connection from: {}", self.stream.peer_addr()?.ip()); - let mut header = [0u8; 2]; + // Read a byte from the stream and determine the version being requested + let mut header = [0u8]; self.stream.read_exact(&mut header).await?; - self.socks_version = header[0]; - self.auth_nmethods = header[1]; + self.socks_version = match SocksVersion::try_from(header[0]) { + Ok(version) => Some(version), + Err(_err) => { + warn!("Init: Unsupported version: SOCKS{}", header[0]); + return self.shutdown().await; + } + }; - // Handle SOCKS4 requests - if header[0] != SOCKS_VERSION { - warn!("Init: Unsupported version: SOCKS{}", self.socks_version); - self.shutdown().await - } - // Valid SOCKS5 - else { - // Authenticate w/ client - self.authenticate().await?; - // Handle requests - self.handle_request().await + if self.socks_version == Some(SocksVersion::V5) { + let mut auth = [0u8]; + self.stream.read_exact(&mut auth).await?; + self.auth_nmethods = auth[0]; + self.authenticate_socks5().await?; } + + self.handle_request().await } async fn send_connect_to_mixnet(&mut self, remote_address: RemoteAddress) { @@ -286,8 +319,17 @@ impl SocksClient { async fn handle_request(&mut self) -> Result<(), SocksProxyError> { debug!("Handling CONNECT Command"); - let request = SocksRequest::from_stream(&mut self.stream).await?; - let remote_address = request.to_string(); + let version = self + .socks_version + .as_ref() + .expect("Must read version before parsing request"); + + let request = match version { + SocksVersion::V4 => SocksRequest::from_stream_socks4(&mut self.stream).await?, + SocksVersion::V5 => SocksRequest::from_stream_socks5(&mut self.stream).await?, + }; + + let remote_address = request.address_string(); // setup for receiving from the mixnet let (mix_sender, mix_receiver) = mpsc::unbounded(); @@ -296,7 +338,10 @@ impl SocksClient { // Use the Proxy to connect to the specified addr/port SocksCommand::Connect => { trace!("Connecting to: {:?}", remote_address.clone()); - self.acknowledge_socks5().await; + match version { + SocksVersion::V4 => self.acknowledge_socks4().await, + SocksVersion::V5 => self.acknowledge_socks5().await, + } self.started_proxy = true; self.controller_sender @@ -328,8 +373,8 @@ impl SocksClient { async fn acknowledge_socks5(&mut self) { self.stream .write_all(&[ - SOCKS_VERSION, - ResponseCode::Success as u8, + SOCKS5_VERSION, + ResponseCodeV5::Success as u8, RESERVED, 1, 127, @@ -343,13 +388,30 @@ impl SocksClient { .unwrap(); } + /// Writes a Socks4 header back to the requesting client's TCP stream, + async fn acknowledge_socks4(&mut self) { + self.stream + .write_all(&[ + 0, //SOCKS4_VERSION, + ResponseCodeV4::Granted as u8, + 0, + 0, + 127, + 0, + 0, + 1, + ]) + .await + .unwrap(); + } + /// Authenticate the incoming request. Each request is checked for its /// authentication method. A user/password request will extract the /// username and password from the stream, then check with the Authenticator /// to see if the resulting user is allowed. /// /// A lot of this could probably be put into the `SocksRequest::from_stream()` - /// constructor, and/or cleaned up with tokio::codec. It's mostly just + /// constructor, and/or cleaned up with `tokio::codec`. It's mostly just /// read-a-byte-or-two. The bytes being extracted look like this: /// /// +----+------+----------+------+------------+ @@ -361,7 +423,7 @@ impl SocksClient { /// Pulling out the stream code into its own home, and moving the if/else logic /// into the Authenticator (where it'll be more easily testable) /// would be a good next step. - async fn authenticate(&mut self) -> Result<(), SocksProxyError> { + async fn authenticate_socks5(&mut self) -> Result<(), SocksProxyError> { debug!("Authenticating w/ {}", self.stream.peer_addr()?.ip()); // Get valid auth methods let methods = self.get_available_methods().await?; @@ -370,7 +432,7 @@ impl SocksClient { let mut response = [0u8; 2]; // Set the version in the response - response[0] = SOCKS_VERSION; + response[0] = SOCKS5_VERSION; if methods.contains(&(AuthenticationMethods::UserPass as u8)) { // Set the default auth method (NO AUTH) response[1] = AuthenticationMethods::UserPass as u8; @@ -406,11 +468,11 @@ impl SocksClient { // Authenticate passwords if self.authenticator.is_allowed(&user) { debug!("Access Granted. User: {}", user.username); - let response = [1, ResponseCode::Success as u8]; + let response = [1, ResponseCodeV5::Success as u8]; self.stream.write_all(&response).await?; } else { debug!("Access Denied. User: {}", user.username); - let response = [1, ResponseCode::Failure as u8]; + let response = [1, ResponseCodeV5::Failure as u8]; self.stream.write_all(&response).await?; // Shutdown @@ -429,7 +491,7 @@ impl SocksClient { response[1] = AuthenticationMethods::NoMethods as u8; self.stream.write_all(&response).await?; self.shutdown().await?; - Err(ResponseCode::Failure.into()) + Err(ResponseCodeV5::Failure.into()) } } diff --git a/clients/socks5/src/socks/mod.rs b/clients/socks5/src/socks/mod.rs index 20dd99f125..2acdaa615b 100644 --- a/clients/socks5/src/socks/mod.rs +++ b/clients/socks5/src/socks/mod.rs @@ -1,5 +1,9 @@ #![forbid(unsafe_code)] +use std::convert::TryFrom; + +use self::types::SocksProxyError; + pub mod authentication; mod client; pub(crate) mod mixnet_responses; @@ -9,6 +13,27 @@ pub mod types; pub mod utils; /// Version of socks -const SOCKS_VERSION: u8 = 0x05; +const SOCKS4_VERSION: u8 = 0x04; +const SOCKS5_VERSION: u8 = 0x05; const RESERVED: u8 = 0x00; + +#[derive(Clone, PartialEq, Eq)] +pub enum SocksVersion { + V4 = 0x04, + V5 = 0x05, +} + +pub struct InvalidSocksVersion; + +impl TryFrom for SocksVersion { + type Error = SocksProxyError; + + fn try_from(version: u8) -> Result { + match version { + SOCKS4_VERSION => Ok(Self::V4), + SOCKS5_VERSION => Ok(Self::V5), + _ => Err(SocksProxyError::UnsupportedProxyVersion(version)), + } + } +} diff --git a/clients/socks5/src/socks/request.rs b/clients/socks5/src/socks/request.rs index 92d687aa90..ecf9b656f3 100644 --- a/clients/socks5/src/socks/request.rs +++ b/clients/socks5/src/socks/request.rs @@ -1,5 +1,7 @@ -use super::types::{AddrType, ResponseCode, SocksProxyError}; -use super::{utils as socks_utils, SOCKS_VERSION}; +use crate::socks::SOCKS4_VERSION; + +use super::types::{AddrType, ResponseCodeV5, SocksProxyError}; +use super::{utils as socks_utils, SOCKS5_VERSION}; use log::*; use std::fmt::{self, Display}; use tokio::io::{AsyncRead, AsyncReadExt}; @@ -15,80 +17,114 @@ pub(crate) struct SocksRequest { } impl SocksRequest { - /// Parse a SOCKS5 request from a TcpStream - pub async fn from_stream(stream: &mut R) -> Result + /// Parse a SOCKS4 request from a `TcpStream` + /// From documents at: + /// - SOCKS4: https://www.openssh.com/txt/socks4.protocol + /// - SOCKS4a: https://www.openssh.com/txt/socks4a.protocol + pub async fn from_stream_socks4(stream: &mut R) -> Result where R: AsyncRead + Unpin, { + log::trace!("read from stream socks4"); + + let mut packet = [0u8; 3]; + stream.read_exact(&mut packet).await?; + + // CD (command) + let Some(command) = SocksCommand::from(packet[0] as usize) else { + log::warn!("Invalid Command"); + return Err(ResponseCodeV5::CommandNotSupported.into()); + }; + + // DSTPORT + let mut port = [0u8; 2]; + port.copy_from_slice(&packet[1..]); + let port = merge_u8_into_u16(port[0], port[1]); + + // DSTIP + let mut ip = [0u8; 4]; + stream.read_exact(&mut ip).await?; + + // USERID + let _userid = read_until_zero(stream).await; + + // SOCKS4a extension + // https://www.openssh.com/txt/socks4a.protocol + // If the IP is 0.0.0.x with x nonzero, read the domain name + let (addr, addr_type) = if ip[..3] == [0, 0, 0] && ip[3] != 0 { + (read_until_zero(stream).await?, AddrType::Domain) + } else { + (ip.to_vec(), AddrType::V4) + }; + + // Return parsed request + Ok(SocksRequest { + version: SOCKS4_VERSION, + command, + addr_type, + addr, + port, + }) + } + /// Parse a SOCKS5 request from a `TcpStream` + /// From: https://www.rfc-editor.org/rfc/rfc1928 + pub async fn from_stream_socks5(stream: &mut R) -> Result + where + R: AsyncRead + Unpin, + { + log::info!("read from stream socks5"); + let mut packet = [0u8; 4]; // Read a byte from the stream and determine the version being requested stream.read_exact(&mut packet).await?; - if packet[0] != SOCKS_VERSION { - warn!("from_stream Unsupported version: SOCKS{}", packet[0]); + // VER + if packet[0] != SOCKS5_VERSION { + warn!("Unsupported version: SOCKS{}", packet[0]); return Err(SocksProxyError::UnsupportedProxyVersion(packet[0])); } - // Get command - let mut command: SocksCommand = SocksCommand::Connect; - match SocksCommand::from(packet[1] as usize) { - Some(com) => { - command = com; - Ok(()) - } - None => { - warn!("Invalid Command"); - Err(ResponseCode::CommandNotSupported) - } - }?; + // CMD + let Some(command) = SocksCommand::from(packet[1] as usize) else { + warn!("Invalid Command"); + return Err(ResponseCodeV5::CommandNotSupported.into()); + }; - // DST.address + // RSV + // packet[2] is reserved - let mut addr_type: AddrType = AddrType::V6; - match AddrType::from(packet[3] as usize) { - Some(addr) => { - addr_type = addr; - Ok(()) - } - None => { - error!("No Addr"); - Err(ResponseCode::AddrTypeNotSupported) - } - }?; + // ATYP + let Some(addr_type) = AddrType::from(packet[3] as usize) else { + error!("No Addr"); + return Err(ResponseCodeV5::AddrTypeNotSupported.into()) + }; - trace!("Getting Addr"); - // Get Addr from addr_type and stream - let addr: Result, SocksProxyError> = match addr_type { + // DST.ADDR + let addr = match addr_type { AddrType::Domain => { - let mut domain_length = [0u8; 1]; + let mut domain_length = [0u8]; stream.read_exact(&mut domain_length).await?; - let mut domain = vec![0u8; domain_length[0] as usize]; stream.read_exact(&mut domain).await?; - - Ok(domain) + domain } AddrType::V4 => { let mut addr = [0u8; 4]; stream.read_exact(&mut addr).await?; - Ok(addr.to_vec()) + addr.to_vec() } AddrType::V6 => { let mut addr = [0u8; 16]; stream.read_exact(&mut addr).await?; - Ok(addr.to_vec()) + addr.to_vec() } }; - let addr = addr?; - - // read DST.port + // DST.PORT let mut port = [0u8; 2]; stream.read_exact(&mut port).await?; - // Merge two u8s into u16 - let port = (u16::from(port[0]) << 8) | u16::from(port[1]); + let port = merge_u8_into_u16(port[0], port[1]); - // Return parsed request Ok(SocksRequest { version: packet[0], command, @@ -97,14 +133,18 @@ impl SocksRequest { port, }) } + + /// Print out the address and port to a String. + /// This might return domain:port, ipv6:port, or ipv4:port. + pub fn address_string(&self) -> String { + let address = socks_utils::pretty_print_addr(&self.addr_type, &self.addr); + format!("{}:{}", address, self.port) + } } impl Display for SocksRequest { - /// Print out the address and port to a String. - /// This might return domain:port, ipv6:port, or ipv4:port. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let address = socks_utils::pretty_print_addr(&self.addr_type, &self.addr); - write!(f, "{}:{}", address, self.port) + write!(f, "{}", self.address_string()) } } @@ -127,3 +167,23 @@ impl SocksCommand { } } } + +fn merge_u8_into_u16(a: u8, b: u8) -> u16 { + (u16::from(a) << 8) | u16::from(b) +} + +async fn read_until_zero(stream: &mut R) -> Result, SocksProxyError> +where + R: AsyncRead + Unpin, +{ + let mut result = Vec::new(); + let mut char = [0u8]; + loop { + stream.read_exact(&mut char).await?; + if char[0] == 0 { + break; + } + result.push(char[0]); + } + Ok(result) +} diff --git a/clients/socks5/src/socks/server.rs b/clients/socks5/src/socks/server.rs index 551f82b788..1046ca00f8 100644 --- a/clients/socks5/src/socks/server.rs +++ b/clients/socks5/src/socks/server.rs @@ -1,8 +1,8 @@ use crate::error::Socks5ClientError; -use super::authentication::Authenticator; -use super::client::SocksClient; -use super::{mixnet_responses::MixnetResponseListener, types::ResponseCode}; +use super::{ + authentication::Authenticator, client::SocksClient, mixnet_responses::MixnetResponseListener, +}; use client_connections::{ConnectionCommandSender, LaneQueueLengths}; use client_core::client::{ inbound_messages::InputMessageSender, received_buffer::ReceivedBufferRequestSender, @@ -85,47 +85,26 @@ impl SphinxSocksServer { loop { tokio::select! { Ok((stream, _remote)) = listener.accept() => { - // TODO Optimize this let mut client = SocksClient::new( stream, self.authenticator.clone(), input_sender.clone(), - self.service_provider, + &self.service_provider, controller_sender.clone(), - self.self_address, + &self.self_address, self.lane_queue_lengths.clone(), self.shutdown.clone(), ); tokio::spawn(async move { - { - match client.run().await { - Ok(_) => {} - Err(error) => { - error!("Error! {}", error); - let error_text = format!("{}", error); - - let response: ResponseCode; - - if error_text.contains("Host") { - response = ResponseCode::HostUnreachable; - } else if error_text.contains("Network") { - response = ResponseCode::NetworkUnreachable; - } else if error_text.contains("ttl") { - response = ResponseCode::TtlExpired - } else { - response = ResponseCode::Failure - } - - if client.error(response).await.is_err() { - warn!("Failed to send error code"); - }; - if client.shutdown().await.is_err() { - warn!("Failed to shutdown TcpStream"); - }; - } + if let Err(err) = client.run().await { + error!("Error! {}", err); + if client.send_error(err).await.is_err() { + warn!("Failed to error code"); + }; + if client.shutdown().await.is_err() { + warn!("Failed to shutdown TcpStream"); }; - // client gets dropped here } }); }, diff --git a/clients/socks5/src/socks/types.rs b/clients/socks5/src/socks/types.rs index b043da5ccc..3d8e5b161c 100644 --- a/clients/socks5/src/socks/types.rs +++ b/clients/socks5/src/socks/types.rs @@ -1,7 +1,17 @@ use snafu::Snafu; -#[derive(Debug, Snafu)] + +/// SOCKS4 Response codes +#[allow(dead_code)] +pub(crate) enum ResponseCodeV4 { + Granted = 0x5a, + RequestRejected = 0x5b, + CannotConnectToIdent = 0x5c, + DifferentUserId = 0x5d, +} + /// Possible SOCKS5 Response Codes -pub(crate) enum ResponseCode { +#[derive(Debug, Snafu)] +pub(crate) enum ResponseCodeV5 { Success = 0x00, #[snafu(display("SOCKS5 Server Failure"))] Failure = 0x01, @@ -48,7 +58,7 @@ where } /// DST.addr variant types -#[derive(PartialEq)] +#[derive(Debug, PartialEq)] pub(crate) enum AddrType { V4 = 0x01, Domain = 0x03,