From 96e3b80901ad78a086771d9ec0115a3ec6aef683 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Wed, 2 Sep 2026 09:40:30 -0700 Subject: [PATCH 1/5] agent: let the application open the agent channel 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 --- src/agent.c | 65 +++++++++++++++++++++++++++++++++++++++++++++++++ src/ssh.c | 42 +------------------------------- wolfssh/agent.h | 8 ++++++ 3 files changed, 74 insertions(+), 41 deletions(-) diff --git a/src/agent.c b/src/agent.c index 33c0eb936..abfbf63e3 100644 --- a/src/agent.c +++ b/src/agent.c @@ -1731,6 +1731,71 @@ int wolfSSH_AGENT_enable(WOLFSSH* ssh, byte isEnabled) } +int wolfSSH_AGENT_ChannelOpen(WOLFSSH* ssh) +{ + WOLFSSH_AGENT_CTX* newAgent = NULL; + WOLFSSH_CHANNEL* newChannel = NULL; + int ret = WS_SUCCESS; + + WLOG_ENTER(); + + if (ssh == NULL) + ret = WS_SSH_NULL_E; + else if (!ssh->useAgent) { + /* Nothing asked for agent forwarding on this session. */ + ret = WS_BAD_ARGUMENT; + } + else if (ssh->agent == NULL) { + /* Server side sets ssh->agent here and nowhere else, so a NULL one + * is the "not opened yet" test. Idempotent so a caller polling for + * the peer's request cannot end up with two agent channels. */ + WLOG(WS_LOG_AGENT, "Starting agent channel"); + + newAgent = wolfSSH_AGENT_new(ssh->ctx->heap); + if (newAgent == NULL) + ret = WS_MEMORY_E; + + if (ret == WS_SUCCESS) { + newChannel = ChannelNew(ssh, ID_CHANTYPE_AUTH_AGENT, + ssh->ctx->windowSz, ssh->ctx->maxPacketSz); + if (newChannel == NULL) + ret = WS_MEMORY_E; + } + + if (ret == WS_SUCCESS) { + ret = SendChannelOpenSession(ssh, newChannel); + + if (ret < WS_SUCCESS + && ret != WS_WANT_WRITE && ret != WS_WANT_READ) { + ChannelDelete(newChannel, ssh->ctx->heap); + } + else { + /* Publish the agent even when the open is only queued, so + * a retry takes the already-open path above rather than + * opening a second channel. */ + ChannelAppend(ssh, newChannel); + newAgent->channel = newChannel->channel; + ssh->agent = newAgent; + newAgent = NULL; + if (ssh->ctx->agentCb) { + ssh->ctx->agentCb(WOLFSSH_AGENT_LOCAL_SETUP, + ssh->agentCbCtx); + } + } + } + + if (newAgent != NULL) + wolfSSH_AGENT_free(newAgent); + } + + if (ssh != NULL) + ssh->error = ret; + + WLOG_LEAVE(ret); + return ret; +} + + int wolfSSH_AGENT_worker(WOLFSSH* ssh) { int ret = WS_SUCCESS; diff --git a/src/ssh.c b/src/ssh.c index ae7d0b2a0..d82868fbc 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -764,52 +764,12 @@ int wolfSSH_accept(WOLFSSH* ssh) #endif /* WOLFSSH_SFTP and !NO_WOLFSSH_SERVER */ #ifdef WOLFSSH_AGENT if (ssh->useAgent) { - WOLFSSH_AGENT_CTX* newAgent; - WOLFSSH_CHANNEL* newChannel; - - WLOG(WS_LOG_AGENT, "Starting agent channel"); - - newAgent = wolfSSH_AGENT_new(ssh->ctx->heap); - if (newAgent == NULL) { - ssh->error = WS_MEMORY_E; - WLOG(WS_LOG_DEBUG, acceptError, - "SERVER_USERAUTH_ACCEPT_DONE", ssh->error); - return WS_ERROR; - } - - newChannel = ChannelNew(ssh, ID_CHANTYPE_AUTH_AGENT, - ssh->ctx->windowSz, ssh->ctx->maxPacketSz); - if (newChannel == NULL) { - wolfSSH_AGENT_free(newAgent); - ssh->error = WS_MEMORY_E; - WLOG(WS_LOG_DEBUG, acceptError, - "SERVER_USERAUTH_ACCEPT_DONE", ssh->error); - return WS_FATAL_ERROR; - } - - ssh->error = SendChannelOpenSession(ssh, newChannel); + ssh->error = wolfSSH_AGENT_ChannelOpen(ssh); if (ssh->error < WS_SUCCESS) { - if (ssh->error == WS_WANT_WRITE || - ssh->error == WS_WANT_READ) { - ChannelAppend(ssh, newChannel); - } - else { - ChannelDelete(newChannel, ssh->ctx->heap); - wolfSSH_AGENT_free(newAgent); - } WLOG(WS_LOG_DEBUG, acceptError, "SERVER_USERAUTH_ACCEPT_DONE", ssh->error); return WS_FATAL_ERROR; } - ChannelAppend(ssh, newChannel); - newAgent->channel = newChannel->channel; - if (ssh->ctx->agentCb) { - ssh->ctx->agentCb(WOLFSSH_AGENT_LOCAL_SETUP, - ssh->agentCbCtx); - } - if (ssh->agent != NULL) - wolfSSH_AGENT_free(ssh->agent); - ssh->agent = newAgent; } #endif /* WOLFSSH_AGENT */ ssh->acceptState = ACCEPT_CLIENT_SESSION_ESTABLISHED; diff --git a/wolfssh/agent.h b/wolfssh/agent.h index 581e3eba9..53e00ae60 100644 --- a/wolfssh/agent.h +++ b/wolfssh/agent.h @@ -181,6 +181,14 @@ WOLFSSH_API int wolfSSH_CTX_set_agent_cb(WOLFSSH_CTX* ctx, WOLFSSH_API int wolfSSH_set_agent_cb_ctx(WOLFSSH* ssh, void* ctx); WOLFSSH_API int wolfSSH_CTX_AGENT_enable(WOLFSSH_CTX* ctx, byte isEnabled); WOLFSSH_API int wolfSSH_AGENT_enable(WOLFSSH* ssh, byte isEnabled); +/* Server side. Opens the auth-agent@openssh.com channel back to the client + * once an auth-agent-req@openssh.com request has set the session up for it. + * wolfSSH_accept() does this itself on the default path; an application that + * drives its own channels returns from accept() before that point and calls + * this instead. Idempotent, so it is safe to poll while waiting for the + * peer's request. Returns WS_SUCCESS, or WS_BAD_ARGUMENT when the session + * never asked for agent forwarding. */ +WOLFSSH_API int wolfSSH_AGENT_ChannelOpen(WOLFSSH* ssh); WOLFSSH_LOCAL int wolfSSH_AGENT_worker(WOLFSSH* ssh); WOLFSSH_API int wolfSSH_AGENT_Relay(WOLFSSH* ssh, const byte* msg, word32* msgSz, byte* rsp, word32* rspSz); From 66057f719ed8e70add3d21f9a00a19f5077f6962 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Wed, 2 Sep 2026 09:47:15 -0700 Subject: [PATCH 2/5] ssh: add opt-in application-driven channels 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 --- src/internal.c | 12 ++++++++++- src/ssh.c | 54 +++++++++++++++++++++++++++++++++++++++++++--- src/wolfsftp.c | 8 +++++-- wolfssh/internal.h | 2 ++ wolfssh/ssh.h | 23 ++++++++++++++++++++ 5 files changed, 93 insertions(+), 6 deletions(-) diff --git a/src/internal.c b/src/internal.c index 10769aef4..e42cf3814 100644 --- a/src/internal.c +++ b/src/internal.c @@ -1666,6 +1666,7 @@ WOLFSSH* SshInit(WOLFSSH* ssh, WOLFSSH_CTX* ctx) ssh->highwaterMark = ctx->highwaterMark; ssh->msgHighwaterMark = ctx->msgHighwaterMark; ssh->maxAuthAttempts = ctx->maxAuthAttempts; + ssh->appChannels = ctx->appChannels; ssh->highwaterCtx = (void*)ssh; ssh->reqSuccessCtx = (void*)ssh; ssh->fs = NULL; @@ -12675,6 +12676,9 @@ static int DoChannelRequest(WOLFSSH* ssh, if (ssh->ctx->channelReqShellCb) { rej = ssh->ctx->channelReqShellCb(channel, ssh->channelReqCtx); } + else { + rej = ssh->appChannels; + } ssh->clientState = CLIENT_DONE; } else if (ChannelRequestIs(type, typeSz, "exec")) { @@ -12684,6 +12688,9 @@ static int DoChannelRequest(WOLFSSH* ssh, if (ssh->ctx->channelReqExecCb) { rej = ssh->ctx->channelReqExecCb(channel, ssh->channelReqCtx); } + else { + rej = ssh->appChannels; + } ssh->clientState = CLIENT_DONE; WLOG(WS_LOG_DEBUG, " command = %s", channel->command); @@ -12695,6 +12702,9 @@ static int DoChannelRequest(WOLFSSH* ssh, if (ssh->ctx->channelReqSubsysCb) { rej = ssh->ctx->channelReqSubsysCb(channel, ssh->channelReqCtx); } + else { + rej = ssh->appChannels; + } ssh->clientState = CLIENT_DONE; WLOG(WS_LOG_DEBUG, " subsystem = %s", channel->command); @@ -12836,7 +12846,7 @@ static int DoChannelRequest(WOLFSSH* ssh, int replyRet; if (rej) { - WLOG(WS_LOG_DEBUG, "Callback rejecting channel request."); + WLOG(WS_LOG_DEBUG, "Rejecting channel request."); } replyRet = SendChannelSuccess(ssh, channelId, (ret == WS_SUCCESS && !rej)); diff --git a/src/ssh.c b/src/ssh.c index d82868fbc..4a26809fc 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -579,6 +579,8 @@ const char acceptState[] = "accept state: %s"; int wolfSSH_accept(WOLFSSH* ssh) { + byte stopState; + WLOG(WS_LOG_DEBUG, "Entering wolfSSH_accept()"); if (ssh == NULL) @@ -598,6 +600,15 @@ int wolfSSH_accept(WOLFSSH* ssh) return WS_INVALID_STATE_E; } + /* In application-driven mode the state machine stops as soon as the + * user is authenticated; everything past that is the application's. + * Only stop there if the session has not already gone by: the loop + * below tests the stop state exactly, so a state it has stepped over + * would never terminate it. */ + stopState = (ssh->appChannels + && ssh->acceptState <= ACCEPT_SERVER_USERAUTH_SENT) ? + ACCEPT_SERVER_USERAUTH_SENT : ACCEPT_CLIENT_SESSION_ESTABLISHED; + /* check if data pending to be sent */ if (ssh->outputBuffer.length > 0 && ssh->acceptState < ACCEPT_CLIENT_SESSION_ESTABLISHED) { @@ -609,7 +620,11 @@ int wolfSSH_accept(WOLFSSH* ssh) ssh->acceptState != ACCEPT_SERVER_USERAUTH_ACCEPT_SENT && ssh->acceptState != ACCEPT_SERVER_KEXINIT_SENT && ssh->acceptState != ACCEPT_KEYED && - ssh->acceptState != ACCEPT_SERVER_CHANNEL_ACCEPT_SENT) { + ssh->acceptState != ACCEPT_SERVER_CHANNEL_ACCEPT_SENT && + /* Never step over where this call is meant to stop. The + * loop below tests for that state exactly, and the SCP and + * SFTP re-entry states sort after it. */ + ssh->acceptState != stopState) { WLOG(WS_LOG_DEBUG, "Advancing accept state"); ssh->acceptState++; } @@ -631,7 +646,7 @@ int wolfSSH_accept(WOLFSSH* ssh) } } - while (ssh->acceptState != ACCEPT_CLIENT_SESSION_ESTABLISHED) { + while (ssh->acceptState != stopState) { switch (ssh->acceptState) { case ACCEPT_BEGIN: @@ -721,6 +736,12 @@ int wolfSSH_accept(WOLFSSH* ssh) } ssh->acceptState = ACCEPT_SERVER_USERAUTH_SENT; WLOG(WS_LOG_DEBUG, acceptState, "SERVER_USERAUTH_SENT"); + if (stopState == ACCEPT_SERVER_USERAUTH_SENT) { + /* The application takes it from here. Tested through + * stopState so a callback that changed the flag during + * this call cannot half-apply it. */ + break; + } FALL_THROUGH; case ACCEPT_SERVER_USERAUTH_SENT: @@ -3897,7 +3918,8 @@ WOLFSSH_CHANNEL* wolfSSH_ChannelFwdNewRemote(WOLFSSH* ssh, if (newChannel != NULL) ChannelAppend(ssh, newChannel); - WLOG(WS_LOG_DEBUG, "Leaving wolfSSH_ChannelFwdNewRemote(), newChannel = %p, ret = %d", + WLOG(WS_LOG_DEBUG, + "Leaving wolfSSH_ChannelFwdNewRemote(), newChannel = %p, ret = %d", newChannel, ret); return newChannel; } @@ -4891,6 +4913,32 @@ int wolfSSH_CTX_SetChannelReqSubsysCb(WOLFSSH_CTX* ctx, } +int wolfSSH_CTX_SetAppChannels(WOLFSSH_CTX* ctx, byte enable) +{ + int ret = WS_SSH_CTX_NULL_E; + + if (ctx != NULL) { + ctx->appChannels = (enable != 0); + ret = WS_SUCCESS; + } + + return ret; +} + + +int wolfSSH_SetAppChannels(WOLFSSH* ssh, byte enable) +{ + int ret = WS_SSH_NULL_E; + + if (ssh != NULL) { + ssh->appChannels = (enable != 0); + ret = WS_SUCCESS; + } + + return ret; +} + + int wolfSSH_SetChannelOpenCtx(WOLFSSH* ssh, void* ctx) { int ret = WS_SSH_NULL_E; diff --git a/src/wolfsftp.c b/src/wolfsftp.c index 88cca98f8..1b7d93cf1 100644 --- a/src/wolfsftp.c +++ b/src/wolfsftp.c @@ -1383,8 +1383,12 @@ int wolfSSH_SFTP_accept(WOLFSSH* ssh) if (ssh->error == WS_WANT_READ || ssh->error == WS_WANT_WRITE) ssh->error = WS_SUCCESS; - /* check accept is done, if not call wolfSSH accept */ - if (ssh->acceptState < ACCEPT_CLIENT_SESSION_ESTABLISHED) { + /* 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"; WLOG(WS_LOG_SFTP, "Trying to do SSH accept first"); diff --git a/wolfssh/internal.h b/wolfssh/internal.h index 588e0c867..468e81dee 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -867,6 +867,7 @@ struct WOLFSSH_CTX { word32 maxAuthAttempts; /* server cap on failed userauth */ byte side; /* client or server */ byte showBanner; + byte appChannels; /* app drives channels, see ssh.h */ #ifdef WOLFSSH_AGENT byte agentEnabled; #endif /* WOLFSSH_AGENT */ @@ -1136,6 +1137,7 @@ struct WOLFSSH { byte serverState; byte processReplyState; byte isKeying; + byte appChannels; /* app drives channels, see ssh.h */ byte authId; /* if using public key or password */ byte supportedAuth[4]; /* supported auth IDs public key , password */ diff --git a/wolfssh/ssh.h b/wolfssh/ssh.h index 545ef9e3c..8a943bc22 100644 --- a/wolfssh/ssh.h +++ b/wolfssh/ssh.h @@ -452,6 +452,29 @@ WOLFSSH_API int wolfSSH_CTX_SetChannelReqSubsysCb(WOLFSSH_CTX* ctx, WOLFSSH_API int wolfSSH_SetChannelReqCtx(WOLFSSH* ssh, void* ctx); WOLFSSH_API void* wolfSSH_GetChannelReqCtx(WOLFSSH* ssh); +/* Application-driven channel handling, server side, off by default. + * + * Off, wolfSSH_accept() runs the session state machine through to an + * established session with the first channel open, as it always has, and a + * shell, exec, or subsystem request with no callback registered for it is + * accepted. + * + * On, wolfSSH_accept() returns WS_SUCCESS as soon as the user has + * authenticated, and the application owns every channel from there, driving + * the session with wolfSSH_worker() and the callbacks above. A shell, exec, + * or subsystem request with no callback registered is then rejected: with + * accept() already returned, nothing is left to service it. + * + * Set it on the context before wolfSSH_new(), or on a session before the + * first wolfSSH_accept() call. Turning it on once accept() has established + * the session has no effect on that session. + * + * The mode drives the session channels itself, so it does not combine with + * the built-in wolfSSH_SFTP_accept() and WS_SCP_INIT entry points; an + * application using those leaves this off. */ +WOLFSSH_API int wolfSSH_CTX_SetAppChannels(WOLFSSH_CTX* ctx, byte enable); +WOLFSSH_API int wolfSSH_SetAppChannels(WOLFSSH* ssh, byte enable); + typedef int (*WS_CallbackChannelEof)(WOLFSSH_CHANNEL* channel, void* ctx); WOLFSSH_API int wolfSSH_CTX_SetChannelEofCb(WOLFSSH_CTX* ctx, WS_CallbackChannelEof cb); From 8121dac4594ed221aef5f82d9c4a425c222462c1 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Wed, 2 Sep 2026 09:49:31 -0700 Subject: [PATCH 3/5] tests: cover application-driven channels 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 --- tests/regress.c | 178 ++++++++++++++++++++++++++++++++++++++++++++++++ tests/unit.c | 62 +++++++++++++++++ 2 files changed, 240 insertions(+) diff --git a/tests/regress.c b/tests/regress.c index eca4fd729..868069561 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -1472,6 +1472,180 @@ static void AssertHandshakeRejectsMutatedReply(const char* keyAlgo, } #ifndef WOLFSSH_NO_RSA_SHA2_256 +/* Counts the shell requests the application-driven server answered. */ +static int appChannelsShellReqCount; + +static int AppChannelsShellCb(WOLFSSH_CHANNEL* channel, void* ctx) +{ + (void)channel; + (void)ctx; + appChannelsShellReqCount++; + return 0; +} + +/* Drive an application-driven server: wolfSSH_accept() is expected to return + * at userauth, so the channel open and the shell request are answered by + * wolfSSH_worker() calls the application makes itself. */ +static void RunAppChannelsHandshake(KexReplyHarness* harness, + KexReplyRunResult* result) +{ + word32 step; + + WMEMSET(result, 0, sizeof(*result)); + result->clientRet = WS_FATAL_ERROR; + result->serverRet = WS_FATAL_ERROR; + + for (step = 0; step < REGRESS_MAX_HANDSHAKE_STEPS; step++) { + if (!result->clientSuccess) { + result->clientRet = wolfSSH_connect(harness->client); + result->clientErr = wolfSSH_get_error(harness->client); + if (result->clientRet == WS_SUCCESS) { + result->clientSuccess = 1; + } + else if (!IsHandshakeRetryable(result->clientErr)) { + result->steps = step + 1; + return; + } + } + + if (!result->serverSuccess) { + result->serverRet = wolfSSH_accept(harness->server); + result->serverErr = wolfSSH_get_error(harness->server); + if (result->serverRet == WS_SUCCESS) { + result->serverSuccess = 1; + } + else if (!IsHandshakeRetryable(result->serverErr)) { + result->steps = step + 1; + return; + } + } + else if (harness->server->clientState < CLIENT_DONE) { + result->serverRet = wolfSSH_worker(harness->server, NULL); + result->serverErr = wolfSSH_get_error(harness->server); + if (result->serverRet < WS_SUCCESS + && result->serverErr != WS_CHAN_RXD + && !IsHandshakeRetryable(result->serverErr)) { + result->steps = step + 1; + return; + } + } + + if (result->clientSuccess && result->serverSuccess + && harness->server->clientState >= CLIENT_DONE) { + result->steps = step + 1; + return; + } + } + + result->steps = REGRESS_MAX_HANDSHAKE_STEPS; +} + +/* With wolfSSH_SetAppChannels() on, accept() stops once the user is + * authenticated and the shell request lands on the callback instead. */ +static void TestAppChannelsAcceptStopsAtUserAuth(void) +{ + KexReplyHarness harness; + KexReplyRunResult result; + + appChannelsShellReqCount = 0; + + InitKexReplyHarness(&harness, "rsa-sha2-256", REGRESS_SERVER_KEY_PATH, + 0, NULL); + AssertIntEQ(wolfSSH_CTX_SetChannelReqShellCb(harness.serverCtx, + AppChannelsShellCb), WS_SUCCESS); + AssertIntEQ(wolfSSH_SetAppChannels(harness.server, 1), WS_SUCCESS); + + RunAppChannelsHandshake(&harness, &result); + + AssertTrue(result.clientSuccess); + AssertTrue(result.serverSuccess); + AssertIntEQ(harness.server->acceptState, ACCEPT_SERVER_USERAUTH_SENT); + AssertIntEQ(harness.server->clientState, CLIENT_DONE); + AssertIntEQ(appChannelsShellReqCount, 1); + AssertIntEQ(harness.client->connectState, + CONNECT_SERVER_CHANNEL_REQUEST_DONE); + AssertFalse(harness.clientIo.sawDisconnect); + AssertFalse(harness.serverIo.sawDisconnect); + + FreeKexReplyHarness(&harness); +} + +/* Same mode, no callback registered: nothing can start the shell once + * accept() has returned, so the request is refused. The default mode + * accepts it, which AssertHandshakeSucceeds() covers. */ +static void TestAppChannelsNoShellCbRejects(void) +{ + KexReplyHarness harness; + KexReplyRunResult result; + + InitKexReplyHarness(&harness, "rsa-sha2-256", REGRESS_SERVER_KEY_PATH, + 0, NULL); + AssertIntEQ(wolfSSH_SetAppChannels(harness.server, 1), WS_SUCCESS); + + RunAppChannelsHandshake(&harness, &result); + + AssertFalse(result.clientSuccess); + AssertTrue(harness.client->connectState < + CONNECT_SERVER_CHANNEL_REQUEST_DONE); + AssertIntEQ(harness.server->acceptState, ACCEPT_SERVER_USERAUTH_SENT); + + FreeKexReplyHarness(&harness); +} + +/* The flag is documented as a context setting first, so pin the setter + * returns and the inheritance wolfSSH_new() does. */ +static void TestAppChannelsCtxInherits(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + + AssertIntEQ(wolfSSH_CTX_SetAppChannels(NULL, 1), WS_SSH_CTX_NULL_E); + AssertIntEQ(wolfSSH_SetAppChannels(NULL, 1), WS_SSH_NULL_E); + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + AssertNotNull(ctx); + + ssh = wolfSSH_new(ctx); + AssertNotNull(ssh); + AssertIntEQ(ssh->appChannels, 0); + wolfSSH_free(ssh); + + AssertIntEQ(wolfSSH_CTX_SetAppChannels(ctx, 1), WS_SUCCESS); + + ssh = wolfSSH_new(ctx); + AssertNotNull(ssh); + AssertIntEQ(ssh->appChannels, 1); + AssertIntEQ(wolfSSH_SetAppChannels(ssh, 0), WS_SUCCESS); + AssertIntEQ(ssh->appChannels, 0); + wolfSSH_free(ssh); + + wolfSSH_CTX_free(ctx); +} + +/* Turning the mode on after accept() established the session must not leave + * the accept loop hunting for a state it has already stepped past. */ +static void TestAppChannelsLateEnableReturns(void) +{ + KexReplyHarness harness; + KexReplyRunResult result; + + InitKexReplyHarness(&harness, "rsa-sha2-256", REGRESS_SERVER_KEY_PATH, + 0, NULL); + + RunKexReplyHandshake(&harness, &result); + + AssertTrue(result.serverSuccess); + AssertIntEQ(harness.server->acceptState, + ACCEPT_CLIENT_SESSION_ESTABLISHED); + + AssertIntEQ(wolfSSH_SetAppChannels(harness.server, 1), WS_SUCCESS); + AssertIntEQ(wolfSSH_accept(harness.server), WS_SUCCESS); + AssertIntEQ(harness.server->acceptState, + ACCEPT_CLIENT_SESSION_ESTABLISHED); + + FreeKexReplyHarness(&harness); +} + static void TestKexDhReplyRejectsRsaSha2_256SigNameDowngrade(void) { AssertHandshakeSucceeds("rsa-sha2-256", REGRESS_SERVER_KEY_PATH); @@ -12302,6 +12476,10 @@ int main(int argc, char** argv) #ifdef KEXDH_REPLY_REGRESS_KEX_ALGO #ifndef WOLFSSH_NO_RSA_SHA2_256 + TestAppChannelsCtxInherits(); + TestAppChannelsAcceptStopsAtUserAuth(); + TestAppChannelsNoShellCbRejects(); + TestAppChannelsLateEnableReturns(); TestKexDhReplyRejectsRsaSha2_256SigNameDowngrade(); #endif #ifndef WOLFSSH_NO_RSA_SHA2_512 diff --git a/tests/unit.c b/tests/unit.c index 1007afcae..a136ac166 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -9245,6 +9245,68 @@ static int test_DoChannelRequest(void) } #endif /* WOLFSSH_SHELL && WOLFSSH_TERM */ + /* Application-driven channels flip the no-callback default: with + * accept() already returned there is nothing left to start a shell, + * exec or subsystem, so all three are refused rather than accepted. */ + { + static const byte paySubsys[] = { + 0x00,0x00,0x00,0x00, /* channelId = 0 */ + 0x00,0x00,0x00,0x09, /* typeSz = 9 */ + 0x73,0x75,0x62,0x73,0x79,0x73, + 0x74,0x65,0x6D, /* "subsystem" */ + 0x01, /* wantReply = 1 */ + 0x00,0x00,0x00,0x04, /* nameSz = 4 */ + 0x73,0x66,0x74,0x70 /* "sftp" */ + }; + struct { + const char* label; + const byte* payload; + word32 payloadSz; + int errBase; + } appCases[] = { + { "shell", payShell, (word32)sizeof(payShell), -495 }, + { "exec", payExec, (word32)sizeof(payExec), -497 }, + { "subsystem", paySubsys, (word32)sizeof(paySubsys), -499 } + }; + int a; + + for (a = 0; a < (int)(sizeof(appCases) / sizeof(appCases[0])); a++) { + word32 idxApp = 0; + int retApp, capMsgId; + + if (wolfSSH_SetAppChannels(ssh, 1) != WS_SUCCESS) { + printf("DoChannelRequest[app-%s]: set failed\n", + appCases[a].label); + result = appCases[a].errBase; + goto done; + } + + s_chanReqCaptureSz = 0; + WMEMSET(s_chanReqCapture, 0, sizeof(s_chanReqCapture)); + + retApp = wolfSSH_TestDoChannelRequest(ssh, + (byte*)appCases[a].payload, appCases[a].payloadSz, + &idxApp); + wolfSSH_SetAppChannels(ssh, 0); + + if (retApp != WS_SUCCESS) { + printf("DoChannelRequest[app-%s]: ret=%d, expected=%d\n", + appCases[a].label, retApp, WS_SUCCESS); + result = appCases[a].errBase; + goto done; + } + + capMsgId = CaptureMsgId(s_chanReqCapture, s_chanReqCaptureSz); + if (capMsgId != (int)MSGID_CHANNEL_FAILURE) { + printf("DoChannelRequest[app-%s]: msg_id=0x%02x, " + "expected=0x%02x\n", appCases[a].label, capMsgId, + MSGID_CHANNEL_FAILURE); + result = appCases[a].errBase - 1; + goto done; + } + } + } + done: wolfSSH_free(ssh); wolfSSH_CTX_free(ctx); From aff373a5bf6671143c078b0c5561b00b9d7d35e1 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Wed, 2 Sep 2026 09:42:42 -0700 Subject: [PATCH 4/5] SCP: let the application start the transfer An application that binds an "scp ..." command to a channel itself has no way to run the transfer; wolfSSH_accept() did it through a WS_SCP_INIT re-entry only that state machine can drive. - add wolfSSH_SCP_accept(), a wrapper over DoScpRequest() reporting WS_SCP_COMPLETE for any non-negative result, as accept() does - a receive-side want reaches the wrapper as a generic error with the want in ssh->error, so report the want itself and let the caller retry - state that resume contract beside the prototype, and clear a stale want on entry the way the other re-entrant entry points do --- src/wolfscp.c | 34 ++++++++++++++++++++++++++++++++++ wolfssh/wolfscp.h | 11 +++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/wolfscp.c b/src/wolfscp.c index 184d057cf..e5d95a3ea 100644 --- a/src/wolfscp.c +++ b/src/wolfscp.c @@ -883,6 +883,40 @@ int DoScpSource(WOLFSSH* ssh) return ret; } +/* Contract is in wolfssh/wolfscp.h. */ +int wolfSSH_SCP_accept(WOLFSSH* ssh) +{ + int ret; + + if (ssh == NULL) + return WS_BAD_ARGUMENT; + + /* Clear a want left by the previous call so the retry starts clean, + * the way the other re-entrant entry points do. */ + if (ssh->error == WS_WANT_READ || ssh->error == WS_WANT_WRITE) + ssh->error = WS_SUCCESS; + + ret = DoScpRequest(ssh); + + if (ret >= WS_SUCCESS) { + /* The tail of DoScpRequest() passes a read count through, so treat + * anything non-negative as done the way wolfSSH_accept() does. */ + ret = WS_SCP_COMPLETE; + } + else { + /* A non-blocking want on a read path surfaces as a generic error + * with the want recorded in ssh->error (see GetInputData), so + * report it as the want the caller is told to retry on. */ + int err = wolfSSH_get_error(ssh); + + if (err == WS_WANT_READ || err == WS_WANT_WRITE) + ret = err; + } + + return ret; +} + + int DoScpRequest(WOLFSSH* ssh) { int ret = WS_SUCCESS; diff --git a/wolfssh/wolfscp.h b/wolfssh/wolfscp.h index 32e9dc7de..1ebeb339f 100644 --- a/wolfssh/wolfscp.h +++ b/wolfssh/wolfscp.h @@ -158,6 +158,17 @@ WOLFSSH_API int wolfSSH_SCP_to(WOLFSSH* ssh, const char* src, const char* dst); WOLFSSH_API int wolfSSH_SCP_from(WOLFSSH* ssh, const char* src, const char* dst); +/* Server side. Drives an SCP transfer on a channel whose "exec scp ..." + * command is already bound. This is the same work wolfSSH_accept() does + * through its WS_SCP_INIT re-entry, exposed so an application can start the + * transfer itself; use one or the other, not both. Call it once + * wolfSSH_accept() has returned and the exec channel-request callback has + * reported an SCP command, not from inside that callback. + * + * Returns WS_SCP_COMPLETE when the transfer is done. On a non-blocking + * socket it returns WS_WANT_READ or WS_WANT_WRITE with the transfer part + * done; call it again on the same session until it completes. */ +WOLFSSH_API int wolfSSH_SCP_accept(WOLFSSH* ssh); #ifdef __cplusplus From 78bc4dbb81a98d56f34a25577bb3b7c183a23e22 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Wed, 2 Sep 2026 09:54:34 -0700 Subject: [PATCH 5/5] echoserver: answer session requests in callbacks 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, so the path this example has always taken stays the one an unflagged run demonstrates. The two are exclusive, since the callbacks answer the requests the accept state machine otherwise answers itself. - 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, which a truncated request leaves behind - 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 - close the accepted socket again, and clear fwdFd on EOF or reset --- examples/echoserver/echoserver.c | 510 +++++++++++++++++++++---------- tests/api.c | 3 + 2 files changed, 360 insertions(+), 153 deletions(-) diff --git a/examples/echoserver/echoserver.c b/examples/echoserver/echoserver.c index 00320b7c8..2e7145df8 100644 --- a/examples/echoserver/echoserver.c +++ b/examples/echoserver/echoserver.c @@ -71,6 +71,10 @@ #include #endif +/* ChildRunning is volatile sig_atomic_t whether or not a shell is compiled + * in, and ssh_worker() reads it unguarded. */ +#include + #ifdef WOLFSSH_SHELL #ifdef HAVE_PTY_H #include @@ -84,7 +88,6 @@ #ifndef USE_WINDOWS_API #include #endif - #include #if defined(__QNX__) || defined(__QNXNTO__) #include #include @@ -126,6 +129,7 @@ static int quit = 0; wolfSSL_Mutex doneLock; #define MAX_PASSWD_RETRY 3 static int passwdRetry = MAX_PASSWD_RETRY; +static volatile sig_atomic_t ChildRunning = 0; #ifndef EXAMPLE_HIGHWATER_MARK @@ -184,7 +188,7 @@ typedef struct WS_FwdCbActionCtx { typedef struct { WOLFSSH* ssh; WS_SOCKET_T fd; - word32 id; + word32 tid; int echo; char nonBlock; #if defined(WOLFSSL_PTHREADS) && defined(WOLFSSL_TEST_GLOBAL_REQ) @@ -199,6 +203,12 @@ typedef struct { WS_FwdCbActionCtx fwdCbCtx; #endif WS_AppCtx shellCtx; +#ifdef WOLFSSH_SFTP + int doSftp; +#endif +#ifdef WOLFSSH_SCP + int doScp; +#endif byte channelBuffer[EXAMPLE_BUFFER_SZ]; /* The EOF drain holds an unsent tail across worker passes, * so it cannot share channelBuffer with the read path. */ @@ -237,7 +247,7 @@ static int dump_stats(thread_ctx_t* ctx) "Statistics for Thread #%u:\r\n" " txCount = %u\r\n rxCount = %u\r\n" " seq = %u\r\n peerSeq = %u\r\n", - ctx->id, txCount, rxCount, seq, peerSeq); + ctx->tid, txCount, rxCount, seq, peerSeq); statsSz = (word32)WSTRLEN(ctx->statsBuffer); fprintf(stderr, "%s", ctx->statsBuffer); @@ -635,8 +645,9 @@ static int wolfSSH_FwdDefaultActions(WS_FwdCbAction action, void* vCtx, else if (action == WOLFSSH_FWD_CHANNEL_ID) { appCtx->channelId = port; } - else + else { ret = WS_FWD_INVALID_ACTION; + } return ret; } @@ -644,6 +655,214 @@ static int wolfSSH_FwdDefaultActions(WS_FwdCbAction action, void* vCtx, #endif /* WOLFSSH_FWD */ +#ifdef WOLFSSH_SHELL +static void ChildSig(int sig) +{ + (void)sig; + ChildRunning = 0; +} + + +#ifdef SHELL_DEBUG +static int termios_show(int fd) +{ + struct termios tios; + int i; + int rc; + + WMEMSET((void *) &tios, 0, sizeof(tios)); + rc = tcgetattr(fd, &tios); + printf("tcgetattr returns=%x\n", rc); + + printf("iflag/oflag/cflag/lflag = %x/%x/%x/%x\n", + (unsigned int)tios.c_iflag, (unsigned int)tios.c_oflag, + (unsigned int)tios.c_cflag, (unsigned int)tios.c_lflag); + printf("c_ispeed/c_ospeed = %x/%x\n", + (unsigned int)tios.c_ispeed, (unsigned int)tios.c_ospeed); + for (i = 0; i < NCCS; i++) { + printf("c_cc[%d] = %hhx\n", i, tios.c_cc[i]); + } + return 0; +} +#endif +#endif /* WOLFSSH_SHELL */ + + +/* Registered in every build, in both modes: with no shell the echoserver + * still has to take the channel to mark it connected, so ssh_worker() will + * echo on it. Returns WS_SUCCESS to accept the request, 1 to reject it. */ +static int wsShellStartCb(WOLFSSH_CHANNEL* channel, void* ctx) +{ + thread_ctx_t* threadCtx = (thread_ctx_t*)ctx; + word32 channelId = 0; + + if (threadCtx == NULL) { + return 1; + } + + /* Our own id: it is what wolfSSH_worker() reports and what the read, + * send, and find calls below take. */ + if (wolfSSH_ChannelGetId(channel, &channelId, WS_CHANNEL_ID_SELF) + != WS_SUCCESS) { + return 1; + } + +#ifdef WOLFSSH_SHELL + /* Echo mode has no shell to start, ssh_worker() echoes the channel data + * back through the SSH stream. */ + if (!threadCtx->echo) { + WOLFSSH* ssh; + const char *userName; + struct passwd *p_passwd; + struct termios tios; + pid_t childPid; + int rc; + + ssh = threadCtx->ssh; + userName = wolfSSH_GetUsername(ssh); + p_passwd = getpwnam((const char *)userName); + if (p_passwd == NULL) { + /* Not actually a user on the system. */ + #ifdef SHELL_DEBUG + fprintf(stderr, "user %s does not exist\n", userName); + #endif + return 1; + } + + childPid = forkpty(&threadCtx->shellCtx.appFd, NULL, NULL, NULL); + + if (childPid < 0) { + /* forkpty failed, so return */ + ChildRunning = 0; + return 1; + } + else if (childPid == 0) { + /* Child process */ + const char *args[] = {"-sh", NULL}; + + signal(SIGINT, SIG_DFL); + + #ifdef SHELL_DEBUG + printf("userName is %s\n", userName); + system("env"); + #endif + + setenv("HOME", p_passwd->pw_dir, 1); + setenv("LOGNAME", p_passwd->pw_name, 1); + rc = chdir(p_passwd->pw_dir); + if (rc != 0) { + /* Never return: the child would run on inside the library + * and write to the parent's socket. */ + _exit(EXIT_FAILURE); + } + + execv("/bin/sh", (char **)args); + _exit(EXIT_FAILURE); + } + #ifdef SHELL_DEBUG + printf("In childPid > 0; getpid=%d\n", (int)getpid()); + #endif + signal(SIGCHLD, ChildSig); + + rc = tcgetattr(threadCtx->shellCtx.appFd, &tios); + if (rc != 0) { + printf("tcgetattr failed: rc =%d,errno=%x\n", rc, errno); + return 1; + } + rc = tcsetattr(threadCtx->shellCtx.appFd, TCSAFLUSH, &tios); + if (rc != 0) { + printf("tcsetattr failed: rc =%d,errno=%x\n", rc, errno); + return 1; + } + + #ifdef SHELL_DEBUG + termios_show(threadCtx->shellCtx.appFd); + #endif + + /* set initial size of terminal based on saved size */ + #if !defined(NO_TERMIOS) && defined(WOLFSSH_TERM) + #if defined(HAVE_SYS_IOCTL_H) + wolfSSH_DoModes(ssh->modes, ssh->modesSz, threadCtx->shellCtx.appFd); + { + struct winsize s = {0}; + + s.ws_col = ssh->widthChar; + s.ws_row = ssh->heightRows; + s.ws_xpixel = ssh->widthPixels; + s.ws_ypixel = ssh->heightPixels; + + ioctl(threadCtx->shellCtx.appFd, TIOCSWINSZ, &s); + } + #endif /* HAVE_SYS_IOCTL_H */ + + wolfSSH_SetTerminalResizeCtx(ssh, (void*)&threadCtx->shellCtx.appFd); + #endif /* !NO_TERMIOS && WOLFSSH_TERM */ + } +#endif /* WOLFSSH_SHELL */ + + /* Claim the channel only once it can be served. Claiming it up front + * would leave the worker driving a connected shell that never started. */ + threadCtx->shellCtx.channelId = channelId; + threadCtx->shellCtx.state = APP_STATE_CONNECTED; + + return WS_SUCCESS; +} + + +#ifdef WOLFSSH_SFTP +static int wsSubsysStartCb(WOLFSSH_CHANNEL* channel, void* vCtx) +{ + int rej = 1; + + if (vCtx && channel) { + thread_ctx_t* threadCtx; + const char* cmd; + WS_SessionType type; + + threadCtx = (thread_ctx_t*)vCtx; + cmd = wolfSSH_ChannelGetSessionCommand(channel); + type = wolfSSH_ChannelGetSessionType(channel); + + /* A truncated subsystem string leaves the command NULL, and this + * runs before anything else has looked at it. */ + if (type == WOLFSSH_SESSION_SUBSYSTEM && cmd != NULL + && WSTRCMP(cmd, "sftp") == 0) { + threadCtx->doSftp = 1; + rej = WS_SUCCESS; + } + } + + return rej; +} +#endif /* WOLFSSH_SFTP */ + + +/* An "scp ..." command starts a transfer, anything else runs as a session, + * the same as a shell request: the echoserver never runs the command. */ +static int wsExecStartCb(WOLFSSH_CHANNEL* channel, void* vCtx) +{ + int rej = 1; + + if (vCtx && channel) { + const char* cmd = wolfSSH_ChannelGetSessionCommand(channel); + +#ifdef WOLFSSH_SCP + if (cmd != NULL && WSTRNCMP(cmd, "scp ", 4) == 0) { + ((thread_ctx_t*)vCtx)->doScp = 1; + rej = WS_SUCCESS; + } + else +#endif /* WOLFSSH_SCP */ + { + rej = wsShellStartCb(channel, vCtx); + } + (void)cmd; + } + + return rej; +} + + #ifdef SHELL_DEBUG static void display_ascii(char *p_buf, @@ -685,30 +904,6 @@ static void buf_dump(unsigned char *buf, int len) return; } - -#ifdef WOLFSSH_SHELL -static int termios_show(int fd) -{ - struct termios tios; - int i; - int rc; - - WMEMSET((void *) &tios, 0, sizeof(tios)); - rc = tcgetattr(fd, &tios); - printf("tcgetattr returns=%x\n", rc); - - printf("iflag/oflag/cflag/lflag = %x/%x/%x/%x\n", - (unsigned int)tios.c_iflag, (unsigned int)tios.c_oflag, - (unsigned int)tios.c_cflag, (unsigned int)tios.c_lflag); - printf("c_ispeed/c_ospeed = %x/%x\n", - (unsigned int)tios.c_ispeed, (unsigned int)tios.c_ospeed); - for (i = 0; i < NCCS; i++) { - printf("c_cc[%d] = %hhx\n", i, tios.c_cc[i]); - } - return 0; -} -#endif /* WOLFSSH_SHELL */ - #endif /* SHELL_DEBUG */ @@ -793,16 +988,6 @@ static int termios_show(int fd) #endif -int ChildRunning = 0; - -#ifdef WOLFSSH_SHELL -static void ChildSig(int sig) -{ - (void)sig; - ChildRunning = 0; -} -#endif - static int ssh_worker(thread_ctx_t* threadCtx) { WOLFSSH* ssh; @@ -815,11 +1000,8 @@ static int ssh_worker(thread_ctx_t* threadCtx) /* Without a shell there is no child to outlive the peer's EOF, and the * read path echoes unconditionally. */ int echoOnly = 1; -#ifdef WOLFSSH_SHELL - const char *userName; - struct passwd *p_passwd; - WS_SOCKET_T childFd = 0; - pid_t childPid; +#ifdef WOLFSSH_AGENT + int agentOpened = 0; #endif #if defined(WOLFSSL_PTHREADS) && defined(WOLFSSL_TEST_GLOBAL_REQ) pthread_t globalReq_th; @@ -838,6 +1020,20 @@ static int ssh_worker(thread_ctx_t* threadCtx) sshFd = wolfSSH_get_fd(ssh); + if (threadCtx->shellCtx.state != APP_STATE_CONNECTED) { + /* The legacy path: wolfSSH_accept() answered the session request + * itself, so no channel-request callback ran to claim the channel. + * Claim it here, on the session accept() established. */ + WOLFSSH_CHANNEL* sessionChannel; + + sessionChannel = wolfSSH_ChannelNext(ssh, NULL); + if (sessionChannel != NULL) { + threadCtx->shellCtx.state = APP_STATE_CONNECTED; + wolfSSH_ChannelGetId(sessionChannel, + &threadCtx->shellCtx.channelId, WS_CHANNEL_ID_SELF); + } + } + #if defined(WOLFSSL_PTHREADS) && defined(WOLFSSL_TEST_GLOBAL_REQ) /* submit Global Request for keep-alive */ rc = pthread_create(&globalReq_th, NULL, global_req, threadCtx); @@ -845,54 +1041,8 @@ static int ssh_worker(thread_ctx_t* threadCtx) printf("pthread_create() failed.\n"); #endif -#ifdef WOLFSSH_SHELL - if (!threadCtx->echo) { - - userName = wolfSSH_GetUsername(ssh); - p_passwd = getpwnam((const char *)userName); - if (p_passwd == NULL) { - /* Not actually a user on the system. */ - #ifdef SHELL_DEBUG - fprintf(stderr, "user %s does not exist\n", userName); - #endif - return WS_FATAL_ERROR; - } - - ChildRunning = 1; - childPid = forkpty(&childFd, NULL, NULL, NULL); - - if (childPid < 0) { - /* forkpty failed, so return */ - ChildRunning = 0; - return WS_FATAL_ERROR; - } - else if (childPid == 0) { - /* Child process */ - const char *args[] = {"-sh", NULL}; - - signal(SIGINT, SIG_DFL); - - #ifdef SHELL_DEBUG - printf("userName is %s\n", userName); - system("env"); - #endif - - setenv("HOME", p_passwd->pw_dir, 1); - setenv("LOGNAME", p_passwd->pw_name, 1); - rc = chdir(p_passwd->pw_dir); - if (rc != 0) { - return WS_FATAL_ERROR; - } - - execv("/bin/sh", (char **)args); - } - } -#endif { /* Parent process */ -#ifdef WOLFSSH_SHELL - struct termios tios; -#endif #ifdef WOLFSSH_AGENT WS_SOCKET_T agentFd = -1; WS_SOCKET_T agentListenFd = threadCtx->agentCtx.listenFd; @@ -903,52 +1053,7 @@ static int ssh_worker(thread_ctx_t* threadCtx) word32 fwdBufferIdx = 0; #endif -#ifdef WOLFSSH_SHELL - if (!threadCtx->echo) { - #ifdef SHELL_DEBUG - printf("In childPid > 0; getpid=%d\n", (int)getpid()); - #endif - signal(SIGCHLD, ChildSig); - - rc = tcgetattr(childFd, &tios); - if (rc != 0) { - printf("tcgetattr failed: rc =%d,errno=%x\n", rc, errno); - return WS_FATAL_ERROR; - } - rc = tcsetattr(childFd, TCSAFLUSH, &tios); - if (rc != 0) { - printf("tcsetattr failed: rc =%d,errno=%x\n", rc, errno); - return WS_FATAL_ERROR; - } - - #ifdef SHELL_DEBUG - termios_show(childFd); - #endif - } - else - ChildRunning = 1; -#else ChildRunning = 1; -#endif - -#if !defined(NO_TERMIOS) && defined(WOLFSSH_TERM) && defined(WOLFSSH_SHELL) -#if defined(HAVE_SYS_IOCTL_H) - /* if not echoing, set initial size of terminal based on saved size */ - if (!threadCtx->echo) { - struct winsize s = {0,0,0,0}; - - wolfSSH_DoModes(ssh->modes, ssh->modesSz, childFd); - s.ws_col = ssh->widthChar; - s.ws_row = ssh->heightRows; - s.ws_xpixel = ssh->widthPixels; - s.ws_ypixel = ssh->heightPixels; - - ioctl(childFd, TIOCSWINSZ, &s); - - wolfSSH_SetTerminalResizeCtx(ssh, (void*)&childFd); - } -#endif /* HAVE_SYS_IOCTL_H */ -#endif /* !NO_TERMIOS && WOLFSSH_TERM && WOLFSSH_SHELL */ while (ChildRunning) { fd_set readFds; @@ -960,11 +1065,23 @@ static int ssh_worker(thread_ctx_t* threadCtx) FD_SET(sshFd, &readFds); maxFd = sshFd; + #ifdef WOLFSSH_AGENT + /* The peer's auth-agent-req lands after wolfSSH_accept() has + * already returned in application-driven mode, so the channel + * answering it is opened here rather than inside accept(). The + * call reports WS_BAD_ARGUMENT until the request arrives. */ + if (!agentOpened + && wolfSSH_AGENT_ChannelOpen(ssh) == WS_SUCCESS) { + agentOpened = 1; + } + #endif + #ifdef WOLFSSH_SHELL - if (!threadCtx->echo) { - FD_SET(childFd, &readFds); - if (childFd > maxFd) - maxFd = childFd; + if (threadCtx->shellCtx.state == APP_STATE_CONNECTED + && threadCtx->shellCtx.appFd >= 0) { + FD_SET(threadCtx->shellCtx.appFd, &readFds); + if (threadCtx->shellCtx.appFd > maxFd) + maxFd = threadCtx->shellCtx.appFd; } #endif /* WOLFSSH_SHELL */ #ifdef WOLFSSH_AGENT @@ -996,6 +1113,7 @@ static int ssh_worker(thread_ctx_t* threadCtx) maxFd = fwdFd; } #endif /* WOLFSSH_FWD */ + rc = select((int)maxFd + 1, &readFds, NULL, NULL, NULL); if (rc == -1) { break; @@ -1011,6 +1129,16 @@ static int ssh_worker(thread_ctx_t* threadCtx) channel. The additional channel is only used with the agent. */ cnt_r = wolfSSH_worker(ssh, &lastChannel); + #ifdef WOLFSSH_SFTP + if (threadCtx->doSftp) { + return WS_SFTP_COMPLETE; + } + #endif + #ifdef WOLFSSH_SCP + if (threadCtx->doScp) { + return WS_SCP_INIT; + } + #endif /* Take the worker's status before the drain below: its * reads and sends latch their own into ssh->error. */ rc = wolfSSH_get_error(ssh); @@ -1089,7 +1217,8 @@ static int ssh_worker(thread_ctx_t* threadCtx) * wolfSSH_ChannelIdRead() has no isKeying gate; the window * credit it owes is parked until the rekey finishes. */ if (rc == WS_CHAN_RXD || rc == WS_REKEYING) { - if (lastChannel == threadCtx->shellCtx.channelId) { + if (threadCtx->shellCtx.state == APP_STATE_CONNECTED && + lastChannel == threadCtx->shellCtx.channelId) { cnt_r = wolfSSH_ChannelIdRead(ssh, threadCtx->shellCtx.channelId, threadCtx->channelBuffer, @@ -1106,7 +1235,8 @@ static int ssh_worker(thread_ctx_t* threadCtx) #endif #ifdef WOLFSSH_SHELL if (!threadCtx->echo) { - cnt_w = (int)write(childFd, + cnt_w = (int)write( + threadCtx->shellCtx.appFd, threadCtx->channelBuffer, cnt_r); } else { @@ -1205,7 +1335,7 @@ static int ssh_worker(thread_ctx_t* threadCtx) * above, which has already run this pass. */ continue; } - else if (rc != WS_WANT_READ) { + else if (rc != WS_WANT_READ && rc != WS_REKEYING) { #ifdef SHELL_DEBUG printf("Break:read sshFd returns %d: errno =%x\n", cnt_r, errno); @@ -1214,11 +1344,11 @@ static int ssh_worker(thread_ctx_t* threadCtx) } } } - #ifdef WOLFSSH_SHELL - if (!threadCtx->echo) { - if (FD_ISSET(childFd, &readFds)) { - cnt_r = (int)read(childFd, + if (threadCtx->shellCtx.state == APP_STATE_CONNECTED + && threadCtx->shellCtx.appFd >= 0) { + if (FD_ISSET(threadCtx->shellCtx.appFd, &readFds)) { + cnt_r = (int)read(threadCtx->shellCtx.appFd, threadCtx->shellCtx.buffer, sizeof threadCtx->shellCtx.buffer); /* This read will return 0 on EOF */ @@ -1333,8 +1463,7 @@ static int ssh_worker(thread_ctx_t* threadCtx) fwdFd = -1; threadCtx->fwdCtx.appFd = -1; if (threadCtx->fwdCbCtx.hostName != NULL) { - WFREE(threadCtx->fwdCbCtx.hostName, - NULL, 0); + WFREE(threadCtx->fwdCbCtx.hostName, NULL, 0); threadCtx->fwdCbCtx.hostName = NULL; } threadCtx->fwdCtx.state = APP_STATE_LISTEN; @@ -1459,8 +1588,10 @@ static int ssh_worker(thread_ctx_t* threadCtx) #endif /* WOLFSSH_FWD */ } #ifdef WOLFSSH_SHELL - if (!threadCtx->echo) - WCLOSESOCKET(childFd); + if (threadCtx->shellCtx.appFd >= 0) { + WCLOSESOCKET(threadCtx->shellCtx.appFd); + threadCtx->shellCtx.appFd = -1; + } #endif } @@ -1472,6 +1603,9 @@ static int ssh_worker(thread_ctx_t* threadCtx) } +/* Seconds to wait on the socket between subsystem-accept attempts. */ +#define ES_ACCEPT_TIMEOUT 1 + #ifdef WOLFSSH_SFTP #define TEST_SFTP_TIMEOUT_SHORT 0 @@ -1718,8 +1852,10 @@ static THREAD_RETURN WOLFSSH_THREAD server_worker(void* vArgs) else { ret = NonBlockSSH_accept(threadCtx->ssh); } + #ifdef WOLFSSH_SCP - /* finish off SCP operation */ + /* The legacy path: accept() reports the scp command and does the + * transfer on re-entry. */ if (ret == WS_SCP_INIT) { if (!threadCtx->nonBlock) ret = wolfSSH_accept(threadCtx->ssh); @@ -1735,6 +1871,8 @@ static THREAD_RETURN WOLFSSH_THREAD server_worker(void* vArgs) break; #ifdef WOLFSSH_SFTP + /* The legacy path: wolfSSH_accept() ran the subsystem request + * itself and handed back a session ready to serve. */ case WS_SFTP_COMPLETE: ret = sftp_worker(threadCtx); break; @@ -1742,6 +1880,48 @@ static THREAD_RETURN WOLFSSH_THREAD server_worker(void* vArgs) case WS_SUCCESS: ret = ssh_worker(threadCtx); + #ifdef WOLFSSH_SCP + if (ret == WS_SCP_INIT) { + /* On a non-blocking socket the transfer comes back part + * done; resume it rather than tearing the session down + * mid-file. */ + do { + ret = wolfSSH_SCP_accept(threadCtx->ssh); + error = wolfSSH_get_error(threadCtx->ssh); + if (ret != WS_SCP_COMPLETE + && (error == WS_WANT_READ + || error == WS_WANT_WRITE)) { + tcp_select(wolfSSH_get_fd(threadCtx->ssh), + ES_ACCEPT_TIMEOUT); + } + } while (ret != WS_SCP_COMPLETE + && (error == WS_WANT_READ || error == WS_WANT_WRITE)); + if (ret == WS_SCP_COMPLETE) { + printf("scp file transfer completed\n"); + ret = 0; + } + } + #endif + #ifdef WOLFSSH_SFTP + if (ret == WS_SFTP_COMPLETE) { + do { + ret = wolfSSH_SFTP_accept(threadCtx->ssh); + error = wolfSSH_get_error(threadCtx->ssh); + /* Wait on the socket between attempts; without this the + * gap before the client's SFTP INIT is a busy spin. */ + if (ret != WS_SFTP_COMPLETE + && (error == WS_WANT_READ + || error == WS_WANT_WRITE)) { + tcp_select(wolfSSH_get_fd(threadCtx->ssh), + ES_ACCEPT_TIMEOUT); + } + } while (ret != WS_SFTP_COMPLETE + && (error == WS_WANT_READ || error == WS_WANT_WRITE)); + } + if (ret == WS_SFTP_COMPLETE) { + ret = sftp_worker(threadCtx); + } + #endif break; } @@ -3101,6 +3281,7 @@ static void ShowUsage(void) #ifdef WOLFSSH_SHELL printf(" -f echo input\n"); #endif + printf(" -A drive channels from the application callbacks\n"); printf(" -p port to connect on, default %d\n", wolfSshPort); printf(" -N use non-blocking sockets\n"); #ifdef WOLFSSH_SFTP @@ -3238,6 +3419,7 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) int userEcc = 0; int peerEcc = 0; int echo = 0; + int appChannels = 0; int ch; word16 port = wolfSshPort; char* readyFile = NULL; @@ -3260,7 +3442,8 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) #endif if (argc > 0) { - const char* optlist = "?1a:d:DefEp:R:Ni:j:i:I:J:K:P:k:b:x:m:c:s:G:H"; + const char* optlist = + "?1a:Ad:DefEp:R:Ni:j:i:I:J:K:P:k:b:x:m:c:s:G:H"; myoptind = 0; while ((ch = mygetopt(argc, argv, optlist)) != -1) { switch (ch) { @@ -3297,6 +3480,10 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) #endif break; + case 'A': + appChannels = 1; + break; + case 'p': if (myoptarg == NULL) { ES_ERROR("NULL port value\n"); @@ -3502,6 +3689,22 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) #ifdef WOLFSSH_FWD wolfSSH_CTX_SetFwdCb(ctx, wolfSSH_FwdDefaultActions, NULL); #endif + /* With -A the echoserver drives its own channels: accept() stops at + * userauth and these callbacks start the shell, subsystem or transfer. + * Off by default, so the path this example has always taken keeps an + * in-tree demo. The two are exclusive: the callbacks answer the session + * requests the accept state machine would otherwise answer itself. */ + /* The shell callback is the only place the pty is forked, so it is + * registered in both modes. accept() honours a registered callback with + * application-driven channels off, so the legacy path keeps its shell. */ + wolfSSH_CTX_SetChannelReqShellCb(ctx, wsShellStartCb); + if (appChannels) { + wolfSSH_CTX_SetAppChannels(ctx, 1); +#ifdef WOLFSSH_SFTP + wolfSSH_CTX_SetChannelReqSubsysCb(ctx, wsSubsysStartCb); +#endif + wolfSSH_CTX_SetChannelReqExecCb(ctx, wsExecStartCb); + } #ifndef NO_FILESYSTEM if (sshPubKeyList) { @@ -3876,6 +4079,7 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) #endif wolfSSH_SetUserAuthCtx(ssh, &pwMapList); wolfSSH_SetKeyingCompletionCbCtx(ssh, (void*)ssh); + wolfSSH_SetChannelReqCtx(ssh, (void*)threadCtx); /* Use the session object for its own highwater callback ctx */ if (defaultHighwater > 0) { @@ -3935,13 +4139,13 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) tcp_set_nonblocking(&clientFd); wolfSSH_set_fd(ssh, (int)clientFd); + threadCtx->fd = clientFd; #if defined(WOLFSSL_PTHREADS) && defined(WOLFSSL_TEST_GLOBAL_REQ) threadCtx->ctx = ctx; #endif threadCtx->ssh = ssh; - threadCtx->fd = clientFd; - threadCtx->id = threadCount++; + threadCtx->tid = threadCount++; threadCtx->nonBlock = nonBlock; threadCtx->echo = echo; threadCtx->shellCtx.privateData = NULL; diff --git a/tests/api.c b/tests/api.c index a0255cdd5..c5e3f7ea6 100644 --- a/tests/api.c +++ b/tests/api.c @@ -7906,6 +7906,9 @@ static void test_wolfSSH_KeyboardInteractive(void) argsCount = 0; args[argsCount++] = "."; args[argsCount++] = "-1"; + /* Echo mode: "test" is not an account on the host, so the echoserver's + * shell callback would refuse the shell request this client sends. */ + args[argsCount++] = "-f"; args[argsCount++] = "-i"; args[argsCount++] = "test:test"; args[argsCount++] = "-p";