Repository navigation
Conversation
…Windows Setting up an SSH key spawns ssh-keygen through tokio::process::Command (key_setup.rs). On Windows that child inherited every socket this process had open when it started, including the ports a tunnel listens on, and held them for its own lifetime. Stopping a tunnel and starting it again in that window failed to bind with os error 10048, and since serve() in tunnel.rs turns a bind failure into TunnelStatus::Failed without a retry, the tunnel stayed down until the app was restarted. The cause is in mio, which created sockets without asking Windows to keep them out of child processes. mio 1.2.1 sets WSA_FLAG_NO_HANDLE_INHERIT on socket creation (tokio-rs/mio#1946); this moves the lockfile to 1.2.3, the current patch of the same minor. Unix was never affected, since Tokio's sockets are close-on-exec there. Measured on Windows 11, before and after, same machine and same base: cargo test -p omnyssh-core --test tunnel, 10 runs mio 1.2.0 41 failures (4-5 per run, 10/10 runs red) mio 1.2.3 10 failures (exactly 1 per run, all of them the unrelated a_rejected_password_fails_without_a_retry) cargo test -p omnyssh-core --lib, 25 runs mio 1.2.0 1-4 runs red on a_port_in_use_fails_the_whole_tunnel_and_binds_nothing mio 1.2.3 0 runs red A standalone reproduction confirms the mechanism rather than inferring it: bind a tokio TcpListener, spawn a child that outlives it, drop the listener, then rebind the same port. On mio 1.2.0 that is refused 5 of 5 times with os error 10048 and the port frees the moment the child exits; on 1.2.1, 1.2.2 and 1.2.3 it succeeds 5 of 5. 1.2.1 is the minimum fixed version. Only mio's version and checksum change. Running `cargo update -p mio` would also rewrite which windows-sys version fourteen other crates resolve to, but that churn is pre-existing lockfile drift: asking cargo to "update" mio to 1.2.0, the version already pinned, produces the same 26 lines. This edits the two lines that matter and leaves the drift for whoever wants to address it separately. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Problem
On Windows, a tunnel can be left permanently unable to rebind its ports after
an SSH key setup runs.
Setting up a key spawns
ssh-keygenthroughtokio::process::Command(
key_setup.rs:280). On Windows that child inherits every socket this processhas open at the moment it starts — including the ports a tunnel is listening on —
and holds them for its own lifetime. Stop a tunnel and start it again inside that
window and
bind_allfails with:serve()intunnel.rs:352turns a bind failure intoTunnelStatus::Failedandreturns without retrying, so the tunnel stays down until the app is restarted. A
host set to start its tunnel with OmnySSH can also fail to come up at all if a key
setup happens to be running.
Unix was never affected — Tokio's sockets are close-on-exec there.
Cause
Not in this repo.
miocreated sockets without asking Windows to keep them out ofchild processes. mio 1.2.1 fixed it by setting
WSA_FLAG_NO_HANDLE_INHERITonsocket creation (tokio-rs/mio#1946).
The lockfile pins 1.2.0.
Evidence
I did not want to infer this from flaky tests, so there are two independent
measurements.
1. The test suite, before and after, same machine and same base commit.
--test tunnel, 10 runs--lib, 25 runsThe 10 remaining tunnel failures are all one test,
a_rejected_password_fails_without_a_retry, which fails for an unrelated reason —see Not fixed here below. The
--libtest that stops failing isa_port_in_use_fails_the_whole_tunnel_and_binds_nothing.2. A standalone reproduction of the mechanism itself, so the conclusion does
not rest on the suite. Bind a tokio
TcpListener, spawn a child that outlives it,drop the listener, then try to rebind the same port:
The port freeing the instant the child exits is what identifies the child as the
holder. 1.2.1 is the minimum fixed version; I went to 1.2.3, the current patch
of the same minor, and am happy to use 1.2.1 instead if you would rather take the
smallest possible step.
Why the lockfile diff is only two lines
cargo update -p mio --precise 1.2.3also rewrites whichwindows-sysversionfourteen other crates resolve to. That churn is pre-existing lockfile drift, not
caused by this bump: asking cargo to "update" mio to 1.2.0 — the version already
pinned — produces the same 26 lines. The set of
windows-sysversions in the graphis identical before and after.
So this edits only mio's
versionandchecksum, and leaves the drift alonerather than smuggling it into a bug fix.
cargo build --lockedaccepts the resultwithout rewriting it. If you would prefer the lockfile normalized in the same PR,
say so and I will add it as a second commit you can drop independently.
Possible overlap with #134
#134 already bumps mio 1.2.0 → 1.2.3, incidentally, as part of its dependency
updates. Nothing in that PR mentions Windows, sockets, tunnels or handle
inheritance, so I don't think it is known there that the bump fixes a bug — worth
flagging either way:
a 37-file feature PR.
Your call entirely — I am not attached to this being the PR that carries it.
Not fixed here
Three tests fail on Windows before and after this change, for a different reason,
so the suite is not green on Windows even with this applied:
tests/login.rs::a_login_with_nothing_to_try_says_what_is_missingtests/login.rs::a_refused_password_is_sent_once_per_logintests/tunnel.rs::a_rejected_password_fails_without_a_retryAll three fail with
SSH key requires a passphrase: C:\Users\<me>\.ssh\id_ed25519.Their
isolate_home()helpers setHOMEandUSERPROFILEto a temp dir, whichworks for
known_hosts(that path usesstd::env::home_dir()), butdefault_key_paths()insession.rsgoes throughdirs::home_dir(), and indirs5.0.1 on Windows that resolves via the Shell known-folder API and ignores%USERPROFILE%. So the tests read the developer's real~/.ssh. CI does not seeit because its Windows runner has no keys.
Happy to open that separately if you want it — it needs a seam in core rather than
a test-only change, so it seemed worth asking first.
Test plan
cargo build --locked— clean, lockfile not rewrittencargo test --locked --no-fail-fast— only the three passphrase tests above failcargo clippy --all-targets -- -D warnings— cleancargo fmt --check— cleanbash scripts/hygiene-guard.sh— passesgit diff --exit-code doc/omny.1— cleanNo source files change, so there is nothing new to unit-test; the behaviour is in a
dependency and the reproduction above is what demonstrates it.
🤖 Generated with Claude Code