feat(shadowsocks): making android::SocketProtect sealed (#1974)

This commit is contained in:
zonyitoo
2025-06-22 02:47:37 +08:00
parent 01337d349c
commit c0bd03ad63
6 changed files with 109 additions and 87 deletions

2
.gitignore vendored
View File

@@ -1,4 +1,4 @@
/target
target
/build/release
/build/target
/build/install

18
Cargo.lock generated
View File

@@ -888,7 +888,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.59.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -1979,7 +1979,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
dependencies = [
"cfg-if",
"windows-targets 0.48.5",
"windows-targets 0.53.2",
]
[[package]]
@@ -3160,6 +3160,17 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "sealed"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22f968c5ea23d555e670b449c1c5e7b2fc399fdaec1d304a17cd48e288abc107"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.102",
]
[[package]]
name = "sec1"
version = "0.7.3"
@@ -3347,6 +3358,7 @@ dependencies = [
"percent-encoding",
"pin-project",
"rand 0.9.1",
"sealed",
"sendfd",
"serde",
"serde_json",
@@ -4370,7 +4382,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.48.0",
"windows-sys 0.59.0",
]
[[package]]

View File

@@ -72,6 +72,7 @@ percent-encoding = "2.1"
futures = "0.3"
trait-variant = "0.1"
dynosaur = "0.2.0"
sealed = "0.6"
socket2 = { version = "0.5", features = ["all"] }
tokio = { version = "1.9.0", features = [

View File

@@ -2,8 +2,6 @@
use std::net::SocketAddr;
#[cfg(target_os = "android")]
pub use self::option::android::{SocketProtect, SocketProtectFn};
#[cfg(unix)]
pub use self::sys::uds::{UnixListener, UnixStream};
pub use self::{

View File

@@ -59,6 +59,9 @@ pub struct ConnectOpts {
/// This is an [Android shadowsocks implementation](https://github.com/shadowsocks/shadowsocks-android) specific feature
#[cfg(target_os = "android")]
pub vpn_protect_path: Option<std::path::PathBuf>,
/// A customizable socket protect implementation for Android for calling `VpnService.protect(fd)`
///
/// see [`ConnectOpts::set_vpn_socket_protect`]
#[cfg(target_os = "android")]
pub vpn_socket_protect: Option<std::sync::Arc<Box<dyn android::SocketProtect + Send + Sync>>>,
@@ -93,67 +96,77 @@ pub struct AcceptOpts {
#[cfg(target_os = "android")]
impl ConnectOpts {
/// Set `vpn_protect_path` for Android VPNService.protect implementation
///
/// Example:
///
/// ```rust
/// // Sync function for calling `VpnService.protect(fd)`
/// opts.set_vpn_socket_protect(|fd| {
/// // Your implementation here
/// // For example, using `jni` to call Android's VpnService.protect(fd)
/// Ok(())
/// });
/// ```
pub fn set_vpn_socket_protect<F>(&mut self, f: F)
where
F: self::android::MakeSocketProtect + Send + Sync + 'static,
F: android::MakeSocketProtect + Send + Sync + 'static,
F::SocketProtectType: android::SocketProtect + Send + Sync + 'static,
{
let protect_fn = Box::new(f.make_socket_protect()) as Box<dyn android::SocketProtect + Send + Sync>;
self.vpn_socket_protect = Some(std::sync::Arc::new(protect_fn))
self.vpn_socket_protect = Some(std::sync::Arc::new(Box::new(f.make_socket_protect())));
}
}
/// Android specific features
#[cfg(target_os = "android")]
pub mod android {
use std::{fmt, io, os::unix::io::RawFd, sync::Arc};
use sealed::sealed;
use std::{fmt, io, os::unix::io::RawFd};
/// Android VPN socket protect implemetation
#[sealed]
pub trait SocketProtect {
/// Protects the socket file descriptor by calling `VpnService.protect(fd)`
fn protect(&self, fd: RawFd) -> io::Result<()>;
}
impl fmt::Debug for dyn SocketProtect + Send + Sync {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SocketProtect").finish_non_exhaustive()
}
}
/// Creating an instance of `SocketProtect`
#[sealed]
pub trait MakeSocketProtect {
type SocketProtect: SocketProtect + Send + Sync;
type SocketProtectType: SocketProtect;
/// Creates an instance of `SocketProtect`
fn make_socket_protect(self) -> Self::SocketProtect;
fn make_socket_protect(self) -> Self::SocketProtectType;
}
/// A function that implements `SocketProtect` trait
pub struct SocketProtectFn<F> {
pub f: F,
f: F,
}
#[sealed]
impl<F> SocketProtect for SocketProtectFn<F>
where
F: Fn(RawFd) -> io::Result<()>,
F: Fn(RawFd) -> io::Result<()> + Send + Sync + 'static,
{
fn protect(&self, fd: RawFd) -> io::Result<()> {
(self.f)(fd)
}
}
impl<F> Clone for SocketProtectFn<Arc<F>> {
fn clone(&self) -> Self {
Self { f: self.f.clone() }
}
}
impl fmt::Debug for dyn SocketProtect + Send + Sync {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("dyn SocketProtect + Send + Sync").finish()
}
}
#[sealed]
impl<F> MakeSocketProtect for F
where
F: Fn(RawFd) -> io::Result<()> + Send + Sync + 'static,
{
type SocketProtect = SocketProtectFn<F>;
type SocketProtectType = SocketProtectFn<F>;
fn make_socket_protect(self) -> Self::SocketProtect {
fn make_socket_protect(self) -> Self::SocketProtectType {
SocketProtectFn { f: self }
}
}

View File

@@ -9,7 +9,6 @@ use std::{
task::{self, Poll},
};
use cfg_if::cfg_if;
use log::{debug, error, warn};
use pin_project::pin_project;
use socket2::{Domain, Protocol, SockAddr, Socket, Type};
@@ -56,22 +55,7 @@ impl TcpStream {
// This is a workaround for VPNService
#[cfg(target_os = "android")]
if !addr.ip().is_loopback() {
use std::time::Duration;
use tokio::time;
if let Some(ref path) = opts.vpn_protect_path {
// RPC calls to `VpnService.protect()`
// Timeout in 3 seconds like shadowsocks-libev
match time::timeout(Duration::from_secs(3), vpn_protect(path, socket.as_raw_fd())).await {
Ok(Ok(..)) => {}
Ok(Err(err)) => return Err(err),
Err(..) => return Err(io::Error::new(ErrorKind::TimedOut, "protect() timeout")),
}
}
if let Some(ref protect) = opts.vpn_socket_protect {
protect.protect(socket.as_raw_fd())?;
}
android::vpn_protect(&socket, opts).await?;
}
// Set SO_MARK for mark-based routing on Linux (since 2.6.25)
@@ -335,24 +319,7 @@ pub async fn bind_outbound_udp_socket(bind_addr: &SocketAddr, config: &ConnectOp
// Any traffic except localhost should be protected
// This is a workaround for VPNService
#[cfg(target_os = "android")]
{
use std::time::Duration;
use tokio::time;
if let Some(ref path) = config.vpn_protect_path {
// RPC calls to `VpnService.protect()`
// Timeout in 3 seconds like shadowsocks-libev
match time::timeout(Duration::from_secs(3), vpn_protect(path, socket.as_raw_fd())).await {
Ok(Ok(..)) => {}
Ok(Err(err)) => return Err(err),
Err(..) => return Err(io::Error::new(ErrorKind::TimedOut, "protect() timeout")),
}
}
if let Some(ref protect) = config.vpn_socket_protect {
protect.protect(socket.as_raw_fd())?;
}
}
android::vpn_protect(&socket, config).await?;
// Set SO_MARK for mark-based routing on Linux (since 2.6.25)
// NOTE: This will require CAP_NET_ADMIN capability (root in most cases)
@@ -403,36 +370,67 @@ fn set_bindtodevice<S: AsRawFd>(socket: &S, iface: &str) -> io::Result<()> {
Ok(())
}
cfg_if! {
if #[cfg(target_os = "android")] {
use std::path::Path;
use tokio::io::AsyncReadExt;
#[cfg(target_os = "android")]
mod android {
use std::{
io::{self, ErrorKind},
os::unix::io::{AsRawFd, RawFd},
path::Path,
time::Duration,
};
use tokio::{io::AsyncReadExt, time};
use super::uds::UnixStream;
use super::super::uds::UnixStream;
use super::ConnectOpts;
/// This is a RPC for Android to `protect()` socket for connecting to remote servers
///
/// https://developer.android.com/reference/android/net/VpnService#protect(java.net.Socket)
///
/// More detail could be found in [shadowsocks-android](https://github.com/shadowsocks/shadowsocks-android) project.
async fn vpn_protect<P: AsRef<Path>>(protect_path: P, fd: RawFd) -> io::Result<()> {
let mut stream = UnixStream::connect(protect_path).await?;
/// This is a RPC for Android to `protect()` socket for connecting to remote servers
///
/// https://developer.android.com/reference/android/net/VpnService#protect(java.net.Socket)
///
/// More detail could be found in [shadowsocks-android](https://github.com/shadowsocks/shadowsocks-android) project.
async fn send_vpn_protect_uds<P: AsRef<Path>>(protect_path: P, fd: RawFd) -> io::Result<()> {
let mut stream = UnixStream::connect(protect_path).await?;
// send fds
let dummy: [u8; 1] = [1];
let fds: [RawFd; 1] = [fd];
stream.send_with_fd(&dummy, &fds).await?;
// send fds
let dummy: [u8; 1] = [1];
let fds: [RawFd; 1] = [fd];
stream.send_with_fd(&dummy, &fds).await?;
// receive the return value
let mut response = [0; 1];
stream.read_exact(&mut response).await?;
// receive the return value
let mut response = [0; 1];
stream.read_exact(&mut response).await?;
if response[0] == 0xFF {
return Err(io::Error::other("protect() failed"));
}
Ok(())
if response[0] == 0xFF {
return Err(io::Error::other("protect() failed"));
}
Ok(())
}
/// Try to run VPNService#protect on Android
///
/// https://developer.android.com/reference/android/net/VpnService#protect(java.net.Socket)
pub async fn vpn_protect<S>(socket: &S, opts: &ConnectOpts) -> io::Result<()>
where
S: AsRawFd + Send + Sync + 'static,
{
// shadowsocks-android uses a Unix domain socket to communicate with the VPNService#protect
if let Some(ref path) = opts.vpn_protect_path {
// RPC calls to `VpnService.protect()`
// Timeout in 3 seconds like shadowsocks-libev
match time::timeout(Duration::from_secs(3), send_vpn_protect_uds(path, socket.as_raw_fd())).await {
Ok(Ok(..)) => {}
Ok(Err(err)) => return Err(err),
Err(..) => return Err(io::Error::new(ErrorKind::TimedOut, "protect() timeout")),
}
}
// Customized SocketProtect
if let Some(ref protect) = opts.vpn_socket_protect {
protect.protect(socket.as_raw_fd())?;
}
Ok(())
}
}