diff --git a/examples/echoserver/echoserver.c b/examples/echoserver/echoserver.c index 8ce9c1528..6149c9159 100644 --- a/examples/echoserver/echoserver.c +++ b/examples/echoserver/echoserver.c @@ -531,6 +531,19 @@ static int wolfSSH_FwdDefaultActions(WS_FwdCbAction action, void* vCtx, ret = -1; } + #ifndef USE_WINDOWS_API + if (ret == 0) { + /* A forward torn down with a connection open leaves this port in + * TIME_WAIT, which would fail the next bind. tcp_listen() sets + * this for the other listeners; do the same here. */ + int on = 1; + if (setsockopt(appCtx->listenFd, SOL_SOCKET, SO_REUSEADDR, + &on, (socklen_t)sizeof(on)) < 0) { + ret = -1; + } + } + #endif + if (ret == 0) { WMEMSET(&addr, 0, sizeof addr); @@ -873,7 +886,6 @@ static int ssh_worker(thread_ctx_t* threadCtx) #endif #ifdef WOLFSSH_FWD WS_SOCKET_T fwdFd = -1; - WS_SOCKET_T fwdListenFd = threadCtx->fwdCtx.listenFd; word32 fwdBufferIdx = 0; #endif @@ -955,10 +967,13 @@ static int ssh_worker(thread_ctx_t* threadCtx) } #endif /* WOLFSSH_AGENT */ #ifdef WOLFSSH_FWD - if (threadCtx->fwdCtx.state == APP_STATE_LISTEN) { - FD_SET(fwdListenFd, &readFds); - if (fwdListenFd > maxFd) - maxFd = fwdListenFd; + /* The fwd callback creates this listener mid-loop; re-read it + * each pass rather than caching it. */ + if (threadCtx->fwdCtx.state == APP_STATE_LISTEN + && threadCtx->fwdCtx.listenFd >= 0) { + FD_SET(threadCtx->fwdCtx.listenFd, &readFds); + if (threadCtx->fwdCtx.listenFd > maxFd) + maxFd = threadCtx->fwdCtx.listenFd; } if (fwdFd >= 0 && threadCtx->fwdCtx.state == APP_STATE_CONNECTED) { @@ -1250,12 +1265,13 @@ static int ssh_worker(thread_ctx_t* threadCtx) } } } - if (threadCtx->fwdCtx.state == APP_STATE_LISTEN) { - if (FD_ISSET(fwdListenFd, &readFds)) { + if (threadCtx->fwdCtx.state == APP_STATE_LISTEN + && threadCtx->fwdCtx.listenFd >= 0) { + if (FD_ISSET(threadCtx->fwdCtx.listenFd, &readFds)) { #ifdef SHELL_DEBUG printf("accepting fwd connection\n"); #endif - fwdFd = accept(fwdListenFd, NULL, NULL); + fwdFd = accept(threadCtx->fwdCtx.listenFd, NULL, NULL); if (fwdFd == -1) { rc = errno; if (rc != SOCKET_EWOULDBLOCK) { diff --git a/examples/portfwd/portfwd.c b/examples/portfwd/portfwd.c index 86521e94e..e61ec4554 100644 --- a/examples/portfwd/portfwd.c +++ b/examples/portfwd/portfwd.c @@ -69,6 +69,8 @@ #endif #define INVALID_FWD_PORT 0 +/* wolfSSH_worker() calls to wait for a want-reply tcpip-forward answer. */ +#define MAX_REPLY_TRIES 100 static const char defaultFwdFromHost[] = "0.0.0.0"; @@ -77,6 +79,23 @@ static inline int findMax(int a, int b) return (a > b) ? a : b; } + +/* Parse a port option. atoi() would wrap an out of range value into a valid + * looking port; 65536 becoming 0 is the bad one, since 0 asks the peer to + * allocate the reverse listener. */ +static word16 parsePort(const char* arg, const char* opt) +{ + char* end = NULL; + long val; + + val = strtol(arg, &end, 10); + if (end == arg || *end != '\0' || val < 0 || val > 65535) { + printf("Port for %s must be 0 to 65535, got \"%s\".\n", opt, arg); + err_sys("bad port argument"); + } + return (word16)val; +} + static void ShowUsage(void) { printf("portfwd %s linked with wolfSSL %s\n" @@ -86,9 +105,13 @@ static void ShowUsage(void) " -u username to authenticate as (REQUIRED)\n" " -P password for username, prompted if omitted\n" " -F host to forward from, default %s\n" - " -f host port to forward from (REQUIRED)\n" + " -f host port to forward from (REQUIRED), 0 with -r\n" + " lets the peer pick the listener port\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); @@ -214,6 +237,159 @@ static int wsPublicKeyCheck(const byte* pubKey, word32 pubKeySz, void* ctx) } +/* State shared with the remote-forward callbacks. One reverse connection at a + * time; a second concurrent channel is refused rather than tracked, since a + * single appFd cannot carry two. 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; + SOCKADDR_IN_T targetAddr; /* fwdToHost:fwdToPort, resolved at startup */ + 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 */ + int replied; /* peer answered the tcpip-forward request */ + int refused; /* ...and the answer was a refusal */ + int badPort; /* ...or named a port outside 1..65535 */ + int wantPortZero; /* the request asked the peer to pick the port */ + word16 boundPort; /* port the peer reported binding */ +} PortfwdState; + + +/* Open a TCP connection to the pre-resolved forward target. Returns -1 on + * failure. Unlike tcp_socket(), nothing here exits the process: this runs + * inside a channel-open callback, where the right answer is to refuse the one + * channel. The address is resolved once at startup so build_addr()'s err_sys() + * on an unresolvable host cannot fire from here. */ +static SOCKET_T connectTarget(const SOCKADDR_IN_T* addr) +{ + SOCKET_T fd; + + fd = socket(((const struct sockaddr_in*)addr)->sin_family, SOCK_STREAM, 0); + if (fd == (SOCKET_T)-1) { + return (SOCKET_T)-1; + } +#ifdef SO_NOSIGPIPE + { + int on = 1; + (void)setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &on, + (socklen_t)sizeof(on)); + } +#endif + 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. Each connection to + * the peer's listener arrives here as a LOCAL_SETUP then 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 name the peer's listener, not our target. */ + (void)address; + (void)port; + if (st->appFd != (SOCKET_T)-1 || st->pending) { + /* Only one tunnelled connection is tracked. Refuse the extra + * channel instead of overwriting the live one, which would + * misdeliver its data and orphan its socket. */ + printf("Refusing a second concurrent forwarded connection.\n"); + ret = WS_FWD_SETUP_E; + break; + } + st->appFd = connectTarget(&st->targetAddr); + 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: + /* The library does not currently emit this action, so this branch + * never runs. The target socket is closed when portfwd_worker() + * leaves its loop. Kept so the handler is right if that changes. */ + (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 requesting client never sees these. */ + default: + break; + } + + return ret; +} + + +/* Request-success callback. Per RFC 4254 7.1 the trailing port is only + * meaningful when 0 was requested, so it is consulted only then; for a fixed + * port the request already names the listener. */ +static int portfwdReqSuccessCb(WOLFSSH* ssh, void* buf, word32 sz, void* ctx) +{ + PortfwdState* st = (PortfwdState*)ctx; + const byte* p = (const byte*)buf; + + (void)ssh; + if (!st->wantPortZero) { + printf("Remote forward established.\n"); + } + else if (p != NULL && sz >= 4) { + word32 boundPort = ((word32)p[0] << 24) | ((word32)p[1] << 16) | + ((word32)p[2] << 8) | (word32)p[3]; + + /* Don't narrow a bogus port into a plausible one. */ + if (boundPort == 0 || boundPort > 65535) { + printf("Peer reported an out of range bound port %u\n", boundPort); + st->badPort = 1; + } + else { + st->boundPort = (word16)boundPort; + printf("Remote forward established; peer bound port %u\n", + boundPort); + } + } + else { + /* A port-0 request has no other way to learn the listener's port. */ + printf("Peer did not report a bound port for a port 0 request.\n"); + st->badPort = 1; + } + st->replied = 1; + return WS_SUCCESS; +} + + +/* Request-failure callback. The peer refused the remote listener. */ +static int portfwdReqFailureCb(WOLFSSH* ssh, void* buf, word32 sz, void* ctx) +{ + PortfwdState* st = (PortfwdState*)ctx; + + (void)ssh; + (void)buf; + (void)sz; + st->replied = 1; + st->refused = 1; + return WS_SUCCESS; +} + + /* * fwdFromHost - address to bind the local listener socket to (default: any) * fwdFromHostPort - port number to bind the local listener socket to @@ -243,7 +419,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; @@ -254,6 +430,10 @@ THREAD_RETURN WOLFSSH_THREAD portfwd_worker(void* args) int ret; int ch; int appFdSet = 0; + int reverse = 0; + int fwdFromPortSet = 0; + PortfwdState fwdState; + int replyTries; struct timeval to; WOLFSSH_CHANNEL* fwdChannel = NULL; byte* appBuffer = NULL; @@ -269,7 +449,12 @@ 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) { + /* Initialized up front: the reads below are guarded by "reverse", but the + * guard and the initializer would otherwise sit in different branches. */ + memset(&fwdState, 0, sizeof(fwdState)); + fwdState.appFd = (SOCKET_T)-1; + + while ((ch = mygetopt(argc, argv, "?rf:h:p:t:u:F:P:R:T:")) != -1) { switch (ch) { case 'h': host = myoptarg; @@ -278,13 +463,14 @@ THREAD_RETURN WOLFSSH_THREAD portfwd_worker(void* args) case 'f': if (myoptarg == NULL) err_sys("null argument found"); - fwdFromPort = (word16)atoi(myoptarg); + fwdFromPort = parsePort(myoptarg, "-f"); + fwdFromPortSet = 1; break; case 'p': if (myoptarg == NULL) err_sys("null argument found"); - port = (word16)atoi(myoptarg); + port = parsePort(myoptarg, "-p"); #if !defined(NO_MAIN_DRIVER) || defined(USE_WINDOWS_API) if (port == 0) err_sys("port number cannot be 0"); @@ -294,7 +480,7 @@ THREAD_RETURN WOLFSSH_THREAD portfwd_worker(void* args) case 't': if (myoptarg == NULL) err_sys("null argument found"); - fwdToPort = (word16)atoi(myoptarg); + fwdToPort = parsePort(myoptarg, "-t"); break; case 'u': @@ -317,6 +503,10 @@ THREAD_RETURN WOLFSSH_THREAD portfwd_worker(void* args) fwdToHost = myoptarg; break; + case 'r': + reverse = 1; + break; + case '?': ShowUsage(); exit(EXIT_SUCCESS); @@ -332,8 +522,12 @@ THREAD_RETURN WOLFSSH_THREAD portfwd_worker(void* args) err_sys("client requires a username parameter."); if (fwdToPort == INVALID_FWD_PORT) err_sys("requires a port to forward to"); - if (fwdFromPort == INVALID_FWD_PORT) + if (!fwdFromPortSet) err_sys("requires a port to forward from"); + /* Port 0 asks the peer to allocate the port, so it needs a peer that + * listens. */ + if (fwdFromPort == INVALID_FWD_PORT && !reverse) + err_sys("port 0 to forward from requires reverse mode"); if (fwdToHost == NULL) fwdToHost = host; @@ -388,15 +582,35 @@ THREAD_RETURN WOLFSSH_THREAD portfwd_worker(void* args) if (ret != WS_SUCCESS) err_sys("Couldn't set the username."); + if (reverse) { + /* The peer listens; its inbound channels reach us through the + * forwarding callback, the bound port through request-success. */ + fwdState.fwdToHost = fwdToHost; + fwdState.fwdToPort = fwdToPort; + fwdState.wantPortZero = (fwdFromPort == 0); + /* Resolved once here, where err_sys() on a bad host is appropriate, + * so the per-channel callback never has to resolve. */ + build_addr(&fwdState.targetAddr, fwdToHost, fwdToPort); + wolfSSH_CTX_SetFwdCb(ctx, portfwdRemoteFwdCb, NULL); + wolfSSH_SetFwdCbCtx(ssh, &fwdState); + wolfSSH_SetReqSuccess(ctx, portfwdReqSuccessCb); + wolfSSH_SetReqSuccessCtx(ssh, &fwdState); + wolfSSH_SetReqFailure(ctx, portfwdReqFailureCb); + wolfSSH_SetReqFailureCtx(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); @@ -411,15 +625,47 @@ 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."); + + /* No listener until the peer answers, and want-reply owes us one. + * Back-pressure and a rekey are progress, not failure. Bounded so a + * peer that never answers is distinguishable from one that refuses. */ + for (replyTries = 0; !fwdState.replied && replyTries < MAX_REPLY_TRIES; + replyTries++) { + ret = wolfSSH_worker(ssh, NULL); + if (ret != WS_SUCCESS && ret != WS_CHAN_RXD && + ret != WS_WANT_READ && ret != WS_WANT_WRITE && + ret != WS_WINDOW_FULL && ret != WS_REKEYING) + err_sys("Couldn't get the remote forward reply."); + } + if (!fwdState.replied) + err_sys("Peer never answered the remote port forward request."); + if (fwdState.refused) + err_sys("Peer refused the remote port forward request."); + if (fwdState.badPort) + err_sys("Peer reported an unusable bound port."); + + /* With -f 0 the peer picked the port. It is the listener's real port, + * so both the ready file and the cancel below must name it. */ + if (fwdState.wantPortZero) + fwdFromPort = fwdState.boundPort; + } + if (readyFile != NULL) { #ifndef NO_FILESYSTEM WFILE* f = NULL; + word16 readyPort = fwdFromPort; + ret = WFOPEN(NULL, &f, readyFile, "w"); if (f != NULL && ret == 0) { char portStr[10]; int l; - l = WSNPRINTF(portStr, sizeof(portStr), "%d\n", (int)fwdFromPort); + l = WSNPRINTF(portStr, sizeof(portStr), "%d\n", (int)readyPort); if (l > 0) { WFWRITE(NULL, portStr, MIN((size_t)l, sizeof(portStr)), 1, f); } @@ -432,8 +678,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; @@ -453,7 +704,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"); } @@ -470,6 +721,31 @@ THREAD_RETURN WOLFSSH_THREAD portfwd_worker(void* args) word32 channelId = 0; ret = wolfSSH_worker(ssh, &channelId); + + /* The worker may have taken a reverse channel. Wire its target + * socket in before the read below, which would otherwise consume + * the first chunk with nowhere to put it. */ + if (reverse && fwdState.pending && !appFdSet) { + WOLFSSH_CHANNEL* newChannel; + + newChannel = wolfSSH_ChannelFind(ssh, fwdState.channelId, + WS_CHANNEL_ID_SELF); + if (fwdState.appFd != (SOCKET_T)-1 && newChannel != NULL) { + appFd = fwdState.appFd; + fwdChannel = newChannel; + FD_SET(appFd, &templateFds); + nFds = findMax((int)sshFd, (int)appFd) + 1; + appFdSet = 1; + } + else if (fwdState.appFd != (SOCKET_T)-1) { + /* The channel did not survive the open. Drop the target + * socket rather than leave it connected but unpolled. */ + WCLOSESOCKET(fwdState.appFd); + fwdState.appFd = (SOCKET_T)-1; + } + fwdState.pending = 0; + } + if (ret == WS_CHAN_RXD) { WOLFSSH_CHANNEL* readChannel; @@ -491,7 +767,7 @@ THREAD_RETURN WOLFSSH_THREAD portfwd_worker(void* args) } } } - if (!appFdSet && FD_ISSET(listenFd, &rxFds)) { + if (!reverse && !appFdSet && FD_ISSET(listenFd, &rxFds)) { appFd = accept(listenFd, (struct sockaddr*)&fwdFromHostAddr, &fwdFromHostAddrSz); if (appFd < 0) @@ -515,12 +791,21 @@ THREAD_RETURN WOLFSSH_THREAD portfwd_worker(void* args) } } + if (reverse) { + /* Best effort: the session is going away regardless, but a failure + * here means the peer's listener may outlive us. */ + ret = wolfSSH_FwdRemoteCancel(ssh, fwdFromHost, fwdFromPort, 0); + if (ret != WS_SUCCESS) + printf("Couldn't cancel the remote port forward, ret = %d\n", ret); + } + 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/scripts/fwd.test.expect b/scripts/fwd.test.expect index 4cf46e519..a4836fed3 100755 --- a/scripts/fwd.test.expect +++ b/scripts/fwd.test.expect @@ -2,24 +2,33 @@ # # SSH Tunnel Test Script # -# Tests an SSH tunnel using wolfSSH and netcat (nc). +# Tests SSH tunnels using wolfSSH and netcat (nc), one phase per forwarding +# direction. In both, the nc client sends each line of a Lorem Ipsum paragraph +# through the tunnel and the nc server echoes it back; both sides verify every +# line. # -# Architecture: +# Phase 1, local forwarding (portfwd listens on 12345): # # [nc client] --plain--> :12345 [wolfssh client] # | # SSH # | -# [wolfssh server] :ephem --plain--> :11111 [nc server] +# [wolfssh server] --plain--> :11111 [nc server] # -# The nc client sends each line of a Lorem Ipsum paragraph through the tunnel -# to the nc server one at a time. The server echoes each line back. Both sides -# verify receipt of every line. +# Phase 2, reverse forwarding (portfwd -r, the server listens on 12346): # -# Ports used: -# 11111 - nc server (plain text backend) -# 12345 - wolfssh client listener (plain text, nc connects here) -# 22222 - wolfSSH rendezvous (SSH, internal use only) +# [nc client] --plain--> :12346 [wolfssh server] +# | +# SSH +# | +# [wolfssh client] --plain--> :11112 [nc server] +# +# Phase 3 repeats phase 2 with "-f 0", where the server picks the listener port +# and reports it in the tcpip-forward reply. Only that reply names the entry +# port, so the phase covers the port-0 path: request, reply parse, and cancel. +# +# The SSH port is OS-allocated (echoserver -R) and each phase uses its own +# forwarding ports, so no phase waits out the previous one's TIME_WAIT. # # Requirements: nc (netcat), expect @@ -31,9 +40,10 @@ set timeout 30 # its select() loop before it ever sends the channel-open. Redirect to # regular files instead, and use portfwd's -R ready file for sync. set srv_log "/tmp/fwd.test.echoserver.[pid].log" +set srv_ready "/tmp/fwd.test.echoserver.[pid].ready" set clt_log "/tmp/fwd.test.portfwd.[pid].log" set clt_ready "/tmp/fwd.test.portfwd.[pid].ready" -file delete -force $srv_log $clt_log $clt_ready +file delete -force $srv_log $srv_ready $clt_log $clt_ready set lorem_lines { {Lorem ipsum dolor sit amet, consectetur adipiscing elit,} @@ -55,7 +65,7 @@ set nc_client_pid "" # --- Cleanup ----------------------------------------------------------------- proc cleanup {} { global nc_client_pid wolfssh_clt_pid wolfssh_srv_pid nc_server_pid - global srv_log clt_log clt_ready + global srv_log srv_ready clt_log clt_ready puts "\n--- Cleaning up ---" foreach pid [list $nc_client_pid $wolfssh_clt_pid $wolfssh_srv_pid $nc_server_pid] { @@ -63,99 +73,188 @@ proc cleanup {} { catch {exec kill $pid} } } - file delete -force $srv_log $clt_log $clt_ready + set nc_client_pid "" + set wolfssh_clt_pid "" + set wolfssh_srv_pid "" + set nc_server_pid "" + file delete -force $srv_log $srv_ready $clt_log $clt_ready puts "Done." } # --- Fail helper ------------------------------------------------------------- +# Dump the logs before cleanup deletes them; they usually name the real cause, +# where the failure here is often just the resulting timeout. +proc dump_log {label path} { + if {![file exists $path]} { + puts " \[$label\] no log at $path" + return + } + if {[catch {open $path r} f]} { + puts " \[$label\] could not open $path" + return + } + set data [read $f] + close $f + set lines [split [string trimright $data] "\n"] + set n [llength $lines] + if {$n > 20} { + set lines [lrange $lines end-19 end] + } + puts " --- $label (last [llength $lines] of $n lines) ---" + foreach line $lines { + puts " $line" + } +} + proc fail {msg} { + global srv_log clt_log + puts "\n\[FAIL\] $msg" + dump_log "server" $srv_log + dump_log "client" $clt_log cleanup exit 1 } -# --- Check prerequisites ----------------------------------------------------- -foreach tool {nc} { - if {[auto_execok $tool] eq ""} { - puts "ERROR: '$tool' not found in PATH" - exit 1 +# --- Wait for a ready file, and return the port it names --------------------- +# The file is created before it is written, so wait for content, not existence. +# Returns 0 on timeout. +proc wait_for_port {path} { + global timeout + + for {set elapsed 0} {$elapsed < $timeout} {incr elapsed} { + if {[file exists $path]} { + set f [open $path r] + set data [string trim [read $f]] + close $f + if {[string is integer -strict $data] && $data > 0} { + return $data + } + } + sleep 1 } + return 0 } -# --- [1] Start nc server ----------------------------------------------------- -puts "\n\[1\] Starting nc server: nc -l 11111" -spawn nc -l 11111 -set nc_server_id $spawn_id -set nc_server_pid [exp_pid] -puts " PID $nc_server_pid - waiting for a connection..." - -# --- [2] Start wolfssh server ------------------------------------------------ -puts "\n\[2\] Starting wolfssh server..." -spawn sh -c "exec ./examples/echoserver/echoserver -1 -f >$srv_log 2>&1" -set wolfssh_srv_id $spawn_id -set wolfssh_srv_pid [exp_pid] -puts " PID $wolfssh_srv_pid - waiting for a connection..." - -# --- [3] Start wolfssh client ------------------------------------------------ -puts "\n\[3\] Starting wolfssh client (plain:12345 -> 11111)..." -spawn sh -c "exec ./examples/portfwd/portfwd -u jill -P upthehill -f 12345 -t 11111 -R $clt_ready >$clt_log 2>&1" -set wolfssh_clt_id $spawn_id -set wolfssh_clt_pid [exp_pid] - -# portfwd writes the listening port to $clt_ready once SSH is up. -set elapsed 0 -while {![file exists $clt_ready] && $elapsed < $timeout} { - sleep 1 - incr elapsed -} -if {![file exists $clt_ready]} { - fail "Timed out waiting for wolfssh client to start (no $clt_ready)" -} -puts " wolfssh client ready (PID $wolfssh_clt_pid)." - -# --- [4] Start nc client ----------------------------------------------------- -puts "\n\[4\] Starting nc client: nc localhost 12345" -spawn nc localhost 12345 -set nc_client_id $spawn_id -set nc_client_pid [exp_pid] -puts " PID $nc_client_pid" - -# Allow the TCP handshake and SSH negotiation to complete -sleep 1 - -# --- [5] Send each line, verify receipt, echo back, verify echo -------------- -set n [llength $lorem_lines] -set i 0 -foreach line $lorem_lines { - incr i - puts "\n\[5.$i/$n\] Client sending: \"$line\"" - send -i $nc_client_id "$line\n" - - expect { - -i $nc_server_id - -ex $line { - puts " \[PASS\] Server received line $i." +# --- Send every line in both directions, verifying each hop ------------------ +proc exchange_lines {} { + global lorem_lines nc_client_id nc_server_id timeout + + set n [llength $lorem_lines] + set i 0 + foreach line $lorem_lines { + incr i + puts "\n \[$i/$n\] Client sending: \"$line\"" + send -i $nc_client_id "$line\n" + + expect { + -i $nc_server_id + -ex $line { + puts " \[PASS\] Server received line $i." + } + timeout { + fail "Server did not receive line $i within ${timeout}s" + } } - timeout { - fail "Server did not receive line $i within ${timeout}s" + + send -i $nc_server_id "$line\n" + + expect { + -i $nc_client_id + -ex $line { + puts " \[PASS\] Client received echo of line $i." + } + timeout { + fail "Client did not receive echo of line $i within ${timeout}s" + } } } +} - send -i $nc_server_id "$line\n" +# --- Run one forwarding phase ------------------------------------------------ +# Whoever listens on entry_port, the tunnel ends at target_port: reverse == 0 +# means portfwd listens, reverse == 1 means it asks the SSH server to. An +# entry_port of 0 (reverse only) lets the peer pick, reported via ready file. +proc run_phase {label reverse entry_port target_port} { + global srv_log srv_ready clt_log clt_ready + global nc_server_id nc_server_pid nc_client_id nc_client_pid + global wolfssh_srv_id wolfssh_srv_pid wolfssh_clt_id wolfssh_clt_pid - expect { - -i $nc_client_id - -ex $line { - puts " \[PASS\] Client received echo of line $i." - } - timeout { - fail "Client did not receive echo of line $i within ${timeout}s" - } + puts "\n=== $label ===" + file delete -force $srv_log $srv_ready $clt_log $clt_ready + + puts "\n\[1\] Starting nc server: nc -l $target_port" + spawn nc -l $target_port + set nc_server_id $spawn_id + set nc_server_pid [exp_pid] + puts " PID $nc_server_pid - waiting for a connection..." + + puts "\n\[2\] Starting wolfssh server..." + spawn sh -c "exec ./examples/echoserver/echoserver -1 -f -R $srv_ready >$srv_log 2>&1" + set wolfssh_srv_id $spawn_id + set wolfssh_srv_pid [exp_pid] + + # -R implies a dynamic SSH port, written only once the listener is bound. + # Waiting for it hands us the port and keeps the client from racing the + # listen(). + set ssh_port [wait_for_port $srv_ready] + if {$ssh_port == 0} { + fail "$label: timed out waiting for wolfssh server (no port in $srv_ready)" + } + puts " PID $wolfssh_srv_pid listening on port $ssh_port." + + if {$reverse} { + puts "\n\[3\] Starting wolfssh client (reverse, peer:$entry_port -> $target_port)..." + set reverse_flag "-r" + } else { + puts "\n\[3\] Starting wolfssh client (plain:$entry_port -> $target_port)..." + set reverse_flag "" + } + spawn sh -c "exec ./examples/portfwd/portfwd -u jill -P upthehill -p $ssh_port \ + $reverse_flag -f $entry_port -t $target_port -R $clt_ready >$clt_log 2>&1" + set wolfssh_clt_id $spawn_id + set wolfssh_clt_pid [exp_pid] + + # portfwd writes the entry port once the tunnel can accept connections -- + # in reverse mode, only after the peer has replied. + set ready_port [wait_for_port $clt_ready] + if {$ready_port == 0} { + fail "$label: timed out waiting for wolfssh client (no port in $clt_ready)" + } + if {$entry_port != 0 && $ready_port != $entry_port} { + fail "$label: asked for entry port $entry_port, client reported $ready_port" + } + puts " wolfssh client ready on port $ready_port (PID $wolfssh_clt_pid)." + + puts "\n\[4\] Starting nc client: nc localhost $ready_port" + spawn nc localhost $ready_port + set nc_client_id $spawn_id + set nc_client_pid [exp_pid] + puts " PID $nc_client_pid" + + # Allow the TCP handshake and SSH negotiation to complete + sleep 1 + + puts "\n\[5\] Exchanging lines..." + exchange_lines + + puts "\n\[6\] $label complete." + cleanup +} + +# --- Check prerequisites ----------------------------------------------------- +foreach tool {nc} { + if {[auto_execok $tool] eq ""} { + puts "ERROR: '$tool' not found in PATH" + exit 1 } } +run_phase "Phase 1: local forwarding" 0 12345 11111 +run_phase "Phase 2: remote (reverse) forwarding" 1 12346 11112 +run_phase "Phase 3: reverse forwarding, peer-allocated port" 1 0 11113 + # --- Done -------------------------------------------------------------------- puts "\n=== TEST PASSED ===\n" -cleanup exit 0 diff --git a/src/internal.c b/src/internal.c index a5fa0ee3b..ba4945512 100644 --- a/src/internal.c +++ b/src/internal.c @@ -15678,6 +15678,66 @@ int SendGlobalRequest(WOLFSSH* ssh, return ret; } + +#ifdef WOLFSSH_FWD +/* Send a "tcpip-forward" or "cancel-tcpip-forward" global request. The bind + * address and port follow the want-reply boolean, an ordering the generic + * SendGlobalRequest() framing cannot express. 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 9f686975b..28ec362f3 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -3005,6 +3005,72 @@ WOLFSSH_CHANNEL* wolfSSH_ChannelFwdNew(WOLFSSH* ssh, return wolfSSH_ChannelFwdNewLocal(ssh, host, hostPort, origin, originPort); } + +/* Send "tcpip-forward", asking the peer to listen on bindAddr:bindPort. Port 0 + * lets the peer choose; with wantReply it names the port it bound through the + * request-success callback (wolfSSH_SetReqSuccess). Inbound connections arrive + * as "forwarded-tcpip" channels via the forwarding callback. + * + * RFC 4254 7.1 defines tcpip-forward as client-to-server, and a server rejects + * the resulting forwarded-tcpip opens, so this is client-only. */ +int wolfSSH_FwdRemoteSetup(WOLFSSH* ssh, const char* bindAddr, + word32 bindPort, int wantReply) +{ + int ret = WS_SUCCESS; + + WLOG(WS_LOG_DEBUG, "Entering wolfSSH_FwdRemoteSetup()"); + + if (ssh == NULL || ssh->ctx == NULL || bindAddr == NULL) + ret = WS_BAD_ARGUMENT; + + /* A port is 16 bits on the wire; 0 asks the peer to allocate one. */ + if (ret == WS_SUCCESS && bindPort > 65535) + ret = WS_BAD_ARGUMENT; + + if (ret == WS_SUCCESS && wantReply != 0 && wantReply != 1) + ret = WS_BAD_ARGUMENT; + + if (ret == WS_SUCCESS && ssh->ctx->side != WOLFSSH_ENDPOINT_CLIENT) + ret = WS_BAD_ARGUMENT; + + if (ret == WS_SUCCESS) + ret = SendGlobalRequestFwd(ssh, bindAddr, bindPort, 0, wantReply); + + WLOG(WS_LOG_DEBUG, "Leaving wolfSSH_FwdRemoteSetup(), ret = %d", ret); + return ret; +} + + +/* Send "cancel-tcpip-forward", tearing down a wolfSSH_FwdRemoteSetup() + * listener. bindPort must be the port the peer bound, which after a port-0 + * request is the one it reported, not 0. Client-only, as with the setup. */ +int wolfSSH_FwdRemoteCancel(WOLFSSH* ssh, const char* bindAddr, + word32 bindPort, int wantReply) +{ + int ret = WS_SUCCESS; + + WLOG(WS_LOG_DEBUG, "Entering wolfSSH_FwdRemoteCancel()"); + + if (ssh == NULL || ssh->ctx == NULL || bindAddr == NULL) + ret = WS_BAD_ARGUMENT; + + /* Unlike the setup, 0 names no listener the peer could have bound. */ + if (ret == WS_SUCCESS && (bindPort == 0 || bindPort > 65535)) + ret = WS_BAD_ARGUMENT; + + if (ret == WS_SUCCESS && wantReply != 0 && wantReply != 1) + ret = WS_BAD_ARGUMENT; + + if (ret == WS_SUCCESS && ssh->ctx->side != WOLFSSH_ENDPOINT_CLIENT) + ret = WS_BAD_ARGUMENT; + + if (ret == WS_SUCCESS) + ret = SendGlobalRequestFwd(ssh, bindAddr, bindPort, 1, wantReply); + + WLOG(WS_LOG_DEBUG, "Leaving wolfSSH_FwdRemoteCancel(), ret = %d", ret); + return ret; +} + #endif /* WOLFSSH_FWD */ diff --git a/tests/api.c b/tests/api.c index bda465eef..a7832bd52 100644 --- a/tests/api.c +++ b/tests/api.c @@ -3845,6 +3845,66 @@ static void test_wolfSSH_SetAlgoList(void) } +#ifdef WOLFSSH_FWD + +/* Argument validation for the remote-forward request APIs. Only the rejection + * paths are exercised here; sending a real request needs a live session, which + * scripts/fwd.test covers. */ +static void test_wolfSSH_FwdRemote_badArgs(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); + AssertNotNull(ctx); + ssh = wolfSSH_new(ctx); + AssertNotNull(ssh); + + /* NULL session or bind address. */ + AssertIntEQ(wolfSSH_FwdRemoteSetup(NULL, "0.0.0.0", 22, 1), + WS_BAD_ARGUMENT); + AssertIntEQ(wolfSSH_FwdRemoteSetup(ssh, NULL, 22, 1), WS_BAD_ARGUMENT); + AssertIntEQ(wolfSSH_FwdRemoteCancel(NULL, "0.0.0.0", 22, 1), + WS_BAD_ARGUMENT); + AssertIntEQ(wolfSSH_FwdRemoteCancel(ssh, NULL, 22, 1), WS_BAD_ARGUMENT); + + /* A port is 16 bits on the wire. */ + AssertIntEQ(wolfSSH_FwdRemoteSetup(ssh, "0.0.0.0", 65536, 1), + WS_BAD_ARGUMENT); + AssertIntEQ(wolfSSH_FwdRemoteCancel(ssh, "0.0.0.0", 65536, 1), + WS_BAD_ARGUMENT); + + /* Port 0 asks the peer to allocate, so it cannot name one to cancel. */ + AssertIntEQ(wolfSSH_FwdRemoteCancel(ssh, "0.0.0.0", 0, 1), + WS_BAD_ARGUMENT); + + /* wantReply is a boolean. */ + AssertIntEQ(wolfSSH_FwdRemoteSetup(ssh, "0.0.0.0", 22, 2), + WS_BAD_ARGUMENT); + AssertIntEQ(wolfSSH_FwdRemoteCancel(ssh, "0.0.0.0", 22, -1), + WS_BAD_ARGUMENT); + + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + + /* tcpip-forward is client-to-server; a server must not send one. */ + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + AssertNotNull(ctx); + ssh = wolfSSH_new(ctx); + AssertNotNull(ssh); + + AssertIntEQ(wolfSSH_FwdRemoteSetup(ssh, "0.0.0.0", 22, 1), + WS_BAD_ARGUMENT); + AssertIntEQ(wolfSSH_FwdRemoteCancel(ssh, "0.0.0.0", 22, 1), + WS_BAD_ARGUMENT); + + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); +} + +#endif /* WOLFSSH_FWD */ + + static void test_wolfSSH_QueryAlgoList(void) { const char* name; @@ -4126,6 +4186,9 @@ int wolfSSH_ApiTest(int argc, char** argv) test_wolfSSH_ReadKey_noTrailingNewline(); test_wolfSSH_QueryAlgoList(); test_wolfSSH_SetAlgoList(); +#ifdef WOLFSSH_FWD + test_wolfSSH_FwdRemote_badArgs(); +#endif #ifdef WOLFSSH_AGENT test_wolfSSH_agent_signrequest_partial_write(); test_wolfSSH_agent_signrequest_wrong_message(); diff --git a/wolfssh/internal.h b/wolfssh/internal.h index 56c5abb04..80e7075f3 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -1243,6 +1243,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,