Skip to content

fix(ssh): replace connection pool poller with lazy acquire and bounded backoff - #2316

Merged
NeoPlays merged 4 commits into
stereum-dev:mainfrom
NeoPlays:vpn-reconnect-login-reject
Sep 10, 2026
Merged

NeoPlays merged 4 commits into
stereum-dev:mainfrom
NeoPlays:vpn-reconnect-login-reject

Conversation

@NeoPlays

Copy link
Copy Markdown
Member

Summary

Switching VPN killed the connection to a node and the next login was rejected instantly at TCP level rather than timing out. The cause was two independent client-side connection storms that tripped fail2ban's stock defaults (maxretry=5, bantime=10m, ban action REJECT), which apply because Stereum installs fail2ban in configure-firewall without a jail.local. sshd's MaxStartups contributes to the immediacy of the refusal.

Storm 1: the 100 ms pool poller. checkConnectionPool grew the pool whenever it held fewer than 6 connections. During a blackhole nothing reached ready, so the pool never grew, the condition never cleared, and it dialled roughly 10 times per second. Measured: 29 attempts in 3 s, about 190 half-open handshakes per 20 s readyTimeout window.

Storm 2: checkConnection. Polled every 2 s by HeaderScreen.vue, it opened a throwaway SSH connection each time, entirely outside the pool. About 9 overlapping 18 s handshakes during an outage, roughly 300 failed attempts over 10 minutes.

Changes

3 commits, 10 files (+445/−62) plus lockfile.

a2366a7e (fix)

Backend:

  • Poller deleted. Connections now open lazily via acquireConnection(), single-flight, so a burst of execs opens one connection.
  • dropConnection() wired to error/end/close so dead sockets leave the pool and connected goes false.
  • Bounded backoff [2s, 5s, 15s, 30s, 60s] with AbortController, one attempt in flight.
  • Keepalive tightened to 10 s x 3.
  • checkConnection became a plain read.

Frontend:

  • State pushed to the renderer over the existing require2FA channel pattern (no preload change).
  • BaseLayout clears stereumStatus/refresh on reconnect so the modal dismisses itself.

Three latent bugs fixed, since the feature did not work without them: headerStore.reconnecting was never declared in the store; ReconnectModal destructured the store and lost reactivity; and services.js set refresh = false on loss with nothing ever setting it back, so the UI latched until reload.

Two races closed: disconnect() no longer emits state (logout was flashing the modal), and an epoch counter discards a handshake that lands after logout. The pool was verified to be re-populating post-logout, and because location.reload() only restarts the renderer, the main process was holding an SSH session to a node you had left.

10411812 (countdown)

Modal shows Attempt 2 of 5 and Next try in 12s, switching to Connecting... during a dial. The backoff emits {attempt, total, phase, waitMs}; BaseLayout converts this to an absolute deadline so a missed tick cannot drift it. Three keys added to en.json (additive; Crowdin picks up the rest).

7987e47a (version + deps)

2.5.2 to 2.5.3. 3 high-severity vulnerabilities down to 0 (@xmldom/xmldom, fast-uri, js-yaml), all resolved inside existing semver ranges, so package.json changed only its version line. Plus 14 semver-compatible updates.

@NeoPlays
NeoPlays merged commit 483f01d into stereum-dev:main Sep 10, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant