mirror of https://github.com/ospab/ostp.git
feat(tun): implement process bypass for TCP/UDP and IP bypass for UDP using existing Extended tables
This commit is contained in:
parent
74b6648db1
commit
486d745d47
|
|
@ -1,4 +1,21 @@
|
|||
fn main() {
|
||||
let route = ostp_tun::windows::windows_route::sys::get_default_ipv4_route();
|
||||
println!("Default IPv4 route: {:?}", route);
|
||||
let socket = std::net::UdpSocket::bind("0.0.0.0:0").unwrap();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ───────────────────────────────
|
||||
let opts = ostp_tun::OstpTunOptions {
|
||||
|
|
@ -167,10 +160,41 @@ pub async fn run_native_tunnel(
|
|||
}
|
||||
a
|
||||
};
|
||||
// Build exclusion matcher for dynamic bypass
|
||||
let current_exclusions = exclusions_rx.borrow().clone();
|
||||
let matcher = crate::tunnel::exclusion::ExclusionMatcher::new(¤t_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(¤t, 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 {
|
||||
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
|
||||
};
|
||||
|
||||
// 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(¤t_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(¤t, 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.
|
||||
|
||||
// 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 Some(mut listener) = tcp_listener else { return; };
|
||||
|
||||
|
|
@ -250,8 +250,21 @@ pub async fn run_native_tunnel(
|
|||
// ── Decide: bypass or tunnel? ─────────────────────────────────
|
||||
let mut should_bypass = false;
|
||||
|
||||
// 1. SNI domain check (belt-and-suspenders for CDNs / late-resolved IPs)
|
||||
if sniff_len > 0 {
|
||||
// 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 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) =
|
||||
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 true {
|
||||
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:");
|
||||
}
|
||||
|
||||
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 matcher = crate::tunnel::exclusion::ExclusionMatcher::new(¤t_exclusions, None, None);
|
||||
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 Some(mut listener) = tcp_listener else { return; };
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ pub async fn run_udp_nat(
|
|||
udp_socket: netstack_smoltcp::UdpSocket,
|
||||
proxy_addr: String,
|
||||
_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 tx = Arc::new(Mutex::new(tx));
|
||||
|
|
@ -33,12 +36,42 @@ pub async fn run_udp_nat(
|
|||
let proxy_addr_clone = proxy_addr.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 {
|
||||
if should_bypass {
|
||||
tracing::debug!("Starting UDP BYPASS session for {}", src);
|
||||
let res = start_udp_bypass_session(src, p_if_idx, p_if_name, &mut session_rx, tx_clone).await;
|
||||
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(
|
||||
client_src: SocketAddr,
|
||||
proxy_addr: String,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -347,7 +347,7 @@
|
|||
<input id="tag-input-processes" class="tag-input-field" type="text"
|
||||
placeholder="chrome.exe" spellcheck="false" autocomplete="off" />
|
||||
</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>
|
||||
|
||||
|
|
|
|||
|
|
@ -333,8 +333,10 @@ pub extern "system" fn Java_net_ostp_client_OstpClientSdk_nativeStartClient(
|
|||
}
|
||||
let shutdown_rx_clone = shutdown_tx.subscribe();
|
||||
let config_clone = config.clone();
|
||||
let (exclusions_tx, exclusions_rx) = tokio::sync::watch::channel(config.exclusions.clone());
|
||||
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));
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ pub mod linux;
|
|||
pub mod macos;
|
||||
|
||||
impl OstpTunInterface {
|
||||
#[allow(unused_variables)]
|
||||
pub async fn create(opts: OstpTunOptions) -> Result<Self> {
|
||||
#[cfg(target_os = "windows")]
|
||||
return windows::create(opts).await;
|
||||
|
|
|
|||
|
|
@ -1300,8 +1300,6 @@ async fn run_app() -> Result<()> {
|
|||
"target": "127.0.0.1:8080"
|
||||
}},
|
||||
|
||||
// Reality (XTLS) / UoT Masquerade parameters
|
||||
|
||||
|
||||
"debug": false
|
||||
}}"#, key)
|
||||
|
|
@ -1347,9 +1345,7 @@ async fn run_app() -> Result<()> {
|
|||
"processes": []
|
||||
}},
|
||||
|
||||
// Reality (XTLS) / WebRTC Masquerade parameters
|
||||
|
||||
// Transport Mode: "udp" (default WebRTC masquerade) or "uot" (TCP XTLS-Reality)
|
||||
// Transport Mode: "udp" (default WebRTC masquerade) or "uot" (TCP UoT)
|
||||
"transport": {{
|
||||
"mode": "udp",
|
||||
"stealth_sni": "www.microsoft.com",
|
||||
|
|
|
|||
Loading…
Reference in New Issue