mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-10-12 15:26:14 -08:00
* refactor(ohos): split facade feature and kernel crates * feat(ohos): protect transport sockets individually * fix(ohos): keep local proxy subnets off tun * fix(ohos): expose valid config enum values * refactor(ohos): finalize reusable core boundary * test(ohos): verify split package contracts * fix(port-forward): support wildcard userspace listeners Keep the existing Host listener intact while adding a DataPlane listener for force-smoltcp IPv4 wildcard rules. Keep literal loopback destinations on the local Host path instead of exporting them through an exit node. * chore(ohos): refresh split workspace lockfile * fix(socket): normalize Windows raw socket handles * refactor(socket): carry VPN protection through host bind options * refactor(socket): simplify protection defaults and TUN ingress * refactor(socket): consolidate native protection and socket creation * fix(socket): protect outbound UDP paths * fix(socket): preserve VPN routing for RPC listeners --------- Co-authored-by: FrankHan <frankhan@FrankHans-Mac-mini.local> Co-authored-by: KKRainbow <443152178@qq.com>
372 lines
12 KiB
Rust
372 lines
12 KiB
Rust
use std::{
|
|
io,
|
|
net::{IpAddr, Ipv6Addr, SocketAddr, SocketAddrV6},
|
|
sync::{Arc, Mutex},
|
|
};
|
|
|
|
use crate::{
|
|
host::dns::{DnsQuery, DnsResolver},
|
|
socket::{
|
|
IpVersion, SocketContext,
|
|
udp::{UdpBindOptions, VirtualUdpSocket, VirtualUdpSocketFactory},
|
|
},
|
|
};
|
|
|
|
use super::{
|
|
codec::{AddrError, Result, SocksError, TargetAddr, new_udp_header, parse_udp_request},
|
|
server::{Socks5ServerRuntime, Socks5UdpAssociation},
|
|
};
|
|
|
|
/// Portable SOCKS command runtime backed exclusively by host capabilities.
|
|
pub(crate) struct HostSocks5ServerRuntime<H>
|
|
where
|
|
H: VirtualUdpSocketFactory,
|
|
{
|
|
host: Arc<H>,
|
|
dns: Arc<dyn DnsResolver>,
|
|
socket_context: SocketContext,
|
|
}
|
|
|
|
impl<H> HostSocks5ServerRuntime<H>
|
|
where
|
|
H: VirtualUdpSocketFactory,
|
|
{
|
|
pub fn new(host: Arc<H>, dns: Arc<dyn DnsResolver>, socket_context: SocketContext) -> Self {
|
|
Self {
|
|
host,
|
|
dns,
|
|
socket_context,
|
|
}
|
|
}
|
|
|
|
async fn resolve_target(&self, target: TargetAddr) -> Result<SocketAddr> {
|
|
match target {
|
|
TargetAddr::Ip(address) => Ok(address),
|
|
TargetAddr::Domain(domain, port) => {
|
|
tracing::debug!(%domain, "attempting SOCKS DNS resolution");
|
|
let address = self
|
|
.dns
|
|
.resolve(DnsQuery::new(domain.clone(), self.socket_context.clone()))
|
|
.await
|
|
.map_err(|error| {
|
|
SocksError::Other(error.context(AddrError::DNSResolutionFailed))
|
|
})?
|
|
.into_iter()
|
|
.next()
|
|
.ok_or_else(|| {
|
|
SocksError::Other(anyhow::Error::new(AddrError::Custom(format!(
|
|
"cannot resolve SOCKS domain {domain}"
|
|
))))
|
|
})?;
|
|
let address = SocketAddr::new(address, port);
|
|
tracing::debug!(%address, "SOCKS domain resolved");
|
|
Ok(address)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn udp_bind_options(&self) -> UdpBindOptions {
|
|
UdpBindOptions::socks5()
|
|
.with_need_protect(false)
|
|
.with_context(self.socket_context.clone().with_ip_version(IpVersion::V6))
|
|
.with_local_addr(Some(SocketAddr::V6(SocketAddrV6::new(
|
|
Ipv6Addr::UNSPECIFIED,
|
|
0,
|
|
0,
|
|
0,
|
|
))))
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl<H> Socks5ServerRuntime for HostSocks5ServerRuntime<H>
|
|
where
|
|
H: VirtualUdpSocketFactory,
|
|
{
|
|
async fn resolve_dns(&self, target_addr: TargetAddr) -> Result<TargetAddr> {
|
|
self.resolve_target(target_addr).await.map(TargetAddr::Ip)
|
|
}
|
|
|
|
async fn bind_udp_association(&self) -> Result<Box<dyn Socks5UdpAssociation>> {
|
|
let inbound = self
|
|
.host
|
|
.bind_udp(self.udp_bind_options())
|
|
.await
|
|
.map_err(SocksError::Other)?;
|
|
Ok(Box::new(HostSocks5UdpAssociation {
|
|
inbound,
|
|
host: self.host.clone(),
|
|
socket_context: self.socket_context.clone(),
|
|
client: Mutex::new(None),
|
|
}))
|
|
}
|
|
}
|
|
|
|
struct HostSocks5UdpAssociation<H>
|
|
where
|
|
H: VirtualUdpSocketFactory,
|
|
{
|
|
inbound: Arc<H::Socket>,
|
|
host: Arc<H>,
|
|
socket_context: SocketContext,
|
|
client: Mutex<Option<SocketAddr>>,
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl<H> Socks5UdpAssociation for HostSocks5UdpAssociation<H>
|
|
where
|
|
H: VirtualUdpSocketFactory,
|
|
{
|
|
fn local_addr(&self) -> io::Result<SocketAddr> {
|
|
self.inbound.local_addr()
|
|
}
|
|
|
|
async fn transfer(self: Box<Self>) -> Result<()> {
|
|
let outbound = self
|
|
.host
|
|
.bind_udp(
|
|
UdpBindOptions::socks5()
|
|
.with_context(self.socket_context.clone().with_ip_version(IpVersion::V6))
|
|
.with_local_addr(Some(SocketAddr::V6(SocketAddrV6::new(
|
|
Ipv6Addr::UNSPECIFIED,
|
|
0,
|
|
0,
|
|
0,
|
|
)))),
|
|
)
|
|
.await
|
|
.map_err(SocksError::Other)?;
|
|
tokio::try_join!(
|
|
transfer_requests(self.as_ref(), outbound.as_ref()),
|
|
transfer_responses(self.as_ref(), outbound.as_ref()),
|
|
)?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
async fn transfer_requests<H>(
|
|
association: &HostSocks5UdpAssociation<H>,
|
|
outbound: &H::Socket,
|
|
) -> Result<()>
|
|
where
|
|
H: VirtualUdpSocketFactory,
|
|
{
|
|
let mut buffer = vec![0u8; 0x10000];
|
|
loop {
|
|
let (size, client) = association.inbound.recv_from(&mut buffer).await?;
|
|
{
|
|
let mut pinned = association.client.lock().unwrap();
|
|
match *pinned {
|
|
None => *pinned = Some(client),
|
|
Some(current) if current == client => {}
|
|
Some(_) => continue,
|
|
}
|
|
}
|
|
|
|
let (fragment, target, data) = parse_udp_request(&buffer[..size]).await?;
|
|
if fragment != 0 {
|
|
tracing::debug!(fragment, "discarding fragmented SOCKS UDP request");
|
|
return Ok(());
|
|
}
|
|
|
|
let target = match target {
|
|
TargetAddr::Ip(address) => address,
|
|
TargetAddr::Domain(_, _) => {
|
|
return Err(io::Error::other(
|
|
"SOCKS UDP domain targets must be explicitly resolved by the client",
|
|
)
|
|
.into());
|
|
}
|
|
};
|
|
outbound.send_to(data, ipv4_mapped_addr(target)).await?;
|
|
}
|
|
}
|
|
|
|
async fn transfer_responses<H>(
|
|
association: &HostSocks5UdpAssociation<H>,
|
|
outbound: &H::Socket,
|
|
) -> Result<()>
|
|
where
|
|
H: VirtualUdpSocketFactory,
|
|
{
|
|
let mut buffer = vec![0u8; 0x10000];
|
|
loop {
|
|
let (size, remote) = outbound.recv_from(&mut buffer).await?;
|
|
let client = *association.client.lock().unwrap();
|
|
let Some(client) = client else {
|
|
continue;
|
|
};
|
|
let mut data = new_udp_header(ipv4_mapped_addr(remote))?;
|
|
data.extend_from_slice(&buffer[..size]);
|
|
association.inbound.send_to(&data, client).await?;
|
|
}
|
|
}
|
|
|
|
fn ipv4_mapped_addr(mut address: SocketAddr) -> SocketAddr {
|
|
if let IpAddr::V4(ipv4) = address.ip() {
|
|
address.set_ip(IpAddr::V6(ipv4.to_ipv6_mapped()));
|
|
}
|
|
address
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::collections::VecDeque;
|
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
|
|
struct StaticDns;
|
|
|
|
#[async_trait::async_trait]
|
|
impl DnsResolver for StaticDns {
|
|
async fn resolve(&self, query: DnsQuery) -> anyhow::Result<Vec<IpAddr>> {
|
|
assert_eq!(query.host, "peer.example");
|
|
Ok(vec!["192.0.2.8".parse().unwrap()])
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct MockSocket {
|
|
receives: Mutex<VecDeque<(Vec<u8>, SocketAddr)>>,
|
|
sends: Mutex<Vec<(Vec<u8>, SocketAddr)>>,
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl VirtualUdpSocket for MockSocket {
|
|
fn local_addr(&self) -> io::Result<SocketAddr> {
|
|
Ok("[::]:42000".parse().unwrap())
|
|
}
|
|
|
|
async fn send_to(&self, data: &[u8], addr: SocketAddr) -> io::Result<usize> {
|
|
self.sends.lock().unwrap().push((data.to_vec(), addr));
|
|
Ok(data.len())
|
|
}
|
|
|
|
async fn recv_from(&self, buf: &mut [u8]) -> io::Result<(usize, SocketAddr)> {
|
|
let (data, addr) = self.receives.lock().unwrap().pop_front().unwrap();
|
|
buf[..data.len()].copy_from_slice(&data);
|
|
Ok((data.len(), addr))
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct MockHost {
|
|
binds: AtomicUsize,
|
|
options: Mutex<Vec<UdpBindOptions>>,
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl VirtualUdpSocketFactory for MockHost {
|
|
type Socket = MockSocket;
|
|
|
|
async fn bind_udp(&self, options: UdpBindOptions) -> anyhow::Result<Arc<Self::Socket>> {
|
|
self.binds.fetch_add(1, Ordering::Relaxed);
|
|
self.options.lock().unwrap().push(options);
|
|
Ok(Arc::new(MockSocket::default()))
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn resolves_and_binds_only_through_host_capabilities() {
|
|
let host = Arc::new(MockHost::default());
|
|
let context = SocketContext::default().with_socket_mark(Some(7));
|
|
let runtime =
|
|
HostSocks5ServerRuntime::new(host.clone(), Arc::new(StaticDns), context.clone());
|
|
|
|
assert_eq!(
|
|
runtime
|
|
.resolve_dns(TargetAddr::Domain("peer.example".into(), 443))
|
|
.await
|
|
.unwrap(),
|
|
TargetAddr::Ip("192.0.2.8:443".parse().unwrap())
|
|
);
|
|
let association = runtime.bind_udp_association().await.unwrap();
|
|
assert_eq!(association.local_addr().unwrap().port(), 42000);
|
|
assert_eq!(host.binds.load(Ordering::Relaxed), 1);
|
|
let options = host.options.lock().unwrap();
|
|
assert_eq!(
|
|
options[0].purpose,
|
|
crate::socket::udp::UdpSocketPurpose::Socks5
|
|
);
|
|
assert_eq!(options[0].context.socket_mark, context.socket_mark);
|
|
assert_eq!(options[0].context.ip_version, IpVersion::V6);
|
|
assert!(
|
|
!options[0].need_protect,
|
|
"SOCKS5 association socket is inbound"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn udp_requests_pin_the_first_client_and_map_ipv4_targets() {
|
|
let client: SocketAddr = "192.0.2.1:1000".parse().unwrap();
|
|
let other_client: SocketAddr = "192.0.2.2:1000".parse().unwrap();
|
|
let target: SocketAddr = "198.51.100.3:53".parse().unwrap();
|
|
let inbound = Arc::new(MockSocket::default());
|
|
let outbound = MockSocket::default();
|
|
|
|
let mut request = new_udp_header(target).unwrap();
|
|
request.extend_from_slice(b"first");
|
|
let mut ignored = new_udp_header(target).unwrap();
|
|
ignored.extend_from_slice(b"ignored");
|
|
let mut fragmented = new_udp_header(target).unwrap();
|
|
fragmented[2] = 1;
|
|
inbound.receives.lock().unwrap().extend([
|
|
(request, client),
|
|
(ignored, other_client),
|
|
(fragmented, client),
|
|
]);
|
|
|
|
let association = HostSocks5UdpAssociation {
|
|
inbound,
|
|
host: Arc::new(MockHost::default()),
|
|
socket_context: SocketContext::default(),
|
|
client: Mutex::new(None),
|
|
};
|
|
transfer_requests(&association, &outbound).await.unwrap();
|
|
|
|
assert_eq!(*association.client.lock().unwrap(), Some(client));
|
|
let sends = outbound.sends.lock().unwrap();
|
|
assert_eq!(sends.len(), 1);
|
|
assert_eq!(sends[0].0, b"first");
|
|
assert_eq!(sends[0].1, "[::ffff:198.51.100.3]:53".parse().unwrap());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn udp_requests_reject_domain_targets_without_dns() {
|
|
let client: SocketAddr = "192.0.2.1:1000".parse().unwrap();
|
|
let inbound = Arc::new(MockSocket::default());
|
|
let outbound = MockSocket::default();
|
|
let mut request = new_udp_header(("peer.example", 53)).unwrap();
|
|
request.extend_from_slice(b"query");
|
|
inbound
|
|
.receives
|
|
.lock()
|
|
.unwrap()
|
|
.push_back((request, client));
|
|
|
|
let association = HostSocks5UdpAssociation {
|
|
inbound,
|
|
host: Arc::new(MockHost::default()),
|
|
socket_context: SocketContext::default(),
|
|
client: Mutex::new(None),
|
|
};
|
|
let error = transfer_requests(&association, &outbound)
|
|
.await
|
|
.unwrap_err();
|
|
|
|
assert!(error.to_string().contains("must be explicitly resolved"));
|
|
assert!(outbound.sends.lock().unwrap().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn udp_response_preserves_ipv6_wire_family_for_mapped_ipv4() {
|
|
let remote = ipv4_mapped_addr("198.51.100.3:53".parse().unwrap());
|
|
let header = new_udp_header(remote).unwrap();
|
|
|
|
assert_eq!(
|
|
header[3],
|
|
crate::gateway::socks5::codec::consts::SOCKS5_ADDR_TYPE_IPV6
|
|
);
|
|
}
|
|
}
|