Files
EasyTier/easytier/src/socket_protector.rs
T
38e2a621bb refactor(ohos): 拆分 OHRS 包并按 socket 精细保护 VPN 流量 (#2543)
* 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>
2026-09-09 22:12:47 +08:00

236 lines
9.0 KiB
Rust

use std::{
io,
sync::{Arc, RwLock},
};
use async_trait::async_trait;
#[cfg(unix)]
use std::os::fd::AsRawFd;
#[cfg(windows)]
use std::os::windows::io::AsRawSocket;
/// Native-only platform callback used by socket creation when `need_protect`
/// is set. The future must resolve only after protection is actually applied;
/// emitting an event without awaiting its acknowledgement is not sufficient.
/// Errors fail creation before bind/connect/listen or exposing an accepted child.
///
/// WASI embedders implement the same contract inside their existing host socket
/// creation operations, using core's encoded bind options, not this raw-FD API.
#[async_trait]
pub trait NativeSocketProtector: Send + Sync + 'static {
async fn protect(&self, socket_handle: u64) -> io::Result<()>;
}
static NATIVE_SOCKET_PROTECTOR: RwLock<Option<Arc<dyn NativeSocketProtector>>> = RwLock::new(None);
/// Installs or removes the process-wide native socket protection capability.
///
/// Instance-specific routing policy still travels in socket options; this hook
/// only exposes a platform service such as Android/iOS/HarmonyOS VPN bypass.
pub fn set_native_socket_protector(protector: Option<Arc<dyn NativeSocketProtector>>) {
let mut guard = NATIVE_SOCKET_PROTECTOR
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*guard = protector;
}
fn native_socket_protector() -> Option<Arc<dyn NativeSocketProtector>> {
#[cfg(all(test, unix))]
if let Ok(protector) = TEST_SOCKET_PROTECTOR.try_with(Arc::clone) {
return Some(protector);
}
NATIVE_SOCKET_PROTECTOR
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
// Isolate factory ordering tests from other tests' sockets in the same process.
#[cfg(all(test, unix))]
tokio::task_local! {
static TEST_SOCKET_PROTECTOR: Arc<dyn NativeSocketProtector>;
}
pub(crate) fn native_socket_protection_available() -> bool {
native_socket_protector().is_some()
}
// socket2 owns the platform-specific handle; SockRef adapts Tokio sockets here.
pub(crate) async fn protect_native_socket(
socket: &socket2::Socket,
need_protect: bool,
) -> io::Result<()> {
if !need_protect {
return Ok(());
}
let Some(protector) = native_socket_protector() else {
return Ok(());
};
#[cfg(unix)]
let handle = u64::try_from(socket.as_raw_fd())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid socket fd"))?;
#[cfg(windows)]
let handle = socket.as_raw_socket() as u64;
protector.protect(handle).await
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
use easytier_core::socket::{
SocketListener,
tcp::{TcpBindOptions, TcpConnectOptions, TcpListenOptions, VirtualTcpListener},
udp::UdpBindOptions,
};
use std::{
os::fd::BorrowedFd,
sync::atomic::{AtomicUsize, Ordering},
time::Duration,
};
use tokio::sync::Semaphore;
struct GateProtector {
calls: AtomicUsize,
gate: Semaphore,
fail: bool,
expect_unbound: bool,
}
impl GateProtector {
fn new(fail: bool, expect_unbound: bool) -> Arc<Self> {
Arc::new(Self {
calls: AtomicUsize::new(0),
gate: Semaphore::new(0),
fail,
expect_unbound,
})
}
}
#[async_trait]
impl NativeSocketProtector for GateProtector {
async fn protect(&self, handle: u64) -> io::Result<()> {
// The caller keeps the socket alive while this borrowed callback runs.
let fd = unsafe { BorrowedFd::borrow_raw(i32::try_from(handle).unwrap()) };
let socket = socket2::SockRef::from(&fd);
if self.expect_unbound || self.calls.load(Ordering::SeqCst) == 0 {
assert_eq!(socket.local_addr()?.as_socket().unwrap().port(), 0);
assert!(
socket.peer_addr().is_err(),
"connect must wait for protect completion"
);
}
self.calls.fetch_add(1, Ordering::SeqCst);
if self.fail {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"test protection failure",
));
}
self.gate.acquire().await.unwrap().forget();
Ok(())
}
}
#[tokio::test]
async fn tcp_connect_waits_for_protection_ack() {
let protector = GateProtector::new(false, true);
TEST_SOCKET_PROTECTOR
.scope(protector.clone(), async {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let options = TcpConnectOptions::direct_connect(listener.local_addr().unwrap());
let connect = crate::socket::tcp::connect_tcp(options);
tokio::pin!(connect);
assert!(futures::poll!(&mut connect).is_pending());
assert_eq!(protector.calls.load(Ordering::SeqCst), 1);
assert!(
tokio::time::timeout(Duration::from_millis(20), listener.accept())
.await
.is_err()
);
protector.gate.add_permits(1);
let _client = connect.await.unwrap();
listener.accept().await.unwrap();
})
.await;
}
#[tokio::test]
async fn protection_failure_blocks_creation_and_explicit_false_bypasses_callback() {
let protector = GateProtector::new(true, true);
TEST_SOCKET_PROTECTOR
.scope(protector.clone(), async {
let local = "127.0.0.1:0".parse().unwrap();
let bind = TcpBindOptions::default().with_local_addr(Some(local));
assert!(
crate::socket::tcp::create_tcp_socket(local, &bind)
.await
.is_err()
);
assert!(
crate::socket::tcp::bind_tcp_listener(TcpListenOptions::direct_connect(local))
.await
.is_err()
);
let udp = UdpBindOptions::direct_connect().with_local_addr(Some(local));
assert!(crate::socket::udp::create_udp_socket(&udp).await.is_err());
assert_eq!(protector.calls.load(Ordering::SeqCst), 3);
for options in [
TcpListenOptions::proxy_nat(local),
TcpListenOptions::socks5(local),
TcpListenOptions::port_forward(local),
TcpListenOptions::port_lease(local),
] {
let listener = crate::socket::tcp::bind_tcp_listener(options)
.await
.unwrap();
let connect = TcpConnectOptions::direct_connect(listener.local_addr().unwrap())
.with_bind(TcpBindOptions::default().with_need_protect(false));
let _client = crate::socket::tcp::connect_tcp(connect).await.unwrap();
listener.accept().await.unwrap();
}
let udp = udp.with_need_protect(false);
crate::socket::udp::create_udp_socket(&udp).await.unwrap();
let mut rpc = crate::proto::rpc::standalone::runtime_rpc_listener(local);
rpc.listen().await.unwrap();
let rpc_addr = rpc.local_url().socket_addrs(|| None).unwrap()[0];
let _client = tokio::net::TcpStream::connect(rpc_addr).await.unwrap();
rpc.accept().await.unwrap();
assert_eq!(protector.calls.load(Ordering::SeqCst), 3);
})
.await;
}
#[tokio::test]
async fn accepted_child_waits_for_inherited_protection() {
let protector = GateProtector::new(false, false);
TEST_SOCKET_PROTECTOR
.scope(protector.clone(), async {
let bind = crate::socket::tcp::bind_tcp_listener(TcpListenOptions::direct_connect(
"127.0.0.1:0".parse().unwrap(),
));
tokio::pin!(bind);
assert!(futures::poll!(&mut bind).is_pending());
assert_eq!(protector.calls.load(Ordering::SeqCst), 1);
protector.gate.add_permits(1);
let listener = bind.await.unwrap();
let _client = tokio::net::TcpStream::connect(listener.local_addr().unwrap())
.await
.unwrap();
let accept = listener.accept();
tokio::pin!(accept);
assert!(
tokio::time::timeout(Duration::from_millis(20), &mut accept)
.await
.is_err()
);
assert_eq!(protector.calls.load(Ordering::SeqCst), 2);
protector.gate.add_permits(1);
accept.await.unwrap();
})
.await;
}
}