ssh: add opt-in application-driven channels - #1233
Conversation
The one server-side site that opens auth-agent@openssh.com sits inside wolfSSH_accept(), so an application driving its own channels cannot reach it: the session records the request and no channel follows. - add wolfSSH_AGENT_ChannelOpen(), the same open lifted out of accept(), which still calls it - it reports WS_BAD_ARGUMENT until the peer asks and is idempotent afterward, so an application can poll it - publish the agent on a queued open too, so a retry after WS_WANT_WRITE finds it rather than opening a second channel and leaking the first
A server that wants to own its channels had no way to get them: accept() ran the session state machine to the end, and a shell, exec or subsystem request with no callback registered was granted regardless. - add wolfSSH_CTX_SetAppChannels() and wolfSSH_SetAppChannels(), off by default, a byte on the context copied into the session - on, accept() returns once the user is authenticated, and a session request with no callback behind it is refused: nothing is left to serve - keep the stop state out of the pending-send advance, so a re-entry with queued output cannot step over where this call is meant to stop - stop early only while the session is short of that state, so turning the mode on afterward cannot leave the loop hunting a state it went past - teach wolfSSH_SFTP_accept() that the mode parks accept() short of an established session, so it stops redoing the handshake on every poll
wolfSSH_SetAppChannels() changes where wolfSSH_accept() stops and what becomes of a session request with no callback behind it, so both modes are exercised. - regress.c drives a server with the pivot on, one with a shell callback and one without, and checks accept() stops at ACCEPT_SERVER_USERAUTH_SENT - regress.c pins the context setter, the session's inheritance of it, and that turning it on after accept() established the session still returns - unit.c checks DoChannelRequest() refuses a shell, exec and subsystem request with no callback once the pivot is on - the untouched AssertHandshakeSucceeds() is the regression gate for a server that registers nothing
There was a problem hiding this comment.
🟡 Changes recommended
There are API contract/documentation mismatches with observable runtime behavior (late enable semantics and return-code documentation) that should be resolved before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR adds an opt-in “application-driven channels” mode for the server side, allowing wolfSSH_accept() to return immediately after user authentication so the application can drive channel lifecycle and request handling via wolfSSH_worker(). It also refactors agent forwarding channel opening for reuse outside wolfSSH_accept(), and updates SFTP acceptance logic and tests to cover both default and application-driven modes.
Changes:
- Add
wolfSSH_CTX_SetAppChannels()/wolfSSH_SetAppChannels()to optionally stopwolfSSH_accept()at post-auth and reject shell/exec/subsystem requests that have no registered callback in this mode. - Update
wolfSSH_accept()state advancement/stop behavior and extract agent channel open intowolfSSH_AGENT_ChannelOpen(). - Extend unit/regress tests to exercise both modes; adjust
wolfSSH_SFTP_accept()to treat the post-auth stop state as “done” for its accept precondition.
File summaries
| File | Description |
|---|---|
| wolfssh/ssh.h | Adds public API and documentation for application-driven channel mode. |
| wolfssh/internal.h | Adds appChannels flag to context/session internal structs. |
| wolfssh/agent.h | Declares wolfSSH_AGENT_ChannelOpen() and documents intended usage. |
| src/ssh.c | Implements new setters and modifies wolfSSH_accept() stop/advance behavior; switches to wolfSSH_AGENT_ChannelOpen(). |
| src/internal.c | Inherits appChannels from context and changes channel-request default handling under app-driven mode. |
| src/agent.c | Implements wolfSSH_AGENT_ChannelOpen() extracted from accept flow. |
| src/wolfsftp.c | Treats post-auth stop state as “accept done” for SFTP accept gating. |
| tests/unit.c | Adds unit coverage ensuring no-callback shell/exec/subsystem requests are refused under app-driven mode. |
| tests/regress.c | Adds regression harness/tests for accept stopping point, inheritance, and late-enable behavior. |
Review details
- Files reviewed: 9/9 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if (ssh != NULL) { | ||
| ssh->appChannels = (enable != 0); | ||
| ret = WS_SUCCESS; | ||
| } |
| * peer's request. Returns WS_SUCCESS, or WS_BAD_ARGUMENT when the session | ||
| * never asked for agent forwarding. */ |
Stacked on #1230, whose commit is the first of the three here; review the
last two. A server that wants to own its channels had no way to get them:
accept() ran the session state machine to the end, and a shell, exec or
subsystem request with no callback registered was granted regardless.
default. On, accept() returns once the user is authenticated and a
session request with no callback behind it is refused.
only while the session is short of that state.
established session, so it stops redoing the handshake on every poll.
an uncallbacked request once the pivot is on.