0801b3c6f8
* exit policy types * simple client for grabbing the policy * moved allowed_hosts to a submodule * started integrating exit policy into a NR * ability to construct ExitPolicyRequestFilter * fixed policy parsing to look for comment char from the left * conditionally setting up request filter * [wip] setting up correct url for exit policy upstream * clap flags for running with exit policy * fixed NR template * updated NR config template * making sure to perform request filtering in separate task * initial, placeholder, exit policy API endpoint * serving exit policy from an embedded NR * double slash sanitization * socks5 query for exit policy * adjusted address policy logging * cargo fmt * Updated exit policy url to point to the correct mainnet file * removed unecessary mutability in filter tests * fixed the code block showing example policy being interpreted as doc test
427 lines
14 KiB
Rust
427 lines
14 KiB
Rust
// Copyright 2020-2023 - Nym Technologies SA <contact@nymtech.net>
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
use crate::{
|
|
make_bincode_serializer, ConnectionId, InsufficientSocketDataError, SocketData,
|
|
Socks5ProtocolVersion, Socks5RequestError,
|
|
};
|
|
use nym_exit_policy::ExitPolicy;
|
|
use nym_service_providers_common::interface::{Serializable, ServiceProviderResponse};
|
|
use serde::{Deserialize, Serialize};
|
|
use tap::TapFallible;
|
|
use thiserror::Error;
|
|
|
|
// don't start tags from 0 for easier backwards compatibility since `NetworkData`
|
|
// used to be a `Response` with tag 1
|
|
// and `ConnectionError` used to be `NetworkRequesterResponse` with tag 2
|
|
#[repr(u8)]
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub enum ResponseFlag {
|
|
NetworkData = 1,
|
|
ConnectionError = 2,
|
|
Query = 3,
|
|
}
|
|
|
|
impl TryFrom<u8> for ResponseFlag {
|
|
type Error = ResponseDeserializationError;
|
|
|
|
fn try_from(value: u8) -> Result<ResponseFlag, ResponseDeserializationError> {
|
|
match value {
|
|
_ if value == (ResponseFlag::NetworkData as u8) => Ok(Self::NetworkData),
|
|
_ if value == (ResponseFlag::ConnectionError as u8) => Ok(Self::ConnectionError),
|
|
_ if value == (ResponseFlag::Query as u8) => Ok(Self::Query),
|
|
value => Err(ResponseDeserializationError::UnknownResponseFlag { value }),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum ResponseDeserializationError {
|
|
#[error("the network data was malformed: {source}")]
|
|
MalformedNetworkData {
|
|
#[from]
|
|
source: InsufficientSocketDataError,
|
|
},
|
|
|
|
#[error("not enough bytes to recover the connection id")]
|
|
ConnectionIdTooShort,
|
|
|
|
#[error("{value} is not a valid response flag")]
|
|
UnknownResponseFlag { value: u8 },
|
|
|
|
#[error("no data provided")]
|
|
NoData,
|
|
|
|
#[error("message is not utf8 encoded: {source}")]
|
|
MalformedErrorMessage {
|
|
#[from]
|
|
source: std::string::FromUtf8Error,
|
|
},
|
|
|
|
#[error("failed to deserialize query response: {source}")]
|
|
QueryDeserializationError {
|
|
#[from]
|
|
source: bincode::Error,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct Socks5Response {
|
|
pub protocol_version: Socks5ProtocolVersion,
|
|
pub content: Socks5ResponseContent,
|
|
}
|
|
|
|
impl Serializable for Socks5Response {
|
|
type Error = Socks5RequestError;
|
|
|
|
// legacy responses had the format of
|
|
// 1 (Message::RESPONSE_FLAG) || <data> for data responses
|
|
// 2 (Message::NR_RESPONSE_FLAG) || <data> for error responses
|
|
// the updated formats use
|
|
// 3 (Socks5ProtocolVersion) || 0 (ResponseFlag::NetworkData) || <data> for data responses
|
|
// 3 (Socks5ProtocolVersion) || 1 (ResponseFlag::ConnectionError) || <data> for error responses
|
|
// so for serialization an optional version tag is prepended
|
|
// and in deserialization it's just the case of shifting the buffer in case of non-legacy response payload
|
|
fn into_bytes(self) -> Vec<u8> {
|
|
if let Some(version) = self.protocol_version.as_u8() {
|
|
std::iter::once(version)
|
|
.chain(self.content.into_bytes())
|
|
.collect()
|
|
} else {
|
|
self.content.into_bytes()
|
|
}
|
|
}
|
|
|
|
fn try_from_bytes(b: &[u8]) -> Result<Self, Self::Error> {
|
|
if b.is_empty() {
|
|
return Err(ResponseDeserializationError::NoData.into());
|
|
}
|
|
|
|
let protocol_version = Socks5ProtocolVersion::from(b[0]);
|
|
let content = if protocol_version.is_legacy() {
|
|
Socks5ResponseContent::try_from_bytes(b)
|
|
} else {
|
|
Socks5ResponseContent::try_from_bytes(&b[1..])
|
|
}?;
|
|
Ok(Socks5Response {
|
|
protocol_version,
|
|
content,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl ServiceProviderResponse for Socks5Response {}
|
|
|
|
impl Socks5Response {
|
|
pub fn new(
|
|
protocol_version: Socks5ProtocolVersion,
|
|
content: Socks5ResponseContent,
|
|
) -> Socks5Response {
|
|
Socks5Response {
|
|
protocol_version,
|
|
content,
|
|
}
|
|
}
|
|
|
|
pub fn new_network_data(
|
|
protocol_version: Socks5ProtocolVersion,
|
|
seq: u64,
|
|
connection_id: ConnectionId,
|
|
data: Vec<u8>,
|
|
is_closed: bool,
|
|
) -> Socks5Response {
|
|
Socks5Response {
|
|
protocol_version,
|
|
content: Socks5ResponseContent::new_network_data(seq, connection_id, data, is_closed),
|
|
}
|
|
}
|
|
|
|
pub fn new_connection_error(
|
|
protocol_version: Socks5ProtocolVersion,
|
|
connection_id: ConnectionId,
|
|
error_message: String,
|
|
) -> Socks5Response {
|
|
Socks5Response {
|
|
protocol_version,
|
|
content: Socks5ResponseContent::new_connection_error(connection_id, error_message),
|
|
}
|
|
}
|
|
|
|
pub fn new_query(
|
|
protocol_version: Socks5ProtocolVersion,
|
|
query_response: QueryResponse,
|
|
) -> Socks5Response {
|
|
Socks5Response {
|
|
protocol_version,
|
|
content: Socks5ResponseContent::Query(query_response),
|
|
}
|
|
}
|
|
|
|
pub fn new_query_error<S: Into<String>>(
|
|
protocol_version: Socks5ProtocolVersion,
|
|
message: S,
|
|
) -> Socks5Response {
|
|
Socks5Response {
|
|
protocol_version,
|
|
content: Socks5ResponseContent::Query(QueryResponse::Error {
|
|
message: message.into(),
|
|
}),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub enum Socks5ResponseContent {
|
|
NetworkData { content: SocketData },
|
|
ConnectionError(ConnectionError),
|
|
Query(QueryResponse),
|
|
}
|
|
|
|
impl Socks5ResponseContent {
|
|
pub fn new_network_data(
|
|
seq: u64,
|
|
connection_id: ConnectionId,
|
|
data: Vec<u8>,
|
|
is_closed: bool,
|
|
) -> Socks5ResponseContent {
|
|
Socks5ResponseContent::NetworkData {
|
|
content: SocketData::new(seq, connection_id, is_closed, data),
|
|
}
|
|
}
|
|
|
|
pub fn new_connection_error(
|
|
connection_id: ConnectionId,
|
|
error_message: String,
|
|
) -> Socks5ResponseContent {
|
|
Socks5ResponseContent::ConnectionError(ConnectionError::new(connection_id, error_message))
|
|
}
|
|
|
|
pub fn into_bytes(self) -> Vec<u8> {
|
|
match self {
|
|
Socks5ResponseContent::NetworkData { content } => {
|
|
std::iter::once(ResponseFlag::NetworkData as u8)
|
|
.chain(content.into_response_bytes_iter())
|
|
.collect()
|
|
}
|
|
Socks5ResponseContent::ConnectionError(res) => {
|
|
std::iter::once(ResponseFlag::ConnectionError as u8)
|
|
.chain(res.into_bytes())
|
|
.collect()
|
|
}
|
|
Socks5ResponseContent::Query(query) => {
|
|
use bincode::Options;
|
|
let query_bytes: Vec<u8> = make_bincode_serializer()
|
|
.serialize(&query)
|
|
.tap_err(|err| {
|
|
log::error!("Failed to serialize query response: {:?}: {err}", query);
|
|
})
|
|
.unwrap_or_default();
|
|
std::iter::once(ResponseFlag::Query as u8)
|
|
.chain(query_bytes)
|
|
.collect()
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn try_from_bytes(b: &[u8]) -> Result<Socks5ResponseContent, ResponseDeserializationError> {
|
|
if b.is_empty() {
|
|
// TODO: bad error type since this branch could be reached in the 'versioned' case
|
|
// after reading 1 byte already
|
|
return Err(ResponseDeserializationError::NoData);
|
|
}
|
|
|
|
let response_flag = ResponseFlag::try_from(b[0])?;
|
|
match response_flag {
|
|
ResponseFlag::NetworkData => Ok(Socks5ResponseContent::NetworkData {
|
|
content: SocketData::try_from_response_bytes(&b[1..])?,
|
|
}),
|
|
ResponseFlag::ConnectionError => Ok(Socks5ResponseContent::ConnectionError(
|
|
ConnectionError::try_from_bytes(&b[1..])?,
|
|
)),
|
|
ResponseFlag::Query => {
|
|
use bincode::Options;
|
|
let query = make_bincode_serializer().deserialize(&b[1..])?;
|
|
Ok(Socks5ResponseContent::Query(query))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn as_query(&self) -> Option<&QueryResponse> {
|
|
match self {
|
|
Socks5ResponseContent::Query(query) => Some(query),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct ConnectionError {
|
|
pub connection_id: ConnectionId,
|
|
pub network_requester_error: String,
|
|
}
|
|
|
|
impl ConnectionError {
|
|
pub fn new(connection_id: ConnectionId, network_requester_error: String) -> Self {
|
|
ConnectionError {
|
|
connection_id,
|
|
network_requester_error,
|
|
}
|
|
}
|
|
|
|
pub fn try_from_bytes(b: &[u8]) -> Result<ConnectionError, ResponseDeserializationError> {
|
|
if b.is_empty() {
|
|
return Err(ResponseDeserializationError::NoData);
|
|
}
|
|
|
|
if b.len() < 8 {
|
|
return Err(ResponseDeserializationError::ConnectionIdTooShort);
|
|
}
|
|
|
|
let mut connection_id_bytes = b.to_vec();
|
|
let network_requester_error_bytes = connection_id_bytes.split_off(8);
|
|
|
|
let connection_id = u64::from_be_bytes([
|
|
connection_id_bytes[0],
|
|
connection_id_bytes[1],
|
|
connection_id_bytes[2],
|
|
connection_id_bytes[3],
|
|
connection_id_bytes[4],
|
|
connection_id_bytes[5],
|
|
connection_id_bytes[6],
|
|
connection_id_bytes[7],
|
|
]);
|
|
let network_requester_error = String::from_utf8(network_requester_error_bytes)?;
|
|
|
|
Ok(ConnectionError {
|
|
connection_id,
|
|
network_requester_error,
|
|
})
|
|
}
|
|
|
|
pub fn into_bytes(self) -> Vec<u8> {
|
|
self.connection_id
|
|
.to_be_bytes()
|
|
.iter()
|
|
.copied()
|
|
.chain(self.network_requester_error.into_bytes())
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[non_exhaustive]
|
|
pub enum QueryResponse {
|
|
OpenProxy(bool),
|
|
Description(String),
|
|
ExitPolicy {
|
|
enabled: bool,
|
|
upstream: String,
|
|
policy: Option<ExitPolicy>,
|
|
},
|
|
Error {
|
|
message: String,
|
|
},
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[cfg(test)]
|
|
mod connection_error_response_serde_tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn simple_serde() {
|
|
let conn_id = 42;
|
|
let network_requester_error = String::from("This is a test msg");
|
|
let response = ConnectionError::new(conn_id, network_requester_error.clone());
|
|
let bytes = response.into_bytes();
|
|
let deserialized_response = ConnectionError::try_from_bytes(&bytes).unwrap();
|
|
|
|
assert_eq!(conn_id, deserialized_response.connection_id);
|
|
assert_eq!(
|
|
network_requester_error,
|
|
deserialized_response.network_requester_error
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn deserialization_errors() {
|
|
let err = ConnectionError::try_from_bytes(&[]).err().unwrap();
|
|
assert!(matches!(err, ResponseDeserializationError::NoData));
|
|
|
|
let bytes: [u8; 5] = [1, 2, 3, 4, 5];
|
|
let err = ConnectionError::try_from_bytes(&bytes).err().unwrap();
|
|
assert!(matches!(
|
|
err,
|
|
ResponseDeserializationError::ConnectionIdTooShort
|
|
));
|
|
|
|
let bytes: Vec<u8> = 42u64
|
|
.to_be_bytes()
|
|
.into_iter()
|
|
.chain([0, 159, 146, 150])
|
|
.collect();
|
|
let err = ConnectionError::try_from_bytes(&bytes).err().unwrap();
|
|
assert!(matches!(
|
|
err,
|
|
ResponseDeserializationError::MalformedErrorMessage { .. }
|
|
));
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod serialize_query_response {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn serialize_there_and_back() {
|
|
let open_proxy = Socks5ResponseContent::Query(QueryResponse::OpenProxy(true));
|
|
let bytes_open_proxy = open_proxy.clone().into_bytes();
|
|
assert_eq!(bytes_open_proxy, vec![3, 0, 1]);
|
|
|
|
let description =
|
|
Socks5ResponseContent::Query(QueryResponse::Description("foo".to_string()));
|
|
let bytes_description = description.clone().into_bytes();
|
|
assert_eq!(bytes_description, vec![3, 1, 3, 102, 111, 111]);
|
|
|
|
let error = Socks5ResponseContent::Query(QueryResponse::Error {
|
|
message: "this is an error".to_string(),
|
|
});
|
|
let bytes_error = error.clone().into_bytes();
|
|
assert_eq!(
|
|
bytes_error,
|
|
vec![
|
|
3, 3, 16, 116, 104, 105, 115, 32, 105, 115, 32, 97, 110, 32, 101, 114, 114,
|
|
111, 114
|
|
]
|
|
);
|
|
|
|
let exit_policy = Socks5ResponseContent::Query(QueryResponse::ExitPolicy {
|
|
enabled: false,
|
|
upstream: "http://foo.bar".to_string(),
|
|
policy: Some(ExitPolicy::new_open()),
|
|
});
|
|
let bytes_exit_policy = exit_policy.clone().into_bytes();
|
|
assert_eq!(
|
|
bytes_exit_policy,
|
|
vec![
|
|
3, 2, 0, 14, 104, 116, 116, 112, 58, 47, 47, 102, 111, 111, 46, 98, 97, 114, 1,
|
|
1, 0, 1, 42, 1, 251, 255, 255
|
|
]
|
|
);
|
|
|
|
let open_proxy2 = Socks5ResponseContent::try_from_bytes(&bytes_open_proxy).unwrap();
|
|
let description2 = Socks5ResponseContent::try_from_bytes(&bytes_description).unwrap();
|
|
let error2 = Socks5ResponseContent::try_from_bytes(&bytes_error).unwrap();
|
|
let exit_policy2 = Socks5ResponseContent::try_from_bytes(&bytes_exit_policy).unwrap();
|
|
|
|
assert_eq!(open_proxy, open_proxy2);
|
|
assert_eq!(description, description2);
|
|
assert_eq!(error, error2);
|
|
assert_eq!(exit_policy, exit_policy2);
|
|
}
|
|
}
|
|
}
|