implement real gateway path.
Some checks failed
CI / Rust (push) Successful in 25s
CI / Android (push) Failing after 2s

This commit is contained in:
2026-05-31 20:10:11 +03:30
parent 442fad6b05
commit 266cae92ce
7 changed files with 1062 additions and 17 deletions

View File

@@ -11,6 +11,7 @@ name = "vpnshare-desktop"
path = "src/main.rs"
[dependencies]
tun2socks = "0.1.10"
vpnshare-core = { path = "../../crates/vpnshare-core" }
vpnshare-proto = { path = "../../crates/vpnshare-proto" }
vpnshare-transport = { path = "../../crates/vpnshare-transport" }

View File

@@ -1,14 +1,627 @@
use vpnshare_core::{GatewayConfig, MtuPolicy};
use vpnshare_proto::{encode_frame, FrameType, PROTOCOL_VERSION};
use std::env;
use std::net::{SocketAddr, TcpStream};
use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode};
use std::thread;
use std::time::{Duration, Instant};
fn main() {
let config = GatewayConfig::default();
let mtu = MtuPolicy::default();
let hello = encode_frame(FrameType::Hello, 0, 1, b"vpnshare-desktop").expect("static hello frame");
println!("VPN Share desktop client skeleton");
println!("protocol=VSHP/{PROTOCOL_VERSION}");
println!("default_gateway_mtu={}", config.default_mtu);
println!("effective_tunnel_mtu={}", mtu.effective_tunnel_mtu());
println!("hello_frame_bytes={}", hello.len());
fn main() -> ExitCode {
let cli = Cli::parse(env::args().skip(1));
match cli.command {
CommandKind::Help => {
print_help();
ExitCode::SUCCESS
}
CommandKind::Status => match DesktopStatus::collect() {
Ok(status) => {
print_status(&status);
ExitCode::SUCCESS
}
Err(error) => {
eprintln!("status failed: {error}");
ExitCode::from(1)
}
},
CommandKind::AndroidLaunch => match DesktopStatus::find_adb().and_then(|adb| adb.launch_android_app()) {
Ok(output) => {
println!("{output}");
ExitCode::SUCCESS
}
Err(error) => {
eprintln!("android launch failed: {error}");
ExitCode::from(1)
}
},
CommandKind::Connect => match DesktopStatus::find_adb().and_then(|adb| adb.connect_usb_socks()) {
Ok(connection) => {
print_connection(&connection);
ExitCode::SUCCESS
}
Err(error) => {
eprintln!("connect failed: {error}");
ExitCode::from(1)
}
},
CommandKind::SystemGateway => match SystemGateway::run() {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("system gateway failed: {error}");
ExitCode::from(1)
}
},
CommandKind::SystemGatewayCleanup => match SystemGateway::cleanup() {
Ok(()) => {
println!("VPN Share system gateway routes removed");
ExitCode::SUCCESS
}
Err(error) => {
eprintln!("cleanup failed: {error}");
ExitCode::from(1)
}
},
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct Cli {
command: CommandKind,
}
impl Cli {
fn parse(args: impl IntoIterator<Item = String>) -> Self {
let mut args = args.into_iter();
let command = match args.next().as_deref() {
None | Some("-h") | Some("--help") | Some("help") => CommandKind::Help,
Some("status") | Some("doctor") => CommandKind::Status,
Some("android-launch") | Some("launch") => CommandKind::AndroidLaunch,
Some("connect") => CommandKind::Connect,
Some("system-gateway") | Some("gateway") | Some("tun") => CommandKind::SystemGateway,
Some("cleanup") | Some("disconnect") | Some("system-gateway-cleanup") => CommandKind::SystemGatewayCleanup,
Some(_) => CommandKind::Help,
};
Self { command }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CommandKind {
Help,
Status,
AndroidLaunch,
Connect,
SystemGateway,
SystemGatewayCleanup,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct DesktopStatus {
os: String,
tun_available: bool,
adb: Option<AdbStatus>,
}
impl DesktopStatus {
fn collect() -> Result<Self, String> {
let adb = match Self::find_adb() {
Ok(adb) => Some(adb.status()?),
Err(_) => None,
};
Ok(Self {
os: env::consts::OS.to_string(),
tun_available: Path::new("/dev/net/tun").exists(),
adb,
})
}
fn find_adb() -> Result<Adb, String> {
let mut candidates = Vec::new();
if let Ok(path) = env::var("VPN_SHARE_ADB") {
candidates.push(PathBuf::from(path));
}
candidates.push(PathBuf::from("/tmp/vpnshare-platform-tools-36/platform-tools/adb"));
candidates.push(PathBuf::from("/tmp/vpnshare-android-sdk/platform-tools/adb"));
candidates.push(PathBuf::from("adb"));
for candidate in candidates {
let output = Command::new(&candidate).arg("version").output();
if matches!(output, Ok(output) if output.status.success()) {
return Ok(Adb { path: candidate });
}
}
Err("adb not found; set VPN_SHARE_ADB or install Android platform-tools".to_string())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct Adb {
path: PathBuf,
}
impl Adb {
fn connect_usb_socks(&self) -> Result<UsbSocksConnection, String> {
let devices_output = self.run(["devices", "-l"])?;
let devices = parse_adb_devices(&devices_output);
let device = devices
.iter()
.find(|device| device.state == "device")
.ok_or_else(|| "no authorized adb device found".to_string())?;
self.launch_android_gateway()?;
self.run(["forward", "--remove", &format!("tcp:{SOCKS_PORT}")]).ok();
self.run(["forward", &format!("tcp:{SOCKS_PORT}"), &format!("tcp:{SOCKS_PORT}")])?;
let endpoint: SocketAddr = format!("127.0.0.1:{SOCKS_PORT}")
.parse()
.map_err(|error| format!("invalid local socks endpoint: {error}"))?;
TcpStream::connect_timeout(&endpoint, Duration::from_secs(3))
.map_err(|error| format!("adb forward is present, but local socks port did not open: {error}"))?;
Ok(UsbSocksConnection {
device_serial: device.serial.clone(),
socks_url: format!("socks5h://127.0.0.1:{SOCKS_PORT}"),
})
}
fn status(&self) -> Result<AdbStatus, String> {
let devices_output = self.run(["devices", "-l"])?;
let devices = parse_adb_devices(&devices_output);
let package_installed = self
.run(["shell", "pm", "path", "org.vpnshare"])
.map(|output| output.contains("package:"))
.unwrap_or(false);
let app_pid = self
.run(["shell", "pidof", "org.vpnshare"])
.ok()
.and_then(|output| output.split_whitespace().next().map(str::to_string));
let gateway_foreground = self
.run(["shell", "dumpsys", "activity", "services", "org.vpnshare"])
.map(|output| output.contains("isForeground=true"))
.unwrap_or(false);
Ok(AdbStatus {
path: self.path.display().to_string(),
devices,
package_installed,
app_pid,
gateway_foreground,
})
}
fn launch_android_app(&self) -> Result<String, String> {
self.run([
"shell",
"am",
"start",
"-W",
"-n",
"org.vpnshare/org.vpnshare.app.MainActivity",
])
}
fn launch_android_gateway(&self) -> Result<String, String> {
self.run([
"shell",
"am",
"start",
"-W",
"-a",
"org.vpnshare.action.START_FROM_DESKTOP",
"-n",
"org.vpnshare/org.vpnshare.app.MainActivity",
])
}
fn run<const N: usize>(&self, args: [&str; N]) -> Result<String, String> {
let output = Command::new(&self.path)
.args(args)
.output()
.map_err(|error| format!("failed to run {}: {error}", self.path.display()))?;
if !output.status.success() {
return Err(String::from_utf8_lossy(&output.stderr).trim().to_string());
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct AdbStatus {
path: String,
devices: Vec<AdbDevice>,
package_installed: bool,
app_pid: Option<String>,
gateway_foreground: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct AdbDevice {
serial: String,
state: String,
detail: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct UsbSocksConnection {
device_serial: String,
socks_url: String,
}
struct SystemGateway;
impl SystemGateway {
fn run() -> Result<(), String> {
ensure_linux()?;
ensure_tun_available()?;
ensure_socks_endpoint_open()?;
ensure_root()?;
println!("Starting VPN Share system gateway");
println!("interface={TUN_NAME}");
println!("socks=127.0.0.1:{SOCKS_PORT}");
let config = tun2socks_config();
let tunnel = thread::spawn(move || tun2socks::main_from_str(&config, -1));
if let Err(error) = wait_for_interface(TUN_NAME, Duration::from_secs(8)) {
tun2socks::quit();
let _ = tunnel.join();
return Err(error);
}
let dns_configured = configure_dns();
if let Err(error) = install_split_default_routes() {
cleanup_split_default_routes();
if dns_configured {
cleanup_dns();
}
tun2socks::quit();
let _ = tunnel.join();
return Err(error);
}
println!();
println!("VPN Share system gateway is active");
println!("All IPv4 traffic is routed through {TUN_NAME}; IPv6 is enabled when the host supports it.");
if dns_configured {
println!("dns=systemd-resolved via 1.1.1.1/8.8.8.8");
} else {
println!("dns=unchanged; install/use systemd-resolved if your current DNS is LAN-only");
}
println!("Keep this process open. Press Ctrl+C to stop.");
println!();
let result = match tunnel.join() {
Ok(Ok(())) => Ok(()),
Ok(Err(code)) => Err(format!("tun2socks exited with code {code}")),
Err(_) => Err("tun2socks thread panicked".to_string()),
};
cleanup_split_default_routes();
if dns_configured {
cleanup_dns();
}
result
}
fn cleanup() -> Result<(), String> {
ensure_linux()?;
ensure_root()?;
cleanup_split_default_routes();
cleanup_dns();
Ok(())
}
}
fn parse_adb_devices(output: &str) -> Vec<AdbDevice> {
output
.lines()
.skip(1)
.filter_map(|line| {
let line = line.trim();
if line.is_empty() {
return None;
}
let mut fields = line.split_whitespace();
let serial = fields.next()?.to_string();
let state = fields.next()?.to_string();
let detail = fields.collect::<Vec<_>>().join(" ");
Some(AdbDevice { serial, state, detail })
})
.collect()
}
fn ensure_linux() -> Result<(), String> {
if env::consts::OS == "linux" {
Ok(())
} else {
Err("system gateway mode is implemented for Linux in this build".to_string())
}
}
fn ensure_tun_available() -> Result<(), String> {
if Path::new("/dev/net/tun").exists() {
Ok(())
} else {
Err("/dev/net/tun is missing; enable the Linux tun module first".to_string())
}
}
fn ensure_socks_endpoint_open() -> Result<(), String> {
let endpoint = socks_endpoint()?;
TcpStream::connect_timeout(&endpoint, Duration::from_secs(2))
.map(|_| ())
.map_err(|error| format!("127.0.0.1:{SOCKS_PORT} is not open ({error}). Run `vpnshare-desktop connect` first."))
}
fn ensure_root() -> Result<(), String> {
let output = Command::new("id")
.arg("-u")
.output()
.map_err(|error| format!("failed to check current user id: {error}"))?;
if !output.status.success() {
return Err(String::from_utf8_lossy(&output.stderr).trim().to_string());
}
let uid = String::from_utf8_lossy(&output.stdout).trim().to_string();
if uid == "0" {
Ok(())
} else {
Err(
"system gateway mode needs root/CAP_NET_ADMIN on the desktop. Run `sudo ./target/debug/vpnshare-desktop system-gateway` after `./target/debug/vpnshare-desktop connect`."
.to_string(),
)
}
}
fn socks_endpoint() -> Result<SocketAddr, String> {
format!("127.0.0.1:{SOCKS_PORT}")
.parse()
.map_err(|error| format!("invalid local socks endpoint: {error}"))
}
fn tun2socks_config() -> String {
format!(
r#"tunnel:
name: {TUN_NAME}
mtu: {TUN_MTU}
multi-queue: false
ipv4: {TUN_IPV4}
ipv6: '{TUN_IPV6}'
socks5:
address: 127.0.0.1
port: {SOCKS_PORT}
udp: 'tcp'
misc:
task-stack-size: 20480
tcp-buffer-size: 65536
connect-timeout: 5000
read-write-timeout: 600000
log-file: stderr
log-level: warn
"#
)
}
fn wait_for_interface(name: &str, timeout: Duration) -> Result<(), String> {
let start = Instant::now();
while start.elapsed() < timeout {
if command_success("ip", &["link", "show", name]) {
return Ok(());
}
thread::sleep(Duration::from_millis(100));
}
Err(format!(
"{name} did not appear; tun2socks could not create the TUN device"
))
}
fn install_split_default_routes() -> Result<(), String> {
run_command(
"ip",
&["route", "replace", "0.0.0.0/1", "dev", TUN_NAME, "metric", "10"],
)?;
run_command(
"ip",
&["route", "replace", "128.0.0.0/1", "dev", TUN_NAME, "metric", "10"],
)?;
if let Err(error) = run_command(
"ip",
&["-6", "route", "replace", "::/1", "dev", TUN_NAME, "metric", "10"],
) {
eprintln!("warning: IPv6 ::/1 route was not installed: {error}");
}
if let Err(error) = run_command(
"ip",
&["-6", "route", "replace", "8000::/1", "dev", TUN_NAME, "metric", "10"],
) {
eprintln!("warning: IPv6 8000::/1 route was not installed: {error}");
}
Ok(())
}
fn cleanup_split_default_routes() {
let routes: &[&[&str]] = &[
&["route", "del", "0.0.0.0/1", "dev", TUN_NAME],
&["route", "del", "128.0.0.0/1", "dev", TUN_NAME],
&["-6", "route", "del", "::/1", "dev", TUN_NAME],
&["-6", "route", "del", "8000::/1", "dev", TUN_NAME],
];
for route in routes {
let _ = run_command("ip", route);
}
}
fn configure_dns() -> bool {
if !command_success("resolvectl", &["--version"]) {
return false;
}
let dns = run_command(
"resolvectl",
&[
"dns",
TUN_NAME,
"1.1.1.1",
"8.8.8.8",
"2606:4700:4700::1111",
"2001:4860:4860::8888",
],
);
let domain = run_command("resolvectl", &["domain", TUN_NAME, "~."]);
match (dns, domain) {
(Ok(_), Ok(_)) => true,
(Err(error), _) | (_, Err(error)) => {
eprintln!("warning: could not configure systemd-resolved DNS for {TUN_NAME}: {error}");
false
}
}
}
fn cleanup_dns() {
if command_success("resolvectl", &["--version"]) {
let _ = run_command("resolvectl", &["revert", TUN_NAME]);
}
}
fn command_success(program: &str, args: &[&str]) -> bool {
Command::new(program)
.args(args)
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
fn run_command(program: &str, args: &[&str]) -> Result<String, String> {
let output = Command::new(program)
.args(args)
.output()
.map_err(|error| format!("failed to run {program}: {error}"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
let message = if stderr.is_empty() { stdout } else { stderr };
return Err(if message.is_empty() {
format!("{program} exited with {}", output.status)
} else {
message
});
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
fn print_help() {
println!("VPN Share desktop CLI");
println!();
println!("Commands:");
println!(" status Check desktop prerequisites and connected Android app state");
println!(" android-launch Bring the Android VPN Share app to the foreground");
println!(" connect Start USB SOCKS5 sharing through the Android gateway");
println!(" system-gateway Create vpnshare0 and route OS traffic through the phone");
println!(" cleanup Remove VPN Share split-default routes");
println!();
println!("Environment:");
println!(" VPN_SHARE_ADB Optional path to adb");
}
fn print_connection(connection: &UsbSocksConnection) {
println!("VPN Share USB SOCKS5 sharing is ready");
println!("device={}", connection.device_serial);
println!("proxy={}", connection.socks_url);
println!();
println!("For a real OS gateway, keep this ADB forward active and run:");
println!(" sudo ./target/debug/vpnshare-desktop system-gateway");
println!();
println!("Proxy endpoint is also available for apps that explicitly support SOCKS5:");
println!(" ALL_PROXY={} curl https://ifconfig.me", connection.socks_url);
}
fn print_status(status: &DesktopStatus) {
println!("VPN Share desktop status");
println!("os={}", status.os);
println!("tun_available={}", status.tun_available);
println!(
"desktop_packet_tunnel={}",
if status.tun_available {
"available"
} else {
"unavailable"
}
);
match &status.adb {
Some(adb) => {
println!("adb={}", adb.path);
println!("adb_devices={}", adb.devices.len());
for device in &adb.devices {
println!(
"device serial={} state={} {}",
device.serial, device.state, device.detail
);
}
println!("android_package_installed={}", adb.package_installed);
println!("android_app_pid={}", adb.app_pid.as_deref().unwrap_or("not_running"));
println!("android_gateway_foreground={}", adb.gateway_foreground);
}
None => {
println!("adb=not_found");
}
}
}
const SOCKS_PORT: u16 = 10808;
const TUN_NAME: &str = "vpnshare0";
const TUN_IPV4: &str = "198.18.0.1";
const TUN_IPV6: &str = "fc00::1";
const TUN_MTU: u16 = 1280;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_default_help() {
assert_eq!(Cli::parse([]).command, CommandKind::Help);
}
#[test]
fn parses_status_aliases() {
assert_eq!(Cli::parse(["doctor".to_string()]).command, CommandKind::Status);
}
#[test]
fn parses_system_gateway_aliases() {
assert_eq!(Cli::parse(["gateway".to_string()]).command, CommandKind::SystemGateway);
assert_eq!(
Cli::parse(["disconnect".to_string()]).command,
CommandKind::SystemGatewayCleanup
);
}
#[test]
fn renders_tun2socks_config() {
let config = tun2socks_config();
assert!(config.contains("name: vpnshare0"));
assert!(config.contains("port: 10808"));
assert!(config.contains("udp: 'tcp'"));
}
#[test]
fn parses_adb_devices() {
let devices = parse_adb_devices("List of devices attached\nR5CX33ESWGH device usb:1-6 model:SM_A556E\n\n");
assert_eq!(
devices,
vec![AdbDevice {
serial: "R5CX33ESWGH".to_string(),
state: "device".to_string(),
detail: "usb:1-6 model:SM_A556E".to_string()
}]
);
}
}