M13.4: AnvilSession::connect_via_jump_hosts (FR-56, NFR-17)#12
Merged
Conversation
…-17)
Adds the public chain manager that drives a ProxyJump chain through
russh `direct-tcpip` channels, with full host-key verification at
every hop.
session.rs:
- pub async fn connect_via_jump_hosts(config, jumps: &[JumpHost]):
1. Reject empty `jumps` with a clear message — callers should use
AnvilSession::connect when no chain is in play.
2. For each hop:
- Build per-hop AnvilConfig via the new jump_to_config helper
(inherits strict_host_key_checking, custom_known_hosts,
verbose from primary; per-hop user / identity_files from the
JumpHost when set, else from primary).
- Build handler pieces.
- First hop: client::connect (TCP).
Subsequent hops: previous handle's
channel_open_direct_tcpip(...) -> Channel -> into_stream() ->
client::connect_stream(...). The bastion-to-bastion transport
is the prev hop's direct-tcpip channel; russh runs a fresh
handshake over it.
- Independent per-hop host-key verification via
GitwayHandler::check_server_key (NFR-17). Mismatch at any
hop aborts the whole chain via ?-propagation; partial-success
path is forbidden.
- authenticate_best on the per-hop session so we can open the
next direct-tcpip channel through this bastion.
3. After the loop, the LAST bastion's handle opens a final
direct-tcpip to config.host:config.port, into_stream, then
client::connect_stream for the target's SSH handshake. Returns
the resulting AnvilSession indistinguishable from connect()'s
output to callers.
- Per-hop ssh_config resolution is NOT done here. The CLI dispatcher
(M13.6) is responsible for populating JumpHost.identity_files from
per-hop ssh_config::resolve calls before invoking this method.
Documented in the doc comment.
- Helper jump_to_config inherits security policy from the primary
config so a user's connection-wide --insecure-skip-host-check
applies to every hop (deliberate: a corporate user troubleshooting
a mismatch at one bastion still has the same global escape hatch).
Tests: 189 lib + 1 matrix + 6 integration; 0 failures. End-to-end
chain test (with two russh::server instances on loopback) lands in
M13.7.
Plan: M13.4 of let-us-plan-on-bright-cosmos.md. Stacked on M13.3 (PR
not yet opened).
This was referenced May 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the public chain manager that drives a
ProxyJumpchain through russhdirect-tcpipchannels with independent host-key verification at every hop (NFR-17). Re-opened on a clean rebase off main; previous PR #9 was auto-closed when its base branch (M13.3) was deleted by the squash-merge.Public surface (additive)
AnvilSession::connect_via_jump_hosts(config: &AnvilConfig, jumps: &[proxy::JumpHost]) -> Result<Self, AnvilError>Per-hop flow
jump_to_confighelper. Inheritsstrict_host_key_checking,custom_known_hosts,verbosefrom primary so a user's connection-wide policy applies to every hop. Per-hop user / identity_files come from the JumpHost when set, else from primary.client::connect(TCP). Subsequent hops: previous handle'schannel_open_direct_tcpip->Channel->into_stream()->client::connect_stream.GitwayHandler::check_server_key(NFR-17). Mismatch at any hop aborts the whole chain via?-propagation; partial-success is forbidden.authenticate_bestso we can open the next hop'sdirect-tcpipthrough it.After the loop, the last bastion's handle opens a final
direct-tcpiptoconfig.host:config.port, thenconnect_streamruns the SSH handshake over that channel for the target.Per-hop ssh_config
Not done here. The CLI dispatcher (M13.6) is responsible for populating
JumpHost.identity_filesfrom per-hopssh_config::resolvecalls before invoking this method.Tests
185 lib + 1 matrix + 6 integration; 0 failures, 5 ignored. End-to-end chain test with two
russh::serverinstances on loopback lands in M13.7.Test plan
cargo fmt/cargo clippy --all-targets -- -D warningsclean.cargo test --lib --tests --lockedgreen.Plan: M13.4 of let-us-plan-on-bright-cosmos.md.
🤖 Generated with Claude Code