Add a match setting for remote forwards

The forwarded-tcpip check is a behaviour change on a shipped API, so an
application that trips over it needs a way out that isn't abandoning
wolfSSH_FwdRemoteSetup(). wolfSSH_SetFwdRemoteMatch() relaxes the check
for the session.

- STRICT is the default and keeps the bind-plus-port rule.
- PORT compares the port alone, for a peer that rewrites the bind
  address it echoes back, which STRICT refuses every open from.
- OFF accepts any open, as wolfSSH did before the check existed.
- Tests cover each setting, and a refused one leaving the default in
  place.
pull/1214/head
John Safranek 2026-08-11 14:12:14 -07:00 committed by philljj
parent 3267d87bf5
commit 69bfbccbf9
5 changed files with 101 additions and 3 deletions

View File

@ -4470,6 +4470,10 @@ static int FwdRemoteMatch(WOLFSSH* ssh, const char* addr, word32 port)
if (ssh == NULL || addr == NULL)
return 0;
/* The application took responsibility for what it accepts. */
if (ssh->fwdRemoteMatch == WOLFSSH_FWD_MATCH_OFF)
return 1;
for (cur = ssh->fwdRemoteList; cur != NULL; cur = cur->next) {
WOLFSSH_FWD_REPLY* newest;
@ -4490,7 +4494,10 @@ static int FwdRemoteMatch(WOLFSSH* ssh, const char* addr, word32 port)
if (!cur->confirmed && newest == NULL)
continue;
if (FwdRemoteAddrIsWild(cur->bindAddr) ||
/* A peer that rewrites the bind it echoes back can still be held to
* the port it was asked for. */
if (ssh->fwdRemoteMatch == WOLFSSH_FWD_MATCH_PORT ||
FwdRemoteAddrIsWild(cur->bindAddr) ||
WSTRCMP(cur->bindAddr, addr) == 0)
return 1;
}

View File

@ -4061,6 +4061,22 @@ int wolfSSH_FwdRemoteCancel(WOLFSSH* ssh, const char* bindAddr,
return ret;
}
int wolfSSH_SetFwdRemoteMatch(WOLFSSH* ssh, byte match)
{
int ret = WS_SUCCESS;
WLOG(WS_LOG_DEBUG, "Entering wolfSSH_SetFwdRemoteMatch()");
if (ssh == NULL || match > WOLFSSH_FWD_MATCH_OFF)
ret = WS_BAD_ARGUMENT;
else
ssh->fwdRemoteMatch = match;
WLOG(WS_LOG_DEBUG, "Leaving wolfSSH_SetFwdRemoteMatch(), ret = %d", ret);
return ret;
}
#endif /* WOLFSSH_FWD */

View File

@ -3668,8 +3668,9 @@ static void InitFwdRemoteHarness(ChannelOpenHarness* harness)
/* Set up a client that asked for one remote forward, then hand it a
* forwarded-tcpip open naming openAddr:openPort. The request the setup sends
* is dropped from the output so the open's response starts at offset 0. */
static void RunForwardedTcpipMatchTest(const char* bindAddr, word32 bindPort,
const char* openAddr, word32 openPort, int expectAccept)
static void RunForwardedTcpipMatchModeTest(byte match, const char* bindAddr,
word32 bindPort, const char* openAddr, word32 openPort,
int expectAccept)
{
ChannelOpenHarness harness;
byte extra[128];
@ -3686,6 +3687,7 @@ static void RunForwardedTcpipMatchTest(const char* bindAddr, word32 bindPort,
InitChannelOpenHarnessClient(&harness, in, inSz);
AssertIntEQ(wolfSSH_CTX_SetFwdCb(harness.ctx, AcceptFwdCb, NULL),
WS_SUCCESS);
AssertIntEQ(wolfSSH_SetFwdRemoteMatch(harness.ssh, match), WS_SUCCESS);
AssertIntEQ(wolfSSH_FwdRemoteSetup(harness.ssh, bindAddr, bindPort, 1),
WS_SUCCESS);
@ -3709,6 +3711,13 @@ static void RunForwardedTcpipMatchTest(const char* bindAddr, word32 bindPort,
FreeChannelOpenHarness(&harness);
}
static void RunForwardedTcpipMatchTest(const char* bindAddr, word32 bindPort,
const char* openAddr, word32 openPort, int expectAccept)
{
RunForwardedTcpipMatchModeTest(WOLFSSH_FWD_MATCH_STRICT, bindAddr,
bindPort, openAddr, openPort, expectAccept);
}
static void TestForwardedTcpipRegisteredIsAccepted(void)
{
/* The open names the forward the client registered, so it goes through. */
@ -4700,6 +4709,46 @@ static void TestForwardedTcpipCancelAnsweredDuringResetupKeepsForward(void)
FreeChannelOpenHarness(&harness);
}
/* A peer that canonicalises the bind it echoes back has every open refused
* under the default, so the port it was asked for can be made the whole
* test. */
static void TestFwdRemoteMatchPortIgnoresBindAddr(void)
{
RunForwardedTcpipMatchModeTest(WOLFSSH_FWD_MATCH_STRICT, "localhost", 8080,
"127.0.0.1", 8080, 0);
RunForwardedTcpipMatchModeTest(WOLFSSH_FWD_MATCH_PORT, "localhost", 8080,
"127.0.0.1", 8080, 1);
/* Relaxing the address does not relax the port. */
RunForwardedTcpipMatchModeTest(WOLFSSH_FWD_MATCH_PORT, "localhost", 8080,
"127.0.0.1", 9999, 0);
}
/* Off is what wolfSSH did before the check existed: the open reaches the
* channel-open policy callback whatever it names. */
static void TestFwdRemoteMatchOffAcceptsUnregistered(void)
{
RunForwardedTcpipMatchModeTest(WOLFSSH_FWD_MATCH_OFF, "127.0.0.1", 8080,
"10.0.0.1", 9999, 1);
}
static void TestFwdRemoteMatchRejectsBadSetting(void)
{
ChannelOpenHarness harness;
InitFwdRemoteHarness(&harness);
AssertIntEQ(wolfSSH_SetFwdRemoteMatch(NULL, WOLFSSH_FWD_MATCH_OFF),
WS_BAD_ARGUMENT);
AssertIntEQ(wolfSSH_SetFwdRemoteMatch(harness.ssh,
WOLFSSH_FWD_MATCH_OFF + 1), WS_BAD_ARGUMENT);
/* A refused setting leaves the default in place. */
AssertIntEQ(harness.ssh->fwdRemoteMatch, WOLFSSH_FWD_MATCH_STRICT);
FreeChannelOpenHarness(&harness);
}
/* Only the peer gives a reply slot back, so a peer that never answers would
* let the queue grow for the life of the session and lengthen every match.
* Refusing has to happen before the request is framed: one whose slot was
@ -10444,6 +10493,9 @@ int main(int argc, char** argv)
TestForwardedTcpipRequestAfterReplyDuringSend();
TestForwardedTcpipCancelAnsweredDuringResetupKeepsForward();
TestForwardedTcpipReentrantCancelDuringSend();
TestFwdRemoteMatchPortIgnoresBindAddr();
TestFwdRemoteMatchOffAcceptsUnregistered();
TestFwdRemoteMatchRejectsBadSetting();
TestFwdReplyQueueIsCapped();
TestForwardedTcpipAppRequestKeepsItsOwnReply();
TestForwardedTcpipWantWriteStillRegisters();

View File

@ -1310,6 +1310,8 @@ struct WOLFSSH {
WOLFSSH_FWD_REPLY* fwdReplyTail;
word32 fwdReplyCount; /* slots queued, kept off the walk it bounds */
byte fwdRemoteTracked; /* wolfSSH_FwdRemoteSetup() was used */
byte fwdRemoteMatch; /* WOLFSSH_FWD_MATCH_*, how strictly an inbound
* forwarded-tcpip must name a registration */
#endif /* WOLFSSH_FWD */
#ifdef WOLFSSH_TERM
WS_CallbackTerminalSize termResizeCb;

View File

@ -308,6 +308,7 @@ DEPRECATED WOLFSSH_API int wolfSSH_ChannelGetFwdFd(
* equal the address the peer reports. Register the spelling the peer will echo
* back, or a wildcard: a peer that canonicalises the bind, answering an open
* for "127.0.0.1" against a registered "localhost", has those opens refused.
* wolfSSH_SetFwdRemoteMatch() relaxes this for peers that need it.
*
* One bindAddr:bindPort is one registration however often it is registered,
* since it is one listener on the peer, so one cancel undoes it. That covers a
@ -339,6 +340,26 @@ WOLFSSH_API int wolfSSH_FwdRemoteSetup(WOLFSSH* ssh, const char* bindAddr,
WOLFSSH_API int wolfSSH_FwdRemoteCancel(WOLFSSH* ssh, const char* bindAddr,
word32 bindPort, int wantReply);
/* How strictly an inbound "forwarded-tcpip" open must name a registration
* made with wolfSSH_FwdRemoteSetup(). */
enum WS_FwdRemoteMatch {
WOLFSSH_FWD_MATCH_STRICT = 0, /* bind and port, the default */
WOLFSSH_FWD_MATCH_PORT = 1, /* port alone, the bind is not compared */
WOLFSSH_FWD_MATCH_OFF = 2 /* accept any open, matching nothing */
};
/* Relax the check wolfSSH_FwdRemoteSetup() turns on for this session. Set it
* before the first setup, since opens are matched from that point.
*
* STRICT is the default and is what RFC 4254 7.2 asks for. PORT is for a peer
* that rewrites the bind address it echoes back but keeps the port, which
* STRICT refuses every open from. OFF accepts any "forwarded-tcpip" open, as
* wolfSSH did before this check existed, leaving the channel-open policy
* callback as the only thing standing between the peer and a new channel.
*
* Returns WS_BAD_ARGUMENT for a NULL session or an unknown setting. */
WOLFSSH_API int wolfSSH_SetFwdRemoteMatch(WOLFSSH* ssh, byte match);
WOLFSSH_API int wolfSSH_ChannelFree(WOLFSSH_CHANNEL* channel);
WOLFSSH_API int wolfSSH_ChannelGetId(WOLFSSH_CHANNEL* channel, word32* id,
byte peer);