ssh: add generic request callbacks

wolfSSH_CTX_SetChannelReqAnyCb() and wolfSSH_CTX_SetGlobalReqAnyCb()
register a callback consulted first for a channel or global request,
with the name, the type-specific part, and whether a reply is wanted. A
tri-state answer grants, refuses, or leaves it to the handling already
there, so a policy reaches the types with no hook of their own.

- the name is the one that arrived, since a copy into a buffer truncates
  a long name and ends it at an embedded NUL, and a policy has to answer
  on what the peer sent
- a grant still parses and records what the library needs, so a granted
  session request commits the session, the typed callbacks are not
  consulted, and a granted unknown type is answered CHANNEL_SUCCESS
- a port-0 tcpip-forward skips the callback, since only the forward
  callback can report the port bound and a policy that had bound a
  listener would then have to be refused, per RFC 4254 7.1
- a client refuses tcpip-forward and cancel-tcpip-forward ahead of any
  policy, matched on the name that arrived so it holds in a build with
  no forwarding, where neither name is in the name table
- window-change, exit-status and exit-signal are cleared of a reply
  before the handling runs, so a refusal leaves them unanswered too, per
  RFC 4254 6.7 and 6.10
- regress.c covers the answers, the data delivered, the names that do
  not fit a copy, and which callbacks each answer leaves out
pull/1251/head
John Safranek 2026-09-02 11:57:27 -07:00 committed by Paul Adelsbach
parent ba8996038d
commit 4477f4744a
5 changed files with 1235 additions and 59 deletions

View File

@ -12188,6 +12188,79 @@ static int DoGlobalRequestFwd(WOLFSSH* ssh,
}
#endif
/* Exact match on a global request name, as ChannelRequestIs() does for a
* channel request type. The name is the one that arrived, so the match is
* on length and bytes. */
static int GlobalRequestIs(const byte* name, word32 nameSz, const char* want)
{
word32 wantSz = (word32)WSTRLEN(want);
return (nameSz == wantSz) && (WMEMCMP(name, want, wantSz) == 0);
}
/* Whether a tcpip-forward is the built-in handling's to answer rather than
* the generic callback's. RFC 4254 7.1: a port-0 request is answered with
* the port bound, which only the forward callback can report, and one
* whose body does not parse has no port to read. Granting either in the
* callback would mean refusing it afterwards, once the callback may
* already have bound a listener. */
static int GlobalRequestNeedsFwdCb(const byte* name, word32 nameSz,
const byte* buf, word32 len, word32 begin)
{
const byte* bindAddr;
word32 bindAddrSz, bindPort = 0, peek = begin;
if (!GlobalRequestIs(name, nameSz, "tcpip-forward")) {
return 0;
}
if (GetStringRef(&bindAddrSz, &bindAddr, buf, len, &peek) != WS_SUCCESS
|| GetUint32(&bindPort, buf, len, &peek) != WS_SUCCESS) {
return 1;
}
return bindPort == 0;
}
/* 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. The name is the one
* that arrived, nameSz bytes, not the truncated copy the older callback
* is handed. */
static int DoGlobalRequestAny(WOLFSSH* ssh, const byte* name, word32 nameSz,
byte* buf, word32 len, word32 begin, byte wantReply, int* ret)
{
int decision, success;
if (ssh->ctx->globalReqAnyCb == NULL) {
return 0;
}
if (GlobalRequestNeedsFwdCb(name, nameSz, buf, len, begin)) {
WLOG(WS_LOG_DEBUG, "DGR: a port-0 forward is left to the handling "
"that can bind it");
return 0;
}
decision = ssh->ctx->globalReqAnyCb(ssh, name, nameSz, buf + begin,
len - begin, wantReply, ssh->globalReqCtx);
if (decision != WOLFSSH_REQ_ACCEPT && decision != WOLFSSH_REQ_REJECT) {
return 0;
}
success = (decision == WOLFSSH_REQ_ACCEPT);
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)
{
@ -12195,6 +12268,8 @@ static int DoGlobalRequest(WOLFSSH* ssh,
int ret = WS_SUCCESS;
char name[80];
word32 nameSz = (word32)sizeof(name);
const byte* wireName;
word32 wireNameSz;
int globReqId = ID_UNKNOWN;
byte wantReply = 0;
@ -12207,8 +12282,19 @@ static int DoGlobalRequest(WOLFSSH* ssh,
}
if (ret == WS_SUCCESS) {
word32 copyBegin;
/* Read twice: the generic callback answers on the name as it
* arrived, where the callback below it has always been handed a
* copy, NUL terminated and truncated to the buffer. */
begin = *idx;
ret = GetString(name, &nameSz, buf, len, &begin);
copyBegin = begin;
ret = GetStringRef(&wireNameSz, &wireName, buf, len, &begin);
if (ret == WS_SUCCESS) {
if (wireName == NULL)
wireName = (const byte*)""; /* an empty name parses as NULL */
ret = GetString(name, &nameSz, buf, len, &copyBegin);
}
}
if (ret == WS_SUCCESS) {
@ -12218,47 +12304,53 @@ static int DoGlobalRequest(WOLFSSH* ssh,
}
if (ret == WS_SUCCESS) {
#ifdef WOLFSSH_FWD
/* RFC 4254 section 7.1: a remote forward is the client's to ask for,
* so a client that receives the request answers a failure rather than
* registering a forward on the peer's say-so. Answered here and not
* in DoGlobalRequestFwd(), so the request body is never parsed and no
* forward state is touched. */
if ((globReqId == ID_GLOBREQ_TCPIP_FWD
|| globReqId == ID_GLOBREQ_TCPIP_FWD_CANCEL)
* forward state is touched. Matched on the name as it arrived rather
* than on an ID: the name table carries these two only in a
* WOLFSSH_FWD build, while a client answers them in any build. */
if ((GlobalRequestIs(wireName, wireNameSz, "tcpip-forward")
|| GlobalRequestIs(wireName, wireNameSz,
"cancel-tcpip-forward"))
&& ssh->ctx->side == WOLFSSH_ENDPOINT_CLIENT) {
WLOG(WS_LOG_WARN, "DGR: rejecting %s received by a client", name);
if (wantReply) {
ret = SendRequestSuccess(ssh, 0);
}
}
else
#endif
switch (globReqId) {
else if (!DoGlobalRequestAny(ssh, wireName, wireNameSz,
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;
}
}
}
@ -13074,12 +13166,14 @@ int wolfSSH_DoModes(const byte* modes, word32 modesSz, int fd)
#endif /* !NO_TERMIOS && WOLFSSH_TERM */
/* Exact match on a channel request type, as NameToIdType() does for names. */
static int ChannelRequestIs(const char* type, word32 typeSz, const char* name)
/* Exact match on a channel request type, as NameToIdType() does for names.
* The type is the name as it arrived, so the match is on length and bytes;
* a type with an embedded NUL is some other type. */
static int ChannelRequestIs(const byte* type, word32 typeSz, const char* name)
{
word32 nameSz = (word32)WSTRLEN(name);
return (typeSz == nameSz) && (WSTRNCMP(type, name, nameSz) == 0);
return (typeSz == nameSz) && (WMEMCMP(type, name, nameSz) == 0);
}
@ -13101,10 +13195,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;
@ -13135,7 +13230,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,17 +13272,22 @@ static int DoChannelRequest(WOLFSSH* ssh,
word32 begin = *idx;
word32 channelId;
word32 typeSz;
char type[32];
const byte* type;
byte wantReply;
int ret, rej = 0;
int ret, rej = 0, granted = 0;
WLOG(WS_LOG_DEBUG, "Entering DoChannelRequest()");
ret = GetUint32(&channelId, buf, len, &begin);
typeSz = (word32)sizeof(type);
if (ret == WS_SUCCESS)
ret = GetString(type, &typeSz, buf, len, &begin);
/* Taken as it arrived rather than copied into a buffer, which would
* truncate a long type and end a type at an embedded NUL. The generic
* callback below answers on what the peer actually sent. */
if (ret == WS_SUCCESS) {
ret = GetStringRef(&typeSz, &type, buf, len, &begin);
if (ret == WS_SUCCESS && type == NULL)
type = (const byte*)""; /* an empty type parses as NULL */
}
if (ret == WS_SUCCESS)
ret = GetBoolean(&wantReply, buf, len, &begin);
@ -13202,9 +13304,45 @@ static int DoChannelRequest(WOLFSSH* ssh,
if (ret == WS_SUCCESS) {
WLOG(WS_LOG_DEBUG, " channelId = %u", channelId);
WLOG(WS_LOG_DEBUG, " type = %s", type);
WLOG(WS_LOG_DEBUG, " type = %.*s", (int)typeSz, 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, typeSz,
buf + begin, len - begin, wantReply,
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 defined(WOLFSSH_TERM) || defined(WOLFSSH_SHELL)
/* RFC 4254 sec 6.10: these are never answered, whatever the handling
* below makes of them. Cleared here rather than in each branch, which
* a refusal or a freed channel skips. */
if (ret == WS_SUCCESS
&& (ChannelRequestIs(type, typeSz, "exit-status")
|| ChannelRequestIs(type, typeSz, "exit-signal"))) {
wantReply = 0;
}
#endif
#if defined(WOLFSSH_SHELL) && defined(WOLFSSH_TERM)
/* RFC 4254 sec 6.7: the same for window-change. */
if (ret == WS_SUCCESS
&& ChannelRequestIs(type, typeSz, "window-change")) {
wantReply = 0;
}
#endif
if (ret == WS_SUCCESS && !rej) {
if (ChannelRequestIs(type, typeSz, "env")) {
char name[WOLFSSH_MAX_NAMESZ];
word32 nameSz;
@ -13229,17 +13367,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")) {
@ -13286,7 +13424,6 @@ static int DoChannelRequest(WOLFSSH* ssh,
else if (ChannelRequestIs(type, typeSz, "window-change")) {
word32 widthChar, heightRows, widthPixels, heightPixels;
wantReply = 0; /* RFC 4254 sec 6.7: no reply for window-change */
ret = GetUint32(&widthChar, buf, len, &begin);
if (ret == WS_SUCCESS)
ret = GetUint32(&heightRows, buf, len, &begin);
@ -13321,7 +13458,6 @@ static int DoChannelRequest(WOLFSSH* ssh,
#endif /* WOLFSSH_SHELL && WOLFSSH_TERM */
#if defined(WOLFSSH_TERM) || defined(WOLFSSH_SHELL)
else if (ChannelRequestIs(type, typeSz, "exit-status")) {
wantReply = 0; /* RFC 4254 sec 6.10: no reply for exit-status */
ret = GetUint32(&ssh->exitStatus, buf, len, &begin);
WLOG(WS_LOG_AGENT, "Got exit status %u.", ssh->exitStatus);
}
@ -13330,7 +13466,6 @@ static int DoChannelRequest(WOLFSSH* ssh,
word32 sigSz;
byte coreDumped;
wantReply = 0; /* RFC 4254 sec 6.10: no reply for exit-signal */
WLOG(WS_LOG_AGENT, "Got exit signal, remote command terminated");
sigSz = WOLFSSH_MAX_NAMESZ;
@ -13364,6 +13499,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;

View File

@ -397,6 +397,19 @@ void wolfSSH_SetReqFailure(WOLFSSH_CTX *ctx, WS_CallbackReqSuccess cb)
ctx->reqFailureCb = cb;
}
int wolfSSH_CTX_SetGlobalReqAnyCb(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()");
@ -5830,6 +5843,20 @@ int wolfSSH_CTX_SetChannelReqSubsysCb(WOLFSSH_CTX* ctx,
}
int wolfSSH_CTX_SetChannelReqAnyCb(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;

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -475,6 +475,45 @@ 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.
*
* Note that 0 is UNHANDLED here, where the older request callbacks above
* read a 0 return as acceptance. A callback of this family returns one of
* these three and not WS_SUCCESS: returning 0 out of habit leaves the
* request to the handling below, which refuses a type the library does
* not know. */
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 as it arrived, typeSz bytes, and data is the
* request's type-specific part, dataSz bytes, for the callback to parse.
* Neither is NUL terminated, and a name may hold any byte, so a policy
* matches on typeSz bytes rather than with the string functions.
* wantReply is what the peer asked for, before the library clears it for
* the types RFC 4254 never answers.
*
* 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 byte* type, word32 typeSz, const byte* data, word32 dataSz,
int wantReply, void* ctx);
WOLFSSH_API int wolfSSH_CTX_SetChannelReqAnyCb(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
@ -546,6 +585,40 @@ 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 a global request, ahead of the forward callback
* that answers tcpip-forward and cancel-tcpip-forward and of the callback
* above that answers the rest, but for the two requests named below.
* name is the request name as it arrived, nameSz bytes, and data is the
* request's type-specific part, dataSz bytes, for the callback to parse,
* so a tcpip-forward naming a port can be set up from here without a
* forward callback. Neither is NUL terminated, and a name may hold any
* byte, so a policy matches on nameSz bytes rather than with the string
* functions. 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.
*
* Two requests never reach this callback, both per RFC 4254 7.1. A
* tcpip-forward asking for port 0 is answered with the port bound, which
* only the forward callback can report, so it is left to the handling
* that can bind it; so is one whose body does not parse, having no port
* to read. Were a policy asked about either, a grant would have to be
* refused once the policy had already bound a listener. And a client
* answers tcpip-forward and cancel-tcpip-forward with a failure, in any
* build, whatever a policy would make of them.
*
* Shares the global request context.
*
* name and data point into the session's input buffer and are good only
* for the length of the call, so a callback keeping either copies it.
* The packet is still being parsed, so the callback must not re-enter
* the receive side of the library on this session -- wolfSSH_worker(),
* wolfSSH_stream_read(), wolfSSH_accept(), the SFTP calls -- which may
* grow or compact that buffer and leave both pointers behind. */
typedef int (*WS_CallbackGlobalReqAny)(WOLFSSH* ssh, const byte* name,
word32 nameSz, const byte* data, word32 dataSz, int wantReply,
void* ctx);
WOLFSSH_API int wolfSSH_CTX_SetGlobalReqAnyCb(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,