Skip to content

Commit cf93843

Browse files
Max Lvclaude
authored andcommitted
Add Android VPN socket protection support
When the `__android_vpn` plugin option is set, protect outbound UDP socket fds by sending them to the Android VPN service via a Unix domain socket at `protect_path` using SCM_RIGHTS. This prevents routing loops when running under shadowsocks-android's VPN mode. - Add `protect.rs` module (Android-only) implementing the fd protection protocol (mirrors v2ray-plugin's utils_android.go) - Extract `create_udp_socket()` helper that binds and protects the QUIC endpoint socket - Use `Endpoint::new()` with pre-created socket instead of `Endpoint::client()` so the fd can be protected before use - Also protect sockets created during timeout rebind Co-Authored-By: Claude (claude-opus-4-6) <noreply@anthropic.com>
1 parent da703be commit cf93843

File tree

4 files changed

+132
-18
lines changed

4 files changed

+132
-18
lines changed

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,8 @@ env_logger = "0.11"
3434
dirs = "6"
3535
rustls-native-certs = "0.8"
3636

37+
[target.'cfg(target_os = "android")'.dependencies]
38+
libc = "0.2"
39+
3740
[dev-dependencies]
3841
serial_test = "3"

src/client.rs

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ use log::LevelFilter;
1616

1717
mod args;
1818
mod common;
19+
#[cfg(target_os = "android")]
20+
mod protect;
1921

2022
#[derive(Parser, Debug)]
2123
#[command(name = "qtun-client")]
@@ -31,6 +33,28 @@ struct Opt {
3133
host: String,
3234
}
3335

36+
/// Create a UDP socket and optionally protect it for Android VPN mode.
37+
fn create_udp_socket(vpn_mode: bool) -> Result<std::net::UdpSocket> {
38+
let socket = if cfg!(target_os = "windows") {
39+
std::net::UdpSocket::bind("0.0.0.0:0")?
40+
} else {
41+
std::net::UdpSocket::bind("[::]:0")?
42+
};
43+
44+
#[cfg(target_os = "android")]
45+
if vpn_mode {
46+
use std::os::unix::io::AsRawFd;
47+
let fd = socket.as_raw_fd();
48+
protect::protect(fd).map_err(|e| anyhow!("failed to protect socket: {:?}", e))?;
49+
info!("socket fd {} protected", fd);
50+
}
51+
52+
#[cfg(not(target_os = "android"))]
53+
let _ = vpn_mode;
54+
55+
Ok(socket)
56+
}
57+
3458
#[tokio::main]
3559
async fn main() -> Result<()> {
3660
// setup log
@@ -46,6 +70,7 @@ async fn main() -> Result<()> {
4670
let mut listen_addr = options.listen;
4771
let mut relay_addr = options.relay;
4872
let mut host = options.host;
73+
let mut vpn_mode = false;
4974

5075
// parse environment variables
5176
if let Ok((ss_local_addr, ss_remote_addr)) = args::parse_env_addr() {
@@ -57,6 +82,10 @@ async fn main() -> Result<()> {
5782
if let Some(h) = ss_plugin_opts.get("host") {
5883
host = h.clone();
5984
}
85+
if ss_plugin_opts.contains_key("__android_vpn") {
86+
vpn_mode = true;
87+
info!("VPN mode enabled");
88+
}
6089
}
6190

6291
let mut roots = rustls::RootCertStore {
@@ -73,12 +102,13 @@ async fn main() -> Result<()> {
73102

74103
client_crypto.alpn_protocols = common::ALPN_QUIC_HTTP.iter().map(|&x| x.into()).collect();
75104

76-
// WAR for Windows endpoint
77-
let mut endpoint = if cfg!(target_os = "windows") {
78-
Endpoint::client("0.0.0.0:0".parse().unwrap())
79-
} else {
80-
Endpoint::client("[::]:0".parse().unwrap())
81-
}?;
105+
let socket = create_udp_socket(vpn_mode)?;
106+
let mut endpoint = Endpoint::new(
107+
quinn::EndpointConfig::default(),
108+
None,
109+
socket,
110+
Arc::new(quinn::TokioRuntime),
111+
)?;
82112
let client_config =
83113
quinn::ClientConfig::new(Arc::new(QuicClientConfig::try_from(client_crypto)?));
84114

@@ -99,7 +129,7 @@ async fn main() -> Result<()> {
99129
let host = Arc::clone(&host);
100130
let endpoint = Arc::clone(&endpoint);
101131

102-
let transfer = transfer(remote, host, endpoint, inbound);
132+
let transfer = transfer(remote, host, endpoint, inbound, vpn_mode);
103133
tokio::spawn(transfer);
104134
}
105135

@@ -111,25 +141,28 @@ async fn transfer(
111141
host: Arc<String>,
112142
endpoint: Arc<Endpoint>,
113143
mut inbound: TcpStream,
144+
vpn_mode: bool,
114145
) -> Result<()> {
115146
let new_conn = endpoint
116147
.connect(*remote, &host)?
117148
.await
118149
.map_err(|e| {
119150
if e == ConnectionError::TimedOut {
120-
let socket = if cfg!(target_os = "windows") {
121-
std::net::UdpSocket::bind("0.0.0.0:0").unwrap()
122-
} else {
123-
std::net::UdpSocket::bind("[::]:0").unwrap()
124-
};
125-
let addr = socket.local_addr().unwrap();
126-
let ret = endpoint.rebind(socket);
127-
match ret {
128-
Ok(_) => {
129-
info!("rebinding to: {}", addr);
151+
match create_udp_socket(vpn_mode) {
152+
Ok(socket) => {
153+
let addr = socket.local_addr().unwrap();
154+
let ret = endpoint.rebind(socket);
155+
match ret {
156+
Ok(_) => {
157+
info!("rebinding to: {}", addr);
158+
}
159+
Err(e) => {
160+
error!("rebind fail: {:?}", e);
161+
}
162+
}
130163
}
131164
Err(e) => {
132-
error!("rebind fail: {:?}", e);
165+
error!("failed to create socket for rebind: {:?}", e);
133166
}
134167
}
135168
}

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
pub mod args;
22
pub mod common;
3+
#[cfg(target_os = "android")]
4+
pub mod protect;

src/protect.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
use std::io;
2+
use std::os::unix::io::RawFd;
3+
use std::os::unix::net::UnixStream;
4+
use std::io::{Read, Write};
5+
use std::os::unix::io::AsRawFd;
6+
use std::time::Duration;
7+
8+
/// Protect a socket fd by sending it to the Android VPN service via
9+
/// a Unix domain socket at "protect_path".
10+
///
11+
/// The protocol:
12+
/// 1. Connect to abstract Unix domain socket "protect_path"
13+
/// 2. Send the fd as SCM_RIGHTS ancillary data with a 1-byte dummy payload
14+
/// 3. Read a 1-byte acknowledgment from the VPN service
15+
///
16+
/// This mirrors the behavior of v2ray-plugin's utils_android.go.
17+
pub fn protect(fd: RawFd) -> io::Result<()> {
18+
let stream = UnixStream::connect("protect_path")?;
19+
stream.set_read_timeout(Some(Duration::from_secs(3)))?;
20+
stream.set_write_timeout(Some(Duration::from_secs(3)))?;
21+
22+
send_fd(&stream, fd)?;
23+
24+
// Wait for 1-byte acknowledgment
25+
let mut buf = [0u8; 1];
26+
(&stream).read_exact(&mut buf)?;
27+
28+
Ok(())
29+
}
30+
31+
/// Send a file descriptor over a Unix domain socket using SCM_RIGHTS.
32+
fn send_fd(stream: &UnixStream, fd: RawFd) -> io::Result<()> {
33+
use libc::{
34+
c_void, cmsghdr, iovec, msghdr, sendmsg, CMSG_DATA, CMSG_FIRSTHDR, CMSG_LEN, CMSG_SPACE,
35+
SCM_RIGHTS, SOL_SOCKET,
36+
};
37+
use std::mem;
38+
use std::ptr;
39+
40+
let dummy: [u8; 1] = [b'!'];
41+
42+
let mut iov = iovec {
43+
iov_base: dummy.as_ptr() as *mut c_void,
44+
iov_len: 1,
45+
};
46+
47+
// Allocate space for the control message containing one fd
48+
let cmsg_space = unsafe { CMSG_SPACE(mem::size_of::<RawFd>() as u32) } as usize;
49+
let mut cmsg_buf = vec![0u8; cmsg_space];
50+
51+
let mut msg: msghdr = unsafe { mem::zeroed() };
52+
msg.msg_iov = &mut iov;
53+
msg.msg_iovlen = 1;
54+
msg.msg_control = cmsg_buf.as_mut_ptr() as *mut c_void;
55+
msg.msg_controllen = cmsg_space as _;
56+
57+
let cmsg: &mut cmsghdr = unsafe { &mut *CMSG_FIRSTHDR(&msg) };
58+
cmsg.cmsg_level = SOL_SOCKET;
59+
cmsg.cmsg_type = SCM_RIGHTS;
60+
cmsg.cmsg_len = unsafe { CMSG_LEN(mem::size_of::<RawFd>() as u32) } as _;
61+
62+
unsafe {
63+
ptr::copy_nonoverlapping(
64+
&fd as *const RawFd as *const u8,
65+
CMSG_DATA(cmsg),
66+
mem::size_of::<RawFd>(),
67+
);
68+
}
69+
70+
let result = unsafe { sendmsg(stream.as_raw_fd(), &msg, 0) };
71+
if result < 0 {
72+
return Err(io::Error::last_os_error());
73+
}
74+
75+
Ok(())
76+
}

0 commit comments

Comments
 (0)