feat(tun): implement process bypass for TCP/UDP and IP bypass for UDP using existing Extended tables

This commit is contained in:
ospab 2026-06-14 00:02:08 +03:00
parent 74b6648db1
commit 486d745d47
8 changed files with 171 additions and 63 deletions

View File

@ -1,4 +1,21 @@
fn main() { fn main() {
let route = ostp_tun::windows::windows_route::sys::get_default_ipv4_route(); let socket = std::net::UdpSocket::bind("0.0.0.0:0").unwrap();
println!("Default IPv4 route: {:?}", route); let port = socket.local_addr().unwrap().port();
println!("Bound UDP to port {}", port);
if let Some(name) = ostp_client::tunnel::process_lookup::get_process_name_from_port_udp(port) {
println!("Found process for UDP port {}: {}", port, name);
} else {
println!("Process not found for UDP port {}", port);
}
let tcp_socket = std::net::TcpListener::bind("0.0.0.0:0").unwrap();
let tcp_port = tcp_socket.local_addr().unwrap().port();
println!("Bound TCP to port {}", tcp_port);
if let Some(name) = ostp_client::tunnel::process_lookup::get_process_name_from_port(tcp_port) {
println!("Found process for TCP port {}: {}", tcp_port, name);
} else {
println!("Process not found for TCP port {}", tcp_port);
}
} }

View File

@ -84,13 +84,6 @@ pub async fn run_native_tunnel(
} }
} }
if !config.exclusions.processes.is_empty() {
tracing::warn!(
"Process-based split tunneling is not fully supported in TUN mode on all platforms \
without WFP/eBPF. Processes in the exclusion list will still be tunneled. \
Use IP or domain exclusions instead."
);
}
// ── 3. Create TUN device via ostp-tun crate ─────────────────────────────── // ── 3. Create TUN device via ostp-tun crate ───────────────────────────────
let opts = ostp_tun::OstpTunOptions { let opts = ostp_tun::OstpTunOptions {
@ -167,10 +160,41 @@ pub async fn run_native_tunnel(
} }
a 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 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 { let mut udp_proxy_task = tokio::spawn(async move {
if let Some(udp_sock) = udp_socket { if let Some(udp_sock) = udp_socket {
super::udp_nat::run_udp_nat(udp_sock, udp_proxy_addr, debug_udp).await; #[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;
} }
}); });
@ -193,32 +217,8 @@ pub async fn run_native_tunnel(
a a
}; };
// Build exclusion matcher for SNI-based domain bypass (fallback / CDN handling)
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");
}
}
});
// Physical interface index was captured at the start of the function. // Physical interface index was captured at the start of the function.
// 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 mut tcp_accept_task = tokio::spawn(async move { let mut tcp_accept_task = tokio::spawn(async move {
let Some(mut listener) = tcp_listener else { return; }; let Some(mut listener) = tcp_listener else { return; };
@ -250,8 +250,21 @@ pub async fn run_native_tunnel(
// ── Decide: bypass or tunnel? ───────────────────────────────── // ── Decide: bypass or tunnel? ─────────────────────────────────
let mut should_bypass = false; let mut should_bypass = false;
// 1. SNI domain check (belt-and-suspenders for CDNs / late-resolved IPs) // 1. Process match via OS Extended TCP Table (Windows)
if sniff_len > 0 { #[cfg(target_os = "windows")]
if !should_bypass {
if let Some(proc_name) = crate::tunnel::process_lookup::get_process_name_from_port(local.port()) {
if matcher.match_process(&proc_name) {
if true {
tracing::debug!("TUN BYPASS (Process match): {} → {remote}", proc_name);
}
should_bypass = true;
}
}
}
// 2. SNI domain check (belt-and-suspenders for CDNs / late-resolved IPs)
if !should_bypass && sniff_len > 0 {
if let Some(sni) = if let Some(sni) =
crate::tunnel::sni_sniff::extract_sni(&sniff_buf[..sniff_len]) crate::tunnel::sni_sniff::extract_sni(&sniff_buf[..sniff_len])
{ {
@ -267,7 +280,7 @@ pub async fn run_native_tunnel(
} }
} }
// 2. Destination IP CIDR check (for IPs not in routing table / IPv6) // 3. Destination IP CIDR check (for IPs not in routing table / IPv6)
if !should_bypass && matcher.match_ip(&remote.ip()) { if !should_bypass && matcher.match_ip(&remote.ip()) {
if true { if true {
tracing::debug!("TUN BYPASS (IP match): {remote}"); tracing::debug!("TUN BYPASS (IP match): {remote}");
@ -540,14 +553,6 @@ pub async fn run_native_tunnel_from_fd(
proxy_addr = proxy_addr.replace("0.0.0.0:", "127.0.0.1:"); proxy_addr = proxy_addr.replace("0.0.0.0:", "127.0.0.1:");
} }
let udp_proxy_addr = proxy_addr.clone();
let debug_udp = debug;
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).await;
}
});
let current_exclusions = exclusions_rx.borrow().clone(); let current_exclusions = exclusions_rx.borrow().clone();
let matcher = crate::tunnel::exclusion::ExclusionMatcher::new(&current_exclusions, None, None); 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_arc = std::sync::Arc::new(tokio::sync::RwLock::new(matcher));
@ -564,6 +569,17 @@ pub async fn run_native_tunnel_from_fd(
} }
}); });
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 mut tcp_accept_task = tokio::spawn(async move {
let Some(mut listener) = tcp_listener else { return; }; let Some(mut listener) = tcp_listener else { return; };

View File

@ -10,6 +10,9 @@ pub async fn run_udp_nat(
udp_socket: netstack_smoltcp::UdpSocket, udp_socket: netstack_smoltcp::UdpSocket,
proxy_addr: String, proxy_addr: String,
_debug: bool, _debug: bool,
matcher: std::sync::Arc<tokio::sync::RwLock<crate::tunnel::exclusion::ExclusionMatcher>>,
phys_if_index: Option<u32>,
phys_if_name: Option<String>,
) { ) {
let (mut rx, tx) = udp_socket.split(); let (mut rx, tx) = udp_socket.split();
let tx = Arc::new(Mutex::new(tx)); let tx = Arc::new(Mutex::new(tx));
@ -33,11 +36,41 @@ pub async fn run_udp_nat(
let proxy_addr_clone = proxy_addr.clone(); let proxy_addr_clone = proxy_addr.clone();
let tx_clone = tx.clone(); let tx_clone = tx.clone();
let mut should_bypass = false;
{
let matcher_guard = matcher.read().await;
if matcher_guard.match_ip(&dst.ip()) {
should_bypass = true;
tracing::debug!("TUN UDP BYPASS (IP match): {} → {}", src, dst);
}
#[cfg(target_os = "windows")]
if !should_bypass {
if let Some(proc_name) = crate::tunnel::process_lookup::get_process_name_from_port_udp(src.port()) {
if matcher_guard.match_process(&proc_name) {
should_bypass = true;
tracing::debug!("TUN UDP BYPASS (Process match): {} ({} → {})", proc_name, src, dst);
}
}
}
}
let p_if_idx = phys_if_index;
let p_if_name = phys_if_name.clone();
tokio::spawn(async move { tokio::spawn(async move {
tracing::debug!("Starting UDP NAT session for {}", src); if should_bypass {
let res = start_udp_session(src, proxy_addr_clone, &mut session_rx, tx_clone).await; tracing::debug!("Starting UDP BYPASS session for {}", src);
if res.is_err() { let res = start_udp_bypass_session(src, p_if_idx, p_if_name, &mut session_rx, tx_clone).await;
tracing::debug!("UDP NAT session for {} ended: {:?}", src, res.err()); if res.is_err() {
tracing::debug!("UDP BYPASS session for {} ended: {:?}", src, res.err());
}
} else {
tracing::debug!("Starting UDP NAT session for {}", src);
let res = start_udp_session(src, proxy_addr_clone, &mut session_rx, tx_clone).await;
if res.is_err() {
tracing::debug!("UDP NAT session for {} ended: {:?}", src, res.err());
}
} }
}); });
} }
@ -58,6 +91,55 @@ pub async fn run_udp_nat(
} }
} }
async fn start_udp_bypass_session(
client_src: SocketAddr,
phys_if_index: Option<u32>,
phys_if_name: Option<String>,
session_rx: &mut mpsc::Receiver<(Vec<u8>, SocketAddr)>,
smoltcp_tx: Arc<Mutex<netstack_smoltcp::udp::WriteHalf>>,
) -> anyhow::Result<()> {
let socket = match client_src {
SocketAddr::V4(_) => UdpSocket::bind("0.0.0.0:0").await?,
SocketAddr::V6(_) => UdpSocket::bind("[::]:0").await?,
};
#[cfg(target_os = "windows")]
if let Some(idx) = phys_if_index {
let _ = crate::tunnel::proxy::bind_socket_to_interface(&socket, client_src.is_ipv6(), idx);
}
#[cfg(target_os = "linux")]
if let Some(ref name) = phys_if_name {
let _ = crate::tunnel::proxy::bind_socket_to_interface(&socket, name);
}
let socket = Arc::new(socket);
let socket_rx = socket.clone();
// Spawn a task to read from physical socket and send back to smoltcp
let tx_clone = smoltcp_tx.clone();
tokio::spawn(async move {
use futures::SinkExt;
let mut buf = [0u8; 65536];
loop {
match socket_rx.recv_from(&mut buf).await {
Ok((n, peer)) => {
let mut lock = tx_clone.lock().await;
let _ = lock.send((buf[..n].to_vec(), peer, client_src)).await;
}
Err(_) => break,
}
}
});
while let Some((payload, dst)) = session_rx.recv().await {
socket.send_to(&payload, dst).await?;
}
Ok(())
}
async fn start_udp_session( async fn start_udp_session(
client_src: SocketAddr, client_src: SocketAddr,
proxy_addr: String, proxy_addr: String,

View File

@ -389,13 +389,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
}, },
); );
}), }),
const SizedBox(height: 16),
_buildToggle('XTLS-Reality', 'Подделка TLS-сессии (Stealth-домен должен быть TLS 1.3)', _realityEnabled, (val) {
setState(() {
_realityEnabled = val;
});
}),
const SizedBox(height: 16),
], ],
), ),
), ),

View File

@ -347,7 +347,7 @@
<input id="tag-input-processes" class="tag-input-field" type="text" <input id="tag-input-processes" class="tag-input-field" type="text"
placeholder="chrome.exe" spellcheck="false" autocomplete="off" /> placeholder="chrome.exe" spellcheck="false" autocomplete="off" />
</div> </div>
<span class="field-hint" id="proc-hint">Only works in TUN mode. Type process name and press Enter.</span> <span class="field-hint" id="proc-hint">Type process name and press Enter.</span>
</div> </div>
</div> </div>

View File

@ -333,8 +333,10 @@ pub extern "system" fn Java_net_ostp_client_OstpClientSdk_nativeStartClient(
} }
let shutdown_rx_clone = shutdown_tx.subscribe(); let shutdown_rx_clone = shutdown_tx.subscribe();
let config_clone = config.clone(); let config_clone = config.clone();
let (exclusions_tx, exclusions_rx) = tokio::sync::watch::channel(config.exclusions.clone());
rt.spawn(async move { rt.spawn(async move {
if let Err(e) = tunnel::native_handler::run_native_tunnel_from_fd(config_clone, shutdown_rx_clone, fd).await { let _tx = exclusions_tx; // keep tx alive
if let Err(e) = tunnel::native_handler::run_native_tunnel_from_fd(config_clone, shutdown_rx_clone, exclusions_rx, fd).await {
add_log(format!("Native TUN exited with error: {}", e)); add_log(format!("Native TUN exited with error: {}", e));
} }
}); });

View File

@ -24,6 +24,7 @@ pub mod linux;
pub mod macos; pub mod macos;
impl OstpTunInterface { impl OstpTunInterface {
#[allow(unused_variables)]
pub async fn create(opts: OstpTunOptions) -> Result<Self> { pub async fn create(opts: OstpTunOptions) -> Result<Self> {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
return windows::create(opts).await; return windows::create(opts).await;

View File

@ -1300,8 +1300,6 @@ async fn run_app() -> Result<()> {
"target": "127.0.0.1:8080" "target": "127.0.0.1:8080"
}}, }},
// Reality (XTLS) / UoT Masquerade parameters
"debug": false "debug": false
}}"#, key) }}"#, key)
@ -1347,9 +1345,7 @@ async fn run_app() -> Result<()> {
"processes": [] "processes": []
}}, }},
// Reality (XTLS) / WebRTC Masquerade parameters // Transport Mode: "udp" (default WebRTC masquerade) or "uot" (TCP UoT)
// Transport Mode: "udp" (default WebRTC masquerade) or "uot" (TCP XTLS-Reality)
"transport": {{ "transport": {{
"mode": "udp", "mode": "udp",
"stealth_sni": "www.microsoft.com", "stealth_sni": "www.microsoft.com",