From 5bdfaf98e7ecc06a71a4add51b676b1959dcf60f Mon Sep 17 00:00:00 2001 From: John Safranek Date: Wed, 2 Sep 2026 11:50:13 -0700 Subject: [PATCH 1/2] internal: commit a session only once accepted 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() stays where it is rather than reporting a session it answered CHANNEL_FAILURE as established. - DoChannelRequestSession() carries the three arms, which differed only in the type and the callback consulted - a refusal puts the type and command back, so a grant an earlier request won still stands - FreeChannelCommand() wipes and releases a command line for both ChannelDelete() and the refusal path - unit.c drives a refused shell, exec and subsystem request through DoChannelRequest() and checks nothing was committed - regress.c checks accept() stays at ACCEPT_SERVER_CHANNEL_ACCEPT_SENT on a refused shell, and that neither divert runs Issue: F-8852 --- src/internal.c | 161 ++++++++++++++++++++++++--------------------- src/ssh.c | 5 +- tests/regress.c | 63 ++++++++++++++++-- tests/unit.c | 133 ++++++++++++++++++++++++++++++++++--- wolfssh/internal.h | 5 +- 5 files changed, 272 insertions(+), 95 deletions(-) diff --git a/src/internal.c b/src/internal.c index 644b080d2..229148b91 100644 --- a/src/internal.c +++ b/src/internal.c @@ -4173,6 +4173,18 @@ static void NotifyFwdLocalCleanup(WOLFSSH_CHANNEL* channel) #endif /* WOLFSSH_FWD */ +/* Wipe a command line, which can carry credentials, and free it. */ +static void FreeChannelCommand(void* heap, char* command, word32 commandSz) +{ + WOLFSSH_UNUSED(heap); + + if (command != NULL) { + WS_FORCEZERO(command, commandSz); + WFREE(command, heap, DYNTYPE_STRING); + } +} + + void ChannelDelete(WOLFSSH_CHANNEL* channel, void* heap) { WOLFSSH_UNUSED(heap); @@ -4200,11 +4212,7 @@ void ChannelDelete(WOLFSSH_CHANNEL* channel, void* heap) channel->channel); } ShrinkBuffer(&channel->extDataBuffer, 1); - /* Scrub the peer's command line, which can carry credentials. */ - if (channel->command != NULL) { - WS_FORCEZERO(channel->command, channel->commandSz); - WFREE(channel->command, heap, DYNTYPE_STRING); - } + FreeChannelCommand(heap, channel->command, channel->commandSz); WFREE(channel, heap, DYNTYPE_CHANNEL); } } @@ -13090,14 +13098,71 @@ static void SetTerminalSize(WOLFSSH* ssh, word32 widthChar, word32 heightRows, #endif /* WOLFSSH_TERM */ -/* Wipe the old command ahead of the GetStringAlloc() that frees it, so a - * repeat request leaves no credentials behind in the freed block. */ -static void ScrubChannelCommand(WOLFSSH_CHANNEL* channel) +/* Answers a shell, exec, or subsystem request. Sets the session type and + * command for the callback to read, and keeps them only if it accepts. */ +static int DoChannelRequestSession(WOLFSSH* ssh, word32 channelId, + WOLFSSH_CHANNEL* channel, byte sessionType, WS_CallbackChannelReq cb, + byte* buf, word32 len, word32* idx, int* rej) { - if (channel->command != NULL) { - WS_FORCEZERO(channel->command, channel->commandSz); - channel->commandSz = 0; + void* heap = ssh->ctx->heap; + byte prevType = channel->sessionType; + byte hasCommand = (sessionType != WOLFSSH_SESSION_SHELL); + char* prevCommand = NULL; + word32 prevCommandSz = 0; + char* command = NULL; + word32 commandSz = 0; + int ret = WS_SUCCESS; + + /* A shell request carries no command, so it leaves the old one alone. + * The others read into a local, so the old survives a refusal. */ + if (hasCommand) { + prevCommand = channel->command; + prevCommandSz = channel->commandSz; + + ret = GetStringAlloc(heap, &command, &commandSz, buf, len, idx); + if (ret == WS_SUCCESS) + WLOG(WS_LOG_DEBUG, " command = %s", command); + else + WLOG(WS_LOG_DEBUG, " command = %s", ""); } + + if (ret == WS_SUCCESS) { + if (hasCommand) { + channel->command = command; + channel->commandSz = commandSz; + } + channel->sessionType = sessionType; + + if (cb != NULL) + *rej = cb(channel, ssh->channelReqCtx); + else + *rej = ssh->appChannels; + + /* A callback may free its own channel, so look it up again. */ + channel = ChannelFind(ssh, channelId, WS_CHANNEL_ID_SELF); + if (channel == NULL) { + /* The new command went with it. */ + FreeChannelCommand(heap, prevCommand, prevCommandSz); + return ret; + } + } + + if (ret == WS_SUCCESS && !*rej) { + FreeChannelCommand(heap, prevCommand, prevCommandSz); + channel->sessionGranted = 1; + ssh->clientState = CLIENT_DONE; + } + else { + /* A refusal changes nothing, so an earlier grant still stands. */ + if (hasCommand) { + FreeChannelCommand(heap, command, commandSz); + channel->command = prevCommand; + channel->commandSz = prevCommandSz; + } + channel->sessionType = prevType; + } + + return ret; } @@ -13110,7 +13175,7 @@ static int DoChannelRequest(WOLFSSH* ssh, word32 typeSz; char type[32]; byte wantReply; - int ret, rej = 0, sessionReq = 0; + int ret, rej = 0; WLOG(WS_LOG_DEBUG, "Entering DoChannelRequest()"); @@ -13160,59 +13225,19 @@ static int DoChannelRequest(WOLFSSH* ssh, } } else if (ChannelRequestIs(type, typeSz, "shell")) { - channel->sessionType = WOLFSSH_SESSION_SHELL; - if (ssh->ctx->channelReqShellCb) { - rej = ssh->ctx->channelReqShellCb(channel, ssh->channelReqCtx); - } - else { - rej = ssh->appChannels; - } - sessionReq = 1; - ssh->clientState = CLIENT_DONE; + ret = DoChannelRequestSession(ssh, channelId, channel, + WOLFSSH_SESSION_SHELL, ssh->ctx->channelReqShellCb, + buf, len, &begin, &rej); } else if (ChannelRequestIs(type, typeSz, "exec")) { - ScrubChannelCommand(channel); - ret = GetStringAlloc(ssh->ctx->heap, - &channel->command, &channel->commandSz, - buf, len, &begin); - if (ret == WS_SUCCESS) - WLOG(WS_LOG_DEBUG, " command = %s", channel->command); - else - WLOG(WS_LOG_DEBUG, " command = %s", ""); - if (ret == WS_SUCCESS) { - channel->sessionType = WOLFSSH_SESSION_EXEC; - if (ssh->ctx->channelReqExecCb) { - rej = ssh->ctx->channelReqExecCb(channel, - ssh->channelReqCtx); - } - else { - rej = ssh->appChannels; - } - } - sessionReq = 1; - ssh->clientState = CLIENT_DONE; + ret = DoChannelRequestSession(ssh, channelId, channel, + WOLFSSH_SESSION_EXEC, ssh->ctx->channelReqExecCb, + buf, len, &begin, &rej); } else if (ChannelRequestIs(type, typeSz, "subsystem")) { - ScrubChannelCommand(channel); - ret = GetStringAlloc(ssh->ctx->heap, - &channel->command, &channel->commandSz, - buf, len, &begin); - if (ret == WS_SUCCESS) - WLOG(WS_LOG_DEBUG, " subsystem = %s", channel->command); - else - WLOG(WS_LOG_DEBUG, " subsystem = %s", ""); - if (ret == WS_SUCCESS) { - channel->sessionType = WOLFSSH_SESSION_SUBSYSTEM; - if (ssh->ctx->channelReqSubsysCb) { - rej = ssh->ctx->channelReqSubsysCb(channel, - ssh->channelReqCtx); - } - else { - rej = ssh->appChannels; - } - } - sessionReq = 1; - ssh->clientState = CLIENT_DONE; + ret = DoChannelRequestSession(ssh, channelId, channel, + WOLFSSH_SESSION_SUBSYSTEM, ssh->ctx->channelReqSubsysCb, + buf, len, &begin, &rej); } #ifdef WOLFSSH_TERM else if (ChannelRequestIs(type, typeSz, "pty-req")) { @@ -13347,20 +13372,6 @@ static int DoChannelRequest(WOLFSSH* ssh, *idx = len; } - /* Record the answer, not the ask: sessionType and command are set before - * the reject decision and stay set on a refusal, so they cannot say - * whether the session was granted. Set even without a wantReply, which - * changes only whether the peer is told. - * - * Look the channel up again rather than reusing the pointer from - * before the callback. A callback may close its own channel, and - * wolfSSH_ChannelFree() frees it, so the old pointer can be dead. */ - if (sessionReq) { - channel = ChannelFind(ssh, channelId, WS_CHANNEL_ID_SELF); - if (channel != NULL) - channel->sessionGranted = (ret == WS_SUCCESS && !rej); - } - if (wantReply) { int replyRet; diff --git a/src/ssh.c b/src/ssh.c index 4432e6c40..f6a5f20d7 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -810,9 +810,8 @@ int wolfSSH_accept(WOLFSSH* ssh) } } - /* Divert only into a granted session. The type and - * command stay set on a refusal, so they do not say - * what was granted. */ + /* Divert only into a granted session; a refusal puts + * the type and command back. */ #ifdef WOLFSSH_SCP if (ssh->channelList != NULL && ssh->channelList->sessionGranted diff --git a/tests/regress.c b/tests/regress.c index 49f4e1c55..c45c2899a 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -1692,6 +1692,58 @@ static void TestAppChannelsLateEnableReturns(void) FreeKexReplyHarness(&harness); } +/* Refuses the session request, and records what the channel showed. */ +static int rejectShellReqCalls; +static WS_SessionType rejectShellReqType; + +static int RejectShellReqCb(WOLFSSH_CHANNEL* channel, void* ctx) +{ + (void)ctx; + rejectShellReqCalls++; + rejectShellReqType = wolfSSH_ChannelGetSessionType(channel); + return 1; +} + +/* A shell request the callback refuses gets CHANNEL_FAILURE and nothing + * more: the channel keeps no session type, and accept() stays where it was, + * waiting on a request it can grant, rather than reporting an established + * session it just refused. */ +static void TestSessionReqRejectedKeepsAcceptWaiting(void) +{ + KexReplyHarness harness; + KexReplyRunResult result; + WOLFSSH_CHANNEL* channel; + WS_SessionType sessionType; + + rejectShellReqCalls = 0; + rejectShellReqType = WOLFSSH_SESSION_UNKNOWN; + + InitKexReplyHarness(&harness, "rsa-sha2-256", REGRESS_SERVER_KEY_PATH, + 0, NULL); + AssertIntEQ(wolfSSH_CTX_SetChannelReqShellCb(harness.serverCtx, + RejectShellReqCb), WS_SUCCESS); + + RunKexReplyHandshake(&harness, &result); + + AssertIntEQ(rejectShellReqCalls, 1); + AssertIntEQ(rejectShellReqType, WOLFSSH_SESSION_SHELL); + AssertFalse(result.clientSuccess); + AssertIntEQ(result.clientErr, WS_CHANOPEN_FAILED); + AssertFalse(result.serverSuccess); + AssertIntEQ(harness.server->acceptState, + ACCEPT_SERVER_CHANNEL_ACCEPT_SENT); + AssertTrue(harness.server->clientState < CLIENT_DONE); + sessionType = wolfSSH_GetSessionType(harness.server); + AssertIntEQ(sessionType, WOLFSSH_SESSION_UNKNOWN); + channel = wolfSSH_ChannelNext(harness.server, NULL); + AssertNotNull(channel); + AssertIntEQ(channel->sessionType, WOLFSSH_SESSION_UNKNOWN); + AssertFalse(harness.clientIo.sawDisconnect); + AssertFalse(harness.serverIo.sawDisconnect); + + FreeKexReplyHarness(&harness); +} + static void TestKexDhReplyRejectsRsaSha2_256SigNameDowngrade(void) { AssertHandshakeSucceeds("rsa-sha2-256", REGRESS_SERVER_KEY_PATH); @@ -4591,9 +4643,10 @@ static void CheckAcceptDivertNeedsSftpGrant(int reject) harness.ssh->acceptState = ACCEPT_SERVER_CHANNEL_ACCEPT_SENT; if (reject) { - AssertIntEQ(wolfSSH_accept(harness.ssh), WS_SUCCESS); + /* Short of CLIENT_DONE, so accept() diverts nowhere. */ + AssertIntEQ(wolfSSH_accept(harness.ssh), WS_FATAL_ERROR); AssertIntEQ(harness.ssh->acceptState, - ACCEPT_CLIENT_SESSION_ESTABLISHED); + ACCEPT_SERVER_CHANNEL_ACCEPT_SENT); } else { /* The control: the same name, granted, does reach the built-in @@ -4647,9 +4700,10 @@ static void CheckAcceptDivertNeedsScpGrant(int reject) harness.ssh->acceptState = ACCEPT_SERVER_CHANNEL_ACCEPT_SENT; if (reject) { - AssertIntEQ(wolfSSH_accept(harness.ssh), WS_SUCCESS); + /* Short of CLIENT_DONE, so accept() diverts nowhere. */ + AssertIntEQ(wolfSSH_accept(harness.ssh), WS_FATAL_ERROR); AssertIntEQ(harness.ssh->acceptState, - ACCEPT_CLIENT_SESSION_ESTABLISHED); + ACCEPT_SERVER_CHANNEL_ACCEPT_SENT); } else { AssertIntEQ(wolfSSH_accept(harness.ssh), WS_SCP_INIT); @@ -14961,6 +15015,7 @@ int main(int argc, char** argv) TestAppChannelsAcceptStopsAtUserAuth(); TestAppChannelsNoShellCbRejects(); TestAppChannelsLateEnableReturns(); + TestSessionReqRejectedKeepsAcceptWaiting(); TestKexDhReplyRejectsRsaSha2_256SigNameDowngrade(); #endif #ifndef WOLFSSH_NO_RSA_SHA2_512 diff --git a/tests/unit.c b/tests/unit.c index 918592553..0d6ca5edb 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -8760,6 +8760,23 @@ static int CaptureMsgId(const byte* buf, word32 len) * A custom IoSend callback captures the outgoing packet in plaintext * (no cipher negotiated on a fresh session). Message ID is read via * CaptureMsgId() using LENGTH_SZ + PAD_LENGTH_SZ. */ +/* A session request callback that refuses everything, and counts. The + * callback sees the session type and command of the request it is vetting; + * what it does not see is a session already committed to the channel. */ +static int s_rejectChanReqCalls; + +static int RejectChanReqCb(WOLFSSH_CHANNEL* channel, void* ctx) +{ + (void)ctx; + s_rejectChanReqCalls++; + if (channel == NULL + || wolfSSH_ChannelGetSessionType(channel) + == WOLFSSH_SESSION_UNKNOWN) { + return 0; + } + return 1; +} + static byte s_chanReqCapture[256]; static word32 s_chanReqCaptureSz = 0; @@ -9012,6 +9029,15 @@ static int test_DoChannelRequest(void) 0x00,0x00,0x00,0x02, /* cmdSz = 2 */ 0x6C,0x73 /* "ls" */ }; + 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" */ + }; static const byte payUnknown[] = { 0x00,0x00,0x00,0x00, /* channelId = 0 */ 0x00,0x00,0x00,0x0C, /* typeSz = 12 */ @@ -9138,6 +9164,78 @@ static int test_DoChannelRequest(void) } } + /* A callback that refuses a shell, exec or subsystem request must leave + * nothing behind: no session type or command on the channel, and the + * client state short of CLIENT_DONE, or wolfSSH_accept() would go on to + * serve the session it just refused. */ + { + struct { + const char* label; + const byte* payload; + word32 payloadSz; + int errBase; + } rejCases[] = { + { "shell", payShell, (word32)sizeof(payShell), -520 }, + { "exec", payExec, (word32)sizeof(payExec), -525 }, + { "subsystem", paySubsys, (word32)sizeof(paySubsys), -530 } + }; + int r; + + wolfSSH_CTX_SetChannelReqShellCb(ctx, RejectChanReqCb); + wolfSSH_CTX_SetChannelReqExecCb(ctx, RejectChanReqCb); + wolfSSH_CTX_SetChannelReqSubsysCb(ctx, RejectChanReqCb); + + for (r = 0; r < (int)(sizeof(rejCases) / sizeof(rejCases[0])); r++) { + word32 idxRej = 0; + int retRej, capMsgId; + + s_chanReqCaptureSz = 0; + WMEMSET(s_chanReqCapture, 0, sizeof(s_chanReqCapture)); + s_rejectChanReqCalls = 0; + + retRej = wolfSSH_TestDoChannelRequest(ssh, + (byte*)rejCases[r].payload, rejCases[r].payloadSz, + &idxRej); + if (retRej != WS_SUCCESS) { + printf("DoChannelRequest[rej-%s]: ret=%d, expected=%d\n", + rejCases[r].label, retRej, WS_SUCCESS); + result = rejCases[r].errBase; + goto done; + } + if (s_rejectChanReqCalls != 1) { + printf("DoChannelRequest[rej-%s]: callback ran %d times\n", + rejCases[r].label, s_rejectChanReqCalls); + result = rejCases[r].errBase - 1; + goto done; + } + capMsgId = CaptureMsgId(s_chanReqCapture, s_chanReqCaptureSz); + if (capMsgId != (int)MSGID_CHANNEL_FAILURE) { + printf("DoChannelRequest[rej-%s]: msg_id=0x%02x, " + "expected=0x%02x\n", rejCases[r].label, capMsgId, + MSGID_CHANNEL_FAILURE); + result = rejCases[r].errBase - 2; + goto done; + } + if (ch->sessionType != WOLFSSH_SESSION_UNKNOWN + || ch->command != NULL) { + printf("DoChannelRequest[rej-%s]: session committed\n", + rejCases[r].label); + result = rejCases[r].errBase - 3; + goto done; + } + if (ssh->clientState == CLIENT_DONE) { + printf("DoChannelRequest[rej-%s]: client state changed\n", + rejCases[r].label); + result = rejCases[r].errBase - 4; + goto done; + } + } + + wolfSSH_CTX_SetChannelReqShellCb(ctx, NULL); + wolfSSH_CTX_SetChannelReqExecCb(ctx, NULL); + wolfSSH_CTX_SetChannelReqSubsysCb(ctx, NULL); + } + for (i = 0; i < (int)(sizeof(cases) / sizeof(cases[0])); i++) { word32 idx = 0; int ret; @@ -9175,6 +9273,32 @@ static int test_DoChannelRequest(void) } } + /* A shell request carries no command, so it must leave the one the + * exec above set alone rather than release it. */ + { + word32 idxShell = 0; + const char* cmd; + + if (wolfSSH_TestDoChannelRequest(ssh, (byte*)payShell, + (word32)sizeof(payShell), &idxShell) != WS_SUCCESS) { + printf("DoChannelRequest[shell-after-exec]: failed\n"); + result = -500; + goto done; + } + cmd = wolfSSH_ChannelGetSessionCommand(ch); + if (cmd == NULL || WSTRCMP(cmd, "ls") != 0) { + printf("DoChannelRequest[shell-after-exec]: command = %s\n", + cmd == NULL ? "(null)" : cmd); + result = -501; + goto done; + } + if (wolfSSH_ChannelGetSessionType(ch) != WOLFSSH_SESSION_SHELL) { + printf("DoChannelRequest[shell-after-exec]: type not shell\n"); + result = -502; + goto done; + } + } + /* RFC 4254 sec 6.10: exit-status and exit-signal must not send a reply * even if the wire wantReply byte is 1. DoChannelRequest overrides * wantReply=0 for these types, so no CHANNEL_SUCCESS/FAILURE packet @@ -9425,15 +9549,6 @@ static int test_DoChannelRequest(void) * 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; diff --git a/wolfssh/internal.h b/wolfssh/internal.h index 16c653251..d3b6a07aa 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -1419,10 +1419,7 @@ struct WOLFSSH_CHANNEL { byte ptyReq : 1; /* flag for if interactive pty request was received */ byte fwdSetupTxd : 1; /* a LOCAL_SETUP succeeded, a cleanup is owed */ byte sessionGranted : 1; /* a shell, exec or subsystem request was - * answered CHANNEL_SUCCESS. sessionType and - * command are recorded before that answer is - * decided and stay set on a refusal, so they - * do not say whether anything was granted. */ + * answered CHANNEL_SUCCESS */ word32 channel; word32 windowSz; word32 maxPacketSz; From aaa6148cd2f3ece173ab1339cd6c20931f728bad Mon Sep 17 00:00:00 2001 From: John Safranek Date: Wed, 2 Sep 2026 11:57:27 -0700 Subject: [PATCH 2/2] ssh: add generic request callbacks 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 --- src/internal.c | 135 ++++++++++++---- src/ssh.c | 27 ++++ tests/regress.c | 381 +++++++++++++++++++++++++++++++++++++++++++++ wolfssh/internal.h | 2 + wolfssh/ssh.h | 44 ++++++ 5 files changed, 561 insertions(+), 28 deletions(-) diff --git a/src/internal.c b/src/internal.c index 229148b91..db7862f73 100644 --- a/src/internal.c +++ b/src/internal.c @@ -12186,6 +12186,56 @@ static int DoGlobalRequestFwd(WOLFSSH* ssh, } #endif +/* Puts a global request to the generic callback, which sees the name and + * the type-specific part to parse itself. Returns 1 when the callback + * settled the request, with any wanted reply sent and *ret carrying the + * result, or 0 to leave it to the built-in handling. */ +static int DoGlobalRequestAny(WOLFSSH* ssh, const char* name, int globReqId, + byte* buf, word32 len, word32 begin, byte wantReply, int* ret) +{ + int decision, success; + + if (ssh->ctx->globalReqAnyCb == NULL) { + return 0; + } + + decision = ssh->ctx->globalReqAnyCb(ssh, name, buf + begin, len - begin, + wantReply, ssh->globalReqCtx); + if (decision != WOLFSSH_REQ_ACCEPT && decision != WOLFSSH_REQ_REJECT) { + return 0; + } + success = (decision == WOLFSSH_REQ_ACCEPT); + +#ifdef WOLFSSH_FWD + /* RFC 4254 7.1: a port-0 request is answered with the port bound, + * which only the forward callback can report. */ + if (success && globReqId == ID_GLOBREQ_TCPIP_FWD) { + const byte* bindAddr; + word32 bindAddrSz, bindPort = 0, peek = begin; + + if (GetStringRef(&bindAddrSz, &bindAddr, buf, len, &peek) + != WS_SUCCESS + || GetUint32(&bindPort, buf, len, &peek) != WS_SUCCESS + || bindPort == 0) { + WLOG(WS_LOG_WARN, "DGR: a port-0 forward needs the forward " + "callback to bind it; rejecting"); + success = 0; + } + } +#else + (void)globReqId; +#endif + + WLOG(WS_LOG_DEBUG, "DGR: global request callback %s", + success ? "granted" : "refused"); + if (wantReply) { + *ret = SendRequestSuccess(ssh, success); + } + + return 1; +} + + static int DoGlobalRequest(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) { @@ -12232,31 +12282,37 @@ static int DoGlobalRequest(WOLFSSH* ssh, } else #endif - switch (globReqId) { + if (!DoGlobalRequestAny(ssh, name, globReqId, buf, len, begin, + wantReply, &ret)) { + switch (globReqId) { #ifdef WOLFSSH_FWD - case ID_GLOBREQ_TCPIP_FWD: - ret = DoGlobalRequestFwd(ssh, buf, len, &begin, wantReply, 0); - wantReply = 0; - break; - case ID_GLOBREQ_TCPIP_FWD_CANCEL: - ret = DoGlobalRequestFwd(ssh, buf, len, &begin, wantReply, 1); - wantReply = 0; - break; + case ID_GLOBREQ_TCPIP_FWD: + ret = DoGlobalRequestFwd(ssh, buf, len, &begin, + wantReply, 0); + wantReply = 0; + break; + case ID_GLOBREQ_TCPIP_FWD_CANCEL: + ret = DoGlobalRequestFwd(ssh, buf, len, &begin, + wantReply, 1); + wantReply = 0; + break; #endif - default: - if (ssh->ctx->globalReqCb != NULL) { - ret = ssh->ctx->globalReqCb(ssh, name, nameSz, wantReply, - (void *)ssh->globalReqCtx); + default: + if (ssh->ctx->globalReqCb != NULL) { + ret = ssh->ctx->globalReqCb(ssh, name, nameSz, + wantReply, (void *)ssh->globalReqCtx); - if (wantReply) { - ret = SendRequestSuccess(ssh, (ret == WS_SUCCESS)); + if (wantReply) { + ret = SendRequestSuccess(ssh, + (ret == WS_SUCCESS)); + } } - } - else if (wantReply) - ret = SendRequestSuccess(ssh, 0); - /* response SSH_MSG_REQUEST_FAILURE to Keep-Alive. - * IETF:draft-ssh-global-requests */ - break; + else if (wantReply) + ret = SendRequestSuccess(ssh, 0); + /* response SSH_MSG_REQUEST_FAILURE to Keep-Alive. + * IETF:draft-ssh-global-requests */ + break; + } } } @@ -13099,10 +13155,11 @@ static void SetTerminalSize(WOLFSSH* ssh, word32 widthChar, word32 heightRows, /* Answers a shell, exec, or subsystem request. Sets the session type and - * command for the callback to read, and keeps them only if it accepts. */ + * command for the callback to read, and keeps them only if it accepts. + * A request the generic callback already granted asks no callback. */ static int DoChannelRequestSession(WOLFSSH* ssh, word32 channelId, WOLFSSH_CHANNEL* channel, byte sessionType, WS_CallbackChannelReq cb, - byte* buf, word32 len, word32* idx, int* rej) + int granted, byte* buf, word32 len, word32* idx, int* rej) { void* heap = ssh->ctx->heap; byte prevType = channel->sessionType; @@ -13133,7 +13190,9 @@ static int DoChannelRequestSession(WOLFSSH* ssh, word32 channelId, } channel->sessionType = sessionType; - if (cb != NULL) + if (granted) + *rej = 0; + else if (cb != NULL) *rej = cb(channel, ssh->channelReqCtx); else *rej = ssh->appChannels; @@ -13175,7 +13234,7 @@ static int DoChannelRequest(WOLFSSH* ssh, word32 typeSz; char type[32]; byte wantReply; - int ret, rej = 0; + int ret, rej = 0, granted = 0; WLOG(WS_LOG_DEBUG, "Entering DoChannelRequest()"); @@ -13203,6 +13262,23 @@ static int DoChannelRequest(WOLFSSH* ssh, WLOG(WS_LOG_DEBUG, " type = %s", type); WLOG(WS_LOG_DEBUG, " wantReply = %u", wantReply); + /* The generic callback sees every request first, with the + * type-specific part to parse itself. A refusal skips the handling + * below; a grant runs it with the decision already made. */ + if (ssh->ctx->channelReqAnyCb != NULL) { + int decision = ssh->ctx->channelReqAnyCb(channel, type, + buf + begin, len - begin, ssh->channelReqCtx); + if (decision == WOLFSSH_REQ_REJECT) { + WLOG(WS_LOG_DEBUG, " channel request callback refused."); + rej = 1; + } + else if (decision == WOLFSSH_REQ_ACCEPT) { + granted = 1; + } + } + } + + if (ret == WS_SUCCESS && !rej) { if (ChannelRequestIs(type, typeSz, "env")) { char name[WOLFSSH_MAX_NAMESZ]; word32 nameSz; @@ -13227,17 +13303,17 @@ static int DoChannelRequest(WOLFSSH* ssh, else if (ChannelRequestIs(type, typeSz, "shell")) { ret = DoChannelRequestSession(ssh, channelId, channel, WOLFSSH_SESSION_SHELL, ssh->ctx->channelReqShellCb, - buf, len, &begin, &rej); + granted, buf, len, &begin, &rej); } else if (ChannelRequestIs(type, typeSz, "exec")) { ret = DoChannelRequestSession(ssh, channelId, channel, WOLFSSH_SESSION_EXEC, ssh->ctx->channelReqExecCb, - buf, len, &begin, &rej); + granted, buf, len, &begin, &rej); } else if (ChannelRequestIs(type, typeSz, "subsystem")) { ret = DoChannelRequestSession(ssh, channelId, channel, WOLFSSH_SESSION_SUBSYSTEM, ssh->ctx->channelReqSubsysCb, - buf, len, &begin, &rej); + granted, buf, len, &begin, &rej); } #ifdef WOLFSSH_TERM else if (ChannelRequestIs(type, typeSz, "pty-req")) { @@ -13362,6 +13438,9 @@ static int DoChannelRequest(WOLFSSH* ssh, WLOG(WS_LOG_AGENT, "Agent callback not set, not using."); } #endif /* WOLFSSH_AGENT */ + else if (granted) { + WLOG(WS_LOG_DEBUG, " unknown channel request type, granted."); + } else { WLOG(WS_LOG_DEBUG, " unknown channel request type, rejecting."); rej = 1; diff --git a/src/ssh.c b/src/ssh.c index f6a5f20d7..8d5c9e77d 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -394,6 +394,19 @@ void wolfSSH_SetReqFailure(WOLFSSH_CTX *ctx, WS_CallbackReqSuccess cb) ctx->reqFailureCb = cb; } +int wolfSSH_CTX_SetGlobalReqCb(WOLFSSH_CTX* ctx, WS_CallbackGlobalReqAny cb) +{ + int ret = WS_SSH_CTX_NULL_E; + + if (ctx != NULL) { + ctx->globalReqAnyCb = cb; + ret = WS_SUCCESS; + } + + return ret; +} + + void wolfSSH_SetGlobalReqCtx(WOLFSSH* ssh, void *ctx) { WLOG(WS_LOG_DEBUG, "Entering wolfSSH_SetGlobalReqCtx()"); @@ -5831,6 +5844,20 @@ int wolfSSH_CTX_SetChannelReqSubsysCb(WOLFSSH_CTX* ctx, } +int wolfSSH_CTX_SetChannelReqCb(WOLFSSH_CTX* ctx, + WS_CallbackChannelReqAny cb) +{ + int ret = WS_SSH_CTX_NULL_E; + + if (ctx != NULL) { + ctx->channelReqAnyCb = cb; + ret = WS_SUCCESS; + } + + return ret; +} + + int wolfSSH_CTX_SetAppChannels(WOLFSSH_CTX* ctx, byte enable) { int ret = WS_SSH_CTX_NULL_E; diff --git a/tests/regress.c b/tests/regress.c index c45c2899a..873db21a9 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -3958,6 +3958,379 @@ static void TestChannelReqSubsysCallbackRuns(void) WOLFSSH_SESSION_SUBSYSTEM), MSGID_CHANNEL_FAILURE); } +/* Builds a plaintext SSH_MSG_CHANNEL_REQUEST with a raw type-specific + * tail, so a test can send any request type. */ +static word32 BuildChannelRequestPacket(word32 recipientChannelId, + const char* type, byte wantReply, const byte* tail, word32 tailSz, + byte* out, word32 outSz) +{ + byte payload[128]; + word32 idx = 0; + + idx = AppendUint32(payload, sizeof(payload), idx, recipientChannelId); + idx = AppendString(payload, sizeof(payload), idx, type); + idx = AppendByte(payload, sizeof(payload), idx, wantReply); + idx = AppendData(payload, sizeof(payload), idx, tail, tailSz); + + return WrapPacket(MSGID_CHANNEL_REQUEST, payload, idx, out, outSz); +} + +/* Builds a plaintext SSH_MSG_GLOBAL_REQUEST with a raw type-specific + * tail. */ +static word32 BuildGlobalRequestPacket(const char* name, byte wantReply, + const byte* tail, word32 tailSz, byte* out, word32 outSz) +{ + byte payload[128]; + word32 idx = 0; + + idx = AppendString(payload, sizeof(payload), idx, name); + idx = AppendByte(payload, sizeof(payload), idx, wantReply); + idx = AppendData(payload, sizeof(payload), idx, tail, tailSz); + + return WrapPacket(MSGID_GLOBAL_REQUEST, payload, idx, out, outSz); +} + +/* What the generic request callbacks saw, and what they answer. */ +static int anyReqCbCalls; +static char anyReqCbName[32]; +static byte anyReqCbData[64]; +static word32 anyReqCbDataSz; +static int anyReqCbWantReply; +static void* anyReqCbCtx; +static int anyReqCbReturn; + +static void ResetAnyReqCb(int cbReturn) +{ + anyReqCbCalls = 0; + anyReqCbName[0] = 0; + anyReqCbDataSz = 0; + anyReqCbWantReply = -1; + anyReqCbCtx = NULL; + anyReqCbReturn = cbReturn; +} + +static void RecordAnyReq(const char* name, const byte* data, word32 dataSz, + void* ctx) +{ + anyReqCbCalls++; + WSTRNCPY(anyReqCbName, name, sizeof(anyReqCbName) - 1); + anyReqCbName[sizeof(anyReqCbName) - 1] = 0; + anyReqCbDataSz = dataSz; + if (dataSz > 0) { + AssertTrue(dataSz <= sizeof(anyReqCbData)); + WMEMCPY(anyReqCbData, data, dataSz); + } + anyReqCbCtx = ctx; +} + +static int RecordingChannelReqAnyCb(WOLFSSH_CHANNEL* channel, + const char* type, const byte* data, word32 dataSz, void* ctx) +{ + AssertNotNull(channel); + RecordAnyReq(type, data, dataSz, ctx); + return anyReqCbReturn; +} + +static int RecordingGlobalReqAnyCb(WOLFSSH* ssh, const char* name, + const byte* data, word32 dataSz, int wantReply, void* ctx) +{ + AssertNotNull(ssh); + RecordAnyReq(name, data, dataSz, ctx); + anyReqCbWantReply = wantReply; + return anyReqCbReturn; +} + +/* A typed session callback that only counts, to show whether the generic + * callback left the request to it. */ +static int typedReqCbCalls; + +static int CountingSessionReqCb(WOLFSSH_CHANNEL* channel, void* ctx) +{ + (void)channel; + (void)ctx; + typedReqCbCalls++; + return 0; +} + +/* Seeds a confirmed session channel on the harness, the state a channel is + * in when requests arrive on it. */ +static WOLFSSH_CHANNEL* SeedConfirmedSessionChannel( + ChannelOpenHarness* harness) +{ + WOLFSSH_CHANNEL* channel; + + channel = ChannelNew(harness->ssh, ID_CHANTYPE_SESSION, 1024, 1024); + AssertNotNull(channel); + AssertIntEQ(ChannelAppend(harness->ssh, channel), WS_SUCCESS); + AssertIntEQ(ChannelUpdatePeer(channel, 5, 1024, 1024), WS_SUCCESS); + channel->openConfirmed = 1; + + return channel; +} + +/* Feeds one packet to the harness and returns the id of the reply, or 0 + * when nothing was sent. */ +static byte ReplyToPacket(ChannelOpenHarness* harness, byte* in, + word32 inSz) +{ + RepointHarnessInput(harness, in, inSz); + AssertIntEQ(DoReceive(harness->ssh), WS_SUCCESS); + AssertIntEQ(harness->io.inOff, harness->io.inSz); + + return harness->io.outSz == 0 ? 0 : ParseMsgId(harness->io.out, + harness->io.outSz); +} + +/* The generic channel request callback sees every request first, with + * the type-specific part to parse itself, and can refuse a type the + * library would otherwise take in. */ +static void TestChannelReqCallbackSeesRequestAndRefuses(void) +{ + ChannelOpenHarness harness; + WOLFSSH_CHANNEL* channel; + byte tail[32]; + word32 tailSz = 0; + byte in[128]; + word32 inSz; + int cbCtx = 0; + + ResetAnyReqCb(WOLFSSH_REQ_REJECT); + InitChannelOpenHarness(&harness, NULL, 0); + AssertIntEQ(wolfSSH_CTX_SetChannelReqCb(harness.ctx, + RecordingChannelReqAnyCb), WS_SUCCESS); + AssertIntEQ(wolfSSH_SetChannelReqCtx(harness.ssh, &cbCtx), WS_SUCCESS); + channel = SeedConfirmedSessionChannel(&harness); + + tailSz = AppendString(tail, sizeof(tail), tailSz, "FOO"); + tailSz = AppendString(tail, sizeof(tail), tailSz, "bar"); + inSz = BuildChannelRequestPacket(channel->channel, "env", 1, + tail, tailSz, in, sizeof(in)); + + AssertIntEQ(ReplyToPacket(&harness, in, inSz), MSGID_CHANNEL_FAILURE); + AssertIntEQ(anyReqCbCalls, 1); + AssertIntEQ(WSTRCMP(anyReqCbName, "env"), 0); + AssertIntEQ(anyReqCbDataSz, tailSz); + AssertIntEQ(WMEMCMP(anyReqCbData, tail, tailSz), 0); + AssertTrue(anyReqCbCtx == &cbCtx); + + /* Left to the built-in handling, the same request is taken in. */ + anyReqCbReturn = WOLFSSH_REQ_UNHANDLED; + AssertIntEQ(ReplyToPacket(&harness, in, inSz), MSGID_CHANNEL_SUCCESS); + AssertIntEQ(anyReqCbCalls, 2); + + FreeChannelOpenHarness(&harness); +} + +/* A type the library does not know is refused unless the callback grants + * it, which is how an application answers its own request types. */ +static void TestChannelReqCallbackGrantsUnknownType(void) +{ + ChannelOpenHarness harness; + WOLFSSH_CHANNEL* channel; + static const byte tail[] = { 1, 2, 3 }; + byte in[128]; + word32 inSz; + + ResetAnyReqCb(WOLFSSH_REQ_ACCEPT); + InitChannelOpenHarness(&harness, NULL, 0); + AssertIntEQ(wolfSSH_CTX_SetChannelReqCb(harness.ctx, + RecordingChannelReqAnyCb), WS_SUCCESS); + channel = SeedConfirmedSessionChannel(&harness); + + inSz = BuildChannelRequestPacket(channel->channel, "x-custom@wolfssh", + 1, tail, sizeof(tail), in, sizeof(in)); + + AssertIntEQ(ReplyToPacket(&harness, in, inSz), MSGID_CHANNEL_SUCCESS); + AssertIntEQ(WSTRCMP(anyReqCbName, "x-custom@wolfssh"), 0); + AssertIntEQ(anyReqCbDataSz, sizeof(tail)); + + anyReqCbReturn = WOLFSSH_REQ_UNHANDLED; + AssertIntEQ(ReplyToPacket(&harness, in, inSz), MSGID_CHANNEL_FAILURE); + + FreeChannelOpenHarness(&harness); +} + +/* Drives one exec request through a fresh harness that registers both the + * generic and the typed exec callback, the generic one set to answer + * anyReturn, and returns the reply id. The harness is left for the caller + * to inspect and free. */ +static byte RunExecThroughBothCallbacks(ChannelOpenHarness* harness, + WOLFSSH_CHANNEL** channel, int anyReturn) +{ + byte tail[32]; + word32 tailSz = 0; + byte in[128]; + word32 inSz; + + ResetAnyReqCb(anyReturn); + typedReqCbCalls = 0; + InitChannelOpenHarness(harness, NULL, 0); + AssertIntEQ(wolfSSH_CTX_SetChannelReqCb(harness->ctx, + RecordingChannelReqAnyCb), WS_SUCCESS); + AssertIntEQ(wolfSSH_CTX_SetChannelReqExecCb(harness->ctx, + CountingSessionReqCb), WS_SUCCESS); + *channel = SeedConfirmedSessionChannel(harness); + + tailSz = AppendString(tail, sizeof(tail), tailSz, "ls"); + inSz = BuildChannelRequestPacket((*channel)->channel, "exec", 1, + tail, tailSz, in, sizeof(in)); + + return ReplyToPacket(harness, in, inSz); +} + +/* A session request the generic callback settles asks the typed callback + * nothing. A grant still commits the session, since the library needs the + * type and command whoever decided; a refusal commits nothing. */ +static void TestChannelReqCallbackSettlesSessionRequest(void) +{ + ChannelOpenHarness harness; + WOLFSSH_CHANNEL* channel; + + AssertIntEQ(RunExecThroughBothCallbacks(&harness, &channel, + WOLFSSH_REQ_ACCEPT), MSGID_CHANNEL_SUCCESS); + AssertIntEQ(typedReqCbCalls, 0); + AssertIntEQ(channel->sessionType, WOLFSSH_SESSION_EXEC); + AssertNotNull(channel->command); + AssertIntEQ(WSTRCMP(channel->command, "ls"), 0); + AssertIntEQ(harness.ssh->clientState, CLIENT_DONE); + FreeChannelOpenHarness(&harness); + + AssertIntEQ(RunExecThroughBothCallbacks(&harness, &channel, + WOLFSSH_REQ_REJECT), MSGID_CHANNEL_FAILURE); + AssertIntEQ(typedReqCbCalls, 0); + AssertIntEQ(channel->sessionType, WOLFSSH_SESSION_UNKNOWN); + AssertNull(channel->command); + AssertTrue(harness.ssh->clientState < CLIENT_DONE); + FreeChannelOpenHarness(&harness); + + AssertIntEQ(RunExecThroughBothCallbacks(&harness, &channel, + WOLFSSH_REQ_UNHANDLED), MSGID_CHANNEL_SUCCESS); + AssertIntEQ(typedReqCbCalls, 1); + AssertIntEQ(channel->sessionType, WOLFSSH_SESSION_EXEC); + FreeChannelOpenHarness(&harness); +} + +/* With application-driven channels on and no shell callback, a shell + * request is refused unless the generic callback grants it. */ +static void TestChannelReqCallbackGrantOverridesAppChannels(void) +{ + ChannelOpenHarness harness; + WOLFSSH_CHANNEL* channel; + byte in[128]; + word32 inSz; + + ResetAnyReqCb(WOLFSSH_REQ_UNHANDLED); + InitChannelOpenHarness(&harness, NULL, 0); + AssertIntEQ(wolfSSH_CTX_SetChannelReqCb(harness.ctx, + RecordingChannelReqAnyCb), WS_SUCCESS); + AssertIntEQ(wolfSSH_SetAppChannels(harness.ssh, 1), WS_SUCCESS); + channel = SeedConfirmedSessionChannel(&harness); + + inSz = BuildChannelRequestPacket(channel->channel, "shell", 1, + NULL, 0, in, sizeof(in)); + + AssertIntEQ(ReplyToPacket(&harness, in, inSz), MSGID_CHANNEL_FAILURE); + AssertIntEQ(channel->sessionType, WOLFSSH_SESSION_UNKNOWN); + + anyReqCbReturn = WOLFSSH_REQ_ACCEPT; + AssertIntEQ(ReplyToPacket(&harness, in, inSz), MSGID_CHANNEL_SUCCESS); + AssertIntEQ(channel->sessionType, WOLFSSH_SESSION_SHELL); + AssertIntEQ(harness.ssh->clientState, CLIENT_DONE); + + FreeChannelOpenHarness(&harness); +} + +/* The generic global request callback sees the name, the type-specific + * part and whether a reply is wanted, and its answer is the reply. Left + * unhandled, a name nothing else answers is refused as before. */ +static void TestGlobalReqCallbackSettlesRequest(void) +{ + ChannelOpenHarness harness; + static const byte tail[] = { 7, 8 }; + byte in[128]; + word32 inSz; + int cbCtx = 0; + + ResetAnyReqCb(WOLFSSH_REQ_ACCEPT); + InitChannelOpenHarness(&harness, NULL, 0); + AssertIntEQ(wolfSSH_CTX_SetGlobalReqCb(harness.ctx, + RecordingGlobalReqAnyCb), WS_SUCCESS); + wolfSSH_SetGlobalReqCtx(harness.ssh, &cbCtx); + + inSz = BuildGlobalRequestPacket("keepalive@openssh.com", 1, + tail, sizeof(tail), in, sizeof(in)); + + AssertIntEQ(ReplyToPacket(&harness, in, inSz), MSGID_REQUEST_SUCCESS); + AssertIntEQ(anyReqCbCalls, 1); + AssertIntEQ(WSTRCMP(anyReqCbName, "keepalive@openssh.com"), 0); + AssertIntEQ(anyReqCbDataSz, sizeof(tail)); + AssertIntEQ(WMEMCMP(anyReqCbData, tail, sizeof(tail)), 0); + AssertIntEQ(anyReqCbWantReply, 1); + AssertTrue(anyReqCbCtx == &cbCtx); + + anyReqCbReturn = WOLFSSH_REQ_REJECT; + AssertIntEQ(ReplyToPacket(&harness, in, inSz), MSGID_REQUEST_FAILURE); + + anyReqCbReturn = WOLFSSH_REQ_UNHANDLED; + AssertIntEQ(ReplyToPacket(&harness, in, inSz), MSGID_REQUEST_FAILURE); + + /* No reply wanted, none sent, whatever the answer. */ + anyReqCbReturn = WOLFSSH_REQ_REJECT; + inSz = BuildGlobalRequestPacket("keepalive@openssh.com", 0, + tail, sizeof(tail), in, sizeof(in)); + AssertIntEQ(ReplyToPacket(&harness, in, inSz), 0); + AssertIntEQ(anyReqCbWantReply, 0); + + FreeChannelOpenHarness(&harness); +} + +#ifdef WOLFSSH_FWD +/* A tcpip-forward the generic callback grants is answered without the + * forward callback, so a forward can be set up from either. A port-0 + * request is the exception: only the forward callback can report the + * port bound, so a grant there is refused. */ +static void TestGlobalReqCallbackAnswersTcpipForward(void) +{ + ChannelOpenHarness harness; + byte tail[32]; + word32 tailSz; + byte in[128]; + word32 inSz; + + ResetAnyReqCb(WOLFSSH_REQ_ACCEPT); + fwdCbCallCount = 0; + InitChannelOpenHarness(&harness, NULL, 0); + AssertIntEQ(wolfSSH_CTX_SetGlobalReqCb(harness.ctx, + RecordingGlobalReqAnyCb), WS_SUCCESS); + AssertIntEQ(wolfSSH_CTX_SetFwdCb(harness.ctx, CountingFwdCb, NULL), + WS_SUCCESS); + + tailSz = AppendString(tail, sizeof(tail), 0, "localhost"); + tailSz = AppendUint32(tail, sizeof(tail), tailSz, 8080); + inSz = BuildGlobalRequestPacket("tcpip-forward", 1, tail, tailSz, + in, sizeof(in)); + + AssertIntEQ(ReplyToPacket(&harness, in, inSz), MSGID_REQUEST_SUCCESS); + AssertIntEQ(anyReqCbCalls, 1); + AssertIntEQ(WSTRCMP(anyReqCbName, "tcpip-forward"), 0); + AssertIntEQ(fwdCbCallCount, 0); + + anyReqCbReturn = WOLFSSH_REQ_UNHANDLED; + AssertIntEQ(ReplyToPacket(&harness, in, inSz), MSGID_REQUEST_SUCCESS); + AssertIntEQ(fwdCbCallCount, 1); + + anyReqCbReturn = WOLFSSH_REQ_ACCEPT; + tailSz = AppendString(tail, sizeof(tail), 0, "localhost"); + tailSz = AppendUint32(tail, sizeof(tail), tailSz, 0); + inSz = BuildGlobalRequestPacket("tcpip-forward", 1, tail, tailSz, + in, sizeof(in)); + AssertIntEQ(ReplyToPacket(&harness, in, inSz), MSGID_REQUEST_FAILURE); + AssertIntEQ(fwdCbCallCount, 1); + + FreeChannelOpenHarness(&harness); +} +#endif /* WOLFSSH_FWD */ + /* What a length-aware session request callback saw. */ static word32 sessionReqCbCommandSz; static word32 sessionReqCbCommandStrLen; @@ -14777,6 +15150,14 @@ int main(int argc, char** argv) TestServerServiceRequestRejectedDuringKeying(); TestFailedSendClearsPendingPlaintext(); TestChannelOpenCallbackRejectSendsOpenFail(); + TestChannelReqCallbackSeesRequestAndRefuses(); + TestChannelReqCallbackGrantsUnknownType(); + TestChannelReqCallbackSettlesSessionRequest(); + TestChannelReqCallbackGrantOverridesAppChannels(); + TestGlobalReqCallbackSettlesRequest(); +#ifdef WOLFSSH_FWD + TestGlobalReqCallbackAnswersTcpipForward(); +#endif TestChannelOpenConfCallbackRuns(); TestChannelOpenFailCallbackRuns(); TestChannelOpenConfCallbackRejects(); diff --git a/wolfssh/internal.h b/wolfssh/internal.h index d3b6a07aa..255f81901 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -856,6 +856,7 @@ struct WOLFSSH_CTX { WS_CallbackUserAuthResult userAuthResultCb; /* User Authentication Result */ WS_CallbackHighwater highwaterCb; /* Data Highwater Mark Callback */ WS_CallbackGlobalReq globalReqCb; /* Global Request Callback */ + WS_CallbackGlobalReqAny globalReqAnyCb; /* Global Request, any name */ WS_CallbackReqSuccess reqSuccessCb; /* Global Request Success Callback */ WS_CallbackReqSuccess reqFailureCb; /* Global Request Failure Callback */ WS_CallbackChannelOpen channelOpenCb; /* Channel Open Requested */ @@ -864,6 +865,7 @@ struct WOLFSSH_CTX { WS_CallbackChannelReq channelReqShellCb; /* Channel Request "Shell" */ WS_CallbackChannelReq channelReqExecCb; /* Channel Request "Exec" */ WS_CallbackChannelReq channelReqSubsysCb; /* Channel Request "Subsystem" */ + WS_CallbackChannelReqAny channelReqAnyCb; /* Channel Request, any */ WS_CallbackChannelEof channelEofCb; /* Channel Eof Callback */ WS_CallbackChannelClose channelCloseCb; /* Channel Close Callback */ #ifdef WOLFSSH_SCP diff --git a/wolfssh/ssh.h b/wolfssh/ssh.h index 8a4ae0caf..c95e80072 100644 --- a/wolfssh/ssh.h +++ b/wolfssh/ssh.h @@ -463,6 +463,34 @@ 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); +/* What a request callback decides. UNHANDLED is what a missing callback + * answers, and leaves the request to the built-in handling. */ +typedef enum WS_ReqCbResult { + WOLFSSH_REQ_UNHANDLED = 0, + WOLFSSH_REQ_ACCEPT, + WOLFSSH_REQ_REJECT +} WS_ReqCbResult; + +/* Consulted first for every channel request, ahead of the three callbacks + * above and of the built-in handling, so a request with no callback of its + * own -- env, pty-req, window-change, exit-status, auth-agent-req, or a + * type the library does not know -- can be granted or refused by policy. + * type is the request name, NUL terminated, and data is the request's + * type-specific part, dataSz bytes, for the callback to parse. + * + * ACCEPT and REJECT settle the request, and the shell, exec and subsystem + * callbacks are not consulted. The library still parses and records what + * it needs from a request it knows, so a session request accepted here + * sets the channel's session type and the modes of an accepted pty-req are + * kept; a request that does not fit its type is refused whatever the + * callback said. A type the library does not know is answered + * CHANNEL_SUCCESS on ACCEPT, where it is otherwise refused. Shares the + * channel request context. */ +typedef int (*WS_CallbackChannelReqAny)(WOLFSSH_CHANNEL* channel, + const char* type, const byte* data, word32 dataSz, void* ctx); +WOLFSSH_API int wolfSSH_CTX_SetChannelReqCb(WOLFSSH_CTX* ctx, + WS_CallbackChannelReqAny cb); + /* Application-driven channel handling, server side, off by default. * * Off, wolfSSH_accept() runs the session state machine through to an @@ -534,6 +562,22 @@ WOLFSSH_API void wolfSSH_SetGlobalReq(WOLFSSH_CTX* ctx, WS_CallbackGlobalReq cb); WOLFSSH_API void wolfSSH_SetGlobalReqCtx(WOLFSSH* ssh, void* ctx); WOLFSSH_API void *wolfSSH_GetGlobalReqCtx(WOLFSSH* ssh); +/* Consulted first for every global request, ahead of the forward callback + * that answers tcpip-forward and cancel-tcpip-forward and of the callback + * above that answers the rest. name is the request name, NUL terminated, + * and data is the request's type-specific part, dataSz bytes, for the + * callback to parse, so a tcpip-forward can be set up from here without a + * forward callback. UNHANDLED leaves the request to those callbacks. + * ACCEPT and REJECT settle it, and no other callback is consulted; the + * reply, when one is wanted, is REQUEST_SUCCESS or REQUEST_FAILURE. A + * port-0 tcpip-forward has to be answered with the port bound, which only + * the forward callback can report, so ACCEPT on one is answered + * REQUEST_FAILURE. A client answers a tcpip-forward with failure before + * this runs, per RFC 4254 7.1. Shares the global request context. */ +typedef int (*WS_CallbackGlobalReqAny)(WOLFSSH* ssh, const char* name, + const byte* data, word32 dataSz, int wantReply, void* ctx); +WOLFSSH_API int wolfSSH_CTX_SetGlobalReqCb(WOLFSSH_CTX* ctx, + WS_CallbackGlobalReqAny cb); typedef int (*WS_CallbackReqSuccess)(WOLFSSH* ssh, void* buf, word32 sz, void* ctx); WOLFSSH_API void wolfSSH_SetReqSuccess(WOLFSSH_CTX* ctx,