From b19e9b0a71d64f8ed69ad5a1f948939c0c6e396f Mon Sep 17 00:00:00 2001 From: Yosuke Shimizu Date: Tue, 9 Jun 2026 10:27:13 +0900 Subject: [PATCH 1/2] Report allocated port in tcpip-forward reply - Reply to a port-0 (dynamic) tcpip-forward with the bound port. - Add WS_FWD_PORT_CHECK (1024) as the status/port boundary in WS_FwdCbError. - Callback returns a WS_FwdCbError status below it, the port at or above it. - DoGlobalRequestFwd reports the port and rejects a port-0 setup with none. - Map a callback rejection to WS_RESOURCE_E so a no-reply request keeps the link. - Update the echoserver reference callbacks (examples and Espressif) to recover the OS-chosen port with getsockname() and return it under the new convention. - Add regress coverage for the allocated-port and rejection paths. Issue: F-5573 Co-authored-by: John Safranek --- examples/echoserver/echoserver.c | 31 +++- .../wolfssh_echoserver/main/echoserver.c | 31 +++- src/internal.c | 47 ++++- tests/regress.c | 166 ++++++++++++++++++ wolfssh/ssh.h | 15 ++ 5 files changed, 286 insertions(+), 4 deletions(-) diff --git a/examples/echoserver/echoserver.c b/examples/echoserver/echoserver.c index 0d0e44f27..15297ae5f 100644 --- a/examples/echoserver/echoserver.c +++ b/examples/echoserver/echoserver.c @@ -529,6 +529,8 @@ static int wolfSSH_FwdDefaultActions(WS_FwdCbAction action, void* vCtx, else if (action == WOLFSSH_FWD_REMOTE_SETUP) { struct sockaddr_in addr; socklen_t addrSz = 0; + socklen_t boundSz = sizeof(addr); + word32 allocatedPort = 0; fwdCbCtx->hostName = WSTRDUP(name, NULL, 0); fwdCbCtx->hostPort = port; @@ -553,7 +555,7 @@ static int wolfSSH_FwdDefaultActions(WS_FwdCbAction action, void* vCtx, } else { printf("Not using IPv6 yet.\n"); - ret = WS_FWD_SETUP_E; + ret = -1; } } @@ -566,8 +568,35 @@ static int wolfSSH_FwdDefaultActions(WS_FwdCbAction action, void* vCtx, ret = listen(appCtx->listenFd, 5); } + if (ret == 0 && port == 0) { + /* The peer requested port 0, so the OS picked the port during + * bind(). Recover it to report back to the caller. */ + WMEMSET(&addr, 0, sizeof addr); + if (getsockname(appCtx->listenFd, + (struct sockaddr*)&addr, &boundSz) == 0) { + allocatedPort = (word32)ntohs(addr.sin_port); + /* The library reads a return below WS_FWD_PORT_CHECK as a + * status, not a port, so an allocated port must be reportable. + * An unprivileged OS-chosen port always is; guard anyway. */ + if (allocatedPort < WS_FWD_PORT_CHECK) { + printf("Allocated port %u not reportable.\n", allocatedPort); + ret = -1; + } + else { + fwdCbCtx->hostPort = allocatedPort; + } + } + else { + printf("getsockname failed for forwarded port.\n"); + ret = -1; + } + } + if (ret == 0) { appCtx->state = APP_STATE_LISTEN; + /* Report any dynamically allocated port to the library through the + * return value; 0 keeps the port the peer requested. */ + ret = (int)allocatedPort; } else { if (fwdCbCtx->hostName != NULL) { diff --git a/ide/Espressif/ESP-IDF/examples/wolfssh_echoserver/main/echoserver.c b/ide/Espressif/ESP-IDF/examples/wolfssh_echoserver/main/echoserver.c index 616e21c6d..50321ac8c 100644 --- a/ide/Espressif/ESP-IDF/examples/wolfssh_echoserver/main/echoserver.c +++ b/ide/Espressif/ESP-IDF/examples/wolfssh_echoserver/main/echoserver.c @@ -521,6 +521,8 @@ static int wolfSSH_FwdDefaultActions(WS_FwdCbAction action, void* vCtx, else if (action == WOLFSSH_FWD_REMOTE_SETUP) { struct sockaddr_in addr; socklen_t addrSz = 0; + socklen_t boundSz = sizeof(addr); + word32 allocatedPort = 0; ctx->hostName = WSTRDUP(name, NULL, 0); ctx->hostPort = port; @@ -545,7 +547,7 @@ static int wolfSSH_FwdDefaultActions(WS_FwdCbAction action, void* vCtx, } else { printf("Not using IPv6 yet.\n"); - ret = WS_FWD_SETUP_E; + ret = -1; } } @@ -558,8 +560,35 @@ static int wolfSSH_FwdDefaultActions(WS_FwdCbAction action, void* vCtx, ret = listen(ctx->listenFd, 5); } + if (ret == 0 && port == 0) { + /* The peer requested port 0, so the OS picked the port during + * bind(). Recover it to report back to the caller. */ + WMEMSET(&addr, 0, sizeof addr); + if (getsockname(ctx->listenFd, + (struct sockaddr*)&addr, &boundSz) == 0) { + allocatedPort = (word32)ntohs(addr.sin_port); + /* The library reads a return below WS_FWD_PORT_CHECK as a + * status, not a port, so an allocated port must be reportable. + * An unprivileged OS-chosen port always is; guard anyway. */ + if (allocatedPort < WS_FWD_PORT_CHECK) { + printf("Allocated port %u not reportable.\n", allocatedPort); + ret = -1; + } + else { + ctx->hostPort = allocatedPort; + } + } + else { + printf("getsockname failed for forwarded port.\n"); + ret = -1; + } + } + if (ret == 0) { ctx->state = FWD_STATE_LISTEN; + /* Report any dynamically allocated port to the library through the + * return value; 0 keeps the port the peer requested. */ + ret = (int)allocatedPort; } else { if (ctx->hostName != NULL) { diff --git a/src/internal.c b/src/internal.c index dfb92d728..1afcaba77 100644 --- a/src/internal.c +++ b/src/internal.c @@ -8957,6 +8957,7 @@ static int DoGlobalRequestFwd(WOLFSSH* ssh, int ret = WS_SUCCESS; char* bindAddr = NULL; word32 bindPort; + word32 requestedPort = 0; WLOG(WS_LOG_DEBUG, "Entering DoGlobalRequestFwd()"); @@ -8975,15 +8976,36 @@ static int DoGlobalRequestFwd(WOLFSSH* ssh, } if (ret == WS_SUCCESS) { + requestedPort = bindPort; WLOG(WS_LOG_INFO, "Requesting forwarding%s for address %s on port %u.", isCancel ? " cancel" : "", bindAddr, bindPort); } if (ret == WS_SUCCESS) { if (ssh->ctx->fwdCb) { - ret = ssh->ctx->fwdCb(isCancel ? WOLFSSH_FWD_REMOTE_CLEANUP : + int cbRet = ssh->ctx->fwdCb(isCancel ? WOLFSSH_FWD_REMOTE_CLEANUP : WOLFSSH_FWD_REMOTE_SETUP, ssh->fwdCbCtx, bindAddr, bindPort); + /* A return at or above WS_FWD_PORT_CHECK is the unprivileged port + * the callback allocated for a remote port-0 request; anything + * below it is a WS_FwdCbError status, where WS_FWD_SUCCESS is + * success and any other value is a rejection. An allocated port is + * only meaningful for a port-0 (dynamic) request and must be a + * valid port number; for a non-zero request the callback should + * return WS_FWD_SUCCESS, so a port-like value is ignored and the + * requested port stands. An out-of-range value for a port-0 + * request leaves bindPort unchanged and is rejected by the + * port-0 compliance check below. */ + if (!isCancel && cbRet >= WS_FWD_PORT_CHECK) { + if (requestedPort == 0 && cbRet <= 65535) { + bindPort = (word32)cbRet; + } + } + else if (cbRet != WS_FWD_SUCCESS) { + WLOG(WS_LOG_WARN, "Forward callback rejected the request, " + "WS_FwdCbError = %d", cbRet); + ret = WS_RESOURCE_E; + } } else { WLOG(WS_LOG_WARN, "No forwarding callback set, rejecting request. " @@ -8992,6 +9014,27 @@ static int DoGlobalRequestFwd(WOLFSSH* ssh, } } + if (ret == WS_SUCCESS && !isCancel) { + /* A remote forward was set up successfully. RFC 4254 7.1 requires a + * port-0 (dynamic) request to be answered with the actual unprivileged + * port allocated. A successful callback that did not report one leaves + * bindPort below WS_FWD_PORT_CHECK, so we cannot comply: undo the setup + * and reject instead of sending a non-compliant success. */ + if (requestedPort == 0 && bindPort < WS_FWD_PORT_CHECK) { + WLOG(WS_LOG_WARN, "Forward callback reported no unprivileged port " + "for a port-0 request; rejecting."); + if (ssh->ctx->fwdCb) { + int cleanupRet = ssh->ctx->fwdCb(WOLFSSH_FWD_REMOTE_CLEANUP, + ssh->fwdCbCtx, bindAddr, bindPort); + if (cleanupRet != WS_SUCCESS) { + WLOG(WS_LOG_WARN, "Forward cleanup after rejection failed, " + "ret = %d", cleanupRet); + } + ret = WS_RESOURCE_E; + } + } + } + if (wantReply) { if (ret == WS_SUCCESS) { if (isCancel) { @@ -9005,7 +9048,7 @@ static int DoGlobalRequestFwd(WOLFSSH* ssh, ret = SendRequestSuccess(ssh, 0); } } - else if (ret == WS_UNIMPLEMENTED_E) { + else if (ret == WS_UNIMPLEMENTED_E || ret == WS_RESOURCE_E) { /* No reply expected; silently reject without terminating connection. */ ret = WS_SUCCESS; } diff --git a/tests/regress.c b/tests/regress.c index de22281bd..86e195000 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -1118,6 +1118,18 @@ static void AssertGlobalRequestReply(const ChannelOpenHarness* harness, } } } + +static word32 ParseGlobalRequestSuccessPort(const byte* packet, word32 packetSz) +{ + word32 port; + + AssertNotNull(packet); + AssertTrue(packetSz >= 10); + AssertIntEQ(packet[5], MSGID_REQUEST_SUCCESS); + WMEMCPY(&port, packet + 6, sizeof(port)); + + return ntohl(port); +} #endif static int RejectChannelOpenCb(WOLFSSH_CHANNEL* channel, void* ctx) @@ -1152,6 +1164,54 @@ static int AcceptFwdCb(WS_FwdCbAction action, void* ctx, return WS_SUCCESS; } + +#define REGRESS_FWD_ALLOC_PORT 49152 + +static int AllocatePortFwdCb(WS_FwdCbAction action, void* ctx, + const char* host, word32 port) +{ + (void)ctx; + (void)host; + + /* A return at or above WS_FWD_PORT_CHECK reports the allocated port for a + * port-0 request; WS_FWD_SUCCESS (0) otherwise. */ + if (action == WOLFSSH_FWD_REMOTE_SETUP && port == 0) + return REGRESS_FWD_ALLOC_PORT; + + return WS_SUCCESS; +} + +/* Accepts the remote setup but never reports an allocated port. Records + * whether the server asks it to clean the setup back up. */ +static int NoPortFwdCb(WS_FwdCbAction action, void* ctx, + const char* host, word32 port) +{ + int* cleanupCalled = (int*)ctx; + (void)host; + (void)port; + + if (action == WOLFSSH_FWD_REMOTE_CLEANUP && cleanupCalled != NULL) + *cleanupCalled = 1; + + return WS_SUCCESS; +} + +/* Rejects the remote setup with a WS_FwdCbError status. The server must send a + * failure and must NOT ask for cleanup, since the setup never succeeded. */ +static int RejectRemoteSetupFwdCb(WS_FwdCbAction action, void* ctx, + const char* host, word32 port) +{ + int* cleanupCalled = (int*)ctx; + (void)host; + (void)port; + + if (action == WOLFSSH_FWD_REMOTE_SETUP) + return WS_FWD_SETUP_E; + if (action == WOLFSSH_FWD_REMOTE_CLEANUP && cleanupCalled != NULL) + *cleanupCalled = 1; + + return WS_SUCCESS; +} #endif @@ -1523,6 +1583,108 @@ static void TestGlobalRequestFwdWithCbSendsSuccess(void) FreeChannelOpenHarness(&harness); } +static void TestGlobalRequestFwdPort0ReturnsAllocatedPort(void) +{ + ChannelOpenHarness harness; + byte in[256]; + word32 inSz; + int ret; + + /* A bind port of 0 asks the server to allocate a port. The success reply + * must carry the port the callback allocated, not the requested 0. */ + inSz = BuildGlobalRequestFwdPacket("0.0.0.0", 0, 0, 1, in, sizeof(in)); + InitChannelOpenHarness(&harness, in, inSz); + AssertIntEQ(wolfSSH_CTX_SetFwdCb(harness.ctx, AllocatePortFwdCb, NULL), + WS_SUCCESS); + + ret = DoReceive(harness.ssh); + + AssertIntEQ(ret, WS_SUCCESS); + AssertGlobalRequestReply(&harness, MSGID_REQUEST_SUCCESS); + AssertIntEQ(ParseGlobalRequestSuccessPort(harness.io.out, harness.io.outSz), + REGRESS_FWD_ALLOC_PORT); + + FreeChannelOpenHarness(&harness); +} + +static void TestGlobalRequestFwdPort0NoAllocSendsFailure(void) +{ + ChannelOpenHarness harness; + byte in[256]; + word32 inSz; + int ret; + int cleanupCalled = 0; + + /* The peer asked the server to choose a port (0), but the callback + * accepts without reporting one. The server must reject and tear the + * setup back down rather than reply with a non-compliant port 0. */ + inSz = BuildGlobalRequestFwdPacket("0.0.0.0", 0, 0, 1, in, sizeof(in)); + InitChannelOpenHarness(&harness, in, inSz); + AssertIntEQ(wolfSSH_CTX_SetFwdCb(harness.ctx, NoPortFwdCb, NULL), + WS_SUCCESS); + AssertIntEQ(wolfSSH_SetFwdCbCtx(harness.ssh, &cleanupCalled), WS_SUCCESS); + + ret = DoReceive(harness.ssh); + + AssertIntEQ(ret, WS_SUCCESS); + AssertGlobalRequestReply(&harness, MSGID_REQUEST_FAILURE); + AssertIntEQ(cleanupCalled, 1); + + FreeChannelOpenHarness(&harness); +} + +static void TestGlobalRequestFwdRemoteSetupErrorSendsFailure(void) +{ + ChannelOpenHarness harness; + byte in[256]; + word32 inSz; + int ret; + int cleanupCalled = 0; + + /* The callback rejects the remote setup with a WS_FwdCbError status (below + * WS_FWD_PORT_CHECK). The server must reply with failure and must not run + * cleanup, since the setup never succeeded. */ + inSz = BuildGlobalRequestFwdPacket("0.0.0.0", 2222, 0, 1, in, sizeof(in)); + InitChannelOpenHarness(&harness, in, inSz); + AssertIntEQ(wolfSSH_CTX_SetFwdCb(harness.ctx, RejectRemoteSetupFwdCb, NULL), + WS_SUCCESS); + AssertIntEQ(wolfSSH_SetFwdCbCtx(harness.ssh, &cleanupCalled), WS_SUCCESS); + + ret = DoReceive(harness.ssh); + + AssertIntEQ(ret, WS_SUCCESS); + AssertGlobalRequestReply(&harness, MSGID_REQUEST_FAILURE); + AssertIntEQ(cleanupCalled, 0); + + FreeChannelOpenHarness(&harness); +} + +static void TestGlobalRequestFwdPort0NoAllocNoReplyKeepsConnection(void) +{ + ChannelOpenHarness harness; + byte in[256]; + word32 inSz; + int ret; + int cleanupCalled = 0; + + /* Same port-0 rejection as above, but wantReply=0. The server must still + * tear the setup back down, send no reply, and keep the connection alive + * rather than treating the rejection as a fatal error. */ + inSz = BuildGlobalRequestFwdPacket("0.0.0.0", 0, 0, 0, in, sizeof(in)); + InitChannelOpenHarness(&harness, in, inSz); + AssertIntEQ(wolfSSH_CTX_SetFwdCb(harness.ctx, NoPortFwdCb, NULL), + WS_SUCCESS); + AssertIntEQ(wolfSSH_SetFwdCbCtx(harness.ssh, &cleanupCalled), WS_SUCCESS); + + ret = DoReceive(harness.ssh); + + AssertIntEQ(ret, WS_SUCCESS); + AssertIntEQ(harness.io.outSz, 0); /* no reply sent */ + AssertIntEQ(cleanupCalled, 1); + + FreeChannelOpenHarness(&harness); +} + static void TestGlobalRequestFwdCancelNoCbSendsFailure(void) { ChannelOpenHarness harness; @@ -3974,6 +4136,10 @@ int main(int argc, char** argv) TestGlobalRequestFwdNoCbSendsFailure(); TestGlobalRequestFwdNoCbNoReplyKeepsConnection(); TestGlobalRequestFwdWithCbSendsSuccess(); + TestGlobalRequestFwdPort0ReturnsAllocatedPort(); + TestGlobalRequestFwdPort0NoAllocSendsFailure(); + TestGlobalRequestFwdRemoteSetupErrorSendsFailure(); + TestGlobalRequestFwdPort0NoAllocNoReplyKeepsConnection(); TestGlobalRequestFwdCancelNoCbSendsFailure(); TestGlobalRequestFwdCancelWithCbSendsSuccess(); TestRequestSuccessWithPortParsesCorrectly(); diff --git a/wolfssh/ssh.h b/wolfssh/ssh.h index 3b3f2279b..6ce19aa9d 100644 --- a/wolfssh/ssh.h +++ b/wolfssh/ssh.h @@ -206,6 +206,21 @@ typedef enum WS_FwdCbError { WS_FWD_PEER_E, } WS_FwdCbError; +#ifndef WS_FWD_PORT_CHECK + /* Boundary of the WS_CallbackFwd return convention below; not an error + * code. The lowest unprivileged port, and must stay above WS_FWD_PEER_E. */ + #define WS_FWD_PORT_CHECK 1024 +#else + #if (WS_FWD_PEER_E > WS_FWD_PORT_CHECK) + #error "WS_FWD_PORT_CHECK set to value in WS_FwdCbError range." + #endif +#endif + +/* Return value: below WS_FWD_PORT_CHECK is a WS_FwdCbError status + * (WS_FWD_SUCCESS is success); at or above it is the unprivileged port a + * WOLFSSH_FWD_REMOTE_SETUP allocated for a port-0 request, for the server to + * report to the peer. A rejected port-0 setup gets a WOLFSSH_FWD_REMOTE_CLEANUP + * even though the setup returned success. */ typedef int (*WS_CallbackFwd)(WS_FwdCbAction action, void* fwdCbCtx, const char* address, word32 port); typedef int (*WS_CallbackFwdIO)(WS_FwdIoCbAction action, void* buf, From 8a91efd4e4a161009674472576b3d8d4690c1e4b Mon Sep 17 00:00:00 2001 From: John Safranek Date: Wed, 24 Jun 2026 13:57:01 -0700 Subject: [PATCH 2/2] Gate forwarded-tcpip opens like direct-tcpip - forwarded-tcpip was never gated; fell through to default-accept channelOpenCb. - Require fwdCb for both forwarding channel types, failing closed without it. - Reject server-side forwarded-tcpip opens before any policy hook runs. - Add regress coverage for both rejections. Issue: F-6275 --- src/internal.c | 55 +++++++++++++++++++++++++++++++++++-------------- tests/regress.c | 28 +++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 15 deletions(-) diff --git a/src/internal.c b/src/internal.c index 1afcaba77..96650a4f4 100644 --- a/src/internal.c +++ b/src/internal.c @@ -9266,23 +9266,45 @@ static int DoChannelOpen(WOLFSSH* ssh, else { ChannelUpdatePeer(newChannel, peerChannelId, peerInitialWindowSz, peerMaxPacketSz); - if (ssh->ctx->channelOpenCb) { - ret = ssh->ctx->channelOpenCb(newChannel, ssh->channelOpenCtx); + #ifdef WOLFSSH_FWD + /* A forwarded-tcpip open is a server-to-client message sent in + * response to a tcpip-forward request, so only a client should + * ever receive one. Reject opens arriving in the wrong direction + * up front, before any policy callback runs or channel state is + * updated. direct-tcpip is intentionally not direction-checked: + * either forwarding side may legitimately request a direct + * forward. */ + if (typeId == ID_CHANTYPE_TCPIP_FORWARD && + ssh->ctx->side == WOLFSSH_ENDPOINT_SERVER) { + WLOG(WS_LOG_WARN, "Rejecting forwarded-tcpip channel open " + "received by a server (wrong direction)"); + fail_reason = OPEN_ADMINISTRATIVELY_PROHIBITED; + ret = WS_ERROR; } - else { - WLOG(WS_LOG_WARN, "No channel open callback set " - "(call wolfSSH_CTX_SetChannelOpenCb()), accepting " - "channel open by default; typeId=%u, peerChannelId=%u", - (word32)typeId, peerChannelId); + #endif /* WOLFSSH_FWD */ + if (ret == WS_SUCCESS) { + if (ssh->ctx->channelOpenCb) { + ret = ssh->ctx->channelOpenCb(newChannel, + ssh->channelOpenCtx); + } + else { + WLOG(WS_LOG_WARN, "No channel open callback set " + "(call wolfSSH_CTX_SetChannelOpenCb()), accepting " + "channel open by default; typeId=%u, " + "peerChannelId=%u", + (word32)typeId, peerChannelId); + } + if (ssh->channelListSz == 0) + ssh->defaultPeerChannelId = peerChannelId; } - if (ssh->channelListSz == 0) - ssh->defaultPeerChannelId = peerChannelId; #ifdef WOLFSSH_FWD - if (typeId == ID_CHANTYPE_TCPIP_DIRECT) { - ChannelUpdateForward(newChannel, - host, hostPort, origin, originPort, isDirect); - + if (ret == WS_SUCCESS && + (typeId == ID_CHANTYPE_TCPIP_DIRECT || + typeId == ID_CHANTYPE_TCPIP_FORWARD)) { if (ssh->ctx->fwdCb) { + ChannelUpdateForward(newChannel, + host, hostPort, origin, originPort, isDirect); + ret = ssh->ctx->fwdCb(WOLFSSH_FWD_LOCAL_SETUP, ssh->fwdCbCtx, host, hostPort); if (ret == WS_SUCCESS) { @@ -9291,8 +9313,11 @@ static int DoChannelOpen(WOLFSSH* ssh, } } else { - WLOG(WS_LOG_WARN, "No forward callback set for direct-tcpip channel," - " failing channel open"); + /* Both forwarding channel types require an explicit policy + * callback; without one, fail closed rather than letting + * the default-accept channelOpenCb path admit them. */ + WLOG(WS_LOG_WARN, "No forward callback set for forwarding " + "channel, failing channel open"); fail_reason = OPEN_ADMINISTRATIVELY_PROHIBITED; ret = WS_ERROR; } diff --git a/tests/regress.c b/tests/regress.c index 86e195000..5d9e46cdb 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -1525,6 +1525,33 @@ static void TestDirectTcpipNoFwdCbSendsOpenFail(void) FreeChannelOpenHarness(&harness); } +static void TestForwardedTcpipOnServerSendsOpenFail(void) +{ + ChannelOpenHarness harness; + byte extra[128]; + byte in[192]; + word32 extraSz; + word32 inSz; + int ret; + + /* forwarded-tcpip is only ever sent server-to-client. A server receiving + * one is the wrong direction and must be rejected even with a fwdCb set, + * before the forwarding policy hook runs. */ + extraSz = BuildDirectTcpipExtra("127.0.0.1", 8080, "127.0.0.1", 2222, + extra, sizeof(extra)); + inSz = BuildChannelOpenPacket("forwarded-tcpip", 9, 0x4000, 0x8000, + extra, extraSz, in, sizeof(in)); + + InitChannelOpenHarness(&harness, in, inSz); + AssertIntEQ(wolfSSH_CTX_SetFwdCb(harness.ctx, AcceptFwdCb, NULL), + WS_SUCCESS); + + ret = DoReceive(harness.ssh); + AssertChannelOpenFailResponse(&harness, ret); + + FreeChannelOpenHarness(&harness); +} + static void TestGlobalRequestFwdNoCbSendsFailure(void) { ChannelOpenHarness harness; @@ -4133,6 +4160,7 @@ int main(int argc, char** argv) #ifdef WOLFSSH_FWD TestDirectTcpipRejectSendsOpenFail(); TestDirectTcpipNoFwdCbSendsOpenFail(); + TestForwardedTcpipOnServerSendsOpenFail(); TestGlobalRequestFwdNoCbSendsFailure(); TestGlobalRequestFwdNoCbNoReplyKeepsConnection(); TestGlobalRequestFwdWithCbSendsSuccess();