* Show host info for Mosh sessions via a stats companion SSH connection Mosh sessions run over UDP through a local mosh-client PTY and carry no ssh2 connection (session.conn), so getServerStats could not open an exec channel and the terminal's host-info bar (CPU/memory/disk/network) stayed empty — unlike SSH sessions (issue #1198). Add a best-effort, non-interactive companion SSH connection that is opened lazily on the first stats poll for a Mosh session, reusing the credentials the Mosh handshake already validated, and assign it to session.conn so the existing stats path works unchanged: - electron/bridges/sshBridge/moshStatsConnection.cjs: new helper that builds the companion connection. It never prompts (only stored password, parseable private key, unencrypted/stored-passphrase identity files, or ssh-agent), shares one in-flight attempt across concurrent polls, treats auth rejection as permanent but transient errors as retryable, and skips host-key verification like the existing one-off execCommand path. - sessionOps.getServerStats: establish the companion connection for Mosh sessions that lack session.conn before running the stats command; degrades gracefully to the existing "not connected" error otherwise. - moshSession.swapToMoshClient: stash the handshake credentials and algorithm settings on session.moshStatsAuth once the handshake succeeds. - terminalBridge closeSession / cleanupAllSessions and the mosh-client exit handler: tear down the companion connection (it has no session.stream). - Forward legacyAlgorithms / skipEcdsaHostKey / algorithmOverrides through the renderer's Mosh starter and the bridge type so the companion negotiates the same algorithms the interactive session would. Tests cover the helper (auth selection, agent fallback, dedup, permanent vs transient failure, late-ready discard), the getServerStats integration, and the moshStatsAuth stash + companion teardown on close. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Address codex review: target SSH host and settle on mid-handshake close - moshSession: store options.hostname (the SSH endpoint) on moshStatsAuth instead of parsed.host. A `MOSH IP` line advertises the UDP endpoint for mosh-client, which can differ from the SSH host on NAT / multi-homed setups; the companion is an SSH connection and must target the SSH host. - moshStatsConnection: resolve the pending attempt from the "close" handler when the socket drops mid-handshake without a prior "ready"/"error", so an awaiting getServerStats call (and session.moshStatsConnPromise) cannot hang indefinitely. Treated as transient so the next poll may retry. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Address codex review: keyboard-interactive password for stats companion PAM-backed SSH servers often offer password auth only via keyboard-interactive, not the plain "password" method. The Mosh handshake's system ssh handles that through its PTY responder, so without it the companion stats connection would fail auth on those hosts even with a saved password, leaving the stats bar empty. When a saved password is present, enable tryKeyboard and attach a non-interactive keyboard-interactive handler that auto-fills the password for a single password prompt (using the existing isAutoFillablePasswordChallenge predicate) and finishes empty on 2FA/OTP/multi-prompt challenges. It auto-fills at most once so a wrong password can't drive a retry loop, and it never shows a modal — the companion stays fully non-interactive. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Address codex review: verify host key before sending a saved password A background companion connection that auto-submits a saved password to an unverified host could disclose it to a spoofed / MITM server (P1). Gate password auth (plain and keyboard-interactive) behind a silent, trusted-only host-key check against Netcatty's known-hosts store: - moshStatsConnection: when the companion would authenticate with a password, attach an ssh2 hostVerifier that accepts only a key already "trusted" in known-hosts (via hostKeyVerifier.classifyHostKey) and rejects unknown/changed keys outright — no prompt, so the password is never sent to an unvetted host and no host-key dialog pops for a background poll. Public-key / agent auth proves possession via a signature and discloses no reusable secret, so it is not gated (matches the existing execCommand precedent; the handshake already vetted the host via system ssh). - Thread knownHosts through the renderer Mosh starter, the bridge type, and session.moshStatsAuth. Note: a password-auth Mosh host that Netcatty has never seen via its own SSH path (so it is absent from Netcatty's known-hosts) will not get the stats companion until its key is known — the safe default. Key/agent-auth hosts are unaffected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Address codex review: transient pre-handshake polls and agent+password auth Two functional gaps in the stats companion: - Missing moshStatsAuth is now transient, not permanent. The renderer can mark a Mosh session "connected" (and start polling) from the SSH bootstrap's visible PTY output before the swap to mosh-client assigns moshStatsAuth. Previously that first poll set moshStatsConnFailed permanently, so the companion was never attempted after the handshake actually completed. Now it just returns null and a later poll retries. - A saved password no longer suppresses ssh-agent auth. A public-key host that authenticates via the agent may still carry a stored password; the companion now offers the agent alongside the password (ssh2 tries agent first) instead of attempting password-only and failing permanently. An explicit private key still suppresses the agent fallback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Address codex review: gate password at authHandler, not whole connection The previous fix installed a trusted-only hostVerifier whenever a password was present. Because ssh2 verifies the host before any auth method, that rejected the entire connection on a host absent from Netcatty's known-hosts — blocking key/agent auth too, even though those never need to send the password. Move the gate from the transport to the auth layer: - A trust-tracking hostVerifier records whether the live host key is trusted (during the transport handshake) and then accepts the transport so public-key / agent auth can proceed on any host. - A function-form authHandler offers none -> agent -> publickey always, and appends password + keyboard-interactive only when the host key is trusted. Result: key/agent auth works on hosts Netcatty hasn't vetted, while a saved password is still never sent to an untrusted host. Public-key / agent auth remains ungated (no reusable secret is disclosed). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Address codex review: isolate Mosh stats connection from session.conn Storing the stats companion on session.conn made it look like the session's primary interactive SSH connection. Other bridges key off session.conn — getSessionPwd assumes its exec channel is a sibling of the interactive shell, and SFTP / MCP exec run over session.conn — but a Mosh session's shell lives on the UDP mosh-client, not this background connection. After a stats poll they could return a bogus cwd or operate over the wrong connection. Keep the companion strictly on session.moshStatsConn: - ensureMoshStatsConnection stores/reuses/clears only session.moshStatsConn. - getServerStats reads session.conn || session.moshStatsConn (real SSH still uses conn; Mosh uses the companion) and only opens one when neither exists. - closeSession / cleanupAllSessions / the mosh-client exit handler tear down session.moshStatsConn. This leaves session.conn untouched for Mosh, so getSessionPwd / SFTP / MCP exec behave exactly as before (no primary SSH connection for Mosh). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Address codex review: don't count pre-handshake Mosh polls as failures useServerStats gives up after 3 consecutive failures. A Mosh session can be marked "connected" (and start polling) from the SSH bootstrap's visible output before swapToMoshClient stores moshStatsAuth, during which ensureMoshStatsConnection returns null. Previously getServerStats reported that as a normal failure, so a handshake taking ~15s (3 polls) would permanently disable stats for the session even after credentials became available. Introduce a `pending` result: - getServerStats returns { success: false, pending: true } for a Mosh session that has no connection yet and no moshStatsAuth and hasn't permanently failed. Once moshStatsAuth is set (or the companion permanently fails), it reports a normal failure again. - useServerStats treats `pending` as neutral: it does not update stats and does not increment the consecutive-failure counter, so polling continues until the handshake completes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Address codex review: verify host key for all Mosh stats companion auth The companion installed a trust-tracking host verifier only when a saved password was present; key/agent-only connections fell back to ssh2's default of accepting any host key. A background, user-invisible connection that authenticated against an unverified host could let a MITM/DNS-spoofed host feed bogus host-info to the user and enumerate the ssh-agent's public keys — breaking the host-key guarantee the interactive session enforces. Attach the host verifier for every auth method and reject an unknown or changed host key outright (never prompting; stats just stay empty). Treat an untrusted host as a permanent failure so polling stops reconnecting. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(mosh): trust system known_hosts for the stats companion host-key check The Mosh stats companion opens a background ssh2 connection and only rides on a host whose live key is already trusted, rejecting unknown/changed keys as a permanent failure. Trust was sourced solely from Netcatty's in-app known-hosts snapshot (options.knownHosts). But a Mosh session is bootstrapped by the system `ssh`, which vets and records the host key in the user's OpenSSH known_hosts (~/.ssh/known_hosts, etc). Netcatty's snapshot is never updated by that handshake, so a host trusted purely via system ssh was misread as "unknown" and the companion permanently disabled — Mosh stats never appeared unless the user manually scanned/imported the host into Netcatty (codex P2). Add a system-known_hosts trust source (systemKnownHosts.cjs) and consult it in the companion verifier when the in-app snapshot does not already vouch for the key. Matching is by the LIVE key's SHA-256 fingerprint, so trust is granted only for the exact key the user's own OpenSSH already trusts; an arbitrary or mismatched key is never accepted. Unknown/changed keys stay rejected and remain a permanent failure. The parser handles the OpenSSH known_hosts(5) format that the in-app scan parser does not fully cover for matching: plain hosts, comma lists, [host]:port, hashed |1|salt|HMAC-SHA1(salt,token) entries (with the bracketed token for non-default ports, verified against ssh-keygen -H), multiple key types, @revoked (forces NOT trusted) and @cert-authority (skipped). Wildcard/negation patterns are deliberately not honored. Paths mirror localFsBridge.readKnownHosts and are cross-platform (incl. Windows %PROGRAMDATA%\ssh\known_hosts). All errors fail closed. ssh2 ships no known_hosts parser, so this is implemented in CommonJS using node:crypto and covered by unit tests (real ssh-keygen hashed fixtures, revoked/cert-authority, fingerprint mismatch, fail-closed) plus companion integration tests (system-only trust accepts; neither source trusts -> rejected + permanent; key rotation not rescued; optional-dependency safety). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
13
types/global/netcatty-bridge-session.d.ts
vendored
13
types/global/netcatty-bridge-session.d.ts
vendored
@@ -29,6 +29,15 @@ declare global {
|
||||
moshServerPath?: string;
|
||||
moshClientPath?: string;
|
||||
agentForwarding?: boolean;
|
||||
// Algorithm settings, forwarded so the host-info stats companion SSH
|
||||
// connection (issue #1198) negotiates the same KEX / cipher / host-key
|
||||
// set the interactive session would.
|
||||
legacyAlgorithms?: boolean;
|
||||
skipEcdsaHostKey?: boolean;
|
||||
algorithmOverrides?: import("../../domain/models").HostAlgorithmOverrides;
|
||||
// Known hosts, used to verify the host key before the stats companion
|
||||
// connection (issue #1198) sends a saved password.
|
||||
knownHosts?: import("../../domain/models").KnownHost[];
|
||||
cols?: number;
|
||||
rows?: number;
|
||||
charset?: string;
|
||||
@@ -111,6 +120,10 @@ declare global {
|
||||
/** Get server stats (CPU, Memory, Disk, Network) from an active SSH session */
|
||||
getServerStats?(sessionId: string): Promise<{
|
||||
success: boolean;
|
||||
// Transient "not ready yet" (e.g. a Mosh session whose SSH handshake is
|
||||
// still in progress, #1198). Callers should keep polling and NOT count
|
||||
// this toward any consecutive-failure give-up.
|
||||
pending?: boolean;
|
||||
error?: string;
|
||||
stats?: {
|
||||
cpu: number | null; // CPU usage percentage (0-100)
|
||||
|
||||
Reference in New Issue
Block a user