use eggress_core::classify_upstream_chain;
use eggress_core::CapabilityResult;
use eggress_uri::ProtocolSpec;
use eggress_uri::ProxyChainSpec;
#[derive(Debug)]
pub enum UdpRelayCapability {
SupportedSocks5,
SupportedComposed,
#[cfg(feature = "shadowsocks")]
SupportedShadowsocks {
method: eggress_protocol_shadowsocks::CipherMethod,
password: String,
},
UnsupportedProtocol {
protocol: String,
},
UnsupportedMultiHop,
}
#[cfg(feature = "shadowsocks")]
fn extract_shadowsocks_creds(
hop: &eggress_uri::ProxyHopSpec,
) -> Option<(eggress_protocol_shadowsocks::CipherMethod, String)> {
if hop.protocols.len() == 1 && hop.protocols[0] == ProtocolSpec::Shadowsocks {
if let Some(ref creds) = hop.credentials {
let method =
eggress_protocol_shadowsocks::CipherMethod::parse_method(&creds.username).ok()?;
Some((method, creds.password.clone()))
} else {
None
}
} else {
None
}
}
pub fn udp_capability(chain: &ProxyChainSpec) -> UdpRelayCapability {
match chain.hops.len() {
0 => UdpRelayCapability::UnsupportedProtocol {
protocol: "direct".to_string(),
},
1 => {
let hop = &chain.hops[0];
if hop.protocols.len() == 1 {
match &hop.protocols[0] {
ProtocolSpec::Socks5 => UdpRelayCapability::SupportedSocks5,
#[cfg(feature = "shadowsocks")]
ProtocolSpec::Shadowsocks => {
if let Some((method, password)) = extract_shadowsocks_creds(hop) {
UdpRelayCapability::SupportedShadowsocks { method, password }
} else {
UdpRelayCapability::UnsupportedProtocol {
protocol: "Shadowsocks (missing credentials)".to_string(),
}
}
}
ProtocolSpec::ShadowsocksR => UdpRelayCapability::UnsupportedProtocol {
protocol: "ShadowsocksR".to_string(),
},
#[cfg(not(feature = "shadowsocks"))]
ProtocolSpec::Shadowsocks => UdpRelayCapability::UnsupportedProtocol {
protocol: "Shadowsocks".to_string(),
},
other => UdpRelayCapability::UnsupportedProtocol {
protocol: format!("{:?}", other),
},
}
} else {
UdpRelayCapability::UnsupportedProtocol {
protocol: format!("{:?}", hop.protocols),
}
}
}
_ => {
if chain.hops.iter().all(|hop| {
hop.protocols.len() == 1
&& match hop.protocols[0] {
ProtocolSpec::Socks5 => true,
#[cfg(feature = "shadowsocks")]
ProtocolSpec::Shadowsocks => extract_shadowsocks_creds(hop).is_some(),
_ => false,
}
}) {
UdpRelayCapability::SupportedComposed
} else {
UdpRelayCapability::UnsupportedMultiHop
}
}
}
}
pub fn udp_capability_from_chain(chain: &ProxyChainSpec) -> UdpRelayCapability {
let caps = classify_upstream_chain(chain);
match caps.udp_associate {
CapabilityResult::Supported => {
if chain.hops.len() == 1 && chain.hops[0].protocols.len() == 1 {
match &chain.hops[0].protocols[0] {
ProtocolSpec::Socks5 => UdpRelayCapability::SupportedSocks5,
#[cfg(feature = "shadowsocks")]
ProtocolSpec::Shadowsocks => {
if let Some((method, password)) = extract_shadowsocks_creds(&chain.hops[0])
{
UdpRelayCapability::SupportedShadowsocks { method, password }
} else {
UdpRelayCapability::UnsupportedProtocol {
protocol: "Shadowsocks (missing credentials)".to_string(),
}
}
}
ProtocolSpec::ShadowsocksR => UdpRelayCapability::UnsupportedProtocol {
protocol: "ShadowsocksR".to_string(),
},
#[cfg(not(feature = "shadowsocks"))]
ProtocolSpec::Shadowsocks => UdpRelayCapability::UnsupportedProtocol {
protocol: "Shadowsocks".to_string(),
},
_ => UdpRelayCapability::SupportedSocks5,
}
} else if chain.hops.iter().all(|hop| {
hop.protocols.len() == 1 && matches!(hop.protocols[0], ProtocolSpec::Socks5)
}) {
UdpRelayCapability::SupportedComposed
} else {
UdpRelayCapability::UnsupportedMultiHop
}
}
CapabilityResult::UnsupportedProtocol { protocol } => {
UdpRelayCapability::UnsupportedProtocol { protocol }
}
CapabilityResult::UnsupportedChain { reason } => {
if reason == "multi-hop" || reason.starts_with("multi-hop") {
UdpRelayCapability::UnsupportedMultiHop
} else if reason == "multi-protocol" && chain.hops.len() == 1 {
UdpRelayCapability::UnsupportedProtocol {
protocol: format!("{:?}", chain.hops[0].protocols),
}
} else if reason == "direct" {
UdpRelayCapability::UnsupportedProtocol {
protocol: "direct".to_string(),
}
} else {
UdpRelayCapability::UnsupportedProtocol { protocol: reason }
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use eggress_uri::{CredentialSpec, EndpointSpec, ProxyHopSpec};
fn chain(hops: Vec<ProxyHopSpec>) -> ProxyChainSpec {
ProxyChainSpec { hops }
}
fn hop(protocols: Vec<ProtocolSpec>) -> ProxyHopSpec {
ProxyHopSpec {
protocols,
endpoint: EndpointSpec {
host: "proxy.example".to_string(),
port: 1080,
},
credentials: None,
rule: None,
local_bind: None,
tls: false,
server_name: None,
insecure: false,
plugins: Vec::new(),
auth_prefix: None,
}
}
fn hop_with_creds(protocols: Vec<ProtocolSpec>) -> ProxyHopSpec {
ProxyHopSpec {
protocols,
endpoint: EndpointSpec {
host: "proxy.example".to_string(),
port: 1080,
},
credentials: Some(CredentialSpec {
username: "user".to_string(),
password: "pass".to_string(),
}),
rule: None,
local_bind: None,
tls: false,
server_name: None,
insecure: false,
plugins: Vec::new(),
auth_prefix: None,
}
}
#[cfg(not(feature = "shadowsocks"))]
#[test]
fn common_build_reports_shadowsocks_udp_as_unsupported() {
let chain = chain(vec![hop(vec![ProtocolSpec::Shadowsocks])]);
assert!(matches!(
udp_capability(&chain),
UdpRelayCapability::UnsupportedProtocol { .. }
));
}
#[test]
fn single_socks5_hop_supported() {
let c = chain(vec![hop(vec![ProtocolSpec::Socks5])]);
assert!(matches!(
udp_capability(&c),
UdpRelayCapability::SupportedSocks5
));
}
#[test]
fn single_socks5_with_credentials_supported() {
let c = chain(vec![hop_with_creds(vec![ProtocolSpec::Socks5])]);
assert!(matches!(
udp_capability(&c),
UdpRelayCapability::SupportedSocks5
));
}
#[test]
fn single_http_hop_unsupported() {
let c = chain(vec![hop(vec![ProtocolSpec::Http])]);
let result = udp_capability(&c);
assert!(matches!(
result,
UdpRelayCapability::UnsupportedProtocol { ref protocol }
if protocol == "Http"
));
}
#[test]
fn single_socks4_hop_unsupported() {
let c = chain(vec![hop(vec![ProtocolSpec::Socks4])]);
let result = udp_capability(&c);
assert!(matches!(
result,
UdpRelayCapability::UnsupportedProtocol { ref protocol }
if protocol == "Socks4"
));
}
#[test]
fn multi_protocol_hop_unsupported() {
let c = chain(vec![hop(vec![ProtocolSpec::Http, ProtocolSpec::Socks5])]);
let result = udp_capability(&c);
assert!(matches!(
result,
UdpRelayCapability::UnsupportedProtocol { .. }
));
}
#[test]
fn multi_hop_chain_unsupported() {
let c = chain(vec![
hop(vec![ProtocolSpec::Socks5]),
hop(vec![ProtocolSpec::Http]),
]);
assert!(matches!(
udp_capability(&c),
UdpRelayCapability::UnsupportedMultiHop
));
}
#[test]
fn two_udp_capable_hops_are_composable() {
let c = chain(vec![
hop(vec![ProtocolSpec::Socks5]),
hop(vec![ProtocolSpec::Socks5]),
]);
assert!(matches!(
udp_capability(&c),
UdpRelayCapability::SupportedComposed
));
}
#[test]
fn empty_hops_unsupported_direct() {
let c = chain(vec![]);
let result = udp_capability(&c);
assert!(matches!(
result,
UdpRelayCapability::UnsupportedProtocol { ref protocol }
if protocol == "direct"
));
}
#[test]
fn from_chain_matches_original_socks5() {
let c = chain(vec![hop(vec![ProtocolSpec::Socks5])]);
let original = udp_capability(&c);
let new = udp_capability_from_chain(&c);
assert_eq!(format!("{:?}", original), format!("{:?}", new));
}
#[test]
fn from_chain_matches_original_socks5_with_creds() {
let c = chain(vec![hop_with_creds(vec![ProtocolSpec::Socks5])]);
let original = udp_capability(&c);
let new = udp_capability_from_chain(&c);
assert_eq!(format!("{:?}", original), format!("{:?}", new));
}
#[test]
fn from_chain_matches_original_http() {
let c = chain(vec![hop(vec![ProtocolSpec::Http])]);
let original = udp_capability(&c);
let new = udp_capability_from_chain(&c);
assert_eq!(format!("{:?}", original), format!("{:?}", new));
}
#[test]
fn from_chain_matches_original_socks4() {
let c = chain(vec![hop(vec![ProtocolSpec::Socks4])]);
let original = udp_capability(&c);
let new = udp_capability_from_chain(&c);
assert_eq!(format!("{:?}", original), format!("{:?}", new));
}
#[test]
fn from_chain_matches_original_multi_protocol() {
let c = chain(vec![hop(vec![ProtocolSpec::Http, ProtocolSpec::Socks5])]);
let original = udp_capability(&c);
let new = udp_capability_from_chain(&c);
assert_eq!(format!("{:?}", original), format!("{:?}", new));
}
#[test]
fn from_chain_matches_original_multi_hop() {
let c = chain(vec![
hop(vec![ProtocolSpec::Socks5]),
hop(vec![ProtocolSpec::Http]),
]);
let original = udp_capability(&c);
let new = udp_capability_from_chain(&c);
assert_eq!(format!("{:?}", original), format!("{:?}", new));
}
#[test]
fn from_chain_matches_original_empty() {
let c = chain(vec![]);
let original = udp_capability(&c);
let new = udp_capability_from_chain(&c);
assert_eq!(format!("{:?}", original), format!("{:?}", new));
}
#[test]
fn from_chain_matches_original_shadowsocks() {
let c = chain(vec![hop_with_creds(vec![ProtocolSpec::Shadowsocks])]);
let original = udp_capability(&c);
let new = udp_capability_from_chain(&c);
assert_eq!(format!("{:?}", original), format!("{:?}", new));
}
#[test]
fn from_chain_matches_original_shadowsocks_no_creds() {
let c = chain(vec![hop(vec![ProtocolSpec::Shadowsocks])]);
let original = udp_capability(&c);
let new = udp_capability_from_chain(&c);
assert_eq!(format!("{:?}", original), format!("{:?}", new));
}
}