feat(tp_sshd): embedded pure-Dart SSH server package - #7
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ttling Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Task 5 of the embedded SSH server plan: the running phase serves real session traffic. - SSHServerChannel: per-channel input/extendedInput streams, outgoing write/writeExtended/sendEof/close with window-aware chunking (a spent send window stalls until a Window_Adjust unblocks it), OpenSSH-style deferred receive-window refills, and the onRequest hook Tasks 6-7 build the session requests on (refuses requests until then). - SSHServerConnection: channel table keyed by our channel number, CHANNEL_OPEN (session-only; other types administratively prohibited), Window_Adjust/Data/Extended_Data/EOF/Close routing (unknown ids ignored as close races), Channel_Request dispatch, and global requests (keepalive@openssh.com answered, everything else refused until Task 9). - Tests: real-client dual pairs (open/failure/keepalive/data/close/ requests) plus raw-transport window tests with tiny windows, and harness helpers (startDualConnection, raw authenticated pair, mirror-based client session-channel opener). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Task 6 of the embedded SSH server plan: session exec speaks only the structured tp1: grammar. - server_process.dart: SSHServerProcess (stdout/stderr/stdin/exitCode/ kill contract for app-backed processes), SSHProcessFactory, SSHHostInfo (const, toJson/fromJson wire format), SSHExecRequest, and TpExecCodec (encode/tryDecode fail-closed, host-info query encode/match, encodeHostInfo). - server_session.dart: handleSessionRequest serves exec — host-info is answered by the server itself (JSON on stdout, exit 0), everything else requires a tp1: payload and a processFactory, then pipes stdout->channel data, stderr->extended data, channel input->stdin, and reports exit-status before EOF/close. Channel close or connection teardown kills the process and closes its streams. Non- tp1: commands are refused and the channel closed — no shell strings ever run. - server_channel.dart: onRequest widened to carry the triggering request and report its outcome (Future<bool>), removing the racy currentRequest field; new sendExitStatus for RFC 4254 6.10. - ssh_server.dart: SSHServerConfig.processFactory / hostInfo, both nullable (null refuses the corresponding request). - Tests: real SSHClient.execute() dual pairs (argv/cwd/env delivery with exit code 17, plain shell string rejected, host-info answered without spawning) and startDualPair grows the two config hooks. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Session channels now serve the interactive half of RFC 4254 §6: pty-req stashes terminal dimensions (TERM in the environment, modes unparsed), env requests accumulate, shell requires the stashed pty-req and spawns through the new SSHServerConfig.ptyFactory (SSHServerPty / SSHPtyDimensions in server_process.dart), window-change resizes the running pty, and signal delivers the RFC 4254 §6.9 name the client sent straight to it. Shell serving pipes through the exec plumbing, so teardown mirrors it: pty exit reports exit-status before EOF and close; channel/connection teardown kills the pty. A session channel takes exactly one lifecycle request: exec or shell once — a second lifecycle request is refused and the channel closed after its failure reply. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
tcpip-forward / cancel-tcpip-forward global requests and forwarded-tcpip channels (RFC 4254 §7). Binds go through the injected SSHBindServerSocket seam (null = forwarding disabled); only 127.0.0.1, ::1 and localhost are ever bound, refused before the seam is consulted otherwise. Accepted connections ride server-initiated forwarded-tcpip channel opens — the channel table now has an outbound open path with pending-open tracking. Port 0 binds ephemeral and the Request_Success payload carries the actual port. Cleanup is bidirectional: TCP close closes the channel, channel close destroys the TCP side, cancel releases the listener, and connection teardown unbinds everything. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Carried review minors, each with its test: - Contain connection-stream errors in the SSHServer accept loop: the error surfaces on SSHServer.done (marked handled so an unawaited done cannot leak a zone error) after live connections are torn down, and the server stops accepting. - Cap simultaneously open channels per connection: new maxChannels config (default 10, OpenSSH parity); excess CHANNEL_OPENs are refused with reason 4 (resource shortage), and the server-initiated (forwarded-tcpip) direction respects the same cap. - Flush queued window-credit data on SSHServerChannel.close within a bounded wait (closeFlushTimeout, default 2 s) so an exec output tail is not truncated by a slow peer; a window that never opens falls back to dropping the tail after the bound. - Fail-safe process-contract errors: an exitCode future that errors tears the pipes down and closes the channel without inventing an exit status; a stdin.add throw is treated as input-side close. Hardening tests (test/server_hardening_test.dart): auth throttle cuts the client off after the attempt budget, a garbage connection is closed without harming the listener, five concurrent connections all authenticate and exec, and a mid-session rekey through the fork's public SSHClient.rekey() keeps the channel alive. Also: direct test for the null-seam tcpip-forward refusal, shared startRawPair/testProbeRequest test helpers, and the package README. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sftp exports Moves the gitlink from 4602aab to the feat/embedded-server head (a12443d): the lib/protocol.dart public export, the server-role ECDH key exchange in SSHTransport, and the sftp primitive exports that tp_sshd builds on. Not pushed — the submodule push is deferred to the finish gate with the human. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A stock OpenSSH sftp client exits by sending CHANNEL_EOF on the session channel and then waiting for the server to close it. The subsystem only awaited the channel finishing, which requires the client's CHANNEL_CLOSE — so a bare EOF deadlocked the session and the client never exited. End the session on the input stream closing instead (the client's EOF, its close, or connection teardown all close it), release the open handles, and close the channel — the same onDone pattern the exec/shell pump already uses. Found driving the package with a real OpenSSH client. Co-Authored-By: Claude <noreply@anthropic.com>
demo_sshd.dart wires every seam to real system resources — a TCP listener, ssh-keygen-bootstrapped keys, a jailed dart:io SFTP filesystem, Process-backed exec, a util-linux `script` pty for shells, and real ServerSocket binds for tcpip-forward — so the package can be exercised end-to-end with a stock OpenSSH client. First run prints the exact ssh/sftp/-R command lines; keys and sandbox live in ~/.tp_sshd_demo. Co-Authored-By: Claude <noreply@anthropic.com>
hhoao
added a commit
that referenced
this pull request
Sep 19, 2026
# Conflicts: # client/lib/app/app_shell.dart # client/test/services/terminal/workspace_shell_connector_test.dart
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
Implements the protocol-server half of the embedded SSH server spec (
docs/specs/2026-09-08-embedded-ssh-server-design.md): QR pairing will no longer depend on a system sshd. Ships in two repos:dartssh2 submodule (
feat/embedded-server, pushed — gitlink bump included):lib/protocol.dart: additive public export of the wire codec / KEX math / algorithm registries (zero behavior change)SSHTransport: server-role ECDH key exchange completion (signs the exchange hash with a host key, role-guarded; client behavior untouched)New package
client/packages/tp_sshd(pure Dart, no Flutter deps):tp1:exec (argv/cwd/env — no shell strings, no quoting), host-info query, pty-backed interactive shellsEverything is dual-tested through the real vendored dartssh2 client (56 package tests + 8 new fork tests; adversarial traffic included: malformed packets, tiny windows, invalid signatures, 6000-entry listings).
Verification
client/packages/tp_sshd:dart analyzeclean,dart test56/56,dart formatcleanclient/packages/dartssh2:dart analyzeclean, suite green vs baseline (only the 23 pre-existing test.rebex.net network failures)flutter analyzeagainst the changed dartssh2: 0 errors in applib//test/and vendored packages (the full app test suite is blocked in fresh checkouts by a pre-existing native_toolchain_c vswhere GBK bug on zh-CN Windows — unrelated to this change)Notes for reviewers
feat/embedded-serveris pushed before this branch🤖 Generated with Claude Code