use std::fmt;
use std::net::{AddrParseError, SocketAddr};
use std::str::FromStr;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use super::socks::Socks5Credentials;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "protocol", rename_all = "lowercase")]
#[non_exhaustive]
pub enum OutboundProxy {
Socks4 {
address: SocketAddr,
#[serde(default, skip_serializing_if = "Option::is_none")]
user_id: Option<String>,
},
Socks5 {
address: SocketAddr,
#[serde(default, skip_serializing_if = "Option::is_none")]
credentials: Option<Socks5Credentials>,
},
}
#[doc(hidden)]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum ResolvedOutboundProxy {
Socks4 {
address: SocketAddr,
user_id: Option<String>,
},
Socks5 {
address: SocketAddr,
credentials: Option<super::socks::ResolvedSocks5Credentials>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum OutboundProxyProtocol {
Socks4,
Socks5,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct OutboundProxyBuilder;
#[derive(Debug, Clone, thiserror::Error)]
pub enum OutboundProxyBuildError {
#[error("invalid {protocol} proxy address {address:?}: {source}")]
InvalidAddress {
protocol: OutboundProxyProtocol,
address: String,
#[source]
source: AddrParseError,
},
#[error("invalid SOCKS4 user ID: {reason}")]
InvalidSocks4UserId {
reason: &'static str,
},
#[error("invalid SOCKS5 credentials: {reason}")]
InvalidSocks5Credentials {
reason: &'static str,
},
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum OutboundProxyParseError {
#[error("outbound proxy URI must include a protocol, for example socks5://127.0.0.1:1080")]
MissingProtocol,
#[error(
"unsupported outbound proxy protocol {protocol:?}; supported protocols are socks4:// and socks5://"
)]
UnsupportedProtocol {
protocol: String,
},
#[error("outbound proxy credentials are not supported in the URI")]
CredentialsNotSupported,
#[error("outbound proxy URI must not include a path, query, or fragment")]
ExtraComponentsNotSupported,
#[error(transparent)]
Build(#[from] OutboundProxyBuildError),
}
#[doc(hidden)]
pub trait OutboundProxyConfig {
fn build(self) -> Result<OutboundProxy, OutboundProxyBuildError>;
}
impl ResolvedOutboundProxy {
pub(crate) fn select_for_destination(
configured: &Option<Arc<Self>>,
guest_dst: SocketAddr,
host_dst: SocketAddr,
) -> Option<Arc<Self>> {
if guest_dst == host_dst {
configured.clone()
} else {
None
}
}
}
impl fmt::Display for OutboundProxy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Socks4 { address, .. } => write!(f, "socks4://{address}"),
Self::Socks5 { address, .. } => write!(f, "socks5://{address}"),
}
}
}
impl fmt::Display for OutboundProxyProtocol {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Socks4 => f.write_str("SOCKS4"),
Self::Socks5 => f.write_str("SOCKS5"),
}
}
}
impl FromStr for OutboundProxy {
type Err = OutboundProxyParseError;
fn from_str(raw: &str) -> Result<Self, Self::Err> {
let (protocol, address) = raw
.split_once("://")
.ok_or(OutboundProxyParseError::MissingProtocol)?;
let protocol = match protocol {
"socks4" => OutboundProxyProtocol::Socks4,
"socks5" => OutboundProxyProtocol::Socks5,
protocol => {
return Err(OutboundProxyParseError::UnsupportedProtocol {
protocol: protocol.to_string(),
});
}
};
if address.contains('@') {
return Err(OutboundProxyParseError::CredentialsNotSupported);
}
if address.contains(['/', '?', '#']) {
return Err(OutboundProxyParseError::ExtraComponentsNotSupported);
}
match protocol {
OutboundProxyProtocol::Socks4 => {
Ok(OutboundProxyBuilder::new().socks4(address).build()?)
}
OutboundProxyProtocol::Socks5 => {
Ok(OutboundProxyBuilder::new().socks5(address).build()?)
}
}
}
}