mirror of https://github.com/ospab/ostp.git
§B: port stability fixes from 0.3.x onto the clean base
Ported only the fixes that actually apply to the pre-refactor base (the handshake fixes 6eb7b36/d65af35 fix bugs the 0.3.1 multi-server refactor introduced into the new outbounds/ostp.rs; the base bridge.rs already waits for the handshake response with retransmit + NAT64 fallback, so they are intentionally skipped). - EMFILE (922cf0b): rlimit::increase_nofile_limit at CLI startup. - Logs (1151726): UoT connect/disconnect → debug; rate-limit the unauthorized-probe log to one line / ~30s so a junk/probe flood can't spam the log (and a client running junk-over-UDP can't self-ban). - Helper lifecycle + bypass routes (b6e78c1): * ostp-tun-helper forces std::process::exit(0) after run_server so the WinTun adapter and its metric-0 default route are reclaimed instead of lingering as a zombie that breaks the next connect. * windows_route: delete_routes_for_dest() purges stale /32s, dedupe bypass IPs, and log add failures at warn!. * windows: retry tun::create through the transient ERROR_INVALID_ PARAMETER window and widen the adapter-index lookup to ~15s. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
7704e0bdb1
commit
38f2d9e659
|
|
@ -1395,6 +1395,7 @@ dependencies = [
|
||||||
"ostp-core",
|
"ostp-core",
|
||||||
"ostp-server",
|
"ostp-server",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
|
"rlimit",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|
@ -1853,6 +1854,15 @@ dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rlimit"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f35ee2729c56bb610f6dba436bf78135f728b7373bdffae2ec815b2d3eb98cc3"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-embed"
|
name = "rust-embed"
|
||||||
version = "8.11.0"
|
version = "8.11.0"
|
||||||
|
|
|
||||||
|
|
@ -285,6 +285,10 @@ pub async fn run_server(
|
||||||
|
|
||||||
// Headless event logger
|
// Headless event logger
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
|
// Rate-limit unauthorized-probe logging so a junk/probe flood can't spam the log
|
||||||
|
// (and so a client running junk-over-UDP can't trigger a self-ban via log noise).
|
||||||
|
let mut probe_window_start: Option<Instant> = None;
|
||||||
|
let mut probe_suppressed: u64 = 0;
|
||||||
while let Some(ev) = ui_event_rx.recv().await {
|
while let Some(ev) = ui_event_rx.recv().await {
|
||||||
match ev {
|
match ev {
|
||||||
UiEvent::Log(msg) => {
|
UiEvent::Log(msg) => {
|
||||||
|
|
@ -303,7 +307,23 @@ pub async fn run_server(
|
||||||
}
|
}
|
||||||
UiEvent::UnauthorizedProbe { peer, bytes } => {
|
UiEvent::UnauthorizedProbe { peer, bytes } => {
|
||||||
if debug {
|
if debug {
|
||||||
tracing::debug!("Unauthorized probe from {peer} ({bytes} bytes)");
|
let now = Instant::now();
|
||||||
|
let elapsed = probe_window_start
|
||||||
|
.map(|s| now.duration_since(s))
|
||||||
|
.unwrap_or(Duration::MAX);
|
||||||
|
if elapsed >= Duration::from_secs(30) {
|
||||||
|
if probe_suppressed > 0 {
|
||||||
|
tracing::debug!(
|
||||||
|
"(+{} more unauthorized probes suppressed in the previous ~30s)",
|
||||||
|
probe_suppressed
|
||||||
|
);
|
||||||
|
}
|
||||||
|
probe_window_start = Some(now);
|
||||||
|
probe_suppressed = 0;
|
||||||
|
tracing::debug!("Unauthorized probe from {peer} ({bytes} bytes)");
|
||||||
|
} else {
|
||||||
|
probe_suppressed += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
UiEvent::PeerSeen { .. } => {}
|
UiEvent::PeerSeen { .. } => {}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ use std::net::SocketAddr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::sync::{mpsc, RwLock};
|
use tokio::sync::{mpsc, RwLock};
|
||||||
use tracing::info;
|
|
||||||
|
|
||||||
pub async fn handle_tcp_connection<S>(
|
pub async fn handle_tcp_connection<S>(
|
||||||
stream: S,
|
stream: S,
|
||||||
|
|
@ -16,7 +15,7 @@ pub async fn handle_tcp_connection<S>(
|
||||||
where
|
where
|
||||||
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
|
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
|
||||||
{
|
{
|
||||||
info!("UoT client connected from {}", peer_addr);
|
tracing::debug!("UoT client connected from {}", peer_addr);
|
||||||
|
|
||||||
// Register this connection in the map
|
// Register this connection in the map
|
||||||
let (tx, mut rx) = mpsc::channel::<Bytes>(16384);
|
let (tx, mut rx) = mpsc::channel::<Bytes>(16384);
|
||||||
|
|
@ -54,6 +53,6 @@ where
|
||||||
});
|
});
|
||||||
|
|
||||||
let _ = tokio::join!(writer_task, reader_task);
|
let _ = tokio::join!(writer_task, reader_task);
|
||||||
info!("UoT client disconnected: {}", peer_addr);
|
tracing::debug!("UoT client disconnected: {}", peer_addr);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,19 @@ async fn main() -> Result<()> {
|
||||||
log_to_file(&format!("Fatal error: {}", e));
|
log_to_file(&format!("Fatal error: {}", e));
|
||||||
}
|
}
|
||||||
log_to_file("Helper exiting");
|
log_to_file("Helper exiting");
|
||||||
Ok(())
|
|
||||||
|
// The WinTun blocking `receive` runs on a thread that `task.abort()` cannot
|
||||||
|
// cancel, so it keeps the adapter handle — and the default route bound to it —
|
||||||
|
// alive and prevents the tokio runtime from shutting down. Without this the
|
||||||
|
// process lingers as a zombie: `ostp_tun` stays Up, its metric-0 default route
|
||||||
|
// competes with the physical one, and the NEXT connect fails to install the
|
||||||
|
// server bypass route, so traffic loops back into a dead tunnel (no internet).
|
||||||
|
// The GUI launches a fresh helper for every connect, so this process has no
|
||||||
|
// more work once run_server returns. Give the synchronous route/firewall
|
||||||
|
// teardown a moment to finish, then force the process to exit so the kernel
|
||||||
|
// reclaims the adapter and every route bound to it. (Ported from 0.3.x b6e78c1.)
|
||||||
|
tokio::time::sleep(Duration::from_millis(800)).await;
|
||||||
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_server(expected_token: String, port: u16) -> Result<()> {
|
async fn run_server(expected_token: String, port: u16) -> Result<()> {
|
||||||
|
|
|
||||||
|
|
@ -72,32 +72,59 @@ pub async fn create(opts: OstpTunOptions) -> Result<OstpTunInterface> {
|
||||||
.mtu(opts.mtu)
|
.mtu(opts.mtu)
|
||||||
.up();
|
.up();
|
||||||
|
|
||||||
let dev = tun::create(&tun_cfg).map_err(|e| anyhow!("Failed to create TUN device: {}", e))?;
|
// The IpHelper calls the `tun` crate performs right after Adapter::create
|
||||||
|
// (set address / mtu) can transiently fail with ERROR_INVALID_PARAMETER
|
||||||
|
// (os error 87) when the freshly created interface is not yet registered
|
||||||
|
// in the IP stack. Retry a few times; on retry the crate reuses the
|
||||||
|
// existing adapter via Adapter::open. (Ported from 0.3.x b6e78c1.)
|
||||||
|
let dev = {
|
||||||
|
let mut attempt = 0;
|
||||||
|
loop {
|
||||||
|
attempt += 1;
|
||||||
|
match tun::create(&tun_cfg) {
|
||||||
|
Ok(d) => break d,
|
||||||
|
Err(e) if attempt < 5 => {
|
||||||
|
tracing::warn!(
|
||||||
|
"TUN device creation attempt {}/5 failed: {} — retrying in 300ms",
|
||||||
|
attempt, e
|
||||||
|
);
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(300)).await;
|
||||||
|
}
|
||||||
|
Err(e) => return Err(anyhow!("Failed to create TUN device after {} attempts: {}", attempt, e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
let dev = tun::AsyncDevice::new(dev).map_err(|e| anyhow!("TUN device async failed: {}", e))?;
|
let dev = tun::AsyncDevice::new(dev).map_err(|e| anyhow!("TUN device async failed: {}", e))?;
|
||||||
tracing::info!("TUN device 'ostp_tun' created.");
|
tracing::info!("TUN device 'ostp_tun' created.");
|
||||||
|
|
||||||
let current_exe = std::env::current_exe()?.to_string_lossy().into_owned();
|
let current_exe = std::env::current_exe()?.to_string_lossy().into_owned();
|
||||||
|
|
||||||
|
// A freshly created WinTun adapter can take several seconds to appear in
|
||||||
|
// GetAdaptersAddresses (it only shows up once it has an operational IPv4
|
||||||
|
// binding). The default route via the TUN is what actually captures
|
||||||
|
// traffic, so this lookup is critical — give it a generous window (~15s).
|
||||||
let mut tun_index = None;
|
let mut tun_index = None;
|
||||||
for _ in 0..20 {
|
for _ in 0..75 {
|
||||||
if let Some(idx) = windows_route::sys::get_interface_index("ostp_tun") {
|
if let Some(idx) = windows_route::sys::get_interface_index("ostp_tun") {
|
||||||
tun_index = Some(idx);
|
tun_index = Some(idx);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(idx) = tun_index {
|
if let Some(idx) = tun_index {
|
||||||
let _ = windows_route::sys::add_ipv4_route(
|
match windows_route::sys::add_ipv4_route(
|
||||||
std::net::Ipv4Addr::new(0, 0, 0, 0),
|
std::net::Ipv4Addr::new(0, 0, 0, 0),
|
||||||
std::net::Ipv4Addr::new(0, 0, 0, 0),
|
std::net::Ipv4Addr::new(0, 0, 0, 0),
|
||||||
std::net::Ipv4Addr::new(10, 1, 0, 1),
|
std::net::Ipv4Addr::new(10, 1, 0, 1),
|
||||||
idx,
|
idx,
|
||||||
5,
|
5,
|
||||||
);
|
) {
|
||||||
tracing::info!("Default route via TUN (if_index={idx}, metric=5) added.");
|
Ok(()) => tracing::info!("Default route via TUN (if_index={idx}, metric=5) added."),
|
||||||
|
Err(e) => tracing::error!("Failed to add default route via TUN (if_index={idx}): {e} — traffic will NOT be captured."),
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
tracing::warn!("Could not find ostp_tun index in routing table — traffic may not be captured.");
|
tracing::error!("Could not find ostp_tun index in routing table after 15s — traffic will NOT be captured.");
|
||||||
}
|
}
|
||||||
|
|
||||||
let exe1 = current_exe.clone();
|
let exe1 = current_exe.clone();
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,34 @@ pub mod sys {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Delete every routing-table entry whose destination is `dest`/`mask`,
|
||||||
|
/// regardless of its gateway or interface. Used to purge stale bypass routes
|
||||||
|
/// left by a previous session (possibly pointing at an old gateway after a
|
||||||
|
/// network change) so a fresh, correct one can be installed. (Ported from 0.3.x b6e78c1.)
|
||||||
|
pub fn delete_routes_for_dest(dest: Ipv4Addr, mask: Ipv4Addr) {
|
||||||
|
unsafe {
|
||||||
|
let mut size: ULONG = 0;
|
||||||
|
if GetIpForwardTable(ptr::null_mut(), &mut size, 0) != ERROR_INSUFFICIENT_BUFFER {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut buf: Vec<u8> = vec![0; size as usize];
|
||||||
|
let table = buf.as_mut_ptr() as *mut MIB_IPFORWARDTABLE;
|
||||||
|
if GetIpForwardTable(table, &mut size, 0) != NO_ERROR {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let want_dest = ipv4_to_dword(dest);
|
||||||
|
let want_mask = ipv4_to_dword(mask);
|
||||||
|
let entries =
|
||||||
|
std::slice::from_raw_parts_mut((*table).table.as_mut_ptr(), (*table).dwNumEntries as usize);
|
||||||
|
for row in entries {
|
||||||
|
if row.dwForwardDest == want_dest && row.dwForwardMask == want_mask {
|
||||||
|
// Delete the exact existing row (its own nexthop/ifindex).
|
||||||
|
let _ = DeleteIpForwardEntry(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Add bypass routes for a list of resolved IP addresses (typically from exclusion config).
|
/// Add bypass routes for a list of resolved IP addresses (typically from exclusion config).
|
||||||
/// Each IP gets a /32 host route via the physical gateway so it bypasses the TUN.
|
/// Each IP gets a /32 host route via the physical gateway so it bypasses the TUN.
|
||||||
/// Returns list of (ip, gw, if_index) that were successfully added, for later cleanup.
|
/// Returns list of (ip, gw, if_index) that were successfully added, for later cleanup.
|
||||||
|
|
@ -178,15 +206,24 @@ pub mod sys {
|
||||||
metric: u32,
|
metric: u32,
|
||||||
) -> Vec<(Ipv4Addr, Ipv4Addr, u32)> {
|
) -> Vec<(Ipv4Addr, Ipv4Addr, u32)> {
|
||||||
let mut added = Vec::new();
|
let mut added = Vec::new();
|
||||||
|
let mut seen = std::collections::HashSet::new();
|
||||||
|
let mask = Ipv4Addr::new(255, 255, 255, 255);
|
||||||
for &ip in ips {
|
for &ip in ips {
|
||||||
let mask = Ipv4Addr::new(255, 255, 255, 255);
|
// The server IP is passed both as server_ip and inside bypass_ips, so
|
||||||
|
// dedupe to avoid a guaranteed "already exists" failure on the second add.
|
||||||
|
if !seen.insert(ip) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Purge any pre-existing /32 for this dest (e.g. a stale route via an
|
||||||
|
// old gateway from a previous session) so add_ipv4_route below installs
|
||||||
|
// the correct one instead of failing with ERROR_OBJECT_ALREADY_EXISTS.
|
||||||
|
delete_routes_for_dest(ip, mask);
|
||||||
match add_ipv4_route(ip, mask, gw, if_index, metric) {
|
match add_ipv4_route(ip, mask, gw, if_index, metric) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
added.push((ip, gw, if_index));
|
added.push((ip, gw, if_index));
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// 87 = ERROR_INVALID_PARAMETER (route may already exist)
|
tracing::warn!("bypass route add {ip}/32 via {gw} (if {if_index}) failed: {e}");
|
||||||
tracing::debug!("bypass route add {ip}/32 via {gw}: {e}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,3 +20,4 @@ tracing.workspace = true
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
ostp-core = { path = "../ostp-core" }
|
ostp-core = { path = "../ostp-core" }
|
||||||
colored = "2.1"
|
colored = "2.1"
|
||||||
|
rlimit = "0.11.0"
|
||||||
|
|
|
||||||
|
|
@ -363,6 +363,10 @@ struct MuxConfig {
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
|
// Raise the open-file-descriptor limit to avoid EMFILE under many concurrent
|
||||||
|
// connections (ported from 0.3.x fix 922cf0b). No-op / best-effort on platforms
|
||||||
|
// where it does not apply.
|
||||||
|
let _ = rlimit::increase_nofile_limit(1048576);
|
||||||
ostp_client::logging::setup_panic_hook();
|
ostp_client::logging::setup_panic_hook();
|
||||||
let _log_guard = ostp_client::logging::init_tracing("info", "ostp-cli", env!("CARGO_PKG_VERSION"));
|
let _log_guard = ostp_client::logging::init_tracing("info", "ostp-cli", env!("CARGO_PKG_VERSION"));
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue