Repository navigation
Conversation
lzfxxx
marked this pull request as ready for review
October 13, 2026 01:00
tufantunc
added a commit
to tufantunc/ssh-mcp
that referenced
this pull request
Nov 6, 2026
) * fix: connect through a bastion on ssh2's JavaScript ciphers (#315) A `via` profile never connected when both hops negotiated chacha20-poly1305 and ssh2's optional native binding was absent. That covers the published Docker image, which installs with --ignore-scripts, and installs without build tools. The inner handshake hung after its first encrypted packet. The cause is ssh2's JavaScript chacha20 cipher (mscdex/ssh2#1523, open). It keeps the nonce in one module-level buffer and writes the encrypted packet length before encrypting the payload. When the socket is another connection's channel, that write runs the outer cipher's encrypt() in the middle of the inner one's, and the outer cipher leaves its own sequence number in the shared buffer. Connections over a bastion now offer chacha20-poly1305 last (FORWARDED_ALGORITHMS). It is moved, not removed, so a chacha20-only server is still reachable. Reproduced on the repo's bastion with ssh2's binding hidden: chacha20 on both hops hangs; any other pairing works; reordering ssh2's encrypt() fixes it. The new integration test runs the hops in a child that cannot load the binding. CI builds the binding, which is why proxy-jump.test.ts never saw this. With the binding hidden, proxy-jump.test.ts fails 3 of 4 without this change ("SSH connection ended during handshake") and passes with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: review round on the #315 workaround - The control test accepted any stall as the ssh2 bug, and its message said the workaround could go for any result but `hang`. The child now reports the last stage it completed. The control requires a stall after the forward, and only `ok` carries the "can go" message. - The unit test reached the private connect(). ensureConnected() is the public handshake-only call. - CI never ran the real via stack on ssh2's JavaScript ciphers, because `npm ci` builds the native binding. A step now reruns proxy-jump with the binding hidden, after checking that the hook hides it. Measured: without the fix that run fails 3 of 4, and with it all pass. - The changeset and the comment claimed a chacha20-only server stays reachable. On the JavaScript path it is still offered chacha20 and still hangs, as before. The corruption also needs chacha20 on any carrying hop, not only the adjacent one. Both now say this. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: mark the inner handshake's completion in the via-chacha child So hang@forwarded means a stall in the inner handshake and nothing after it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.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.
Fix native ChaCha20-Poly1305 nonce corruption during nested writes
Target repository: mscdex/ssh2. Draft for upstream review.
Baseline tested: ssh2 1.17.0, Node 22.20.0, macOS, optional native binding absent.
Updated 2026-10-12: master HEAD a3a39d7
still contains the pattern. GitHub connector reads verified the SHA and searched
open/closed issues and PRs. No equivalent fix was found within the documented
search coverage; see upstream-search.md for limits. Highest tag is v1.17.0;
GitHub Releases is empty, and npm registry publication state was not checked.
Summary
ChaChaPolyCipherNative.encrypt(packet) + allocate a nonce buffer scoped to this invocation encrypt packet length invoke onWrite(length) encrypt payload using the same invocation's nonceThe native implementation uses a module-scoped mutable nonce buffer.
The synchronous onWrite callback can encrypt through another cipher, as
in nested SSH forwarding. That nested call changes the shared sequence
number before the original cipher encrypts its payload. The packet length
is valid, but the peer decrypts an invalid payload.
Use a zero-initialized 16-byte buffer scoped to encrypt(). This isolates
the nonce across different cipher instances in nested callbacks without
changing cipher negotiation or SSH access policy.
Evidence
nested service-request payload validation; regression exits 1.
regression exits 0.
The integrated test/test-protocol-chacha-reentrancy.js forces the native
cipher and decipher, validates authenticated decryption and exact payload,
and covers standalone/nested writes at sequence numbers 0, 3 and 0xFFFFFFFF.
It fails on unpatched HEAD and passes all six cases with the patch.
node test/test.jsentry point was rerun on2026-10-13 using Node 22.20.0 on macOS with normal approval for localhost
and test sockets. It exited 1: 12 test files passed, including the native
six-case regression and upstream crypto; three failed. The OpenSSH 10.3 RSA
integration test reported client exit 255; server-hostkeys and SFTP tests
reported localhost AggregateError ETIMEDOUT. These failures have not been
diagnosed or confirmed against an unpatched baseline in this execution.
One unsupported OpenSSL cipher case was skipped. This PR does not claim
the full suite passes.
patching. Existing local dependencies were copied; no software was installed.
Compiled optional binding and Windows/Linux were not tested. Lint was not run
because ESLint and the upstream config package are unavailable locally.
Merge Danger
Door: two-way. Revert the invocation-local allocation to undo the change.
Blast Radius: cryptography.
One small buffer allocation per packet on this native path. Benchmarking and
upstream review are appropriate. Optional compiled binding and other ciphers
are unchanged. This does not claim to make same-instance recursive encryption
or all module-scoped scratch storage generally reentrant.