ssh: add generic request callbacks - #1236
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
There are confirmed correctness/security issues around silently truncated request names/types being passed to the new generic callbacks (and a readiness-state bug in wolfSSH_SFTP_accept() under app-driven mode) that should be addressed before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR adds “generic” (any-name) callbacks for SSH channel requests and global requests, enabling centralized policy decisions (accept/reject/unhandled) before the existing type-specific callbacks and built-in handling. It also introduces an application-driven channel mode that stops wolfSSH_accept() after authentication so applications can drive channel open/request handling via wolfSSH_worker(), with accompanying agent-channel and SFTP adjustments plus expanded unit/regression coverage.
Changes:
- Add
wolfSSH_CTX_SetChannelReqCb()andwolfSSH_CTX_SetGlobalReqCb()to allow first-pass policy decisions for all channel/global requests. - Add
wolfSSH_{CTX_,}SetAppChannels()and update accept/worker flows to support application-driven channel handling. - Add
wolfSSH_AGENT_ChannelOpen()helper and extend tests (unit.c,regress.c) to validate callback interactions and app-driven semantics.
File summaries
| File | Description |
|---|---|
| wolfssh/ssh.h | Public API additions: generic request callback types, tri-state result enum, and app-driven channel mode API/docs. |
| wolfssh/internal.h | Extend WOLFSSH_CTX/WOLFSSH with generic callback pointers and appChannels flags. |
| wolfssh/agent.h | Add public API for server-side agent channel opening in app-driven mode. |
| src/ssh.c | Implement new setters and modify wolfSSH_accept() to support stopping at authenticated state when app-driven. |
| src/internal.c | Route channel/global requests through new generic callbacks; refactor session request handling. |
| src/wolfsftp.c | Adjust SFTP accept logic to account for app-driven accept state behavior. |
| src/agent.c | Implement wolfSSH_AGENT_ChannelOpen() and refactor agent channel open path. |
| tests/unit.c | Add unit coverage around session-request rejection and app-driven “no default callback acceptance” behavior. |
| tests/regress.c | Add regression tests for generic callbacks, app-driven accept stopping point, and callback bypass rules. |
Review details
- Files reviewed: 9/9 changed files
- Comments generated: 3
- 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 (!DoGlobalRequestAny(ssh, name, globReqId, buf, len, begin, | ||
| wantReply, &ret)) { | ||
| switch (globReqId) { |
| word32 channelId; | ||
| word32 typeSz; | ||
| char type[32]; | ||
| byte wantReply; | ||
| int ret, rej = 0; | ||
| int ret, rej = 0, granted = 0; |
| /* check accept is done, if not call wolfSSH accept. In | ||
| * application-driven mode accept() parks at ACCEPT_SERVER_USERAUTH_SENT | ||
| * and never advances, so that state counts as done here. */ | ||
| if (ssh->acceptState < ACCEPT_CLIENT_SESSION_ESTABLISHED | ||
| && !(ssh->appChannels | ||
| && ssh->acceptState >= ACCEPT_SERVER_USERAUTH_SENT)) { | ||
| byte name[] = "sftp"; |
a7de103 to
d70cae4
Compare
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 on a client session, and is idempotent after, 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 - flush what is left of a queued open on the next call, rather than reporting a success the peer never saw - record ssh->error from the send alone, so neither a poll ahead of the request nor a failed allocation stops accept() continuing
wolfSSH_AGENT_ChannelOpen() answers a poll on a session that is over with WS_FATAL_ERROR and WS_DISCONNECT in ssh->error, the shape every other public sender uses: no channel opened, nothing on the wire, RFC 4253 section 11.1. wolfSSH_accept() gates the open it drives, so the new public entry point is the only way in. - promote SendAfterDisconnect() to WOLFSSH_LOCAL so agent.c uses the same helper as every other public sender - leave an open queued before the disconnect unflushed, the rule wolfSSH_shutdown() applies to all but its own disconnect - keep WS_DISCONNECT in ssh->error at the accept() call site, which used to overwrite it with the status the open returns
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
DoChannelRequest() reads ssh->appChannels when the request arrives, so turning the mode on after accept() established the session still refuses an uncallbacked shell, exec or subsystem request from then on. Only accept()'s stopping point is pinned, by the guard around stopState. - say the flag reaches the requests that follow, and that what it cannot do is move where accept() returns - drive a shell request over the wire in both modes from the late-enable test, pinning the behaviour the header now describes
A shell, exec or subsystem request changes the channel only once the callback accepts it. The session type and command are set for the callback to read and put back if it refuses, and CLIENT_DONE follows acceptance alone, so wolfSSH_accept() no longer reports an established session, or starts SFTP, on a request it answered CHANNEL_FAILURE. - DoChannelRequestSession() carries the three arms, which differed only in the type and the callback consulted - unit.c drives a refused shell, exec and subsystem request through DoChannelRequest() and checks nothing was committed - regress.c runs a server whose shell callback refuses and checks accept() stays at ACCEPT_SERVER_CHANNEL_ACCEPT_SENT Issue: F-8852
wolfSSH_CTX_SetChannelReqCb() and wolfSSH_CTX_SetGlobalReqCb() register a callback consulted first for every channel and global request, with the name and the type-specific part to parse. A tri-state answer grants, refuses, or leaves the request to the callbacks and handling already there, so a policy reaches the types with no hook of their own. - a grant still parses and records what the library needs, so a session request granted here commits the session, and the shell, exec and subsystem callbacks are not consulted - a type the library does not know is answered CHANNEL_SUCCESS on a grant - a granted port-0 tcpip-forward is refused, since only the forward callback can report the port bound, per RFC 4254 7.1 - regress.c covers the answers, the data delivered, and which callbacks each answer leaves out
d70cae4 to
0c27f30
Compare
Stacked on #1235, whose six commits are the first here; review the last
one. wolfSSH_CTX_SetChannelReqCb() and wolfSSH_CTX_SetGlobalReqCb()
register a callback consulted first for every channel and global request,
with the name and the type-specific part to parse.
callbacks and handling already there, so a policy reaches the types
with no hook of their own.
session request commits the session and the shell, exec and subsystem
callbacks are not consulted.
grant; a granted port-0 tcpip-forward is refused, per RFC 4254 7.1.
each answer leaves out.