Skip to content

echoserver: answer session requests in callbacks - #1234

Draft
ejohnstown wants to merge 13 commits into
wolfSSL:masterfrom
ejohnstown:ccb-phase2-4e
Draft

echoserver: answer session requests in callbacks#1234
ejohnstown wants to merge 13 commits into
wolfSSL:masterfrom
ejohnstown:ccb-phase2-4e

Conversation

@ejohnstown

Copy link
Copy Markdown
Contributor

Stacked on #1233, and it also carries #1231's commit verbatim, so only
the last commit is new here. With -A the echoserver drives its own
channels: accept() stops at userauth and the callbacks below start the
shell, SFTP or SCP session. Off by default, and the two are exclusive.

  • wsShellStartCb() forks the pty, so it is registered in both modes and
    claims the channel only once there is a shell behind it.
  • wsExecStartCb() takes an "scp " command as a transfer and any other
    command as an echo session; wsSubsysStartCb() guards a NULL command.
  • ssh_worker() drives the session through shellCtx.appFd, and claims the
    channel itself when no callback did.
  • Resume a subsystem accept that returns a want, waiting on the socket
    between attempts rather than spinning.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Public-header documentation for new APIs/modes is currently inconsistent with the implemented return semantics and supported flows, which can mislead API consumers.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR extends wolfSSH’s server-side channel handling to support an opt-in, application-driven channel mode where wolfSSH_accept() stops after user authentication and the application drives session/channel progress via callbacks and wolfSSH_worker(). It also updates the echoserver example and adds/extends tests to cover the new behavior and retry semantics for non-blocking flows.

Changes:

  • Add application-driven channel mode (wolfSSH_CTX_SetAppChannels(), wolfSSH_SetAppChannels()) and adjust wolfSSH_accept()/channel-request handling accordingly.
  • Expose server-side helpers for app-driven flows (notably wolfSSH_SCP_accept() and wolfSSH_AGENT_ChannelOpen()), and adapt SFTP accept behavior in this mode.
  • Update echoserver to support a -A mode (callbacks start shell/SFTP/SCP sessions) and add regression/unit test coverage.
File summaries
File Description
wolfssh/wolfscp.h Documents new server-side SCP accept API for application-driven transfers.
wolfssh/ssh.h Documents new application-driven channel mode and adds public setters.
wolfssh/internal.h Adds appChannels flag to WOLFSSH_CTX and WOLFSSH structs.
wolfssh/agent.h Declares wolfSSH_AGENT_ChannelOpen() for app-driven agent forwarding channel open.
src/ssh.c Updates wolfSSH_accept() stop-state logic; adds setters for app-channels; refactors agent open to new helper.
src/internal.c Changes default “no callback” behavior for shell/exec/subsystem requests when app-driven mode is enabled.
src/wolfsftp.c Makes wolfSSH_SFTP_accept() treat app-driven accept stop-state as “accept done” for callers.
src/wolfscp.c Implements wolfSSH_SCP_accept() wrapper over SCP request handling with WANT propagation.
src/agent.c Implements wolfSSH_AGENT_ChannelOpen() helper for server-side agent channel open.
tests/unit.c Adds unit coverage ensuring no-callback session requests are refused in app-driven mode.
tests/regress.c Adds regression coverage for accept stopping at userauth, callback dispatch, inheritance, and late-enable behavior.
tests/api.c Adjusts keyboard-interactive test client args to avoid shell-callback refusal in echo mode.
examples/echoserver/echoserver.c Adds -A mode and callback-driven session start; updates non-blocking retry loops for SFTP/SCP; refactors shell startup into callback.
Review details
  • Files reviewed: 13/13 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.

Comment thread wolfssh/agent.h Outdated
Comment thread wolfssh/ssh.h Outdated

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fenrir Automated Review — PR #1234

Scan targets checked: wolfssh-bugs, wolfssh-src

Findings: 4
4 finding(s) posted as inline comments (see file-level comments below)

This review was generated automatically by Fenrir. Reported findings require changes before merge.

Comment thread examples/echoserver/echoserver.c Outdated
Comment thread examples/echoserver/echoserver.c
Comment thread examples/echoserver/echoserver.c Outdated
Comment thread src/agent.c
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
- regress.c re-enters a parked accept() with output still queued, which
  is the one path that flushes before reading the state, and pins that
  it leaves the state on the stop
- unit.c checks DoChannelRequest() refuses a shell, exec and subsystem
  request with no callback once the pivot is on
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
In application-driven mode wolfSSH_accept() parks at userauth, so the
sftp test its divert applies never runs. wolfSSH_SFTP_accept() applies
it itself: the session channel must be a subsystem the application's
callback granted sftp on, or the call returns WS_INVALID_STATE_E and
leaves the wire alone without recording an error.

- gate the app-channels branch on wolfSSH_GetSessionType() and
  wolfSSH_GetSessionCommand(), the same test accept() makes
- ask for that grant in every accept state: below the user-auth stop
  accept() returns with no channel open, and past the stop there is no
  accept() left that could have checked anything
- say in ssh.h that the mode serves SFTP through that grant and never
  reaches the SCP entry point
- regress.c refuses the call with no channel, ahead of accept(), on a
  granted shell and on an established one, and serves an INIT on a
  granted sftp subsystem
wolfSSH_SFTP_accept() serves an application-driven session only on a
channel whose subsystem request was answered CHANNEL_SUCCESS.
DoChannelRequest() records the session type and command before it
decides, and leaves both set on a refusal, so they cannot say by
themselves whether anything was granted.

- add channel->sessionGranted, set from the answer a shell, exec or
  subsystem request gets rather than from the request arriving
- look the channel up again before recording it: a callback may close
  its own channel, and wolfSSH_ChannelFree() frees it
- log a request's strings where they are known good: once the parse
  has succeeded, and ahead of a callback that may free the channel
- gate the app-channels path on that flag alongside the session type
  and the command
- cover a refusal from both sides, no callback registered and a
  callback that rejects, and a callback that frees its channel
The built-in SFTP server takes a session only when the subsystem name
is sftp, matched whole. DoChannelRequest() keeps the parsed length in
channel->commandSz, so neither wolfSSH_SFTP_accept()'s grant gate nor
wolfSSH_accept()'s divert serves "sftpx" or "sftp\0evil".

- cover a granted name longer than sftp, one of its length, and one
  running past an embedded NUL
- cover the divert with those three names and a control that diverts
- exec keeps its command length too

Issue: F-11665
CheckSftpAcceptRefusesUngranted() drives the same refusal two ways, a
registered subsystem callback saying no and app channels standing in
for a missing one, and asserted nothing that told them apart. Assert
the call count each case expects.
An application vetting an exec or subsystem request in its channel
request callback is handed the command as a C string, which stops at an
embedded NUL. wolfSSH_ChannelGetSessionCommandSz() and
wolfSSH_GetSessionCommandSz() report the parsed wire length, so a
callback can match a name whole the way DoChannelRequest() does.

- both accessors report 0 for a NULL channel or session
- wolfSSH_GetSessionCommand() defers to the channel accessor
- correct the trace name in wolfSSH_ChannelGetSessionCommand()
- cover a callback seeing "sftp\0evil" through exec and subsystem
DoChannelRequest() records the session type and asks the exec and
subsystem callbacks whether to grant a session only when the command
string parsed. A failed parse is refused on ret alone, and
channel->command still holds an earlier request's value rather than the
one being answered.

- cover a command length header running past the end of the packet, on
  exec and on subsystem

Issue: F-11674
ChannelDelete() wipes the peer's exec or subsystem command line before
releasing it, the way it already wipes the decrypted inputBuffer just
above. A command line can carry a password or a token among its
arguments.

- cover the wipe with the retain-on-free allocator
- release the test's hand-built channel on a setup failure

Issue: F-8850
DoChannelRequest() returns early when the header parse fails, so the
ret == WS_SUCCESS test that followed it could never be false. The
channel lookup moves into the else, which is the only way the function
reaches it.

Issue: F-11657
With -A the echoserver drives its own channels: accept() returns at
userauth and the callbacks below start the shell, SFTP or SCP session.
Off by default. The two modes are exclusive, since the callbacks answer
the session requests the accept state machine otherwise answers itself.

- wsShellStartCb() forks the pty, so it is registered in either mode,
  and claims the channel only once there is a shell behind it; a second
  request is refused rather than forking over the running shell
- wsExecStartCb() takes an "scp " command as a transfer and any other
  command as a session, and is registered in either mode, since the
  legacy path has always started a shell for an exec request too
- wsSubsysStartCb() is registered only with -A, as accept() serves sftp
  itself, and guards a NULL command, which a truncated request leaves
  behind
- ssh_worker() drives the session through shellCtx.appFd, claims the
  channel itself when no callback did, and leaves an SFTP or SCP
  handoff through its cleanup so the pty master still closes
- open the agent channel from the select loop, since the peer's
  auth-agent-req lands after accept() has returned, and read the
  listener from the context each pass because it appears mid-loop
- resume a subsystem accept that returns a want, waiting on the socket
  between attempts rather than spinning
- close the accepted socket again, clear fwdFd on EOF or reset, and
  stay in the loop on WS_REKEYING, which the read arm already handles
- key ChildRunning's sig_atomic_t on WOLFSSH_SHELL, the only build
  with the SIGCHLD handler that writes it, so a target whose libc has
  no signal.h still compiles
- ask for echo mode in the keyboard-interactive test, which has no
  account on the host for the shell callback to fork a shell for
The echoserver's -A mode runs an accepted scp command through
wolfSSH_SCP_accept(). Reaching that call's want retry path takes a
non-blocking server, which -N supplies.

- copy to and from an app-driven server in scp.test
- check the entry point's null-session argument
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants