diff --git a/src/client/legacy/connect/proxy/mod.rs b/src/client/legacy/connect/proxy/mod.rs index b7a7c141..56ca3291 100644 --- a/src/client/legacy/connect/proxy/mod.rs +++ b/src/client/legacy/connect/proxy/mod.rs @@ -1,5 +1,6 @@ //! Proxy helpers - +mod socks; mod tunnel; +pub use self::socks::{SocksV4, SocksV5}; pub use self::tunnel::Tunnel; diff --git a/src/client/legacy/connect/proxy/socks/mod.rs b/src/client/legacy/connect/proxy/socks/mod.rs new file mode 100644 index 00000000..20eac572 --- /dev/null +++ b/src/client/legacy/connect/proxy/socks/mod.rs @@ -0,0 +1,121 @@ +mod v5; +pub use v5::{SocksV5, SocksV5Error}; + +mod v4; +pub use v4::{SocksV4, SocksV4Error}; + +use bytes::BytesMut; + +use hyper::rt::Read; + +#[derive(Debug)] +pub enum SocksError { + Inner(C), + Io(std::io::Error), + + DnsFailure, + MissingHost, + MissingPort, + + V4(SocksV4Error), + V5(SocksV5Error), + + Parsing(ParsingError), + Serialize(SerializeError), +} + +#[derive(Debug)] +pub enum ParsingError { + Incomplete, + WouldOverflow, + Other, +} + +#[derive(Debug)] +pub enum SerializeError { + WouldOverflow, +} + +async fn read_message(mut conn: &mut T, buf: &mut BytesMut) -> Result> +where + T: Read + Unpin, + M: for<'a> TryFrom<&'a mut BytesMut, Error = ParsingError>, +{ + let mut tmp = [0; 513]; + + loop { + let n = crate::rt::read(&mut conn, &mut tmp).await?; + buf.extend_from_slice(&tmp[..n]); + + match M::try_from(buf) { + Err(ParsingError::Incomplete) => { + if n == 0 { + if buf.spare_capacity_mut().len() == 0 { + return Err(SocksError::Parsing(ParsingError::WouldOverflow)); + } else { + return Err(std::io::Error::new( + std::io::ErrorKind::UnexpectedEof, + "unexpected eof", + ) + .into()); + } + } + } + Err(err) => return Err(err.into()), + Ok(res) => return Ok(res), + } + } +} + +impl std::fmt::Display for SocksError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("SOCKS error: ")?; + + match self { + Self::Inner(_) => f.write_str("failed to create underlying connection"), + Self::Io(_) => f.write_str("io error during SOCKS handshake"), + + Self::DnsFailure => f.write_str("could not resolve to acceptable address type"), + Self::MissingHost => f.write_str("missing destination host"), + Self::MissingPort => f.write_str("missing destination port"), + + Self::Parsing(_) => f.write_str("failed parsing server response"), + Self::Serialize(_) => f.write_str("failed serialize request"), + + Self::V4(e) => e.fmt(f), + Self::V5(e) => e.fmt(f), + } + } +} + +impl std::error::Error for SocksError {} + +impl From for SocksError { + fn from(err: std::io::Error) -> Self { + Self::Io(err) + } +} + +impl From for SocksError { + fn from(err: ParsingError) -> Self { + Self::Parsing(err) + } +} + +impl From for SocksError { + fn from(err: SerializeError) -> Self { + Self::Serialize(err) + } +} + +impl From for SocksError { + fn from(err: SocksV4Error) -> Self { + Self::V4(err) + } +} + +impl From for SocksError { + fn from(err: SocksV5Error) -> Self { + Self::V5(err) + } +} diff --git a/src/client/legacy/connect/proxy/socks/v4/errors.rs b/src/client/legacy/connect/proxy/socks/v4/errors.rs new file mode 100644 index 00000000..5fdbd05c --- /dev/null +++ b/src/client/legacy/connect/proxy/socks/v4/errors.rs @@ -0,0 +1,22 @@ +use super::Status; + +#[derive(Debug)] +pub enum SocksV4Error { + IpV6, + Command(Status), +} + +impl From for SocksV4Error { + fn from(err: Status) -> Self { + Self::Command(err) + } +} + +impl std::fmt::Display for SocksV4Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::IpV6 => f.write_str("IPV6 is not supported"), + Self::Command(status) => status.fmt(f), + } + } +} diff --git a/src/client/legacy/connect/proxy/socks/v4/messages.rs b/src/client/legacy/connect/proxy/socks/v4/messages.rs new file mode 100644 index 00000000..c29d677f --- /dev/null +++ b/src/client/legacy/connect/proxy/socks/v4/messages.rs @@ -0,0 +1,131 @@ +use super::super::{ParsingError, SerializeError}; + +use bytes::{Buf, BufMut, BytesMut}; +use std::net::SocketAddrV4; + +/// +-----+-----+----+----+----+----+----+----+-------------+------+------------+------+ +/// | VN | CD | DSTPORT | DSTIP | USERID | NULL | DOMAIN | NULL | +/// +-----+-----+----+----+----+----+----+----+-------------+------+------------+------+ +/// | 1 | 1 | 2 | 4 | Variable | 1 | Variable | 1 | +/// +-----+-----+----+----+----+----+----+----+-------------+------+------------+------+ +/// ^^^^^^^^^^^^^^^^^^^^^ +/// optional: only do IP is 0.0.0.X +#[derive(Debug)] +pub struct Request<'a>(pub &'a Address); + +/// +-----+-----+----+----+----+----+----+----+ +/// | VN | CD | DSTPORT | DSTIP | +/// +-----+-----+----+----+----+----+----+----+ +/// | 1 | 1 | 2 | 4 | +/// +-----+-----+----+----+----+----+----+----+ +/// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +/// ignore: only for SOCKSv4 BIND +#[derive(Debug)] +pub struct Response(pub Status); + +#[derive(Debug)] +pub enum Address { + Socket(SocketAddrV4), + Domain(String, u16), +} + +#[derive(Debug, PartialEq)] +pub enum Status { + Success = 90, + Failed = 91, + IdentFailure = 92, + IdentMismatch = 93, +} + +impl Request<'_> { + pub fn write_to_buf(&self, mut buf: B) -> Result { + match self.0 { + Address::Socket(socket) => { + if buf.remaining_mut() < 10 { + return Err(SerializeError::WouldOverflow); + } + + buf.put_u8(0x04); // Version + buf.put_u8(0x01); // CONNECT + + buf.put_u16(socket.port()); // Port + buf.put_slice(&socket.ip().octets()); // IP + + buf.put_u8(0x00); // USERID + buf.put_u8(0x00); // NULL + + Ok(10) + } + + Address::Domain(domain, port) => { + if buf.remaining_mut() < 10 + domain.len() + 1 { + return Err(SerializeError::WouldOverflow); + } + + buf.put_u8(0x04); // Version + buf.put_u8(0x01); // CONNECT + + buf.put_u16(*port); // IP + buf.put_slice(&[0x00, 0x00, 0x00, 0xFF]); // Invalid IP + + buf.put_u8(0x00); // USERID + buf.put_u8(0x00); // NULL + + buf.put_slice(domain.as_bytes()); // Domain + buf.put_u8(0x00); // NULL + + Ok(10 + domain.len() + 1) + } + } + } +} + +impl TryFrom<&mut BytesMut> for Response { + type Error = ParsingError; + + fn try_from(buf: &mut BytesMut) -> Result { + if buf.remaining() < 8 { + return Err(ParsingError::Incomplete); + } + + if buf.get_u8() != 0x00 { + return Err(ParsingError::Other); + } + + let status = buf.get_u8().try_into()?; + let _addr = { + let port = buf.get_u16(); + let mut ip = [0; 4]; + buf.copy_to_slice(&mut ip); + + SocketAddrV4::new(ip.into(), port) + }; + + return Ok(Self(status)); + } +} + +impl TryFrom for Status { + type Error = ParsingError; + + fn try_from(byte: u8) -> Result { + Ok(match byte { + 90 => Self::Success, + 91 => Self::Failed, + 92 => Self::IdentFailure, + 93 => Self::IdentMismatch, + _ => return Err(ParsingError::Other), + }) + } +} + +impl std::fmt::Display for Status { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::Success => "success", + Self::Failed => "server failed to execute command", + Self::IdentFailure => "server ident service failed", + Self::IdentMismatch => "server ident service did not recognise client identifier", + }) + } +} diff --git a/src/client/legacy/connect/proxy/socks/v4/mod.rs b/src/client/legacy/connect/proxy/socks/v4/mod.rs new file mode 100644 index 00000000..b99bac56 --- /dev/null +++ b/src/client/legacy/connect/proxy/socks/v4/mod.rs @@ -0,0 +1,183 @@ +mod errors; +pub use errors::*; + +mod messages; +use messages::*; + +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use std::net::{IpAddr, SocketAddr, SocketAddrV4, ToSocketAddrs}; + +use http::Uri; +use hyper::rt::{Read, Write}; +use tower_service::Service; + +use bytes::BytesMut; + +use pin_project_lite::pin_project; + +/// Tunnel Proxy via SOCKSv4 +/// +/// This is a connector that can be used by the `legacy::Client`. It wraps +/// another connector, and after getting an underlying connection, it established +/// a TCP tunnel over it using SOCKSv4. +#[derive(Debug, Clone)] +pub struct SocksV4 { + inner: C, + config: SocksConfig, +} + +#[derive(Debug, Clone)] +struct SocksConfig { + proxy: Uri, + local_dns: bool, +} + +pin_project! { + // Not publicly exported (so missing_docs doesn't trigger). + // + // We return this `Future` instead of the `Pin>` directly + // so that users don't rely on it fitting in a `Pin>` slot + // (and thus we can change the type in the future). + #[must_use = "futures do nothing unless polled"] + #[allow(missing_debug_implementations)] + pub struct Handshaking { + #[pin] + fut: BoxHandshaking, + _marker: std::marker::PhantomData + } +} + +type BoxHandshaking = Pin>> + Send>>; + +impl SocksV4 { + /// Create a new SOCKSv4 handshake service + /// + /// Wraps an underlying connector and stores the address of a tunneling + /// proxying server. + /// + /// A `SocksV4` can then be called with any destination. The `dst` passed to + /// `call` will not be used to create the underlying connection, but will + /// be used in a SOCKS handshake with the proxy destination. + pub fn new(proxy_dst: Uri, connector: C) -> Self { + Self { + inner: connector, + config: SocksConfig::new(proxy_dst), + } + } + + /// Resolve domain names locally on the client, rather than on the proxy server. + /// + /// Disabled by default as local resolution of domain names can be detected as a + /// DNS leak. + pub fn local_dns(mut self, local_dns: bool) -> Self { + self.config.local_dns = local_dns; + self + } +} + +impl SocksConfig { + pub fn new(proxy: Uri) -> Self { + Self { + proxy, + local_dns: false, + } + } + + async fn execute( + self, + mut conn: T, + host: String, + port: u16, + ) -> Result> + where + T: Read + Write + Unpin, + { + let address = match host.parse::() { + Ok(IpAddr::V6(_)) => return Err(SocksV4Error::IpV6.into()), + Ok(IpAddr::V4(ip)) => Address::Socket(SocketAddrV4::new(ip.into(), port)), + Err(_) => { + if self.local_dns { + (host, port) + .to_socket_addrs()? + .find_map(|s| { + if let SocketAddr::V4(v4) = s { + Some(Address::Socket(v4)) + } else { + None + } + }) + .ok_or(super::SocksError::DnsFailure)? + } else { + Address::Domain(host, port) + } + } + }; + + let mut send_buf = BytesMut::with_capacity(1024); + let mut recv_buf = BytesMut::with_capacity(1024); + + // Send Request + let req = Request(&address); + let n = req.write_to_buf(&mut send_buf)?; + crate::rt::write_all(&mut conn, &send_buf[..n]).await?; + + // Read Response + let res: Response = super::read_message(&mut conn, &mut recv_buf).await?; + if res.0 == Status::Success { + Ok(conn) + } else { + Err(SocksV4Error::Command(res.0).into()) + } + } +} + +impl Service for SocksV4 +where + C: Service, + C::Future: Send + 'static, + C::Response: Read + Write + Unpin + Send + 'static, + C::Error: Send + 'static, +{ + type Response = C::Response; + type Error = super::SocksError; + type Future = Handshaking; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx).map_err(super::SocksError::Inner) + } + + fn call(&mut self, dst: Uri) -> Self::Future { + let config = self.config.clone(); + let connecting = self.inner.call(config.proxy.clone()); + + let fut = async move { + let port = dst.port().map(|p| p.as_u16()).unwrap_or(443); + let host = dst + .host() + .ok_or(super::SocksError::MissingHost)? + .to_string(); + + let conn = connecting.await.map_err(super::SocksError::Inner)?; + config.execute(conn, host, port).await + }; + + Handshaking { + fut: Box::pin(fut), + _marker: Default::default(), + } + } +} + +impl Future for Handshaking +where + F: Future>, +{ + type Output = Result>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + self.project().fut.poll(cx) + } +} diff --git a/src/client/legacy/connect/proxy/socks/v5/errors.rs b/src/client/legacy/connect/proxy/socks/v5/errors.rs new file mode 100644 index 00000000..06b1a9a8 --- /dev/null +++ b/src/client/legacy/connect/proxy/socks/v5/errors.rs @@ -0,0 +1,47 @@ +use super::Status; + +#[derive(Debug)] +pub enum SocksV5Error { + HostTooLong, + Auth(AuthError), + Command(Status), +} + +#[derive(Debug)] +pub enum AuthError { + Unsupported, + MethodMismatch, + Failed, +} + +impl From for SocksV5Error { + fn from(err: Status) -> Self { + Self::Command(err) + } +} + +impl From for SocksV5Error { + fn from(err: AuthError) -> Self { + Self::Auth(err) + } +} + +impl std::fmt::Display for SocksV5Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::HostTooLong => f.write_str("host address is more than 255 characters"), + Self::Command(e) => e.fmt(f), + Self::Auth(e) => e.fmt(f), + } + } +} + +impl std::fmt::Display for AuthError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::Unsupported => "server does not support user/pass authentication", + Self::MethodMismatch => "server implements authentication incorrectly", + Self::Failed => "credentials not accepted", + }) + } +} diff --git a/src/client/legacy/connect/proxy/socks/v5/messages.rs b/src/client/legacy/connect/proxy/socks/v5/messages.rs new file mode 100644 index 00000000..ddf93538 --- /dev/null +++ b/src/client/legacy/connect/proxy/socks/v5/messages.rs @@ -0,0 +1,347 @@ +use super::super::{ParsingError, SerializeError}; + +use bytes::{Buf, BufMut, BytesMut}; +use std::net::SocketAddr; + +/// +----+----------+----------+ +/// |VER | NMETHODS | METHODS | +/// +----+----------+----------+ +/// | 1 | 1 | 1 to 255 | +/// +----+----------+----------+ +#[derive(Debug)] +pub struct NegotiationReq<'a>(pub &'a AuthMethod); + +/// +----+--------+ +/// |VER | METHOD | +/// +----+--------+ +/// | 1 | 1 | +/// +----+--------+ +#[derive(Debug)] +pub struct NegotiationRes(pub AuthMethod); + +/// +----+------+----------+------+----------+ +/// |VER | ULEN | UNAME | PLEN | PASSWD | +/// +----+------+----------+------+----------+ +/// | 1 | 1 | 1 to 255 | 1 | 1 to 255 | +/// +----+------+----------+------+----------+ +#[derive(Debug)] +pub struct AuthenticationReq<'a>(pub &'a str, pub &'a str); + +/// +----+--------+ +/// |VER | STATUS | +/// +----+--------+ +/// | 1 | 1 | +/// +----+--------+ +#[derive(Debug)] +pub struct AuthenticationRes(pub bool); + +/// +----+-----+-------+------+----------+----------+ +/// |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | +/// +----+-----+-------+------+----------+----------+ +/// | 1 | 1 | X'00' | 1 | Variable | 2 | +/// +----+-----+-------+------+----------+----------+ +#[derive(Debug)] +pub struct ProxyReq<'a>(pub &'a Address); + +/// +----+-----+-------+------+----------+----------+ +/// |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT | +/// +----+-----+-------+------+----------+----------+ +/// | 1 | 1 | X'00' | 1 | Variable | 2 | +/// +----+-----+-------+------+----------+----------+ +#[derive(Debug)] +pub struct ProxyRes(pub Status); + +#[repr(u8)] +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum AuthMethod { + NoAuth = 0x00, + UserPass = 0x02, + NoneAcceptable = 0xFF, +} + +#[derive(Debug)] +pub enum Address { + Socket(SocketAddr), + Domain(String, u16), +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum Status { + Success, + GeneralServerFailure, + ConnectionNotAllowed, + NetworkUnreachable, + HostUnreachable, + ConnectionRefused, + TtlExpired, + CommandNotSupported, + AddressTypeNotSupported, +} + +impl NegotiationReq<'_> { + pub fn write_to_buf(&self, buf: &mut BytesMut) -> Result { + if buf.capacity() - buf.len() < 3 { + return Err(SerializeError::WouldOverflow); + } + + buf.put_u8(0x05); // Version + buf.put_u8(0x01); // Number of authentication methods + buf.put_u8(*self.0 as u8); // Authentication method + + Ok(3) + } +} + +impl TryFrom<&mut BytesMut> for NegotiationRes { + type Error = ParsingError; + + fn try_from(buf: &mut BytesMut) -> Result { + if buf.remaining() < 2 { + return Err(ParsingError::Incomplete); + } + + if buf.get_u8() != 0x05 { + return Err(ParsingError::Other); + } + + let method = buf.get_u8().try_into()?; + Ok(Self(method)) + } +} + +impl AuthenticationReq<'_> { + pub fn write_to_buf(&self, buf: &mut BytesMut) -> Result { + if buf.capacity() - buf.len() < 3 + self.0.len() + self.1.len() { + return Err(SerializeError::WouldOverflow); + } + + buf.put_u8(0x01); // Version + + buf.put_u8(self.0.len() as u8); // Username length (guarenteed to be 255 or less) + buf.put_slice(self.0.as_bytes()); // Username + + buf.put_u8(self.1.len() as u8); // Password length (guarenteed to be 255 or less) + buf.put_slice(self.1.as_bytes()); // Password + + Ok(3 + self.0.len() + self.1.len()) + } +} + +impl TryFrom<&mut BytesMut> for AuthenticationRes { + type Error = ParsingError; + + fn try_from(buf: &mut BytesMut) -> Result { + if buf.remaining() < 2 { + return Err(ParsingError::Incomplete); + } + + if buf.get_u8() != 0x01 { + return Err(ParsingError::Other); + } + + if buf.get_u8() == 0 { + Ok(Self(true)) + } else { + Ok(Self(false)) + } + } +} + +impl ProxyReq<'_> { + pub fn write_to_buf(&self, buf: &mut BytesMut) -> Result { + let addr_len = match self.0 { + Address::Socket(SocketAddr::V4(_)) => 1 + 4 + 2, + Address::Socket(SocketAddr::V6(_)) => 1 + 16 + 2, + Address::Domain(ref domain, _) => 1 + 1 + domain.len() + 2, + }; + + if buf.capacity() - buf.len() < 3 + addr_len { + return Err(SerializeError::WouldOverflow); + } + + buf.put_u8(0x05); // Version + buf.put_u8(0x01); // TCP tunneling command + buf.put_u8(0x00); // Reserved + let _ = self.0.write_to_buf(buf); // Address + + Ok(3 + addr_len) + } +} + +impl TryFrom<&mut BytesMut> for ProxyRes { + type Error = ParsingError; + + fn try_from(buf: &mut BytesMut) -> Result { + if buf.remaining() < 2 { + return Err(ParsingError::Incomplete); + } + + // VER + if buf.get_u8() != 0x05 { + return Err(ParsingError::Other); + } + + // REP + let status = buf.get_u8().try_into()?; + + // RSV + if buf.get_u8() != 0x00 { + return Err(ParsingError::Other); + } + + // ATYP + ADDR + Address::try_from(buf)?; + + Ok(Self(status)) + } +} + +impl Address { + pub fn write_to_buf(&self, buf: &mut BytesMut) -> Result { + match self { + Self::Socket(SocketAddr::V4(v4)) => { + if buf.capacity() - buf.len() < 1 + 4 + 2 { + return Err(SerializeError::WouldOverflow); + } + + buf.put_u8(0x01); + buf.put_slice(&v4.ip().octets()); + buf.put_u16(v4.port()); // Network Order/BigEndian for port + + Ok(7) + } + + Self::Socket(SocketAddr::V6(v6)) => { + if buf.capacity() - buf.len() < 1 + 16 + 2 { + return Err(SerializeError::WouldOverflow); + } + + buf.put_u8(0x04); + buf.put_slice(&v6.ip().octets()); + buf.put_u16(v6.port()); // Network Order/BigEndian for port + + Ok(19) + } + + Self::Domain(domain, port) => { + if buf.capacity() - buf.len() < 1 + 1 + domain.len() + 2 { + return Err(SerializeError::WouldOverflow); + } + + buf.put_u8(0x03); + buf.put_u8(domain.len() as u8); // Guarenteed to be less than 255 + buf.put_slice(domain.as_bytes()); + buf.put_u16(*port); + + Ok(4 + domain.len()) + } + } + } +} + +impl TryFrom<&mut BytesMut> for Address { + type Error = ParsingError; + + fn try_from(buf: &mut BytesMut) -> Result { + if buf.remaining() < 2 { + return Err(ParsingError::Incomplete); + } + + Ok(match buf.get_u8() { + 0x01 => { + let mut ip = [0; 4]; + + if buf.remaining() < 6 { + return Err(ParsingError::Incomplete); + } + + buf.copy_to_slice(&mut ip); + let port = buf.get_u16(); + + Self::Socket(SocketAddr::new(ip.into(), port)) + } + + 0x03 => { + let len = buf.get_u8(); + + if len == 0 { + return Err(ParsingError::Other); + } else if buf.remaining() < (len as usize) + 2 { + return Err(ParsingError::Incomplete); + } + + let domain = std::str::from_utf8(&buf[..len as usize]) + .map_err(|_| ParsingError::Other)? + .to_string(); + + let port = buf.get_u16(); + + Self::Domain(domain, port) + } + + 0x04 => { + let mut ip = [0; 16]; + + if buf.remaining() < 6 { + return Err(ParsingError::Incomplete); + } + buf.copy_to_slice(&mut ip); + let port = buf.get_u16(); + + Self::Socket(SocketAddr::new(ip.into(), port)) + } + + _ => return Err(ParsingError::Other), + }) + } +} + +impl TryFrom for Status { + type Error = ParsingError; + + fn try_from(byte: u8) -> Result { + Ok(match byte { + 0x00 => Self::Success, + + 0x01 => Self::GeneralServerFailure, + 0x02 => Self::ConnectionNotAllowed, + 0x03 => Self::NetworkUnreachable, + 0x04 => Self::HostUnreachable, + 0x05 => Self::ConnectionRefused, + 0x06 => Self::TtlExpired, + 0x07 => Self::CommandNotSupported, + 0x08 => Self::AddressTypeNotSupported, + _ => return Err(ParsingError::Other), + }) + } +} + +impl TryFrom for AuthMethod { + type Error = ParsingError; + + fn try_from(byte: u8) -> Result { + Ok(match byte { + 0x00 => Self::NoAuth, + 0x02 => Self::UserPass, + 0xFF => Self::NoneAcceptable, + + _ => return Err(ParsingError::Other), + }) + } +} + +impl std::fmt::Display for Status { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::Success => "success", + Self::GeneralServerFailure => "general server failure", + Self::ConnectionNotAllowed => "connection not allowed", + Self::NetworkUnreachable => "network unreachable", + Self::HostUnreachable => "host unreachable", + Self::ConnectionRefused => "connection refused", + Self::TtlExpired => "ttl expired", + Self::CommandNotSupported => "command not supported", + Self::AddressTypeNotSupported => "address type not supported", + }) + } +} diff --git a/src/client/legacy/connect/proxy/socks/v5/mod.rs b/src/client/legacy/connect/proxy/socks/v5/mod.rs new file mode 100644 index 00000000..890a5328 --- /dev/null +++ b/src/client/legacy/connect/proxy/socks/v5/mod.rs @@ -0,0 +1,315 @@ +mod errors; +pub use errors::*; + +mod messages; +use messages::*; + +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use std::net::{IpAddr, SocketAddr, ToSocketAddrs}; + +use http::Uri; +use hyper::rt::{Read, Write}; +use tower_service::Service; + +use bytes::BytesMut; + +use pin_project_lite::pin_project; + +/// Tunnel Proxy via SOCKSv5 +/// +/// This is a connector that can be used by the `legacy::Client`. It wraps +/// another connector, and after getting an underlying connection, it established +/// a TCP tunnel over it using SOCKSv5. +#[derive(Debug, Clone)] +pub struct SocksV5 { + inner: C, + config: SocksConfig, +} + +#[derive(Debug, Clone)] +pub struct SocksConfig { + proxy: Uri, + proxy_auth: Option<(String, String)>, + + local_dns: bool, + optimistic: bool, +} + +#[derive(Debug)] +enum State { + SendingNegReq, + ReadingNegRes, + SendingAuthReq, + ReadingAuthRes, + SendingProxyReq, + ReadingProxyRes, +} + +pin_project! { + // Not publicly exported (so missing_docs doesn't trigger). + // + // We return this `Future` instead of the `Pin>` directly + // so that users don't rely on it fitting in a `Pin>` slot + // (and thus we can change the type in the future). + #[must_use = "futures do nothing unless polled"] + #[allow(missing_debug_implementations)] + pub struct Handshaking { + #[pin] + fut: BoxHandshaking, + _marker: std::marker::PhantomData + } +} + +type BoxHandshaking = Pin>> + Send>>; + +impl SocksV5 { + /// Create a new SOCKSv5 handshake service. + /// + /// Wraps an underlying connector and stores the address of a tunneling + /// proxying server. + /// + /// A `SocksV5` can then be called with any destination. The `dst` passed to + /// `call` will not be used to create the underlying connection, but will + /// be used in a SOCKS handshake with the proxy destination. + pub fn new(proxy_dst: Uri, connector: C) -> Self { + Self { + inner: connector, + config: SocksConfig::new(proxy_dst), + } + } + + /// Use User/Pass authentication method during handshake. + /// + /// Username and Password must be maximum of 255 characters each. + /// 0 length strings are allowed despite RFC prohibiting it. This is done so that + /// for compatablity with server implementations that require it for IP authentication. + pub fn with_auth(mut self, user: String, pass: String) -> Self { + self.config.proxy_auth = Some((user, pass)); + self + } + + /// Resolve domain names locally on the client, rather than on the proxy server. + /// + /// Disabled by default as local resolution of domain names can be detected as a + /// DNS leak. + pub fn local_dns(mut self, local_dns: bool) -> Self { + self.config.local_dns = local_dns; + self + } + + /// Send all messages of the handshake optmistically (without waiting for server response). + /// + /// Typical SOCKS handshake with auithentication takes 3 round trips. Optimistic sending + /// can reduce round trip times and dramatically increase speed of handshake at the cost of + /// reduced portability; many server implementations do not support optimistic sending as it + /// is not defined in the RFC (RFC 1928). + /// + /// Recommended to ensure connector works correctly without optimistic sending before trying + /// with optimistic sending. + pub fn send_optimistically(mut self, optimistic: bool) -> Self { + self.config.optimistic = optimistic; + self + } +} + +impl SocksConfig { + fn new(proxy: Uri) -> Self { + Self { + proxy, + proxy_auth: None, + + local_dns: false, + optimistic: false, + } + } + + async fn execute( + self, + mut conn: T, + host: String, + port: u16, + ) -> Result> + where + T: Read + Write + Unpin, + { + let address = match host.parse::() { + Ok(ip) => Address::Socket(SocketAddr::new(ip, port)), + Err(_) if host.len() <= 255 => { + if self.local_dns { + let socket = (host, port) + .to_socket_addrs()? + .next() + .ok_or(super::SocksError::DnsFailure)?; + + Address::Socket(socket) + } else { + Address::Domain(host, port) + } + } + Err(_) => return Err(SocksV5Error::HostTooLong.into()), + }; + + let method = if self.proxy_auth.is_some() { + AuthMethod::UserPass + } else { + AuthMethod::NoAuth + }; + + let mut recv_buf = BytesMut::with_capacity(513); // Max length of valid recievable message is 513 from Auth Request + let mut send_buf = BytesMut::with_capacity(262); // Max length of valid sendable message is 262 from Auth Response + let mut state = State::SendingNegReq; + + loop { + match state { + State::SendingNegReq => { + let req = NegotiationReq(&method); + + let start = send_buf.len(); + req.write_to_buf(&mut send_buf)?; + crate::rt::write_all(&mut conn, &send_buf[start..]).await?; + + if self.optimistic { + if method == AuthMethod::UserPass { + state = State::SendingAuthReq; + } else { + state = State::SendingProxyReq; + } + } else { + state = State::ReadingNegRes; + } + } + + State::ReadingNegRes => { + let res: NegotiationRes = super::read_message(&mut conn, &mut recv_buf).await?; + + if res.0 == AuthMethod::NoneAcceptable { + return Err(SocksV5Error::Auth(AuthError::Unsupported).into()); + } + + if res.0 != method { + return Err(SocksV5Error::Auth(AuthError::MethodMismatch).into()); + } + + if self.optimistic { + if res.0 == AuthMethod::UserPass { + state = State::ReadingAuthRes; + } else { + state = State::ReadingProxyRes; + } + } else { + if res.0 == AuthMethod::UserPass { + state = State::SendingAuthReq; + } else { + state = State::SendingProxyReq; + } + } + } + + State::SendingAuthReq => { + let (user, pass) = self.proxy_auth.as_ref().unwrap(); + let req = AuthenticationReq(&user, &pass); + + let start = send_buf.len(); + req.write_to_buf(&mut send_buf)?; + crate::rt::write_all(&mut conn, &send_buf[start..]).await?; + + if self.optimistic { + state = State::SendingProxyReq; + } else { + state = State::ReadingAuthRes; + } + } + + State::ReadingAuthRes => { + let res: AuthenticationRes = + super::read_message(&mut conn, &mut recv_buf).await?; + + if !res.0 { + return Err(SocksV5Error::Auth(AuthError::Failed).into()); + } + + if self.optimistic { + state = State::ReadingProxyRes; + } else { + state = State::SendingProxyReq; + } + } + + State::SendingProxyReq => { + let req = ProxyReq(&address); + + let start = send_buf.len(); + req.write_to_buf(&mut send_buf)?; + crate::rt::write_all(&mut conn, &send_buf[start..]).await?; + + if self.optimistic { + state = State::ReadingNegRes; + } else { + state = State::ReadingProxyRes; + } + } + + State::ReadingProxyRes => { + let res: ProxyRes = super::read_message(&mut conn, &mut recv_buf).await?; + + if res.0 == Status::Success { + return Ok(conn); + } else { + return Err(SocksV5Error::Command(res.0).into()); + } + } + } + } + } +} + +impl Service for SocksV5 +where + C: Service, + C::Future: Send + 'static, + C::Response: Read + Write + Unpin + Send + 'static, + C::Error: Send + 'static, +{ + type Response = C::Response; + type Error = super::SocksError; + type Future = Handshaking; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx).map_err(super::SocksError::Inner) + } + + fn call(&mut self, dst: Uri) -> Self::Future { + let config = self.config.clone(); + let connecting = self.inner.call(config.proxy.clone()); + + let fut = async move { + let port = dst.port().map(|p| p.as_u16()).unwrap_or(443); + let host = dst + .host() + .ok_or(super::SocksError::MissingHost)? + .to_string(); + + let conn = connecting.await.map_err(super::SocksError::Inner)?; + config.execute(conn, host, port).await + }; + + Handshaking { + fut: Box::pin(fut), + _marker: Default::default(), + } + } +} + +impl Future for Handshaking +where + F: Future>, +{ + type Output = Result>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + self.project().fut.poll(cx) + } +} diff --git a/tests/proxy.rs b/tests/proxy.rs index f828bc1a..86c386f5 100644 --- a/tests/proxy.rs +++ b/tests/proxy.rs @@ -1,8 +1,9 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::net::TcpListener; +use tokio::net::{TcpListener, TcpStream}; use tower_service::Service; -use hyper_util::client::legacy::connect::{proxy::Tunnel, HttpConnector}; +use hyper_util::client::legacy::connect::proxy::{SocksV4, SocksV5, Tunnel}; +use hyper_util::client::legacy::connect::HttpConnector; #[cfg(not(miri))] #[tokio::test] @@ -35,3 +36,442 @@ async fn test_tunnel_works() { t1.await.expect("task 1"); t2.await.expect("task 2"); } + +#[cfg(not(miri))] +#[tokio::test] +async fn test_socks_v5_without_auth_works() { + let proxy_tcp = TcpListener::bind("127.0.0.1:0").await.expect("bind"); + let proxy_addr = proxy_tcp.local_addr().expect("local_addr"); + let proxy_dst = format!("http://{proxy_addr}").parse().expect("uri"); + + let target_tcp = TcpListener::bind("127.0.0.1:0").await.expect("bind"); + let target_addr = target_tcp.local_addr().expect("local_addr"); + let target_dst = format!("http://{target_addr}").parse().expect("uri"); + + let mut connector = SocksV5::new(proxy_dst, HttpConnector::new()); + + // Client + // + // Will use `SocksV5` to establish proxy tunnel. + // Will send "Hello World!" to the target and receive "Goodbye!" back. + let t1 = tokio::spawn(async move { + let conn = connector.call(target_dst).await.expect("tunnel"); + let mut tcp = conn.into_inner(); + + tcp.write_all(b"Hello World!").await.expect("write 1"); + + let mut buf = [0u8; 64]; + let n = tcp.read(&mut buf).await.expect("read 1"); + assert_eq!(&buf[..n], b"Goodbye!"); + }); + + // Proxy + // + // Will receive CONNECT command from client. + // Will connect to target and success code back to client. + // Will blindly tunnel between client and target. + let t2 = tokio::spawn(async move { + let (mut to_client, _) = proxy_tcp.accept().await.expect("accept"); + let mut buf = [0u8; 513]; + + // negotiation req/res + let n = to_client.read(&mut buf).await.expect("read 1"); + assert_eq!(&buf[..n], [0x05, 0x01, 0x00]); + + to_client.write_all(&[0x05, 0x00]).await.expect("write 1"); + + // command req/rs + let [p1, p2] = target_addr.port().to_be_bytes(); + let [ip1, ip2, ip3, ip4] = [0x7f, 0x00, 0x00, 0x01]; + let message = [0x05, 0x01, 0x00, 0x01, ip1, ip2, ip3, ip4, p1, p2]; + let n = to_client.read(&mut buf).await.expect("read 2"); + assert_eq!(&buf[..n], message); + + let mut to_target = TcpStream::connect(target_addr).await.expect("connect"); + + let message = [0x05, 0x00, 0x00, 0x01, ip1, ip2, ip3, ip4, p1, p2]; + to_client.write_all(&message).await.expect("write 2"); + + let (from_client, from_target) = + tokio::io::copy_bidirectional(&mut to_client, &mut to_target) + .await + .expect("proxy"); + + assert_eq!(from_client, 12); + assert_eq!(from_target, 8) + }); + + // Target server + // + // Will accept connection from proxy server + // Will receive "Hello World!" from the client and return "Goodbye!" + let t3 = tokio::spawn(async move { + let (mut io, _) = target_tcp.accept().await.expect("accept"); + let mut buf = [0u8; 64]; + + let n = io.read(&mut buf).await.expect("read 1"); + assert_eq!(&buf[..n], b"Hello World!"); + + io.write_all(b"Goodbye!").await.expect("write 1"); + }); + + t1.await.expect("task - client"); + t2.await.expect("task - proxy"); + t3.await.expect("task - target"); +} + +#[cfg(not(miri))] +#[tokio::test] +async fn test_socks_v5_with_auth_works() { + let proxy_tcp = TcpListener::bind("127.0.0.1:0").await.expect("bind"); + let proxy_addr = proxy_tcp.local_addr().expect("local_addr"); + let proxy_dst = format!("http://{proxy_addr}").parse().expect("uri"); + + let target_tcp = TcpListener::bind("127.0.0.1:0").await.expect("bind"); + let target_addr = target_tcp.local_addr().expect("local_addr"); + let target_dst = format!("http://{target_addr}").parse().expect("uri"); + + let mut connector = + SocksV5::new(proxy_dst, HttpConnector::new()).with_auth("user".into(), "pass".into()); + + // Client + // + // Will use `SocksV5` to establish proxy tunnel. + // Will send "Hello World!" to the target and receive "Goodbye!" back. + let t1 = tokio::spawn(async move { + let conn = connector.call(target_dst).await.expect("tunnel"); + let mut tcp = conn.into_inner(); + + tcp.write_all(b"Hello World!").await.expect("write 1"); + + let mut buf = [0u8; 64]; + let n = tcp.read(&mut buf).await.expect("read 1"); + assert_eq!(&buf[..n], b"Goodbye!"); + }); + + // Proxy + // + // Will receive CONNECT command from client. + // Will connect to target and success code back to client. + // Will blindly tunnel between client and target. + let t2 = tokio::spawn(async move { + let (mut to_client, _) = proxy_tcp.accept().await.expect("accept"); + let mut buf = [0u8; 513]; + + // negotiation req/res + let n = to_client.read(&mut buf).await.expect("read 1"); + assert_eq!(&buf[..n], [0x05, 0x01, 0x02]); + + to_client.write_all(&[0x05, 0x02]).await.expect("write 1"); + + // auth req/res + let n = to_client.read(&mut buf).await.expect("read 2"); + let [u1, u2, u3, u4] = b"user"; + let [p1, p2, p3, p4] = b"pass"; + let message = [0x01, 0x04, *u1, *u2, *u3, *u4, 0x04, *p1, *p2, *p3, *p4]; + assert_eq!(&buf[..n], message); + + to_client.write_all(&[0x01, 0x00]).await.expect("write 2"); + + // command req/res + let n = to_client.read(&mut buf).await.expect("read 3"); + let [p1, p2] = target_addr.port().to_be_bytes(); + let [ip1, ip2, ip3, ip4] = [0x7f, 0x00, 0x00, 0x01]; + let message = [0x05, 0x01, 0x00, 0x01, ip1, ip2, ip3, ip4, p1, p2]; + assert_eq!(&buf[..n], message); + + let mut to_target = TcpStream::connect(target_addr).await.expect("connect"); + + let message = [0x05, 0x00, 0x00, 0x01, ip1, ip2, ip3, ip4, p1, p2]; + to_client.write_all(&message).await.expect("write 3"); + + let (from_client, from_target) = + tokio::io::copy_bidirectional(&mut to_client, &mut to_target) + .await + .expect("proxy"); + + assert_eq!(from_client, 12); + assert_eq!(from_target, 8) + }); + + // Target server + // + // Will accept connection from proxy server + // Will receive "Hello World!" from the client and return "Goodbye!" + let t3 = tokio::spawn(async move { + let (mut io, _) = target_tcp.accept().await.expect("accept"); + let mut buf = [0u8; 64]; + + let n = io.read(&mut buf).await.expect("read 1"); + assert_eq!(&buf[..n], b"Hello World!"); + + io.write_all(b"Goodbye!").await.expect("write 1"); + }); + + t1.await.expect("task - client"); + t2.await.expect("task - proxy"); + t3.await.expect("task - target"); +} + +#[cfg(not(miri))] +#[tokio::test] +async fn test_socks_v5_with_server_resolved_domain_works() { + let proxy_tcp = TcpListener::bind("127.0.0.1:0").await.expect("bind"); + let proxy_addr = proxy_tcp.local_addr().expect("local_addr"); + let proxy_addr = format!("http://{proxy_addr}").parse().expect("uri"); + + let mut connector = SocksV5::new(proxy_addr, HttpConnector::new()) + .with_auth("user".into(), "pass".into()) + .local_dns(false); + + // Client + // + // Will use `SocksV5` to establish proxy tunnel. + // Will send "Hello World!" to the target and receive "Goodbye!" back. + let t1 = tokio::spawn(async move { + let _conn = connector + .call("https://hyper.rs:443".try_into().unwrap()) + .await + .expect("tunnel"); + }); + + // Proxy + // + // Will receive CONNECT command from client. + // Will connect to target and success code back to client. + // Will blindly tunnel between client and target. + let t2 = tokio::spawn(async move { + let (mut to_client, _) = proxy_tcp.accept().await.expect("accept"); + let mut buf = [0u8; 513]; + + // negotiation req/res + let n = to_client.read(&mut buf).await.expect("read 1"); + assert_eq!(&buf[..n], [0x05, 0x01, 0x02]); + + to_client.write_all(&[0x05, 0x02]).await.expect("write 1"); + + // auth req/res + let n = to_client.read(&mut buf).await.expect("read 2"); + let [u1, u2, u3, u4] = b"user"; + let [p1, p2, p3, p4] = b"pass"; + let message = [0x01, 0x04, *u1, *u2, *u3, *u4, 0x04, *p1, *p2, *p3, *p4]; + assert_eq!(&buf[..n], message); + + to_client.write_all(&[0x01, 0x00]).await.expect("write 2"); + + // command req/res + let n = to_client.read(&mut buf).await.expect("read 3"); + + let host = "hyper.rs"; + let port: u16 = 443; + let mut message = vec![0x05, 0x01, 0x00, 0x03, host.len() as u8]; + message.extend(host.bytes()); + message.extend(port.to_be_bytes()); + assert_eq!(&buf[..n], message); + + let mut message = vec![0x05, 0x00, 0x00, 0x03, host.len() as u8]; + message.extend(host.bytes()); + message.extend(port.to_be_bytes()); + to_client.write_all(&message).await.expect("write 3"); + }); + + t1.await.expect("task - client"); + t2.await.expect("task - proxy"); +} + +#[cfg(not(miri))] +#[tokio::test] +async fn test_socks_v5_with_locally_resolved_domain_works() { + let proxy_tcp = TcpListener::bind("127.0.0.1:0").await.expect("bind"); + let proxy_addr = proxy_tcp.local_addr().expect("local_addr"); + let proxy_addr = format!("http://{proxy_addr}").parse().expect("uri"); + + let mut connector = SocksV5::new(proxy_addr, HttpConnector::new()) + .with_auth("user".into(), "pass".into()) + .local_dns(true); + + // Client + // + // Will use `SocksV5` to establish proxy tunnel. + // Will send "Hello World!" to the target and receive "Goodbye!" back. + let t1 = tokio::spawn(async move { + let _conn = connector + .call("https://hyper.rs:443".try_into().unwrap()) + .await + .expect("tunnel"); + }); + + // Proxy + // + // Will receive CONNECT command from client. + // Will connect to target and success code back to client. + // Will blindly tunnel between client and target. + let t2 = tokio::spawn(async move { + let (mut to_client, _) = proxy_tcp.accept().await.expect("accept"); + let mut buf = [0u8; 513]; + + // negotiation req/res + let n = to_client.read(&mut buf).await.expect("read 1"); + assert_eq!(&buf[..n], [0x05, 0x01, 0x02]); + + to_client.write_all(&[0x05, 0x02]).await.expect("write 1"); + + // auth req/res + let n = to_client.read(&mut buf).await.expect("read 2"); + let [u1, u2, u3, u4] = b"user"; + let [p1, p2, p3, p4] = b"pass"; + let message = [0x01, 0x04, *u1, *u2, *u3, *u4, 0x04, *p1, *p2, *p3, *p4]; + assert_eq!(&buf[..n], message); + + to_client.write_all(&[0x01, 0x00]).await.expect("write 2"); + + // command req/res + let n = to_client.read(&mut buf).await.expect("read 3"); + let message = [0x05, 0x01, 0x00, 0x01]; + assert_eq!(&buf[..4], message); + assert_eq!(n, 4 + 4 + 2); + + let message = vec![0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0]; + to_client.write_all(&message).await.expect("write 3"); + }); + + t1.await.expect("task - client"); + t2.await.expect("task - proxy"); +} + +#[cfg(not(miri))] +#[tokio::test] +async fn test_socks_v4_works() { + let proxy_tcp = TcpListener::bind("127.0.0.1:0").await.expect("bind"); + let proxy_addr = proxy_tcp.local_addr().expect("local_addr"); + let proxy_dst = format!("http://{proxy_addr}").parse().expect("uri"); + + let target_tcp = TcpListener::bind("127.0.0.1:0").await.expect("bind"); + let target_addr = target_tcp.local_addr().expect("local_addr"); + let target_dst = format!("http://{target_addr}").parse().expect("uri"); + + let mut connector = SocksV4::new(proxy_dst, HttpConnector::new()); + + // Client + // + // Will use `SocksV4` to establish proxy tunnel. + // Will send "Hello World!" to the target and receive "Goodbye!" back. + let t1 = tokio::spawn(async move { + let conn = connector.call(target_dst).await.expect("tunnel"); + let mut tcp = conn.into_inner(); + + tcp.write_all(b"Hello World!").await.expect("write 1"); + + let mut buf = [0u8; 64]; + let n = tcp.read(&mut buf).await.expect("read 1"); + assert_eq!(&buf[..n], b"Goodbye!"); + }); + + // Proxy + // + // Will receive CONNECT command from client. + // Will connect to target and success code back to client. + // Will blindly tunnel between client and target. + let t2 = tokio::spawn(async move { + let (mut to_client, _) = proxy_tcp.accept().await.expect("accept"); + let mut buf = [0u8; 512]; + + let [p1, p2] = target_addr.port().to_be_bytes(); + let [ip1, ip2, ip3, ip4] = [127, 0, 0, 1]; + let message = [4, 0x01, p1, p2, ip1, ip2, ip3, ip4, 0, 0]; + let n = to_client.read(&mut buf).await.expect("read"); + assert_eq!(&buf[..n], message); + + let mut to_target = TcpStream::connect(target_addr).await.expect("connect"); + + let message = [0, 90, p1, p2, ip1, ip2, ip3, ip4]; + to_client.write_all(&message).await.expect("write"); + + let (from_client, from_target) = + tokio::io::copy_bidirectional(&mut to_client, &mut to_target) + .await + .expect("proxy"); + + assert_eq!(from_client, 12); + assert_eq!(from_target, 8) + }); + + // Target server + // + // Will accept connection from proxy server + // Will receive "Hello World!" from the client and return "Goodbye!" + let t3 = tokio::spawn(async move { + let (mut io, _) = target_tcp.accept().await.expect("accept"); + let mut buf = [0u8; 64]; + + let n = io.read(&mut buf).await.expect("read 1"); + assert_eq!(&buf[..n], b"Hello World!"); + + io.write_all(b"Goodbye!").await.expect("write 1"); + }); + + t1.await.expect("task - client"); + t2.await.expect("task - proxy"); + t3.await.expect("task - target"); +} + +#[cfg(not(miri))] +#[tokio::test] +async fn test_socks_v5_optimistic_works() { + let proxy_tcp = TcpListener::bind("127.0.0.1:0").await.expect("bind"); + let proxy_addr = proxy_tcp.local_addr().expect("local_addr"); + let proxy_dst = format!("http://{proxy_addr}").parse().expect("uri"); + + let target_addr = std::net::SocketAddr::new([127, 0, 0, 1].into(), 1234); + let target_dst = format!("http://{target_addr}").parse().expect("uri"); + + let mut connector = SocksV5::new(proxy_dst, HttpConnector::new()) + .with_auth("ABC".into(), "XYZ".into()) + .send_optimistically(true); + + // Client + // + // Will use `SocksV5` to establish proxy tunnel. + // Will send "Hello World!" to the target and receive "Goodbye!" back. + let t1 = tokio::spawn(async move { + let _ = connector.call(target_dst).await.expect("tunnel"); + }); + + // Proxy + // + // Will receive SOCKS handshake from client. + // Will connect to target and success code back to client. + // Will blindly tunnel between client and target. + let t2 = tokio::spawn(async move { + let (mut to_client, _) = proxy_tcp.accept().await.expect("accept"); + let [p1, p2] = target_addr.port().to_be_bytes(); + + let mut buf = [0; 22]; + let request = vec![ + 5, 1, 2, // Negotiation + 1, 3, 65, 66, 67, 3, 88, 89, 90, // Auth ("ABC"/"XYZ") + 5, 1, 0, 1, 127, 0, 0, 1, p1, p2, // Reply + ]; + + let response = vec![ + 5, 2, // Negotiation, + 1, 0, // Auth, + 5, 0, 0, 1, 127, 0, 0, 1, p1, p2, // Reply + ]; + + // Accept all handshake messages + to_client.read_exact(&mut buf).await.expect("read"); + assert_eq!(request.as_slice(), buf); + + // Send all handshake messages back + to_client + .write_all(response.as_slice()) + .await + .expect("write"); + + to_client.flush().await.expect("flush"); + }); + + t1.await.expect("task - client"); + t2.await.expect("task - proxy"); +}