ostp/ostp-client/src/tunnel/inbounds/tun.rs.bak

745 lines
61 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use anyhow::{anyhow, Result};
use tokio::sync::watch;
// ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ
// Windows / Linux desktop TUN
// ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ
#[cfg(any(target_os = "windows", target_os = "linux"))]
pub async fn run_native_tunnel(
config: crate::config::ClientConfig,
mut shutdown: watch::Receiver<bool>,
mut exclusions_rx: watch::Receiver<crate::config::ExclusionConfig>,
) -> Result<()> {
use std::net::ToSocketAddrs;
use netstack_smoltcp::StackBuilder;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use futures::{StreamExt, SinkExt};
#[cfg(target_os = "linux")]
{
use std::io::{self, IsTerminal, Write};
if io::stdout().is_terminal() {
println!("\n===================================================================");
println!("WARNING: TUN mode will modify the system routing table.");
println!("If you are connected to a headless server via SSH, you may lose");
println!("your connection when default routes are redirected into the tunnel.");
println!("===================================================================\n");
print!("Are you sure you want to initialize the TUN interface? [yes/no]: ");
io::stdout().flush().unwrap();
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
let ans = input.trim().to_lowercase();
if ans != "y" && ans != "yes" {
return Err(anyhow!("TUN initialization aborted by user."));
}
}
}
let debug = config.debug;
tracing::info!("Initializing NATIVE TUN tunnel (smoltcp)...");
// Capture physical interface index for bypass BEFORE we create the TUN device and alter routes.
#[cfg(target_os = "windows")]
let phys_if_for_bypass: Option<u32> = ostp_tun::windows::windows_route::sys::get_default_ipv4_route().map(|(_, idx)| idx);
#[cfg(not(target_os = "windows"))]
let phys_if_for_bypass: Option<u32> = None;
// ΓöÇΓöÇ 1. Resolve server IP ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ
let server_ip = config
.ostp
.server_addr
.to_socket_addrs()
.map_err(|e| anyhow!("Failed to resolve server IP: {}", e))?
.next()
.map(|a| a.ip())
.ok_or_else(|| anyhow!("Could not resolve server host"))?;
#[allow(unused_variables)]
let server_ip_str = server_ip.to_string();
// ── 2. Resolve excluded domains → IP addresses for bypass routing ─────────
let mut bypass_ips: Vec<std::net::IpAddr> = Vec::new();
// Server IP always bypasses TUN
bypass_ips.push(server_ip);
for ip_str in &config.exclusions.ips {
let host = ip_str.split('/').next().unwrap_or(ip_str);
if let Ok(ip) = host.parse() {
bypass_ips.push(ip);
}
}
for domain in &config.exclusions.domains {
match tokio::net::lookup_host((domain.as_str(), 443u16)).await {
Ok(addrs) => {
for addr in addrs {
bypass_ips.push(addr.ip());
}
}
Err(e) => {
tracing::warn!("Failed to pre-resolve excluded domain {domain}: {e}");
}
}
}
// ΓöÇΓöÇ 3. Create TUN device via ostp-tun crate ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ
let opts = ostp_tun::OstpTunOptions {
server_ip,
bypass_ips,
dns_server: config.dns_server.clone(),
kill_switch: config.kill_switch,
mtu: config.ostp.mtu as u16,
wintun_path: None,
};
let tun_interface = ostp_tun::OstpTunInterface::create(opts)
.await
.map_err(|e| anyhow!("Failed to create OstpTunInterface: {}", e))?;
let dev = tun_interface.device;
let _route_guard = tun_interface.guard;
// ΓöÇΓöÇ 7. Build smoltcp network stack ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ
let (stack, tcp_runner, udp_socket, tcp_listener) = StackBuilder::default()
.stack_buffer_size(1024)
.tcp_buffer_size(1024)
.udp_buffer_size(1024)
.enable_tcp(true)
.enable_udp(true)
.mtu(config.ostp.mtu)
.build()?;
let mut runner_task = tokio::spawn(async move {
if let Some(runner) = tcp_runner {
let _ = runner.await;
}
});
// ΓöÇΓöÇ 8. Wire TUN Γåö smoltcp stack ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ
let (mut stack_sink, mut stack_stream) = stack.split();
let (mut tun_read, mut tun_write) = tokio::io::split(dev);
let mut tun_to_stack = tokio::spawn(async move {
let mut buf = vec![0u8; 65536];
loop {
match tun_read.read(&mut buf).await {
Ok(0) => break,
Ok(n) => {
let frame = buf[..n].to_vec();
if let Err(e) = stack_sink.send(frame).await {
if e.kind() == std::io::ErrorKind::BrokenPipe {
break;
}
}
}
Err(e) => {
tracing::debug!("tun_read error: {e}");
}
}
}
});
let mut stack_to_tun = tokio::spawn(async move {
while let Some(Ok(frame)) = stack_stream.next().await {
if let Err(e) = tun_write.write(&frame).await {
tracing::debug!("tun_write error: {e}");
}
}
});
// ΓöÇΓöÇ 9. UDP: forward everything through OSTP proxy ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ
// UDP exclusions are handled at the routing table level (step 5), so
// UDP packets for excluded IPs never reach smoltcp at all.
let udp_proxy_addr = {
let mut a = config.local_proxy.bind_addr.clone();
if a.starts_with("0.0.0.0:") {
a = a.replace("0.0.0.0:", "127.0.0.1:");
}
a
};
// Build exclusion matcher for dynamic bypass
let current_exclusions = exclusions_rx.borrow().clone();
let matcher = crate::tunnel::exclusion::ExclusionMatcher::new(&current_exclusions, None, None);
let matcher_arc = std::sync::Arc::new(tokio::sync::RwLock::new(matcher));
let matcher_clone = matcher_arc.clone();
tokio::spawn(async move {
while let Ok(_) = exclusions_rx.changed().await {
let current = exclusions_rx.borrow().clone();
let new_matcher = crate::tunnel::exclusion::ExclusionMatcher::new(&current, None, None);
*matcher_clone.write().await = new_matcher;
if true {
tracing::debug!("Desktop TUN exclusions hot-reloaded");
}
}
});
// Linux: physical interface name for SO_BINDTODEVICE
#[cfg(target_os = "linux")]
let linux_phys_name = crate::tunnel::proxy::get_linux_physical_if_name();
#[cfg(not(target_os = "linux"))]
let linux_phys_name: Option<String> = None;
let _ = &linux_phys_name; // suppress unused warning on Windows
let debug_udp = debug;
let udp_matcher = matcher_arc.clone();
#[cfg(target_os = "linux")]
let udp_lin_name = linux_phys_name.clone();
let mut udp_proxy_task = tokio::spawn(async move {
if let Some(udp_sock) = udp_socket {
#[cfg(target_os = "linux")]
super::udp_nat::run_udp_nat(udp_sock, udp_proxy_addr, debug_udp, udp_matcher, phys_if_for_bypass, udp_lin_name).await;
#[cfg(not(target_os = "linux"))]
super::udp_nat::run_udp_nat(udp_sock, udp_proxy_addr, debug_udp, udp_matcher, phys_if_for_bypass, None).await;
}
});
// ΓöÇΓöÇ 10. TCP: forward to OSTP proxy (with domain-level bypass via SNI) ΓöÇΓöÇΓöÇΓöÇΓöÇ
//
// For IP-based exclusions: handled by routing table → packets never arrive here.
// For domain-based exclusions: The IP is already in routing table (pre-resolved in
// step 3), so most traffic won't arrive. As a belt-and-suspenders fallback,
// we also sniff TLS SNI and bypass if it matches ΓÇö this covers CDN cases where
// the IP wasn't known at startup.
//
// For bypassed connections we bind the outgoing socket to the physical interface
// (IP_UNICAST_IF) so it goes out via the real NIC, not TUN.
let proxy_addr_tcp = {
let mut a = config.local_proxy.bind_addr.clone();
if a.starts_with("0.0.0.0:") {
a = a.replace("0.0.0.0:", "127.0.0.1:");
}
a
};
// Physical interface index was captured at the start of the function.
let mut tcp_accept_task = tokio::spawn(async move {
let Some(mut listener) = tcp_listener else { return; };
while let Some((mut stream, local, remote)) = listener.next().await {
let proxy_addr = proxy_addr_tcp.clone();
let matcher_arc = matcher_arc.clone();
#[cfg(target_os = "linux")]
let lin_name = linux_phys_name.clone();
tokio::spawn(async move {
let matcher = matcher_arc.read().await.clone();
if debug {
tracing::debug!("TUN TCP {local} → {remote}");
}
// ΓöÇΓöÇ Sniff TLS ClientHello for SNI ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ
let mut sniff_buf = [0u8; 2048];
let sniff_len =
match tokio::time::timeout(
std::time::Duration::from_millis(100),
stream.read(&mut sniff_buf),
)
.await
{
Ok(Ok(n)) => n,
_ => 0,
};
// ΓöÇΓöÇ Decide: bypass or tunnel? ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ
let mut should_bypass = false;
// 1. Process match via OS Extended TCP Table (Windows)
#[cfg(target_os = "windows")]
if !should_bypass {
if let Some(proc_name) = crate::tunnel::process_lookup::get_process_name_from_port(local.port()) {
if debug {
tracing::debug!("TUN TCP lookup: port {} -> process {}", local.port(), proc_name);
}
if matcher.match_process(&proc_name) {
if debug {
tracing::debug!("TUN TCP BYPASS (Process match): {} → {remote}", proc_name);
}
should_bypass = true;
}
} else {
if debug {
tracing::debug!("TUN TCP lookup: port {} -> no process found", local.port());
}
}
}
// 2. SNI domain check (belt-and-suspenders for CDNs / late-resolved IPs)
if !should_bypass && sniff_len > 0 {
if let Some(sni) =
crate::tunnel::sni_sniff::extract_sni(&sniff_buf[..sniff_len])
{
if debug {
tracing::debug!("TUN SNI: {sni}");
}
if matcher.match_domain(&sni) {
if debug {
tracing::info!("TUN TCP BYPASS (SNI domain): {sni} → {remote}");
}
should_bypass = true;
}
}
}
// 3. Destination IP CIDR check (for IPs not in routing table / IPv6)
if !should_bypass && matcher.match_ip(&remote.ip()) {
if debug {
tracing::info!("TUN TCP BYPASS (IP match): {remote}");
}
should_bypass = true;
}
// ΓöÇΓöÇ Bypass path: direct TCP bypassing TUN ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ
if should_bypass {
let socket = match remote {
std::net::SocketAddr::V4(_) => tokio::net::TcpSocket::new_v4(),
std::net::SocketAddr::V6(_) => tokio::net::TcpSocket::new_v6(),
};
let Ok(socket) = socket else { return; };
// Bind to physical interface so packets don't loop back into TUN
#[cfg(target_os = "windows")]
if let Some(idx) = phys_if_for_bypass {
if let Err(e) = crate::tunnel::proxy::bind_socket_to_interface(
&socket,
remote.is_ipv6(),
idx,
) {
tracing::error!("TUN TCP BYPASS failed to bind to physical interface {}: {}", idx, e);
} else {
if debug {
tracing::info!("TUN TCP BYPASS bound to physical interface {}", idx);
}
}
} else {
tracing::warn!("TUN TCP BYPASS has no physical interface index!");
}
#[cfg(target_os = "linux")]
if let Some(ref name) = lin_name {
let _ = crate::tunnel::proxy::bind_socket_to_interface(&socket, name);
}
match tokio::time::timeout(
std::time::Duration::from_secs(10),
socket.connect(remote),
)
.await
{
Ok(Ok(mut direct)) => {
if sniff_len > 0 {
if direct.write_all(&sniff_buf[..sniff_len]).await.is_err() {
return;
}
}
let _ = tokio::io::copy_bidirectional(&mut stream, &mut direct).await;
}
_ => {
tracing::debug!("Direct bypass connect to {remote} failed");
}
}
return;
}
// ΓöÇΓöÇ Tunnel path: forward via local OSTP SOCKS5 proxy ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ
let Ok(mut socks) = tokio::net::TcpStream::connect(&proxy_addr).await else {
return;
};
// SOCKS5 handshake (no auth)
if socks.write_all(&[5, 1, 0]).await.is_err() { return; }
let mut buf2 = [0u8; 2];
if socks.read_exact(&mut buf2).await.is_err() || buf2[0] != 5 || buf2[1] != 0 {
return;
}
// CONNECT request
let mut req = vec![5u8, 1, 0];
match remote.ip() {
std::net::IpAddr::V4(v4) => {
req.push(1);
req.extend_from_slice(&v4.octets());
}
std::net::IpAddr::V6(v6) => {
req.push(4);
req.extend_from_slice(&v6.octets());
}
}
req.extend_from_slice(&remote.port().to_be_bytes());
if socks.write_all(&req).await.is_err() { return; }
let mut rep = [0u8; 10];
if socks.read_exact(&mut rep).await.is_err() || rep[1] != 0 { return; }
// Replay sniffed bytes
if sniff_len > 0 && socks.write_all(&sniff_buf[..sniff_len]).await.is_err() {
return;
}
let _ = tokio::io::copy_bidirectional(&mut stream, &mut socks).await;
});
}
});
tracing::info!("NATIVE TUN tunnel active.");
tokio::select! {
_ = shutdown.changed() => {}
_ = &mut runner_task => {}
_ = &mut tun_to_stack => {}
_ = &mut stack_to_tun => {}
_ = &mut udp_proxy_task => {}
_ = &mut tcp_accept_task => {}
}
tracing::info!("Deactivating NATIVE TUN tunnel...");
// ΓöÇΓöÇ Cleanup ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ
// Cleanup is handled automatically by the _route_guard Drop trait in ostp-tun
Ok(())
}
// ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ
// Stub for unsupported platforms
// ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
pub async fn run_native_tunnel(
_config: crate::config::ClientConfig,
_shutdown: watch::Receiver<bool>,
_exclusions_rx: watch::Receiver<crate::config::ExclusionConfig>,
) -> Result<()> {
Err(anyhow!("Native TUN tunnel is only supported on Windows/Linux"))
}
// ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ
// Android: TUN from file-descriptor (opened by VpnService)
// ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ
#[cfg(target_os = "android")]
pub async fn run_native_tunnel_from_fd(
config: crate::config::ClientConfig,
mut shutdown: watch::Receiver<bool>,
mut exclusions_rx: watch::Receiver<crate::config::ExclusionConfig>,
fd: i32,
) -> Result<()> {
use netstack_smoltcp::StackBuilder;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use futures::{StreamExt, SinkExt};
use std::os::unix::io::{FromRawFd, AsRawFd};
let debug = config.debug;
tracing::info!("Initializing NATIVE TUN tunnel on Android (FD {})", fd);
unsafe {
let flags = libc::fcntl(fd, libc::F_GETFL);
if flags >= 0 {
libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK);
}
}
let read_fd = unsafe { libc::dup(fd) };
if read_fd < 0 {
return Err(anyhow!("Failed to dup tun fd for reading"));
}
let file = unsafe { std::fs::File::from_raw_fd(read_fd) };
let tun_stream = tokio::io::unix::AsyncFd::new(file)?;
let (stack, tcp_runner, udp_socket, tcp_listener) = StackBuilder::default()
.stack_buffer_size(1024)
.tcp_buffer_size(1024)
.udp_buffer_size(1024)
.enable_tcp(true)
.enable_udp(true)
.mtu(config.ostp.mtu)
.build()?;
let mut runner_task = tokio::spawn(async move {
if let Some(runner) = tcp_runner {
let _ = runner.await;
}
});
let (mut stack_sink, mut stack_stream) = stack.split();
let _tun_to_stack = tokio::spawn(async move {
let mut buf = vec![0u8; 65536];
loop {
let mut guard = match tun_stream.readable().await {
Ok(g) => g,
Err(_) => break,
};
let n = match guard.try_io(|inner| {
let res = unsafe {
libc::read(
inner.as_raw_fd(),
buf.as_mut_ptr() as *mut libc::c_void,
buf.len(),
)
};
if res < 0 {
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::WouldBlock {
Err(err)
} else {
Ok(0_isize)
}
} else {
Ok(res)
}
}) {
Ok(Ok(n)) if n > 0 => n as usize,
Ok(Ok(_)) => continue,
Ok(Err(_)) => continue,
Err(_) => continue,
};
let frame = buf[..n].to_vec();
if let Err(e) = stack_sink.send(frame).await {
if e.kind() == std::io::ErrorKind::BrokenPipe {
break;
}
}
}
});
let write_fd = unsafe { libc::dup(fd) };
if write_fd < 0 {
return Err(anyhow!("Failed to dup tun fd for writing"));
}
unsafe {
let flags = libc::fcntl(write_fd, libc::F_GETFL);
if flags >= 0 {
libc::fcntl(write_fd, libc::F_SETFL, flags | libc::O_NONBLOCK);
}
}
let write_file = unsafe { std::fs::File::from_raw_fd(write_fd) };
let tun_write_stream = tokio::io::unix::AsyncFd::new(write_file)?;
let _stack_to_tun = tokio::spawn(async move {
while let Some(Ok(frame)) = stack_stream.next().await {
let mut written = 0;
while written < frame.len() {
let mut guard = match tun_write_stream.writable().await {
Ok(g) => g,
Err(_) => break,
};
let res = guard.try_io(|inner| {
let res = unsafe {
libc::write(
inner.as_raw_fd(),
frame[written..].as_ptr() as *const libc::c_void,
frame.len() - written,
)
};
if res < 0 {
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::WouldBlock {
Err(err)
} else {
Ok(res)
}
} else {
Ok(res)
}
});
match res {
Ok(Ok(n)) if n > 0 => written += n as usize,
Ok(Ok(_)) => break,
Ok(Err(_)) => break,
Err(_) => continue,
}
}
}
});
let mut proxy_addr = config.local_proxy.bind_addr.clone();
if proxy_addr.starts_with("0.0.0.0:") {
proxy_addr = proxy_addr.replace("0.0.0.0:", "127.0.0.1:");
}
let current_exclusions = exclusions_rx.borrow().clone();
let matcher = crate::tunnel::exclusion::ExclusionMatcher::new(&current_exclusions, None, None);
let matcher_arc = std::sync::Arc::new(tokio::sync::RwLock::new(matcher));
let matcher_clone = matcher_arc.clone();
tokio::spawn(async move {
while let Ok(_) = exclusions_rx.changed().await {
let current = exclusions_rx.borrow().clone();
let new_matcher = crate::tunnel::exclusion::ExclusionMatcher::new(&current, None, None);
*matcher_clone.write().await = new_matcher;
if true {
tracing::debug!("Android TUN exclusions hot-reloaded");
}
}
});
let udp_proxy_addr = proxy_addr.clone();
let debug_udp = debug;
let udp_matcher = matcher_arc.clone();
let mut udp_proxy_task = tokio::spawn(async move {
if let Some(udp_sock) = udp_socket {
super::udp_nat::run_udp_nat(udp_sock, udp_proxy_addr, debug_udp, udp_matcher, None, None).await;
}
});
let mut tcp_accept_task = tokio::spawn(async move {
let Some(mut listener) = tcp_listener else { return; };
while let Some((mut stream, local, remote)) = listener.next().await {
let proxy_addr = proxy_addr.clone();
let matcher_arc = matcher_arc.clone();
tokio::spawn(async move {
let matcher = matcher_arc.read().await.clone();
if true {
tracing::debug!("Android TUN TCP {local} → {remote}");
}
// Sniff SNI
let mut sniff_buf = [0u8; 2048];
let sniff_len =
match tokio::time::timeout(
std::time::Duration::from_millis(100),
stream.read(&mut sniff_buf),
)
.await
{
Ok(Ok(n)) => n,
_ => 0,
};
let mut should_bypass = false;
// 1. SNI domain
if sniff_len > 0 {
if let Some(sni) =
crate::tunnel::sni_sniff::extract_sni(&sniff_buf[..sniff_len])
{
if true { tracing::debug!("Android TUN SNI: {sni}"); }
if matcher.match_domain(&sni) {
should_bypass = true;
}
}
}
// 2. Process (Android: /proc/net lookup)
if !should_bypass {
if let Some(exe) =
crate::tunnel::process_lookup::get_process_name_from_port(local.port())
{
if true {
tracing::debug!("Android TUN port {} → EXE: {}", local.port(), exe);
}
if matcher.match_process(&exe) {
should_bypass = true;
}
}
}
// 3. IP CIDR
if !should_bypass && matcher.match_ip(&remote.ip()) {
should_bypass = true;
}
// Bypass: connect directly (Android VPN service already protects the socket
// from re-entering the TUN through VpnService.protect())
if should_bypass {
if true {
tracing::debug!("Android TUN BYPASS: {remote}");
}
let socket = match remote {
std::net::SocketAddr::V4(_) => tokio::net::TcpSocket::new_v4(),
std::net::SocketAddr::V6(_) => tokio::net::TcpSocket::new_v6(),
};
let Ok(socket) = socket else { return; };
match tokio::time::timeout(
std::time::Duration::from_secs(10),
socket.connect(remote),
)
.await
{
Ok(Ok(mut direct)) => {
if sniff_len > 0 {
if direct.write_all(&sniff_buf[..sniff_len]).await.is_err() {
return;
}
}
let _ = tokio::io::copy_bidirectional(&mut stream, &mut direct).await;
}
_ => {
tracing::debug!("Android bypass connect to {remote} failed");
}
}
return;
}
// Tunnel via SOCKS5 proxy
let Ok(mut socks) = tokio::net::TcpStream::connect(&proxy_addr).await else {
return;
};
if socks.write_all(&[5, 1, 0]).await.is_err() { return; }
let mut buf2 = [0u8; 2];
if socks.read_exact(&mut buf2).await.is_err() || buf2[0] != 5 || buf2[1] != 0 {
return;
}
let mut req = vec![5u8, 1, 0];
match remote.ip() {
std::net::IpAddr::V4(v4) => {
req.push(1);
req.extend_from_slice(&v4.octets());
}
std::net::IpAddr::V6(v6) => {
req.push(4);
req.extend_from_slice(&v6.octets());
}
}
req.extend_from_slice(&remote.port().to_be_bytes());
if socks.write_all(&req).await.is_err() { return; }
let mut rep = [0u8; 10];
if socks.read_exact(&mut rep).await.is_err() || rep[1] != 0 { return; }
if sniff_len > 0 && socks.write_all(&sniff_buf[..sniff_len]).await.is_err() {
return;
}
let _ = tokio::io::copy_bidirectional(&mut stream, &mut socks).await;
});
}
});
tracing::info!("NATIVE TUN (Android) tunnel active.");
tokio::select! {
_ = shutdown.changed() => {}
_ = &mut runner_task => {}
_ = _tun_to_stack => {}
_ = _stack_to_tun => {}
_ = &mut udp_proxy_task => {}
_ = &mut tcp_accept_task => {}
}
tracing::info!("NATIVE TUN (Android) deactivated.");
Ok(())
}
#[cfg(not(target_os = "android"))]
pub async fn run_native_tunnel_from_fd(
_config: crate::config::ClientConfig,
_shutdown: watch::Receiver<bool>,
_exclusions_rx: watch::Receiver<crate::config::ExclusionConfig>,
_fd: i32,
) -> Result<()> {
Err(anyhow!("Native TUN from FD is only supported on Android"))
}