Implement client-side SOCKS handshake
This commit is contained in:
parent
d06dcefd7b
commit
af99979689
|
@ -13,6 +13,7 @@ repository = "https://gitlab.torproject.org/tpo/core/arti.git/"
|
|||
|
||||
[features]
|
||||
default = ["proxy-handshake"]
|
||||
client-handshake = []
|
||||
proxy-handshake = []
|
||||
|
||||
[dependencies]
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
MODIFIED: Added SocksClientHandshake for client-side support.
|
||||
|
|
@ -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(),
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)?;
|
||||
|
||||
|
|
|
@ -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")))]
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue