mirror of https://github.com/ospab/ostp.git
fix: post-suspend reconnect no longer strands the machine without internet
Two separate problems reported after waking a laptop: the app sits on "connecting" forever, and there is NO working internet at all — not just no tunnel. Plus typing in the GUI's exclusion fields lagged by seconds. 1. Resume reconnect retried forever (a regression I introduced when making the resume reconnect retry instead of firing once). handle_keepalive(force=true) deliberately skips the hard-timeout branch, and that branch is the one that releases the SystemProxyGuard. So a resume campaign that never succeeded also never gave up, and the system proxy stayed pointed at our local listener indefinitely — which kills all browser traffic, tunnel or not, and explains "no internet even from my ISP". Now bounded: after 45s of failed resume reconnects, hand back to the ordinary stall path, which restores the proxy (or, with kill switch on, keeps blocking deliberately). Measured on the wall clock, because Instant does not advance across suspend on Windows — QPC stops — so a monotonic deadline cannot bound anything that starts at wake. That same property is why the pre-existing 25s/180s stall checks never fired here either. 2. GUI froze while typing. Every debounced save (400ms, so it fires during natural pauses in typing) called set_autostart — a Windows registry write — even when the checkbox had not changed, and, while connected, wrote the config and ran reload_tunnel, tearing down and rebuilding the tunnel. Worst in the exclusion fields, which is exactly where it was reported. Autostart now applies only on change; the tunnel hot-reload only when a setting the tunnel actually reads has changed, on a 1.5s debounce so it lands after editing rather than between keystrokes. The cheap local save still runs on every keystroke.
This commit is contained in:
parent
df1a14d15c
commit
e483af541f
|
|
@ -23,6 +23,12 @@ use crate::tunnel::{ProxyEvent, ProxyToClientMsg};
|
|||
/// candidate address is tried.
|
||||
const UOT_CONNECT_TIMEOUT: Duration = Duration::from_secs(4);
|
||||
|
||||
/// How long to keep retrying a resume-triggered reconnect before handing the
|
||||
/// problem back to the ordinary stall path. That path is what releases the
|
||||
/// system proxy, so this is really a bound on how long the machine may be left
|
||||
/// with no working internet at all after waking.
|
||||
const RESUME_RECONNECT_GIVE_UP: Duration = Duration::from_secs(45);
|
||||
|
||||
static SOCKET_PROTECTOR: std::sync::OnceLock<Box<dyn Fn(i32) -> bool + Send + Sync>> = std::sync::OnceLock::new();
|
||||
|
||||
pub fn set_socket_protector<F>(f: F)
|
||||
|
|
@ -147,6 +153,11 @@ pub struct Bridge {
|
|||
/// fire at all. Retrying until success removes the dependency on either.
|
||||
forced_reconnect_pending: bool,
|
||||
last_forced_reconnect_try: Instant,
|
||||
/// Wall-clock start of the current resume-reconnect campaign, used to bound
|
||||
/// it. Wall clock rather than Instant because the monotonic clock does not
|
||||
/// advance across suspend on Windows, so it cannot measure anything that
|
||||
/// begins at wake.
|
||||
forced_reconnect_started: Option<SystemTime>,
|
||||
}
|
||||
|
||||
impl Bridge {
|
||||
|
|
@ -185,6 +196,7 @@ impl Bridge {
|
|||
last_valid_recv: Instant::now(),
|
||||
forced_reconnect_pending: false,
|
||||
last_forced_reconnect_try: Instant::now(),
|
||||
forced_reconnect_started: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -268,9 +280,45 @@ impl Bridge {
|
|||
"Resumed after ~{}s suspend — forcing clean reconnect", wall_gap.as_secs()
|
||||
))).await;
|
||||
self.forced_reconnect_pending = true;
|
||||
self.forced_reconnect_started = Some(SystemTime::now());
|
||||
self.last_forced_reconnect_try = Instant::now() - Duration::from_secs(60);
|
||||
}
|
||||
|
||||
// Give up if resume reconnects keep failing. Retrying forever
|
||||
// looks harmless but is not: the system proxy stays pointed at
|
||||
// our local listener the whole time, so the machine has NO
|
||||
// working internet — not merely no tunnel — while the UI sits
|
||||
// on "connecting". Handing the retry to the ordinary keepalive
|
||||
// path restores the proxy through its hard-timeout branch,
|
||||
// which force=true deliberately skips.
|
||||
//
|
||||
// Measured on the wall clock: Instant does not advance across
|
||||
// suspend on Windows (QPC stops), so a monotonic deadline can
|
||||
// not bound anything that starts at wake.
|
||||
if self.forced_reconnect_pending {
|
||||
let pending_for = self
|
||||
.forced_reconnect_started
|
||||
.and_then(|t| t.elapsed().ok())
|
||||
.unwrap_or_default();
|
||||
if pending_for > RESUME_RECONNECT_GIVE_UP {
|
||||
self.forced_reconnect_pending = false;
|
||||
self.forced_reconnect_started = None;
|
||||
let _ = tx.send(UiEvent::Log(format!(
|
||||
"Reconnect after suspend failed for {}s — releasing the system \
|
||||
proxy so normal traffic works; will keep retrying in the \
|
||||
background",
|
||||
pending_for.as_secs()
|
||||
))).await;
|
||||
// Make the ordinary stall path fire on the next
|
||||
// keepalive tick: it is the one that tears the proxy
|
||||
// back down (or, with kill switch on, deliberately
|
||||
// keeps blocking).
|
||||
self.last_valid_recv = Instant::now()
|
||||
.checked_sub(Duration::from_secs(3600))
|
||||
.unwrap_or_else(Instant::now);
|
||||
}
|
||||
}
|
||||
|
||||
// Keep retrying a resume-triggered reconnect until one lands.
|
||||
// The first attempt fires within half a second of waking, when
|
||||
// the NIC is typically still reassociating, so treating it as
|
||||
|
|
@ -286,6 +334,7 @@ impl Bridge {
|
|||
// success check rather than "we tried".
|
||||
if self.last_valid_recv.elapsed() < Duration::from_secs(3) {
|
||||
self.forced_reconnect_pending = false;
|
||||
self.forced_reconnect_started = None;
|
||||
let _ = tx.send(UiEvent::Log("Reconnected after suspend".into())).await;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -660,6 +660,13 @@ function loadSettingsIntoForm() {
|
|||
updateClientVisibility();
|
||||
}
|
||||
|
||||
// Last values actually pushed to the OS / backend, so repeated saves that did
|
||||
// not change them stay free. Undefined until the first save, which is correct:
|
||||
// the first one should apply.
|
||||
let lastAppliedAutostart;
|
||||
let lastAppliedTunnelConfig;
|
||||
let hotReloadTimer;
|
||||
|
||||
function collectAndSaveSettings() {
|
||||
const s = {
|
||||
tun: inTun.checked,
|
||||
|
|
@ -686,19 +693,41 @@ function collectAndSaveSettings() {
|
|||
fragChunk: parseInt(inFragChunk.value) || 2,
|
||||
fragSleep: !isNaN(parseInt(inFragSleep.value)) ? parseInt(inFragSleep.value) : 2,
|
||||
};
|
||||
// Cheap and local: safe to run on every debounced keystroke.
|
||||
saveClientSettings(s);
|
||||
updateClientVisibility();
|
||||
|
||||
// Set autostart
|
||||
invoke('set_autostart', { enable: s.launchStartup }).catch(() => {});
|
||||
// Everything below talks to the OS or restarts the tunnel. Running it per
|
||||
// keystroke is what made typing in the exclusion fields lag by seconds: the
|
||||
// 400ms debounce fires during natural pauses in typing, and each firing hit
|
||||
// the Windows registry and then tore down and rebuilt the tunnel.
|
||||
|
||||
// Hot-reload exclusions if connected
|
||||
// Only touch autostart when it actually changed — this is a registry write.
|
||||
if (s.launchStartup !== lastAppliedAutostart) {
|
||||
lastAppliedAutostart = s.launchStartup;
|
||||
invoke('set_autostart', { enable: s.launchStartup }).catch(() => {});
|
||||
}
|
||||
|
||||
// Hot-reload the tunnel only when something it actually reads has changed,
|
||||
// and on a much longer debounce: a reload is disruptive, so it should land
|
||||
// once the user has stopped editing rather than between keystrokes.
|
||||
if (appState === 'connected') {
|
||||
const cfg = buildConfig();
|
||||
if (cfg) {
|
||||
invoke('save_config', { jsonContent: JSON.stringify(cfg, null, 2) })
|
||||
.then(() => invoke('reload_tunnel'))
|
||||
.catch(() => {});
|
||||
const tunnelRelevant = JSON.stringify([
|
||||
s.tun, s.killSwitch, s.mux, s.muxSessions, s.mtu, s.dns, s.socks,
|
||||
s.exDomains, s.exIps, s.exProcs, s.junkEnabled, s.junkPcMin, s.junkPcMax,
|
||||
s.junkPsMin, s.junkPsMax, s.tcpFrag, s.fragChunk, s.fragSleep,
|
||||
]);
|
||||
if (tunnelRelevant !== lastAppliedTunnelConfig) {
|
||||
clearTimeout(hotReloadTimer);
|
||||
hotReloadTimer = setTimeout(() => {
|
||||
lastAppliedTunnelConfig = tunnelRelevant;
|
||||
const cfg = buildConfig();
|
||||
if (cfg) {
|
||||
invoke('save_config', { jsonContent: JSON.stringify(cfg, null, 2) })
|
||||
.then(() => invoke('reload_tunnel'))
|
||||
.catch(() => {});
|
||||
}
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue