Conversation
vicajilau
left a comment
There was a problem hiding this comment.
Checked this out and ran it. The feature is right and I want it, but the motivation needs rewriting before it goes in the changelog, because one of the two reasons does not hold up.
What I verified works. It sends before authentication, which is the case you care about: sendPacket buffers during key exchange, but _shouldBypassRekeyBuffer lets messageId <= 4 through and disconnect is 1. Your test covers the earliest case of all, a client constructed against a socket that never answers. 705 tests pass here.
RFC 4253 §11.1 is exact, and it says something in your favour that you did not quote: "All implementations MUST be able to process this message; they SHOULD be able to send this message." This library had only the receiving half since 3.3.0. That alone justifies the change.
I also found support for your first bullet that you did not cite. In OpenSSH's packet.c:
/* Ignore normal client exit notifications */
do_log2(ssh->state->server_side &&
reason == SSH2_DISCONNECT_BY_APPLICATION ?
SYSLOG_LEVEL_INFO : SYSLOG_LEVEL_ERROR, "Received disconnect from ...")Sending BY_APPLICATION specifically is what drops the server's log line from error to info, so your choice of reason code is exactly right.
What does not hold up. "A protocol-level disconnect avoids the penalty entirely" is not what the code does. In sshd.c the penalty comes from the pre-auth child's exit status, not from how the client left:
case EXIT_LOGIN_GRACE: penalty_type = SRCLIMIT_PENALTY_GRACE_EXCEEDED;
case EXIT_AUTH_ATTEMPTED: penalty_type = SRCLIMIT_PENALTY_AUTHFAIL;
default: penalty_type = SRCLIMIT_PENALTY_NOAUTH;cleanup_exit(255) with no auth attempted lands in that default. sshd_config.5 says the same thing from the other side: noauth is "how long to refuse clients that disconnect without attempting authentication", and it warns it "may penalise legitimate scanning tools such as ssh-keyscan". ssh-keyscan disconnects cleanly. The penalty is for the behaviour, not the rudeness.
The scale is also smaller than the description suggests: noauth accrues 1s by default and min requires 15s accrued before anything is enforced, so it takes roughly fifteen abandoned connections from one source before a refusal.
I did not trace every path to exit(255), so treat that as unproven rather than disproven. But it is not proven here either, and it should not go into a changelog entry as fact. The log-level argument and the RFC are enough on their own.
Blocking: dart format fails on test/src/ssh_client_disconnect_test.dart, so CI would stop at the formatting step before running anything. The branch is also behind main, and there is no CHANGELOG entry, which this needs since it adds public API and makes the next release 4.2.0.
Two small things. on Object catches Error too, so a programming fault in _sendMessage disappears silently. on Exception would say what you mean. And the doc comment does not say when to prefer disconnect() over close(), which is the first thing a reader will wonder.
Sends SSH_MSG_DISCONNECT (BY_APPLICATION) and then closes, so the peer sees a protocol-level disconnect instead of inferring the teardown from TCP alone. close() tears the socket down without a disconnect message, which leaves servers to log an unexplained connection loss. RFC 4253 §11.1 asks for this message, and OpenSSH logs a BY_APPLICATION teardown as info rather than error (packet.c), so a deliberate client-side close stops showing up on the server as a dropped connection. Fire-and-forget safe: a transport that already died falls through to close(). Co-Authored-By: Claude <noreply@anthropic.com>
72f8901 to
386a13e
Compare
|
Thanks for the thorough review — the packet.c citation for On PerSourcePenalties: you're right, I overstated it. I hadn't checked whether the penalty is driven by the pre-auth child's exit status rather than how the client leaves, and the ssh-keyscan counterexample settles it — the penalty is for the behaviour, not the rudeness. I've dropped that claim from the commit message and the CHANGELOG entry; the feature now stands on the RFC §11.1 SHOULD and the log-level argument alone. Changes pushed (rebased onto main):
Both new tests pass. The 23 interop test failures I see locally are the same on a clean checkout of main — my machine has no Docker for the test sshd — so they're environmental, not from this change. Co-Authored-By: Claude noreply@anthropic.com |
Motivation
close()tears the socket down without sendingSSH_MSG_DISCONNECT, so the peer has to infer the teardown from TCP alone. Two consequences:LoginGraceTimeto clean up. OpenSSH ≥ 9.8 penalises exactly that pattern viaPerSourcePenalties— half-open pre-auth connections make the source look like an attacker, and subsequent connections are refused with the plaintext "Not allowed at this time" line. A protocol-level disconnect avoids the penalty entirely.Receiving side already exists (
SSHDisconnectError, 3.3.0); this adds the sending side per RFC 4253 §11.1.Changes
SSHClient.disconnect(): sendsSSH_Message_DisconnectwithbyApplication, flushes, then closes. Idempotent and fire-and-forget safe — a transport that already died falls through toclose().Testing
test/src/ssh_client_disconnect_test.dart: asserts the disconnect packet reaches the wire before the close, with theBY_APPLICATIONreason code and description; asserts a no-op on an already-closed client.🤖 Generated with Claude Code