Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## [4.1.0] - Unreleased
- Added `SSHClient.pipelineChannelRequests`, off by default, which sends all of a session's channel requests before reading any reply instead of waiting for each one in turn. `execute` and `shell` send `env`, agent forwarding, `pty-req` and `x11-req` ahead of `exec` or `shell`, and each of them cost a round trip: against a server 40 ms away, `execute` with a pty measured 246 ms before and 206 ms after. RFC 4254 §5.4 permits sending further messages without waiting and §4 requires the peer to answer a channel's requests in the order it received them, which is what `ssh(1)` relies on when it does the same thing. Setting it also adopts OpenSSH's reporting, because it has to: the command is on the wire before a refusal can come back, so a refused `pty-req`, `env`, agent forwarding or `x11-req` is reported through `printDebug` and the command runs, the way `ssh(1)` prints `PTY allocation request failed on channel 0` and carries on, rather than throwing an error that means the command has already run. Only a refused `exec` or `shell` still throws `SSHChannelRequestError`, because nothing has run when that one fails. Leaving it unset changes nothing, down to the order the requests go out and the message of every error [#243].
- Added a `dartssh2-nopty` account to the interop server, which `PermitTTY no` applies to, so both sides of a refused `pty-req` are exercised against a real OpenSSH: by default the command does not run, and with pipelining it does [#243].
- Documented what `handshakeTimeout` and `authTimeout` actually bound. Both are null by default, so out of the box nothing stops a server that accepts the connection and then goes quiet from leaving the client waiting for as long as the socket stays open, and `keepAliveInterval` does not cover it because it only starts once the connection is up. Both are also deadlines over the whole phase rather than inactivity timers, and the callbacks run inside them: `onVerifyHostKey` counts against the first, `onPasswordRequest` and `onUserInfoRequest` against the second. A value picked to bound an unresponsive server will cut off a user who is slow reading a fingerprint or typing a password, which the README's own 15 second example would have done [#247].

## [4.0.1] - 2026-09-03
- Fixed a channel stalling permanently when data arrived before the application subscribed to it. `StreamController.isPaused` is true until something listens, which suppressed every `SSH_MSG_CHANNEL_WINDOW_ADJUST`, and Dart delivers the first subscription as `onListen` rather than `onResume` — the only hook wired — so a peer that legally filled the advertised window in that gap was left at zero credit with no further data able to arrive and trigger a grant. Reproduced at the client's own sizes with 64 packets of 32 KiB into a 2 MiB window, before and after the first listener and through `.map()`, in every case zero adjustments. The remote forwarding example in the README reaches it: it awaits `Socket.connect()` for each connection before subscribing, and the forwarded channel is created with no listener attached [#244].
Expand Down Expand Up @@ -439,6 +440,7 @@
[#237]: https://github.com/vicajilau/dartssh2/pull/237
[#239]: https://github.com/vicajilau/dartssh2/pull/239
[#243]: https://github.com/vicajilau/dartssh2/issues/243
[#247]: https://github.com/vicajilau/dartssh2/pull/247
[#244]: https://github.com/vicajilau/dartssh2/pull/244

[@linhanyu]: https://github.com/linhanyu
Expand Down
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,19 @@ void main() async {
}
```

By default, these parameters are `null` (no timeout is enforced). Without these timeouts, the connection or authentication process could hang indefinitely if the remote server becomes unresponsive.
By default both are `null` and nothing is enforced, so a server that accepts
the connection and then stops responding leaves the client waiting for as long
as the socket stays open. `keepAliveInterval` does not cover this, since it
only starts once the connection is up.

Pick the values against how long a **person** might take, not how long the
network should take. Both are deadlines over the whole phase rather than
inactivity timers, and your own callbacks run inside them: `onVerifyHostKey`
counts against `handshakeTimeout`, and `onPasswordRequest` and
`onUserInfoRequest` count against `authTimeout`. The 15 seconds above is fine
for the literal password in this example and far too short for a real prompt,
where someone reading a fingerprint or typing a passphrase will blow through
it and lose the connection.

### Spawn a shell on remote host

Expand Down
21 changes: 21 additions & 0 deletions lib/src/ssh_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,30 @@ class SSHClient {
final Duration? keepAliveInterval;

/// Maximum time to wait for the SSH transport handshake to complete.
///
/// Null by default, so nothing bounds the handshake out of the box: a peer
/// that accepts the connection and then stops sending leaves the client
/// waiting for as long as the socket stays open. [keepAliveInterval] does
/// not cover this, since it only starts once the connection is up.
///
/// This is a deadline over the whole handshake rather than an inactivity
/// timer, and [onVerifyHostKey] is awaited inside it. If that callback puts
/// a fingerprint in front of someone, the time they spend deciding counts
/// against this, so a value short enough to be a useful bound on an
/// unresponsive server will also drop connections a person was about to
/// accept.
final Duration? handshakeTimeout;

/// Maximum time to wait for authentication after the transport is ready.
///
/// Null by default, with the same consequence described on
/// [handshakeTimeout].
///
/// Also a deadline rather than an inactivity timer, and it spans the
/// authentication callbacks: [onPasswordRequest] and [onUserInfoRequest] are
/// awaited inside it. A value picked to bound an unresponsive server will
/// also cut off a user who is slow typing a password, so pick it against how
/// long a person might take, not how long the network should take.
final Duration? authTimeout;

/// Function called when additional host keys are received. This is an OpenSSH
Expand Down
Loading