Skip to content

feat(ssh): add relay-shell chain mode and harden bastion auth flow#252

Open
RoryChou-flux wants to merge 4 commits intobinaricat:mainfrom
RoryChou-flux:codex/relay-shell-pr
Open

feat(ssh): add relay-shell chain mode and harden bastion auth flow#252
RoryChou-flux wants to merge 4 commits intobinaricat:mainfrom
RoryChou-flux:codex/relay-shell-pr

Conversation

@RoryChou-flux
Copy link
Contributor

Summary

Add a new host-chain connection mode: relay-shell, and wire it end-to-end from UI to runtime/electron bridge.
Also harden bastion auth behavior and relay error handling.

What changed

  1. Domain/UI/State
  • Add HostChainConnectionMode and connectionMode to host chain model.
  • Add chain mode selector in Host Details > Chain panel.
  • Propagate jumpMode from terminal/SFTP callers to backend.
  • Include connectionMode in managed-source sync diff detection.
  1. SSH relay-shell backend
  • Implement relay-shell path: connect to first jump host, open shell, run inner ssh.
  • Rebuild relay auth config with shared auth helper.
  • Prefer keyboard-interactive in password-only bastion flows.
  • Add proxy support for relay first hop.
  • Extend relay ready timeout for MFA scenarios.
  • Add safe command building helpers and shell escaping.
  1. Reliability fixes
  • Relay auth failure now emits netcatty:auth:failed (parity with normal chain path).
  • Relay close/error/timeout paths now do consistent cleanup.
  • Fix IPv6 formatting in relay command target/jump formatting.
  • Improve relay startup command timing: prompt-detection + timeout fallback.
  • Avoid empty hostChain persistence (connectionMode without hostIds).
  1. SFTP behavior
  • Explicitly reject SFTP when jumpMode === relay-shell in backend.
  • Surface user-friendly localized error in SFTP modal.
  1. Config serialization/import
  • Skip ProxyJump for relay-shell (runtime-only mode) and add export note.
  • Keep import/export storage consistent with optional default mode representation.

Validation

  • npm run -s lint
  • node --check electron/bridges/sshBridge.cjs
  • node --check electron/bridges/sftpBridge.cjs
  • node --check electron/bridges/sshAuthHelper.cjs

Known limitation

In relay-shell mode, inner SSH failure may occur after outer shell is established; this is surfaced via terminal output/exit rather than guaranteed early structured connect failure.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eb0d1c4b1a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


stream.on("data", onData);
stream.stderr?.on("data", onStderr);
timer = setTimeout(sendOnce, RELAY_STARTUP_DELAY_MS);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid timeout-injecting startup command during relay auth

In relay-shell mode, scheduleRelayStartupCommand always sends startupCommand after a fixed 3s timeout even if no shell prompt was detected. When the inner SSH hop is still waiting on password/OTP/MFA input (a common case this mode is meant to support), that timeout writes the startup command into the authentication prompt, which can fail authentication or corrupt the challenge flow before the session is ready.

Useful? React with 👍 / 👎.

* Regex that matches safe SSH identifiers (hostnames, usernames, ports).
* Rejects shell metacharacters to prevent command injection.
*/
const SAFE_SSH_IDENTIFIER = /^[a-zA-Z0-9._:@\[\]\-]+$/;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Permit scoped IPv6 hosts in relay-shell safety validation

The relay-shell safety regex rejects %, so valid scoped IPv6 addresses like fe80::1%eth0 are treated as unsafe and assertSafeSshValue throws before connecting. This is a functional regression specific to relay-shell for link-local IPv6 targets/jump hosts that previously worked through normal SSH paths.

Useful? React with 👍 / 👎.

@RoryChou-flux
Copy link
Contributor Author

Follow-up fixes (review-focused)

Addressed the two concrete risks found in the relay-shell path, with minimal scope and clear behavior boundaries.

1) Relay-shell is now explicitly single-hop (end-to-end)

Why:

  • Multi-hop relay-shell could not reliably use saved credentials for hop Bug: 图标太大了 #2+ because the inner ssh -J runs on the relay host shell.
  • That created a UI/runtime mismatch: configurable in UI, but often failing at runtime.

What changed:

  • Backend hard guard: reject relay-shell + multi-hop with a clear error.
  • UI guard: in relay-shell mode, adding more than 1 jump host is disabled.
  • Save-time normalization: relay-shell host chains are persisted as single-hop (first hop only).

2) Startup command fallback is no longer silent in relay-shell

Why:

  • Relay-shell startup command is prompt-gated to avoid breaking MFA/auth prompts.
  • If no prompt is detected in time, command could appear to be “configured” but never run, without user-visible feedback.

What changed:

  • If prompt is still not detected after timeout, a terminal-visible message is emitted telling the user the startup command was not auto-run and showing the exact command to run manually.
  • Frontend no longer marks relay-shell startup command as executed in this uncertain path, avoiding false-positive execution bookkeeping.

Validation

  • npm run lint --silent passed.

If you'd like, I can also add a small E2E/integration test pass specifically for relay-shell single-hop and startup timeout messaging in a follow-up commit.

Taozhouchin and others added 4 commits March 4, 2026 23:40
Some bastion hosts disable AllowTcpForwarding, which prevents the
default proxy-tunnel mode (direct-tcpip channel forwarding) from
working. Relay-shell mode addresses this by SSHing into the first
jump host and executing `ssh -tt` in its shell to reach the target.

Core changes:

- sshBridge: relay-shell connection flow with command-injection
  prevention (assertSafeSshValue allowlist + shellEscapeForSh),
  IPv6 bracket handling, auth failure detection, and cleanup parity
  with the normal SSH path (chainConnections, auth:failed events)
- sshAuthHelper: add preferKeyboardInteractive option to avoid
  duplicate OTP pushes on bastion hosts that treat password-auth
  attempts as separate challenge triggers
- scheduleRelayStartupCommand: detect inner SSH readiness via shell
  prompt pattern matching (with sliding 512-byte probe buffer) and
  a 3-second timeout fallback
- startupCommand handling moved from frontend setTimeout to backend
  stream-level for more accurate timing across all modes
- SFTP guard: relay-shell mode does not support SFTP (requires
  direct-tcpip subsystem); early error in sftpBridge + unified
  getSftpErrorMessage in the frontend
- HostDetailsPanel: connection mode selector in ChainPanel with
  save-time hostChain cleanup (strip empty chains, omit defaults)
- sshConfigSerializer: skip ProxyJump for relay-shell (no SSH
  config equivalent)
- isAuthenticationError extracted as shared helper, replacing three
  inline checks

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@RoryChou-flux RoryChou-flux force-pushed the codex/relay-shell-pr branch from b01a19a to 0b0510f Compare March 4, 2026 15:41
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.

2 participants