// Copyright 2020-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::{ make_bincode_serializer, InsufficientSocketDataError, SocketData, Socks5ProtocolVersion, Socks5RequestError, Socks5Response, }; use nym_service_providers_common::interface::{Serializable, ServiceProviderRequest}; use nym_sphinx_addressing::clients::{Recipient, RecipientFormattingError}; use serde::{Deserialize, Serialize}; use std::fmt::{Debug, Formatter}; use tap::TapFallible; use thiserror::Error; pub type ConnectionId = u64; pub type RemoteAddress = String; #[repr(u8)] #[derive(Clone, Copy, Debug)] pub enum RequestFlag { Connect = 0, Send = 1, Query = 2, } impl TryFrom for RequestFlag { type Error = RequestDeserializationError; fn try_from(value: u8) -> Result { match value { _ if value == (RequestFlag::Connect as u8) => Ok(Self::Connect), _ if value == (RequestFlag::Send as u8) => Ok(Self::Send), _ if value == (RequestFlag::Query as u8) => Ok(Self::Query), value => Err(RequestDeserializationError::UnknownRequestFlag { value }), } } } #[derive(Debug, Error)] pub enum RequestDeserializationError { #[error("not enough bytes to recover the length of the address")] AddressLengthTooShort, #[error("not enough bytes to recover the address")] AddressTooShort, #[error("not enough bytes to recover the connection id")] ConnectionIdTooShort, #[error("no data provided")] NoData, #[error("{value} is not a valid request flag")] UnknownRequestFlag { value: u8 }, #[error("too short return address")] ReturnAddressTooShort, #[error("malformed return address - {0}")] MalformedReturnAddress(RecipientFormattingError), #[error("failed to deserialize query request: {source}")] QueryDeserializationError { #[from] source: bincode::Error, }, #[error(transparent)] InvalidSocketData(#[from] InsufficientSocketDataError), } impl RequestDeserializationError { pub fn is_malformed_return(&self) -> bool { matches!(self, RequestDeserializationError::MalformedReturnAddress(_)) } } #[derive(Clone, PartialEq, Eq)] pub struct ConnectRequest { // TODO: is connection_id redundant now? pub conn_id: ConnectionId, pub remote_addr: RemoteAddress, pub return_address: Option, } impl Debug for ConnectRequest { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("ConnectRequest") .field("conn_id", &self.conn_id) .field("remote_addr", &self.remote_addr) .field( "return_address", &self.return_address.map(|r| r.to_string()), ) .finish() } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct SendRequest { pub data: SocketData, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[non_exhaustive] pub enum QueryRequest { OpenProxy, Description, ExitPolicy, } #[derive(Debug, Clone)] pub struct Socks5Request { pub protocol_version: Socks5ProtocolVersion, pub content: Socks5RequestContent, } impl Serializable for Socks5Request { type Error = Socks5RequestError; // legacy requests had the format of // 0 (Message::REQUEST_FLAG) || 0 (RequestFlag::Connect) || for connect requests // 0 (Message::REQUEST_FLAG) || 1 (RequestFlag::Send) || for send requests // the updated formats use // 3 (Socks5ProtocolVersion) || 0 (RequestFlag::Connect) || for connect requests // 3 (Socks5ProtocolVersion) || 1 (RequestFlag::Send) || for send requests // in both cases, the actual data is serialized the same way, so the process is quite straight forward fn into_bytes(self) -> Vec { if let Some(version) = self.protocol_version.as_u8() { std::iter::once(version) .chain(self.content.into_bytes()) .collect() } else { std::iter::once(Self::LEGACY_TYPE_TAG) .chain(self.content.into_bytes()) .collect() } } fn try_from_bytes(b: &[u8]) -> Result { if b.is_empty() { return Err(RequestDeserializationError::NoData.into()); } let protocol_version = Socks5ProtocolVersion::from(b[0]); if protocol_version > Self::max_supported_version() { return Err(Socks5RequestError::UnsupportedProtocolVersion { protocol_version }); } // TODO: handle the case then protocol version if less then the current one. Then we should // make sure to only respond with the same version Ok(Socks5Request { protocol_version, content: Socks5RequestContent::try_from_bytes(&b[1..])?, }) } } impl ServiceProviderRequest for Socks5Request { type ProtocolVersion = Socks5ProtocolVersion; type Response = Socks5Response; type Error = Socks5RequestError; fn provider_specific_version(&self) -> Self::ProtocolVersion { self.protocol_version } fn max_supported_version() -> Self::ProtocolVersion { Socks5ProtocolVersion::new_current() } } impl Socks5Request { // type tag that used to be prepended to all request messages const LEGACY_TYPE_TAG: u8 = 0x00; pub fn new( protocol_version: Socks5ProtocolVersion, content: Socks5RequestContent, ) -> Socks5Request { Socks5Request { protocol_version, content, } } pub fn new_connect( protocol_version: Socks5ProtocolVersion, conn_id: ConnectionId, remote_addr: RemoteAddress, return_address: Option, ) -> Socks5Request { Socks5Request { protocol_version, content: Socks5RequestContent::new_connect(conn_id, remote_addr, return_address), } } pub fn new_send(protocol_version: Socks5ProtocolVersion, data: SocketData) -> Socks5Request { Socks5Request { protocol_version, content: Socks5RequestContent::new_send(data), } } pub fn new_query( protocol_version: Socks5ProtocolVersion, query: QueryRequest, ) -> Socks5Request { Socks5Request { protocol_version, content: Socks5RequestContent::Query(query), } } } /// A request from a SOCKS5 client that a Nym Socks5 service provider should /// take an action for an application using a (probably local) Nym Socks5 proxy. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Socks5RequestContent { /// Start a new TCP connection to the specified `RemoteAddress` and send /// the request data up the connection. /// All responses produced on this `ConnectionId` should come back to the specified `Recipient` Connect(Box), /// Re-use an existing TCP connection, sending more request data up it. Send(SendRequest), Query(QueryRequest), } impl Socks5RequestContent { /// Construct a new Request::Connect instance pub fn new_connect( conn_id: ConnectionId, remote_addr: RemoteAddress, return_address: Option, ) -> Socks5RequestContent { Socks5RequestContent::Connect(Box::new(ConnectRequest { conn_id, remote_addr, return_address, })) } /// Construct a new Request::Send instance pub fn new_send(data: SocketData) -> Socks5RequestContent { Socks5RequestContent::Send(SendRequest { data }) } /// Deserialize the request type, connection id, destination address and port, /// and the request body from bytes. /// // TODO: this was already inaccurate // /// Serialized bytes looks like this: // /// // /// -------------------------------------------------------------------------------------- // /// request_flag | connection_id | address_length | remote_address_bytes | request_data | // /// 1 | 8 | 2 | address_length | ... | // /// -------------------------------------------------------------------------------------- /// /// The request_flag tells us whether this is a new connection request (`new_connect`), /// an already-established connection we should send up (`new_send`), or /// a request to close an established connection (`new_close`). // connect: // RequestFlag::Connect || CONN_ID || ADDR_LEN || ADDR || // // send: // RequestFlag::Send || CONN_ID || LOCAL_CLOSED || DATA // where DATA: SEQ || TRUE_DATA pub fn try_from_bytes(b: &[u8]) -> Result { // each request needs to at least contain flag and ConnectionId if b.is_empty() { return Err(RequestDeserializationError::NoData); } match RequestFlag::try_from(b[0])? { RequestFlag::Connect => { if b.len() < 9 { return Err(RequestDeserializationError::ConnectionIdTooShort); } let conn_id = u64::from_be_bytes([b[1], b[2], b[3], b[4], b[5], b[6], b[7], b[8]]); let connect_request_bytes = &b[9..]; // we need to be able to read at least 2 bytes that specify address length if connect_request_bytes.len() < 2 { return Err(RequestDeserializationError::AddressLengthTooShort); } let address_length = u16::from_be_bytes([connect_request_bytes[0], connect_request_bytes[1]]) as usize; if connect_request_bytes.len() < 2 + address_length { return Err(RequestDeserializationError::AddressTooShort); } let address_start = 2; let address_end = address_start + address_length; let address_bytes = &connect_request_bytes[address_start..address_end]; let remote_address = String::from_utf8_lossy(address_bytes).to_string(); // just a temporary reference to mid-slice for ease of use let recipient_data_bytes = &connect_request_bytes[address_end..]; let return_address = if recipient_data_bytes.is_empty() { None } else { if recipient_data_bytes.len() != Recipient::LEN { return Err(RequestDeserializationError::ReturnAddressTooShort); } let mut return_bytes = [0u8; Recipient::LEN]; return_bytes.copy_from_slice(&recipient_data_bytes[..Recipient::LEN]); Some( Recipient::try_from_bytes(return_bytes) .map_err(RequestDeserializationError::MalformedReturnAddress)?, ) }; Ok(Socks5RequestContent::new_connect( conn_id, remote_address, return_address, )) } RequestFlag::Send => Ok(Socks5RequestContent::Send(SendRequest { data: SocketData::try_from_request_bytes(&b[1..])?, })), RequestFlag::Query => { use bincode::Options; let query = make_bincode_serializer().deserialize(&b[1..])?; Ok(Socks5RequestContent::Query(query)) } } } /// Serialize a Socks5 request into bytes, so that it can be packed inside /// a Sphinx packet, sent through the mixnet, and deserialized by the Socks5 /// service provider which will make the request. pub fn into_bytes(self) -> Vec { match self { // connect is: CONN_FLAG || CONN_ID || REMOTE_LEN || REMOTE || RETURN Socks5RequestContent::Connect(req) => { let remote_address_bytes = req.remote_addr.into_bytes(); let remote_address_bytes_len = remote_address_bytes.len() as u16; let iter = std::iter::once(RequestFlag::Connect as u8) .chain(req.conn_id.to_be_bytes()) .chain(remote_address_bytes_len.to_be_bytes()) .chain(remote_address_bytes); if let Some(return_address) = req.return_address { iter.chain(return_address.to_bytes()).collect() } else { iter.collect() } } Socks5RequestContent::Send(req) => std::iter::once(RequestFlag::Send as u8) .chain(req.data.into_request_bytes_iter()) .collect(), Socks5RequestContent::Query(query) => { use bincode::Options; let query_bytes: Vec = make_bincode_serializer() .serialize(&query) .tap_err(|err| { log::error!("Failed to serialize query request: {:?}: {err}", query); }) .unwrap_or_default(); std::iter::once(RequestFlag::Query as u8) .chain(query_bytes) .collect() } } } } #[cfg(test)] mod request_deserialization_tests { use super::*; mod all_request_types { use super::*; #[test] fn returns_error_when_zero_bytes() { let request_bytes = Vec::new(); match Socks5RequestContent::try_from_bytes(&request_bytes).unwrap_err() { RequestDeserializationError::NoData => {} _ => unreachable!(), } } #[test] fn returns_error_when_connection_id_too_short() { let request_bytes = [RequestFlag::Connect as u8, 1, 2, 3, 4, 5, 6, 7].to_vec(); // 7 bytes connection id match Socks5RequestContent::try_from_bytes(&request_bytes).unwrap_err() { RequestDeserializationError::ConnectionIdTooShort => {} _ => unreachable!(), } } } #[cfg(test)] mod sending_data_over_a_new_connection { use super::*; #[test] fn returns_error_when_address_length_is_too_short() { let request_bytes1 = [RequestFlag::Connect as u8, 1, 2, 3, 4, 5, 6, 7, 8].to_vec(); // 8 bytes connection id, 0 bytes address length (2 were expected) let request_bytes2 = [RequestFlag::Connect as u8, 1, 2, 3, 4, 5, 6, 7, 8, 0].to_vec(); // 8 bytes connection id, 1 bytes address length (2 were expected) match Socks5RequestContent::try_from_bytes(&request_bytes1).unwrap_err() { RequestDeserializationError::AddressLengthTooShort => {} _ => unreachable!(), } match Socks5RequestContent::try_from_bytes(&request_bytes2).unwrap_err() { RequestDeserializationError::AddressLengthTooShort => {} _ => unreachable!(), } } #[test] fn returns_error_when_address_too_short_for_given_address_length() { let request_bytes = [RequestFlag::Connect as u8, 1, 2, 3, 4, 5, 6, 7, 8, 0, 1].to_vec(); // 8 bytes connection id, 2 bytes address length, missing address match Socks5RequestContent::try_from_bytes(&request_bytes).unwrap_err() { RequestDeserializationError::AddressTooShort => {} _ => unreachable!(), } } #[test] fn returns_error_for_when_return_address_is_too_short() { // this one has "foo.com" remote address and correct 8 bytes of connection_id let request_bytes_prefix = [ RequestFlag::Connect as u8, 1, 2, 3, 4, 5, 6, 7, 8, 0, 7, 102, 111, 111, 46, 99, 111, 109, ]; let recipient = Recipient::try_from_base58_string("CytBseW6yFXUMzz4SGAKdNLGR7q3sJLLYxyBGvutNEQV.4QXYyEVc5fUDjmmi8PrHN9tdUFV4PCvSJE1278cHyvoe@4sBbL1ngf1vtNqykydQKTFh26sQCw888GpUqvPvyNB4f").unwrap(); let recipient_bytes = recipient.to_bytes(); // take only part of actual recipient let request_bytes: Vec<_> = request_bytes_prefix .iter() .cloned() .chain(recipient_bytes.iter().take(40).cloned()) .collect(); match Socks5RequestContent::try_from_bytes(&request_bytes).unwrap_err() { RequestDeserializationError::ReturnAddressTooShort => {} _ => unreachable!(), } } #[test] fn returns_error_for_when_return_address_is_malformed() { // this one has "foo.com" remote address and correct 8 bytes of connection_id let request_bytes_prefix = [ RequestFlag::Connect as u8, 1, 2, 3, 4, 5, 6, 7, 8, 0, 7, 102, 111, 111, 46, 99, 111, 109, ]; let recipient = Recipient::try_from_base58_string("CytBseW6yFXUMzz4SGAKdNLGR7q3sJLLYxyBGvutNEQV.4QXYyEVc5fUDjmmi8PrHN9tdUFV4PCvSJE1278cHyvoe@4sBbL1ngf1vtNqykydQKTFh26sQCw888GpUqvPvyNB4f").unwrap(); let mut recipient_bytes = recipient.to_bytes(); // mess up few bytes recipient_bytes[0] = 255; recipient_bytes[15] ^= 1; recipient_bytes[31] ^= 1; let request_bytes: Vec<_> = request_bytes_prefix .iter() .cloned() .chain(recipient_bytes) .collect(); assert!(Socks5RequestContent::try_from_bytes(&request_bytes) .unwrap_err() .is_malformed_return()); } #[test] fn works_when_request_is_sized_properly_even_without_data() { // this one has "foo.com" remote address, correct 8 bytes of connection_id, and 0 bytes request data let request_bytes = [ RequestFlag::Connect as u8, 1, 2, 3, 4, 5, 6, 7, 8, 0, 7, 102, 111, 111, 46, 99, 111, 109, ] .to_vec(); let recipient = Recipient::try_from_base58_string("CytBseW6yFXUMzz4SGAKdNLGR7q3sJLLYxyBGvutNEQV.4QXYyEVc5fUDjmmi8PrHN9tdUFV4PCvSJE1278cHyvoe@4sBbL1ngf1vtNqykydQKTFh26sQCw888GpUqvPvyNB4f").unwrap(); let recipient_bytes = recipient.to_bytes(); let request_bytes: Vec<_> = request_bytes.into_iter().chain(recipient_bytes).collect(); let request = Socks5RequestContent::try_from_bytes(&request_bytes).unwrap(); match request { Socks5RequestContent::Connect(req) => { assert_eq!("foo.com".to_string(), req.remote_addr); assert_eq!(u64::from_be_bytes([1, 2, 3, 4, 5, 6, 7, 8]), req.conn_id); assert_eq!( req.return_address.unwrap().to_bytes().to_vec(), recipient.to_bytes().to_vec() ); } _ => unreachable!(), } } #[test] fn works_when_request_is_sized_properly_and_has_data() { // this one has a 1-byte remote address, correct 8 bytes of connection_id, and 3 bytes request data let request_bytes = [ RequestFlag::Connect as u8, 1, 2, 3, 4, 5, 6, 7, 8, 0, 7, 102, 111, 111, 46, 99, 111, 109, ] .to_vec(); let recipient = Recipient::try_from_base58_string("CytBseW6yFXUMzz4SGAKdNLGR7q3sJLLYxyBGvutNEQV.4QXYyEVc5fUDjmmi8PrHN9tdUFV4PCvSJE1278cHyvoe@4sBbL1ngf1vtNqykydQKTFh26sQCw888GpUqvPvyNB4f").unwrap(); let recipient_bytes = recipient.to_bytes(); let request_bytes: Vec<_> = request_bytes.into_iter().chain(recipient_bytes).collect(); let request = Socks5RequestContent::try_from_bytes(&request_bytes).unwrap(); match request { Socks5RequestContent::Connect(req) => { assert_eq!("foo.com".to_string(), req.remote_addr); assert_eq!(u64::from_be_bytes([1, 2, 3, 4, 5, 6, 7, 8]), req.conn_id); assert_eq!( req.return_address.unwrap().to_bytes().to_vec(), recipient.to_bytes().to_vec() ); } _ => unreachable!(), } } } #[cfg(test)] mod sending_additional_data_over_an_existing_connection { use super::*; #[test] fn works_when_request_is_sized_properly_even_without_data() { // correct 8 bytes of connection_id, 1 byte of local_closed, 8 bytes of sequence and 0 bytes request data let request_bytes = [ RequestFlag::Send as u8, 1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0, 1, ] .to_vec(); let request = Socks5RequestContent::try_from_bytes(&request_bytes).unwrap(); match request { Socks5RequestContent::Send(SendRequest { data }) => { assert_eq!( u64::from_be_bytes([1, 2, 3, 4, 5, 6, 7, 8]), data.header.connection_id ); assert!(!data.header.local_socket_closed); assert_eq!(1, data.header.seq); assert!(data.data.is_empty()); } _ => unreachable!(), } } #[test] fn works_when_request_is_sized_properly_and_has_data() { // correct 8 bytes of connection_id, 1 byte of local_closed, 8 bytes of sequence and 3 bytes request data (all 255) let request_bytes = [ RequestFlag::Send as u8, 1, 2, 3, 4, 5, 6, 7, 8, 1, 0, 0, 0, 0, 0, 0, 0, 1, 255, 255, 255, ] .to_vec(); let request = Socks5RequestContent::try_from_bytes(&request_bytes).unwrap(); match request { Socks5RequestContent::Send(SendRequest { data }) => { assert_eq!( u64::from_be_bytes([1, 2, 3, 4, 5, 6, 7, 8]), data.header.connection_id ); assert!(data.header.local_socket_closed); assert_eq!(1, data.header.seq); assert_eq!(vec![255, 255, 255], data.data); } _ => unreachable!(), } } } #[cfg(test)] mod serialize_query_request { use super::*; #[test] fn serialize_there_and_back() { let open_proxy = Socks5RequestContent::Query(QueryRequest::OpenProxy); let bytes_open_proxy = open_proxy.clone().into_bytes(); assert_eq!(bytes_open_proxy, vec![2, 0]); let description = Socks5RequestContent::Query(QueryRequest::Description); let bytes_description = description.clone().into_bytes(); assert_eq!(bytes_description, vec![2, 1]); let open_proxy2 = Socks5RequestContent::try_from_bytes(&bytes_open_proxy).unwrap(); let description2 = Socks5RequestContent::try_from_bytes(&bytes_description).unwrap(); assert_eq!(open_proxy, open_proxy2); assert_eq!(description, description2); } } }