Skip to content

feat(ssh): support full multi-hop jump chain (#356)#1058

Merged
lollipopkit merged 2 commits intomainfrom
lollipopkit/issue356-v2
Feb 27, 2026
Merged

feat(ssh): support full multi-hop jump chain (#356)#1058
lollipopkit merged 2 commits intomainfrom
lollipopkit/issue356-v2

Conversation

@lollipopkit
Copy link
Owner

@lollipopkit lollipopkit commented Feb 27, 2026

Summary

  • Support full multi-hop jump chain traversal for SSH (e.g. b -> a -> c when connecting c).
  • Enable chained jump selection in server edit UI while preventing cyclic configurations.
  • Fix SFTP isolate path so it receives full jump-server context and per-hop private keys.

Changes

  • Add jump-chain utilities for cycle detection and chain collection.
  • Update server edit page to allow selecting servers with existing jump settings as jump hops.
  • Add save-time cycle validation to block invalid chains.
  • Update genClient recursion to:
    • carry visited-set cycle protection,
    • resolve each hop's own private key,
    • work with injected jump/key maps for isolate mode.
  • Update ensureKnownHostKey recursion with cycle protection and injected jump maps.
  • Update SFTP request/worker to pass full jump chain and key maps to isolate worker.
  • Add unit tests for jump-cycle detection and jump-chain collection.

Verification

  • flutter analyze lib test
  • flutter test test/jump_chain_test.dart test/ssh_config_test.dart

Linked Issue

Summary by CodeRabbit

  • New Features

    • Safer jump-host handling with preloaded keys and resolved jump hosts for background transfers.
  • Bug Fixes

    • Detects and prevents jump-chain cycles (fails saves and connection attempts with a clear error).
    • UI now hides/prevents selecting jump servers that would create invalid cycles.
  • Tests

    • Added unit tests covering jump-chain detection and reachable-jump resolution.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 27, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5f9fbee and 3b72f08.

📒 Files selected for processing (3)
  • lib/core/utils/jump_chain.dart
  • lib/view/page/server/edit/actions.dart
  • test/jump_chain_test.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/jump_chain_test.dart

📝 Walkthrough

Walkthrough

Adds utilities for detecting and traversing jump-server chains and uses them across the codebase. Introduces wouldCreateJumpCycle and collectJumpServers in lib/core/utils/jump_chain.dart. Enhances SSH client creation (genClient) and host-key verification (ensureKnownHostKey) in lib/core/utils/server.dart to accept preloaded maps (privateKeysByKeyId, jumpSpisById) and a visitedServerIds set for cycle detection and isolate-mode recursion. Extends SFTP request model (SftpReq) with jumpSpisById and privateKeysByKeyId. Server edit UI validates jump selections against cycles. Adds unit tests for jump-chain utilities.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding support for multi-hop SSH jump chains, which is the primary objective of the PR.
Linked Issues check ✅ Passed The PR implements full multi-hop jump chain support as required by #356, including cycle detection, per-hop private key resolution, and SFTP isolate context handling.
Out of Scope Changes check ✅ Passed All changes are directly scoped to supporting multi-hop jump chains: cycle detection utilities, recursive jump traversal, SFTP context updates, UI cycle validation, and related tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch lollipopkit/issue356-v2

Comment @coderabbitai help to get the list of available commands and usage tips.

coderabbitai[bot]

This comment was marked as resolved.

@lollipopkit lollipopkit merged commit bc69686 into main Feb 27, 2026
1 check passed
@lollipopkit lollipopkit deleted the lollipopkit/issue356-v2 branch February 27, 2026 16:12
Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 6 additional findings in Devin Review.

Open in Devin Review

Choose a reason for hiding this comment

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

🟡 ensureKnownHostKey doesn't pass jumpSpisById to its genClient call, breaking isolate-mode jump chain resolution

The ensureKnownHostKey function was updated in this PR to accept jumpSpisById (and visitedServerIds) and correctly passes them through to its own recursive calls. However, the final genClient call at lib/core/utils/server.dart:386-391 does not forward jumpSpisById or visitedServerIds.

Root Cause and Impact

When ensureKnownHostKey is called with a non-null jumpSpisById map (intended for isolate mode where Hive stores are unavailable), it correctly uses it to resolve jump servers at lib/core/utils/server.dart:371-372:

final jumpSpi = jumpId != null ? (jumpSpisById?[jumpId] ?? Stores.server.box.get(jumpId)) : null;

And correctly passes it to its own recursive call at lib/core/utils/server.dart:379:

jumpSpisById: jumpSpisById,

But the genClient call at line 386 omits it:

final client = await genClient( spi, timeout: timeout, onKeyboardInteractive: onKeyboardInteractive, knownHostFingerprints: cache, );

Inside genClient, when the target server has a jump chain, it attempts to resolve jump servers via jumpSpisById?[jumpId] ?? Stores.server.box.get(jumpId) (lib/core/utils/server.dart:101). Since jumpSpisById was not passed, it's null, and the code falls back to Stores.server.box.get() which would crash in an isolate context.

Impact: Currently ensureKnownHostKey is only called from the main thread (lib/core/utils/host_key_helper.dart:18), so this doesn't crash today. However, the PR specifically added the jumpSpisById parameter to support isolate-mode usage, making this an incomplete implementation — the parameter is accepted and partially propagated but not passed to the key call that needs it.

(Refers to lines 386-391)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant