Implement client-side SOCKS handshake

This commit is contained in:
Nick Mathewson 2022-09-27 12:41:48 -04:00
parent d06dcefd7b
commit af99979689
8 changed files with 323 additions and 7 deletions

View File

@ -13,6 +13,7 @@ repository = "https://gitlab.torproject.org/tpo/core/arti.git/"
[features]
default = ["proxy-handshake"]
client-handshake = []
proxy-handshake = []
[dependencies]

View File

@ -0,0 +1,2 @@
MODIFIED: Added SocksClientHandshake for client-side support.

View File

@ -37,6 +37,10 @@ pub enum Error {
#[error("SOCKS handshake was finished; no need to call this again")]
AlreadyFinished(tor_error::Bug),
/// The SOCKS proxy refused our authentication.
#[error("SOCKS Authentication failed")]
AuthRejected,
/// The program (perhaps this module, perhaps Arti, perhaps the caller) is buggy
#[error("Bug while handling SOCKS handshake")]
Bug(#[from] tor_error::Bug),
@ -57,6 +61,7 @@ impl HasKind for Error {
}
E::Syntax | E::Decode(_) | E::BadProtocol(_) => EK::LocalProtocolViolation,
E::NotImplemented(_) => EK::NotImplemented,
E::AuthRejected => EK::LocalProtocolFailed,
E::AlreadyFinished(e) => e.kind(),
E::Bug(e) => e.kind(),
}

View File

@ -1,5 +1,7 @@
//! Implement the socks handshakes.
#[cfg(feature = "client-handshake")]
pub(crate) mod client;
#[cfg(feature = "proxy-handshake")]
pub(crate) mod proxy;
@ -8,6 +10,12 @@ use std::net::IpAddr;
use tor_bytes::Result as BytesResult;
use tor_bytes::{EncodeResult, Error as BytesError, Readable, Reader, Writeable, Writer};
/// Constant for Username/Password-style authentication.
/// (See RFC 1929)
const USERNAME_PASSWORD: u8 = 0x02;
/// Constant for "no authentication".
const NO_AUTHENTICATION: u8 = 0x00;
/// An action to take in response to a SOCKS handshake message.
#[derive(Clone, Debug)]
#[non_exhaustive]
@ -15,7 +23,7 @@ pub struct Action {
/// If nonzero, this many bytes should be drained from the
/// client's inputs.
pub drain: usize,
/// If nonempty, this reply should be sent to the client.
/// If nonempty, this reply should be sent to the other party.
pub reply: Vec<u8>,
/// If true, then this handshake is over, either successfully or not.
pub finished: bool,

View File

@ -0,0 +1,292 @@
//! Implementation for a SOCKS client handshake.
use super::{Action, NO_AUTHENTICATION, USERNAME_PASSWORD};
use crate::msg::{SocksAddr, SocksAuth, SocksRequest, SocksStatus, SocksVersion};
use crate::{Error, Result, TResult, Truncated};
use tor_bytes::{Reader, Writer};
use tor_error::{internal, into_internal};
use std::net::IpAddr;
/// The client (initiator) side of a SOCKS handshake.
#[derive(Clone, Debug)]
pub struct SocksClientHandshake {
/// The request that we are currently trying to negotiate with the proxy.
request: SocksRequest,
/// Our current state in negotiating that request.
state: State,
/// If present, the return message that we received from the proxy.
status: Option<SocksStatus>,
}
/// An internal state for a `SocksClientHandshake`.
#[derive(Clone, Debug)]
enum State {
/// We have sent nothing yet.
Initial,
/// We have sent a SOCKS4 request, and are waiting for a response.
Socks4Wait,
/// We have sent a SOCKS5 init message, and are waiting to hear what kind
/// of authentication to use.
Socks5AuthWait,
/// We have sent a SOCKS5 username/password, and are waiting to hear whether
/// it's accepted.
Socks5UsernameWait,
/// We have sent a SOCKS5 request, and are waiting for a response.
Socks5Wait,
/// We have received the final reply from the proxy. This reply may be
/// successful or unsuccessful, depending on the value of
/// `SocksClientHandshake::status`.
Done,
/// The handshake has failed and no further progress can be made.
Failed,
}
impl SocksClientHandshake {
/// Construct a new [`SocksClientHandshake`] that will attempt to negotiate
/// with a peer using `request`.
pub fn new(request: SocksRequest) -> Self {
SocksClientHandshake {
request,
state: State::Initial,
status: None,
}
}
/// Consume this handshake's state; if it finished successfully,
/// return a SocksStatus.
pub fn into_status(self) -> Option<SocksStatus> {
self.status
}
/// Try to advance a SocksProxyHandshake, given some proxy input in
/// `input`.
///
/// If there isn't enough input, gives a [`Truncated`]. Other
/// errors indicate a failure.
///
/// On success, return an Action describing what to tell the proxy,
/// and how much of its input to consume.
pub fn handshake(&mut self, input: &[u8]) -> TResult<Action> {
use State::*;
let rv = match self.state {
Initial => match self.request.version() {
SocksVersion::V4 => self.send_v4(),
SocksVersion::V5 => self.send_v5_initial(),
},
Socks4Wait => self.handle_v4(input),
Socks5AuthWait => self.handle_v5_auth(input),
Socks5UsernameWait => self.handle_v5_username_ack(input),
Socks5Wait => self.handle_v5_final(input),
Done => Err(Error::AlreadyFinished(internal!(
"called handshake() after handshaking succeeded"
))),
Failed => Err(Error::AlreadyFinished(internal!(
"called handshake() after handshaking failed"
))),
};
match rv {
Err(Error::Decode(tor_bytes::Error::Truncated)) => Err(Truncated::new()),
Err(e) => {
self.state = State::Failed;
Ok(Err(e))
}
Ok(a) => Ok(Ok(a)),
}
}
/// Send the client side of the socks 4 handshake.
fn send_v4(&mut self) -> Result<Action> {
let mut msg = Vec::new();
msg.write_u8(4);
msg.write_u8(self.request.command().into());
msg.write_u16(self.request.port());
let use_v4a = match self.request.addr() {
SocksAddr::Ip(IpAddr::V4(ipv4)) => {
msg.write_u32((*ipv4).into());
false
}
_ => {
msg.write_u32(1);
true
}
};
match self.request.auth() {
SocksAuth::NoAuth => msg.write_u8(0),
SocksAuth::Socks4(s) => {
msg.write_all(s);
msg.write_u8(0);
}
SocksAuth::Username(_, _) => {
return Err(internal!("tried to send socks5 auth over socks4.").into())
}
}
if use_v4a {
// We are using socks4a, so we need to send the address now.
msg.write_all(self.request.addr().to_string().as_bytes());
msg.write_u8(0);
}
self.state = State::Socks4Wait;
Ok(Action {
drain: 0,
reply: msg,
finished: false,
})
}
/// Handle a SOCKSv4 response.
fn handle_v4(&mut self, input: &[u8]) -> Result<Action> {
let mut r = Reader::from_slice(input);
let ver = r.take_u8()?;
if ver != 0 {
return Err(Error::Syntax);
}
let status = r.take_u8()?;
// We ignore these.
let _port = r.take_u16()?;
let _ip = r.take_u32()?;
self.state = State::Done;
self.status = Some(SocksStatus::from_socks4_status(status));
Ok(Action {
drain: r.consumed(),
reply: Vec::new(),
finished: true,
})
}
/// Send our initial socks5 message (which negotiates our authentication methods).
fn send_v5_initial(&mut self) -> Result<Action> {
let mut msg = Vec::new();
msg.write_u8(5);
match self.request.auth() {
SocksAuth::NoAuth => {
msg.write_u8(1); // 1 method.
msg.write_u8(NO_AUTHENTICATION);
}
SocksAuth::Socks4(_) => return Err(internal!("Mismatched authentication type").into()),
SocksAuth::Username(_, _) => {
msg.write_u8(2); // 2 methods.
msg.write_u8(USERNAME_PASSWORD);
msg.write_u8(NO_AUTHENTICATION);
}
}
self.state = State::Socks5AuthWait;
Ok(Action {
drain: 0,
reply: msg,
finished: false,
})
}
/// Try to handle a socks5 reply telling us what authentication method to
/// use, and reply as appropriate.
fn handle_v5_auth(&mut self, input: &[u8]) -> Result<Action> {
let mut r = Reader::from_slice(input);
let ver = r.take_u8()?;
if ver != 5 {
return Err(Error::Syntax);
}
let auth = r.take_u8()?;
let (msg, next_state) = match auth {
USERNAME_PASSWORD => (self.generate_v5_username_auth()?, State::Socks5UsernameWait),
NO_AUTHENTICATION => (self.generate_v5_command()?, State::Socks5Wait),
other => {
return Err(Error::NotImplemented(
format!("authentication type {}", other).into(),
))
}
};
self.state = next_state;
Ok(Action {
drain: r.consumed(),
reply: msg,
finished: false,
})
}
/// Try to handle a reply from the socks5 proxy to acknowledge our
/// username/password authentication, and reply as appropriate.
fn handle_v5_username_ack(&mut self, input: &[u8]) -> Result<Action> {
let mut r = Reader::from_slice(input);
let ver = r.take_u8()?;
if ver != 1 {
return Err(Error::Syntax);
}
let result = r.take_u8()?;
if result != 0 {
return Err(Error::AuthRejected);
}
self.state = State::Socks5Wait;
Ok(Action {
drain: r.consumed(),
reply: self.generate_v5_command()?,
finished: false,
})
}
/// Handle a final socks5 reply.
fn handle_v5_final(&mut self, input: &[u8]) -> Result<Action> {
let mut r = Reader::from_slice(input);
let ver = r.take_u8()?;
if ver != 5 {
return Err(Error::Syntax);
}
let status: SocksStatus = r.take_u8()?.into();
let _reserved = r.take_u8()?;
let _addr: SocksAddr = r.extract()?;
let _port = r.take_u16()?;
self.state = State::Done;
self.status = Some(status);
Ok(Action {
drain: r.consumed(),
reply: Vec::new(),
finished: true,
})
}
/// Return a message to perform username/password authentication.
fn generate_v5_username_auth(&self) -> Result<Vec<u8>> {
if let SocksAuth::Username(username, pass) = self.request.auth() {
let mut msg = Vec::new();
msg.write_u8(1); // version
let mut n = msg.write_nested_u8len();
n.write_all(username);
n.finish().map_err(into_internal!("id too long"))?;
let mut n = msg.write_nested_u8len();
n.write_all(pass);
n.finish().map_err(into_internal!("password too long"))?;
Ok(msg)
} else {
// Can't perform this authentication when it wasn't what we asked for.
Err(Error::Syntax)
}
}
/// Return a message to encode our final socks5 request.
fn generate_v5_command(&self) -> Result<Vec<u8>> {
let mut msg = Vec::new();
msg.write_u8(5); // version
msg.write_u8(self.request.command().into());
msg.write_u8(0); // reserved.
msg.write(self.request.addr())
.map_err(into_internal!("Can't encode address"))?;
msg.write_u16(self.request.port());
Ok(msg)
}
}

View File

@ -137,18 +137,13 @@ impl SocksProxyHandshake {
/// Socks5: initial handshake to negotiate authentication method.
fn s5_initial(&mut self, input: &[u8]) -> Result<Action> {
use super::{NO_AUTHENTICATION, USERNAME_PASSWORD};
let mut r = Reader::from_slice(input);
let version: SocksVersion = r.take_u8()?.try_into()?;
if version != SocksVersion::V5 {
return Err(internal!("called on wrong handshake type {:?}", version).into());
}
/// Constant for Username/Password-style authentication.
/// (See RFC 1929)
const USERNAME_PASSWORD: u8 = 0x02;
/// Constant for "no authentication".
const NO_AUTHENTICATION: u8 = 0x00;
let nmethods = r.take_u8()?;
let methods = r.take(nmethods as usize)?;

View File

@ -104,6 +104,10 @@ pub use handshake::Action;
#[cfg_attr(docsrs, doc(cfg(feature = "proxy-handshake")))]
pub use handshake::proxy::SocksProxyHandshake;
#[cfg(feature = "client-handshake")]
#[cfg_attr(docsrs, doc(cfg(feature = "client-handshake")))]
pub use handshake::client::SocksClientHandshake;
#[deprecated(since = "0.5.2", note = "Use SocksProxyHandshake instead.")]
#[cfg(feature = "proxy-handshake")]
#[cfg_attr(docsrs, doc(cfg(feature = "proxy-handshake")))]

View File

@ -155,6 +155,15 @@ impl SocksStatus {
_ => 0x5B,
}
}
/// Create a status from a SOCKS4 or SOCKS4a reply code.
pub(crate) fn from_socks4_status(status: u8) -> Self {
match status {
0x5A => SocksStatus::SUCCEEDED,
0x5B => SocksStatus::GENERAL_FAILURE,
0x5C | 0x5D => SocksStatus::NOT_ALLOWED,
_ => SocksStatus::GENERAL_FAILURE,
}
}
}
impl TryFrom<String> for SocksHostname {