From e56e763de016bf5ae7029828f820bd3b3f824f77 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Wed, 24 Jun 2026 16:44:50 -0700 Subject: [PATCH] Add client-side remote port forwarding - Add wolfSSH_FwdRemoteSetup/Cancel to request tcpip-forward - Add SendGlobalRequestFwd to frame the request per RFC 4254 - Add portfwd -r reverse mode using fwd and req-success cbs Issue: ZD-28167 --- examples/portfwd/portfwd.c | 176 ++++++++++++++++++++++++++++++++++--- src/internal.c | 61 +++++++++++++ src/ssh.c | 33 +++++++ wolfssh/internal.h | 4 + wolfssh/ssh.h | 4 + 5 files changed, 266 insertions(+), 12 deletions(-) diff --git a/examples/portfwd/portfwd.c b/examples/portfwd/portfwd.c index 355e5adb4..4425465de 100644 --- a/examples/portfwd/portfwd.c +++ b/examples/portfwd/portfwd.c @@ -88,7 +88,10 @@ static void ShowUsage(void) " -F host to forward from, default %s\n" " -f host port to forward from (REQUIRED)\n" " -T host to forward to, default to host\n" - " -t port to forward to (REQUIRED)\n", + " -t port to forward to (REQUIRED)\n" + " -r remote (reverse) forward: ask the SSH server to\n" + " listen on -F/-f and tunnel connections back to\n" + " the local -T/-t target\n", LIBWOLFSSH_VERSION_STRING, LIBWOLFSSL_VERSION_STRING, wolfSshIp, wolfSshPort, defaultFwdFromHost); @@ -212,6 +215,102 @@ static int wsPublicKeyCheck(const byte* pubKey, word32 pubKeySz, void* ctx) } +/* State shared with the remote-forward callbacks. A single reverse connection + * is supported here; a production tool would key a table by channel id. */ +typedef struct PortfwdState { + const char* fwdToHost; /* local target to connect inbound channels to */ + word16 fwdToPort; + SOCKET_T appFd; /* socket to the local target, -1 when idle */ + word32 channelId; /* id of the inbound forwarded-tcpip channel */ + int pending; /* a new channel is waiting to be wired up */ + word16 boundPort; /* port the peer reported binding */ +} PortfwdState; + + +/* Open a TCP connection to the local forward target. Returns -1 on failure. */ +static SOCKET_T connectTarget(const char* host, word16 port) +{ + SOCKADDR_IN_T addr; + SOCKET_T fd; + + build_addr(&addr, host, port); + tcp_socket(&fd, ((struct sockaddr_in*)&addr)->sin_family); + if (connect(fd, (const struct sockaddr*)&addr, sizeof(addr)) != 0) { + WCLOSESOCKET(fd); + return (SOCKET_T)-1; + } + return fd; +} + + +/* Forwarding callback for client-side remote forwarding. The peer opens a + * forwarded-tcpip channel for each inbound connection on its listener; the + * library reports it here as a LOCAL_SETUP followed by a CHANNEL_ID. */ +static int portfwdRemoteFwdCb(WS_FwdCbAction action, void* ctx, + const char* address, word32 port) +{ + PortfwdState* st = (PortfwdState*)ctx; + int ret = WS_FWD_SUCCESS; + + switch (action) { + case WOLFSSH_FWD_LOCAL_SETUP: + /* address/port describe the peer's bound listener, not the local + * target, so connect to the configured forward-to address. */ + (void)address; + (void)port; + st->appFd = connectTarget(st->fwdToHost, st->fwdToPort); + if (st->appFd == (SOCKET_T)-1) { + printf("Couldn't connect to forward target %s:%u\n", + st->fwdToHost, st->fwdToPort); + ret = WS_FWD_SETUP_E; + } + break; + case WOLFSSH_FWD_CHANNEL_ID: + /* The new channel's id arrives in the port argument. */ + st->channelId = port; + st->pending = 1; + break; + case WOLFSSH_FWD_LOCAL_CLEANUP: + (void)address; + (void)port; + if (st->appFd != (SOCKET_T)-1) { + WCLOSESOCKET(st->appFd); + st->appFd = (SOCKET_T)-1; + } + break; + case WOLFSSH_FWD_REMOTE_SETUP: + case WOLFSSH_FWD_REMOTE_CLEANUP: + /* Server-side actions; a client requesting remote forwarding does + * not receive these. */ + default: + break; + } + + return ret; +} + + +/* Request-success callback. The reply to a want-reply tcpip-forward carries the + * bound port (relevant when port 0 was requested). */ +static int portfwdReqSuccessCb(WOLFSSH* ssh, void* buf, word32 sz, void* ctx) +{ + PortfwdState* st = (PortfwdState*)ctx; + const byte* p = (const byte*)buf; + + (void)ssh; + if (p != NULL && sz >= 4) { + word32 boundPort = ((word32)p[0] << 24) | ((word32)p[1] << 16) | + ((word32)p[2] << 8) | (word32)p[3]; + st->boundPort = (word16)boundPort; + printf("Remote forward established; peer bound port %u\n", boundPort); + } + else { + printf("Remote forward established.\n"); + } + return WS_SUCCESS; +} + + /* * fwdFromHost - address to bind the local listener socket to (default: any) * fwdFromHostPort - port number to bind the local listener socket to @@ -241,7 +340,7 @@ THREAD_RETURN WOLFSSH_THREAD portfwd_worker(void* args) SOCKET_T sshFd; SOCKADDR_IN_T fwdFromHostAddr; socklen_t fwdFromHostAddrSz = sizeof(fwdFromHostAddr); - SOCKET_T listenFd; + SOCKET_T listenFd = -1; SOCKET_T appFd = -1; int argc = ((func_args*)args)->argc; char** argv = ((func_args*)args)->argv; @@ -252,6 +351,8 @@ THREAD_RETURN WOLFSSH_THREAD portfwd_worker(void* args) int ret; int ch; int appFdSet = 0; + int reverse = 0; + PortfwdState fwdState; struct timeval to; WOLFSSH_CHANNEL* fwdChannel = NULL; byte* appBuffer = NULL; @@ -267,7 +368,7 @@ THREAD_RETURN WOLFSSH_THREAD portfwd_worker(void* args) ((func_args*)args)->return_code = 0; - while ((ch = mygetopt(argc, argv, "?f:h:p:t:u:F:P:R:T:")) != -1) { + while ((ch = mygetopt(argc, argv, "?rf:h:p:t:u:F:P:R:T:")) != -1) { switch (ch) { case 'h': host = myoptarg; @@ -315,6 +416,10 @@ THREAD_RETURN WOLFSSH_THREAD portfwd_worker(void* args) fwdToHost = myoptarg; break; + case 'r': + reverse = 1; + break; + case '?': ShowUsage(); exit(EXIT_SUCCESS); @@ -386,15 +491,32 @@ THREAD_RETURN WOLFSSH_THREAD portfwd_worker(void* args) if (ret != WS_SUCCESS) err_sys("Couldn't set the username."); + if (reverse) { + /* The peer (SSH server) does the listening; we receive its inbound + * forwarded-tcpip channels through the forwarding callback and the + * bound-port reply through the request-success callback. */ + memset(&fwdState, 0, sizeof(fwdState)); + fwdState.fwdToHost = fwdToHost; + fwdState.fwdToPort = fwdToPort; + fwdState.appFd = (SOCKET_T)-1; + wolfSSH_CTX_SetFwdCb(ctx, portfwdRemoteFwdCb, NULL); + wolfSSH_SetFwdCbCtx(ssh, &fwdState); + wolfSSH_SetReqSuccess(ctx, portfwdReqSuccessCb); + wolfSSH_SetReqSuccessCtx(ssh, &fwdState); + } + /* Socket to SSH peer. */ build_addr(&hostAddr, host, port); tcp_socket(&sshFd, ((struct sockaddr_in *)&hostAddr)->sin_family); - /* Receive from client application or connect to server application. */ - build_addr(&fwdFromHostAddr, fwdFromHost, fwdFromPort); - tcp_socket(&listenFd, ((struct sockaddr_in *)&fwdFromHostAddr)->sin_family); + if (!reverse) { + /* Receive from client application or connect to server application. */ + build_addr(&fwdFromHostAddr, fwdFromHost, fwdFromPort); + tcp_socket(&listenFd, + ((struct sockaddr_in *)&fwdFromHostAddr)->sin_family); - tcp_listen(&listenFd, &fwdFromPort, 1); + tcp_listen(&listenFd, &fwdFromPort, 1); + } printf("Connecting to the SSH server...\n"); ret = connect(sshFd, (const struct sockaddr *)&hostAddr, hostAddrSz); @@ -409,6 +531,13 @@ THREAD_RETURN WOLFSSH_THREAD portfwd_worker(void* args) if (ret != WS_SUCCESS) err_sys("Couldn't connect SFTP"); + if (reverse) { + /* Ask the server to open a listener and tunnel connections back. */ + ret = wolfSSH_FwdRemoteSetup(ssh, fwdFromHost, fwdFromPort, 1); + if (ret != WS_SUCCESS) + err_sys("Couldn't request remote port forward."); + } + if (readyFile != NULL) { #ifndef NO_FILESYSTEM WFILE* f = NULL; @@ -430,8 +559,13 @@ THREAD_RETURN WOLFSSH_THREAD portfwd_worker(void* args) FD_ZERO(&templateFds); FD_SET(sshFd, &templateFds); - FD_SET(listenFd, &templateFds); - nFds = findMax(sshFd, listenFd) + 1; + if (!reverse) { + FD_SET(listenFd, &templateFds); + nFds = findMax(sshFd, listenFd) + 1; + } + else { + nFds = (int)sshFd + 1; + } for (;;) { rxFds = templateFds; @@ -451,7 +585,7 @@ THREAD_RETURN WOLFSSH_THREAD portfwd_worker(void* args) if ((appFdSet && FD_ISSET(appFd, &errFds)) || FD_ISSET(sshFd, &errFds) || - FD_ISSET(listenFd, &errFds)) { + (!reverse && FD_ISSET(listenFd, &errFds))) { err_sys("some socket had an error"); } @@ -488,8 +622,22 @@ THREAD_RETURN WOLFSSH_THREAD portfwd_worker(void* args) } } } + + /* A reverse channel may have been opened by the peer while the + * worker ran. Wire its local target socket into the select set. */ + if (reverse && fwdState.pending && !appFdSet) { + appFd = fwdState.appFd; + fwdChannel = wolfSSH_ChannelFind(ssh, fwdState.channelId, + WS_CHANNEL_ID_SELF); + if (appFd != (SOCKET_T)-1 && fwdChannel != NULL) { + FD_SET(appFd, &templateFds); + nFds = findMax((int)sshFd, (int)appFd) + 1; + appFdSet = 1; + } + fwdState.pending = 0; + } } - if (!appFdSet && FD_ISSET(listenFd, &rxFds)) { + if (!reverse && !appFdSet && FD_ISSET(listenFd, &rxFds)) { appFd = accept(listenFd, (struct sockaddr*)&fwdFromHostAddr, &fwdFromHostAddrSz); if (appFd < 0) @@ -513,12 +661,16 @@ THREAD_RETURN WOLFSSH_THREAD portfwd_worker(void* args) } } + if (reverse) + wolfSSH_FwdRemoteCancel(ssh, fwdFromHost, fwdFromPort, 0); + ret = wolfSSH_shutdown(ssh); if (ret != WS_SUCCESS) err_sys("Closing port forward stream failed."); WCLOSESOCKET(sshFd); - WCLOSESOCKET(listenFd); + if (listenFd != (SOCKET_T)-1) + WCLOSESOCKET(listenFd); WCLOSESOCKET(appFd); wolfSSH_free(ssh); wolfSSH_CTX_free(ctx); diff --git a/src/internal.c b/src/internal.c index 22c070ca9..0f166a83b 100644 --- a/src/internal.c +++ b/src/internal.c @@ -14409,6 +14409,67 @@ int SendGlobalRequest(WOLFSSH* ssh, return ret; } + +#ifdef WOLFSSH_FWD +/* Send a "tcpip-forward" (or "cancel-tcpip-forward") global request asking the + * peer to set up (or tear down) a remote listener. The request-specific data + * follows the want-reply boolean, so the generic SendGlobalRequest() framing + * (which places the boolean last) cannot express it. See RFC 4254 7.1. */ +int SendGlobalRequestFwd(WOLFSSH* ssh, + const char* bindAddr, word32 bindPort, int isCancel, int wantReply) +{ + byte* output; + word32 idx = 0; + word32 reqNameSz; + word32 bindAddrSz; + const char* reqName; + int ret = WS_SUCCESS; + + WLOG(WS_LOG_DEBUG, "Entering SendGlobalRequestFwd()"); + + if (ssh == NULL || bindAddr == NULL) + ret = WS_BAD_ARGUMENT; + + if (ret == WS_SUCCESS) { + reqName = isCancel ? "cancel-tcpip-forward" : "tcpip-forward"; + reqNameSz = (word32)WSTRLEN(reqName); + bindAddrSz = (word32)WSTRLEN(bindAddr); + + ret = PreparePacket(ssh, MSG_ID_SZ + LENGTH_SZ + reqNameSz + BOOLEAN_SZ + + LENGTH_SZ + bindAddrSz + UINT32_SZ); + } + + if (ret == WS_SUCCESS) { + output = ssh->outputBuffer.buffer; + idx = ssh->outputBuffer.length; + + output[idx++] = MSGID_GLOBAL_REQUEST; + c32toa(reqNameSz, output + idx); + idx += LENGTH_SZ; + WMEMCPY(output + idx, reqName, reqNameSz); + idx += reqNameSz; + output[idx++] = (byte)(wantReply != 0); + c32toa(bindAddrSz, output + idx); + idx += LENGTH_SZ; + WMEMCPY(output + idx, bindAddr, bindAddrSz); + idx += bindAddrSz; + c32toa(bindPort, output + idx); + idx += UINT32_SZ; + + ssh->outputBuffer.length = idx; + + ret = BundlePacket(ssh); + } + + if (ret == WS_SUCCESS) + ret = wolfSSH_SendPacket(ssh); + + WLOG(WS_LOG_DEBUG, "Leaving SendGlobalRequestFwd(), ret = %d", ret); + + return ret; +} +#endif /* WOLFSSH_FWD */ + static const char cannedLangTag[] = "en-us"; static const word32 cannedLangTagSz = (word32)sizeof(cannedLangTag) - 1; diff --git a/src/ssh.c b/src/ssh.c index 45761d808..a8881b78a 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -2875,6 +2875,39 @@ WOLFSSH_CHANNEL* wolfSSH_ChannelFwdNew(WOLFSSH* ssh, return wolfSSH_ChannelFwdNewLocal(ssh, host, hostPort, origin, originPort); } + +/* Ask the peer to open a remote listener (client-side remote port forwarding). + * Sends a "tcpip-forward" global request for bindAddr:bindPort. A bindPort of 0 + * asks the peer to choose a port; with wantReply set the peer reports it back + * through the request-success callback (wolfSSH_SetReqSuccess). The peer then + * opens a "forwarded-tcpip" channel for each inbound connection, delivered + * through the forwarding callback (wolfSSH_CTX_SetFwdCb). */ +int wolfSSH_FwdRemoteSetup(WOLFSSH* ssh, const char* bindAddr, + word32 bindPort, int wantReply) +{ + WLOG(WS_LOG_DEBUG, "Entering wolfSSH_FwdRemoteSetup()"); + if (ssh == NULL || bindAddr == NULL) + return WS_BAD_ARGUMENT; + if (wantReply != 0 && wantReply != 1) + return WS_BAD_ARGUMENT; + return SendGlobalRequestFwd(ssh, bindAddr, bindPort, 0, wantReply); +} + + +/* Tear down a remote listener previously set up with wolfSSH_FwdRemoteSetup(). + * Sends a "cancel-tcpip-forward" global request for the same bindAddr:bindPort + * that was requested. */ +int wolfSSH_FwdRemoteCancel(WOLFSSH* ssh, const char* bindAddr, + word32 bindPort, int wantReply) +{ + WLOG(WS_LOG_DEBUG, "Entering wolfSSH_FwdRemoteCancel()"); + if (ssh == NULL || bindAddr == NULL) + return WS_BAD_ARGUMENT; + if (wantReply != 0 && wantReply != 1) + return WS_BAD_ARGUMENT; + return SendGlobalRequestFwd(ssh, bindAddr, bindPort, 1, wantReply); +} + #endif /* WOLFSSH_FWD */ diff --git a/wolfssh/internal.h b/wolfssh/internal.h index 66437b021..59a49a4ed 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -1148,6 +1148,10 @@ WOLFSSH_LOCAL int SendGlobalRequestFwdSuccess(WOLFSSH * ssh, int success, word32 port); WOLFSSH_LOCAL int SendGlobalRequest(WOLFSSH * ssh, const unsigned char * data, word32 dataSz, int reply); +#ifdef WOLFSSH_FWD +WOLFSSH_LOCAL int SendGlobalRequestFwd(WOLFSSH* ssh, + const char* bindAddr, word32 bindPort, int isCancel, int wantReply); +#endif WOLFSSH_LOCAL int SendDebug(WOLFSSH* ssh, byte alwaysDisplay, const char* msg); WOLFSSH_LOCAL int SendServiceRequest(WOLFSSH* ssh, byte serviceId); WOLFSSH_LOCAL int SendServiceAccept(WOLFSSH* ssh, byte serviceId); diff --git a/wolfssh/ssh.h b/wolfssh/ssh.h index 6ce19aa9d..fefa1b358 100644 --- a/wolfssh/ssh.h +++ b/wolfssh/ssh.h @@ -245,6 +245,10 @@ DEPRECATED WOLFSSH_API int wolfSSH_ChannelSetFwdFd(WOLFSSH_CHANNEL* channel, int fwdFd); DEPRECATED WOLFSSH_API int wolfSSH_ChannelGetFwdFd( const WOLFSSH_CHANNEL* channel); +WOLFSSH_API int wolfSSH_FwdRemoteSetup(WOLFSSH* ssh, const char* bindAddr, + word32 bindPort, int wantReply); +WOLFSSH_API int wolfSSH_FwdRemoteCancel(WOLFSSH* ssh, const char* bindAddr, + word32 bindPort, int wantReply); WOLFSSH_API int wolfSSH_ChannelFree(WOLFSSH_CHANNEL* channel); WOLFSSH_API int wolfSSH_ChannelGetId(WOLFSSH_CHANNEL* channel, word32* id,