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() {
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(¤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 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(¤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.
|
// 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(¤t_exclusions, None, None);
|
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_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; };
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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"
|
<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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue