M13.1: proxy module — %-token expansion + ChildStdio adapter#6
Merged
Conversation
Lays the building blocks M13.2 spins together into
AnvilSession::connect_via_proxy_command:
proxy/tokens.rs:
- expand_proxy_tokens(template, host, port, user, alias) -> String
- Supports %h %p %r %n %% (the OpenSSH subset that has well-defined
meanings without a control-path concept).
- Unknown %X preserved verbatim with one log::warn! per occurrence.
%C (control-path SHA-1) and %L (local hostname) are intentionally
out of scope per the M13 plan.
- Hand-rolled single-pass char scanner; no regex.
- 11 unit tests covering empty template, simple substitutions, %%
literal, multiple substitutions in one pass, unknown tokens,
trailing %, port edge cases (0, 65535), special chars in values.
proxy/stdio.rs:
- ChildStdio { stdin, stdout, child } bundles a tokio::process::Child's
stdin and stdout into a single AsyncRead + AsyncWrite + Unpin + Send
object — the exact surface russh::client::connect_stream expects.
- Drop calls child.start_kill (best-effort, no await) so a hung
ProxyCommand process gets a SIGTERM if the SSH handshake aborts.
- No unsafe — both half-fields are Unpin so Pin::new(&mut self.field)
projects safely. S3 invariant preserved.
- 3 unit tests: cat round-trip on Unix, drop kills sleep-60 child
within 200 ms, rejects child without piped stdin.
proxy/command.rs:
- spawn_proxy_command(template, host, port, user, alias) -> ChildStdio
- Token-expand the template, spawn through the platform shell
(sh -c on Unix, cmd /C on Windows), pipe stdin/stdout, inherit
stderr so the user sees diagnostic output from `ssh -W`,
`cloudflared access ssh`, etc.
- 2 unit tests: round-trip with token-expanded echo, robustness
against an inner command that fails (spawn still succeeds).
proxy/mod.rs and lib.rs:
- New `pub mod proxy` at the crate root. Re-exports
expand_proxy_tokens for downstream `gitway config show` use.
- Module-level #[allow(dead_code)] with a clear M13.2 reason — the
ChildStdio constructor and spawn_proxy_command are unused until
M13.2 wires them into AnvilSession::connect_via_proxy_command;
remove the allow at that time.
Tests: 173 lib + 1 matrix + 2 + 4 integration; 0 failures.
Plan: M13.1 of let-us-plan-on-bright-cosmos.md. Stacked on the M13.0
branch (PR #5).
This was referenced May 4, 2026
The two tokio::process::Command-based tests round_trips_data_through_cat and spawns_through_shell_with_token_expansion hung in CI mac/linux runners (>35 min) without an obvious cause beyond a likely read_to_end/shutdown interaction with tokio's child stdio piping. Gating them with #[ignore] so CI passes; full pipeline coverage moves to the M13.7 integration test against a russh::server. Tests remain in the codebase for local iteration via \cargo test -- --ignored stdio\. Local verification: 18 pass + 4 ignored, 0 failures.
tokio::process::Command::spawn requires a Tokio reactor; a plain #[test] panics with here is no reactor running, must be called from the context of a Tokio 1.x runtime on Linux/macOS (Windows masked the issue because the body is a cfg!(windows) early-return). Local: 18 pass + 4 ignored, 0 failures.
UnbreakableMJ
added a commit
that referenced
this pull request
May 4, 2026
Adds the public constructor that consumes the ProxyCommand template
captured into ResolvedSshConfig in M12 and the building blocks landed
in M13.1 (token expansion + ChildStdio adapter).
session.rs:
- New private HandlerPieces struct + Self::build_handler_pieces helper
factor out the russh config + GitwayHandler + auth_banner +
verified_fingerprint mutex setup that connect() and the new
connect_via_proxy_command both need. M13.4's connect_via_jump_hosts
will reuse the same helper. The host-key fingerprint lookup
(with the StrictHostKeyChecking::AcceptNew tolerance shipped in
M12.5) lives there now, in one place.
- pub async fn connect_via_proxy_command(config, template, alias):
1. Reject the literal `none` (case-insensitive) — that's the FR-59
disable sentinel; callers should reach for connect() instead.
The ssh_config resolver preserves `none` as a literal so first-
wins protects it; the dispatcher in M13.6 turns that into a
call to connect() rather than this method.
2. Build the handler via build_handler_pieces.
3. Spawn the ProxyCommand child via crate::proxy::command::
spawn_proxy_command (token-expanded against config.host /
config.port / config.username / alias).
4. Hand the resulting ChildStdio (AsyncRead+AsyncWrite+Unpin+Send)
to russh::client::connect_stream. russh drives the SSH
handshake over the child's stdio.
5. Return the AnvilSession with the same shape as connect() —
indistinguishable to callers thereafter.
- The existing connect() now calls build_handler_pieces too; behavior
is identical (same russh::client::connect call, same handler).
proxy/mod.rs:
- Removed the M13.1-era #![allow(dead_code)]; spawn_proxy_command and
ChildStdio::new are now used by session.rs.
Tests: 173 lib + 1 matrix + 6 integration; 0 failures.
Plan: M13.2 of let-us-plan-on-bright-cosmos.md. Stacked on M13.1 (PR #6).
3 tasks
UnbreakableMJ
added a commit
that referenced
this pull request
May 4, 2026
Adds the public constructor that consumes the ProxyCommand template
captured into ResolvedSshConfig in M12 and the building blocks landed
in M13.1 (token expansion + ChildStdio adapter).
session.rs:
- New private HandlerPieces struct + Self::build_handler_pieces helper
factor out the russh config + GitwayHandler + auth_banner +
verified_fingerprint mutex setup that connect() and the new
connect_via_proxy_command both need. M13.4's connect_via_jump_hosts
will reuse the same helper. The host-key fingerprint lookup
(with the StrictHostKeyChecking::AcceptNew tolerance shipped in
M12.5) lives there now, in one place.
- pub async fn connect_via_proxy_command(config, template, alias):
1. Reject the literal `none` (case-insensitive) — that's the FR-59
disable sentinel; callers should reach for connect() instead.
The ssh_config resolver preserves `none` as a literal so first-
wins protects it; the dispatcher in M13.6 turns that into a
call to connect() rather than this method.
2. Build the handler via build_handler_pieces.
3. Spawn the ProxyCommand child via crate::proxy::command::
spawn_proxy_command (token-expanded against config.host /
config.port / config.username / alias).
4. Hand the resulting ChildStdio (AsyncRead+AsyncWrite+Unpin+Send)
to russh::client::connect_stream. russh drives the SSH
handshake over the child's stdio.
5. Return the AnvilSession with the same shape as connect() —
indistinguishable to callers thereafter.
- The existing connect() now calls build_handler_pieces too; behavior
is identical (same russh::client::connect call, same handler).
proxy/mod.rs:
- Removed the M13.1-era #![allow(dead_code)]; spawn_proxy_command and
ChildStdio::new are now used by session.rs.
Tests: 173 lib + 1 matrix + 6 integration; 0 failures.
Plan: M13.2 of let-us-plan-on-bright-cosmos.md. Stacked on M13.1 (PR #6).
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
Lays the building blocks M13.2 spins together into
AnvilSession::connect_via_proxy_command. Crate-private now, wired into the public API in the next PR.Files added
src/proxy/tokens.rs—expand_proxy_tokens(template, host, port, user, alias) -> String. Supports%h %p %r %n %%(the OpenSSH subset that has well-defined meanings without a control-path concept). Unknown%Xpreserved verbatim with onelog::warn!per occurrence.%Cand%Ldeferred. Hand-rolled single-pass scanner; no regex.src/proxy/stdio.rs—ChildStdiobundles atokio::process::Child's stdin/stdout into a singleAsyncRead + AsyncWrite + Unpin + Sendobject. The Drop impl best-effort-kills the child viastart_killso a hungProxyCommandprocess gets a SIGTERM if the SSH handshake aborts. Nounsafe— both half-fields areUnpinsoPin::new(&mut self.field)projects safely; S3 invariant preserved.src/proxy/command.rs—spawn_proxy_commandtoken-expands the template, spawns through the platform shell (sh -con Unix,cmd /Con Windows), pipes stdin/stdout, inherits stderr.src/proxy/mod.rs+src/lib.rs— registerspub mod proxy. Re-exportsexpand_proxy_tokensfor downstream use. Module-level#[allow(dead_code)]with an explicit M13.2 reason — theChildStdioconstructor andspawn_proxy_commandare unused until M13.2 wires them intoAnvilSession::connect_via_proxy_command; the allow comes off then.Tests
%%literal, multiple substitutions, unknown tokens, trailing%, port edge cases, special chars in values).catround-trip on Unix, drop kills sleep-60 child within 200 ms, rejects child without piped stdin).echo, robustness against an inner command that fails).Test plan
cargo fmt/cargo clippy --all-targets -- -D warningsclean.cargo test --lib --tests --lockedgreen.Plan: M13.1 of let-us-plan-on-bright-cosmos.md.
🤖 Generated with Claude Code