@@ -1966,6 +1966,51 @@ static int RejectDirectTcpipSetup(WS_FwdCbAction action, void* ctx,
19661966 return WS_SUCCESS ;
19671967}
19681968
1969+ #define REGRESS_FWD_ACTION_MAX 8
1970+ static WS_FwdCbAction fwdActions [REGRESS_FWD_ACTION_MAX ];
1971+ static word32 fwdActionCount ;
1972+
1973+ /* Records the actions the library asked for, so a test can assert the
1974+ * sequence a forwarding channel produces. */
1975+ static int RecordingFwdCb (WS_FwdCbAction action , void * ctx ,
1976+ const char * host , word32 port )
1977+ {
1978+ (void )ctx ;
1979+ (void )host ;
1980+ (void )port ;
1981+
1982+ if (fwdActionCount < REGRESS_FWD_ACTION_MAX )
1983+ fwdActions [fwdActionCount ] = action ;
1984+ fwdActionCount ++ ;
1985+
1986+ return WS_SUCCESS ;
1987+ }
1988+
1989+ /* As RecordingFwdCb, but refuses the channel-id handoff so the open fails
1990+ * after a successful setup. */
1991+ static int RecordingRejectChannelIdFwdCb (WS_FwdCbAction action , void * ctx ,
1992+ const char * host , word32 port )
1993+ {
1994+ int ret = RecordingFwdCb (action , ctx , host , port );
1995+
1996+ if (action == WOLFSSH_FWD_CHANNEL_ID )
1997+ return WS_FWD_NOT_AVAILABLE ;
1998+
1999+ return ret ;
2000+ }
2001+
2002+ /* As RecordingFwdCb, but refuses the setup itself. */
2003+ static int RecordingRejectSetupFwdCb (WS_FwdCbAction action , void * ctx ,
2004+ const char * host , word32 port )
2005+ {
2006+ int ret = RecordingFwdCb (action , ctx , host , port );
2007+
2008+ if (action == WOLFSSH_FWD_LOCAL_SETUP )
2009+ return WS_FWD_SETUP_E ;
2010+
2011+ return ret ;
2012+ }
2013+
19692014/* Counts every action the library asks for. File scope rather than reached
19702015 * through the callback ctx, so a zero reading means the hook did not run and
19712016 * cannot instead mean the ctx stopped being delivered. */
@@ -3806,6 +3851,226 @@ static void TestDirectTcpipFwdCbRejectsChannelId(void)
38063851 FreeChannelOpenHarness (& harness );
38073852}
38083853
3854+ /* A forwarding channel that opened with a LOCAL_SETUP has to report a
3855+ * LOCAL_CLEANUP when it closes, or an application has no hook to release what
3856+ * it set up. */
3857+ static void TestDirectTcpipCloseSendsLocalCleanup (void )
3858+ {
3859+ ChannelOpenHarness harness ;
3860+ byte extra [128 ];
3861+ byte in [192 ];
3862+ byte in2 [64 ];
3863+ word32 extraSz ;
3864+ word32 inSz ;
3865+ word32 in2Sz ;
3866+ word32 selfChannelId ;
3867+ int ret ;
3868+
3869+ fwdActionCount = 0 ;
3870+
3871+ extraSz = BuildDirectTcpipExtra ("127.0.0.1" , 8080 , "127.0.0.1" , 2222 ,
3872+ extra , sizeof (extra ));
3873+ inSz = BuildChannelOpenPacket ("direct-tcpip" , 9 , 0x4000 , 0x8000 ,
3874+ extra , extraSz , in , sizeof (in ));
3875+
3876+ InitChannelOpenHarness (& harness , in , inSz );
3877+ AssertIntEQ (wolfSSH_CTX_SetFwdCb (harness .ctx , RecordingFwdCb , NULL ),
3878+ WS_SUCCESS );
3879+
3880+ ret = DoReceive (harness .ssh );
3881+ AssertIntEQ (ret , WS_SUCCESS );
3882+ AssertIntEQ (ParseMsgId (harness .io .out , harness .io .outSz ),
3883+ MSGID_CHANNEL_OPEN_CONF );
3884+ AssertIntEQ (harness .ssh -> channelListSz , 1 );
3885+ AssertIntEQ (fwdActionCount , 2 );
3886+ AssertIntEQ (fwdActions [0 ], WOLFSSH_FWD_LOCAL_SETUP );
3887+ AssertIntEQ (fwdActions [1 ], WOLFSSH_FWD_CHANNEL_ID );
3888+
3889+ selfChannelId = harness .ssh -> channelList -> channel ;
3890+ in2Sz = BuildChannelClosePacket (selfChannelId , in2 , sizeof (in2 ));
3891+ RepointHarnessInput (& harness , in2 , in2Sz );
3892+
3893+ ret = DoReceive (harness .ssh );
3894+ AssertIntEQ (ret , WS_CHANNEL_CLOSED );
3895+ AssertIntEQ (harness .ssh -> channelListSz , 0 );
3896+ AssertIntEQ (fwdActionCount , 3 );
3897+ AssertIntEQ (fwdActions [2 ], WOLFSSH_FWD_LOCAL_CLEANUP );
3898+
3899+ FreeChannelOpenHarness (& harness );
3900+ }
3901+
3902+ /* Closes a channel this side created itself, which never saw a
3903+ * LOCAL_SETUP, and reports how many forwarding actions that produced. */
3904+ static word32 CloseSelfOpenedChannel (byte channelType )
3905+ {
3906+ ChannelOpenHarness harness ;
3907+ WOLFSSH_CHANNEL * channel ;
3908+ byte in [64 ];
3909+ word32 inSz ;
3910+ word32 selfChannelId ;
3911+
3912+ fwdActionCount = 0 ;
3913+
3914+ InitChannelOpenHarness (& harness , NULL , 0 );
3915+ AssertIntEQ (wolfSSH_CTX_SetFwdCb (harness .ctx , RecordingFwdCb , NULL ),
3916+ WS_SUCCESS );
3917+
3918+ channel = ChannelNew (harness .ssh , channelType , 1024 , 1024 );
3919+ AssertNotNull (channel );
3920+ AssertIntEQ (ChannelUpdatePeer (channel , 5 , 1024 , 1024 ), WS_SUCCESS );
3921+ AssertIntEQ (ChannelAppend (harness .ssh , channel ), WS_SUCCESS );
3922+ channel -> openConfirmed = 1 ;
3923+ selfChannelId = channel -> channel ;
3924+
3925+ inSz = BuildChannelClosePacket (selfChannelId , in , sizeof (in ));
3926+ RepointHarnessInput (& harness , in , inSz );
3927+
3928+ AssertIntEQ (DoReceive (harness .ssh ), WS_CHANNEL_CLOSED );
3929+ AssertIntEQ (harness .ssh -> channelListSz , 0 );
3930+
3931+ FreeChannelOpenHarness (& harness );
3932+
3933+ return fwdActionCount ;
3934+ }
3935+
3936+ /* wolfSSH_ChannelFwdNewLocal() and wolfSSH_ChannelFwdNewRemote() build a
3937+ * forwarding channel without a LOCAL_SETUP, so its close owes no cleanup:
3938+ * the application never armed one and would be freeing what it does not
3939+ * own. This is why the emission is gated on the channel, not its type. */
3940+ static void TestLocalForwardCloseSendsNoCleanup (void )
3941+ {
3942+ AssertIntEQ (CloseSelfOpenedChannel (ID_CHANTYPE_TCPIP_DIRECT ), 0 );
3943+ }
3944+
3945+ /* And a session channel produces no forwarding action at all. */
3946+ static void TestSessionCloseSendsNoFwdAction (void )
3947+ {
3948+ AssertIntEQ (CloseSelfOpenedChannel (ID_CHANTYPE_SESSION ), 0 );
3949+ }
3950+
3951+ /* A setup that succeeded is owed its cleanup even when the open goes on to
3952+ * fail, or the application keeps a socket for a channel that never existed.
3953+ * The channel-id handoff rejecting is the reachable way in. */
3954+ static void TestFailedOpenAfterSetupSendsCleanup (void )
3955+ {
3956+ ChannelOpenHarness harness ;
3957+ byte extra [128 ];
3958+ byte in [192 ];
3959+ word32 extraSz ;
3960+ word32 inSz ;
3961+ int ret ;
3962+
3963+ fwdActionCount = 0 ;
3964+
3965+ extraSz = BuildDirectTcpipExtra ("127.0.0.1" , 8080 , "127.0.0.1" , 2222 ,
3966+ extra , sizeof (extra ));
3967+ inSz = BuildChannelOpenPacket ("direct-tcpip" , 9 , 0x4000 , 0x8000 ,
3968+ extra , extraSz , in , sizeof (in ));
3969+
3970+ InitChannelOpenHarness (& harness , in , inSz );
3971+ AssertIntEQ (wolfSSH_CTX_SetFwdCb (harness .ctx ,
3972+ RecordingRejectChannelIdFwdCb , NULL ), WS_SUCCESS );
3973+
3974+ ret = DoReceive (harness .ssh );
3975+ AssertChannelOpenFailResponse (& harness , ret );
3976+ AssertIntEQ (harness .ssh -> channelListSz , 0 );
3977+ AssertIntEQ (fwdActionCount , 3 );
3978+ AssertIntEQ (fwdActions [0 ], WOLFSSH_FWD_LOCAL_SETUP );
3979+ AssertIntEQ (fwdActions [1 ], WOLFSSH_FWD_CHANNEL_ID );
3980+ AssertIntEQ (fwdActions [2 ], WOLFSSH_FWD_LOCAL_CLEANUP );
3981+
3982+ FreeChannelOpenHarness (& harness );
3983+ }
3984+
3985+ /* A setup that reported failure set nothing up, so it is owed no cleanup;
3986+ * one would have the application release what it never acquired. */
3987+ static void TestRejectedSetupSendsNoCleanup (void )
3988+ {
3989+ ChannelOpenHarness harness ;
3990+ byte extra [128 ];
3991+ byte in [192 ];
3992+ word32 extraSz ;
3993+ word32 inSz ;
3994+ int ret ;
3995+
3996+ fwdActionCount = 0 ;
3997+
3998+ extraSz = BuildDirectTcpipExtra ("127.0.0.1" , 8080 , "127.0.0.1" , 2222 ,
3999+ extra , sizeof (extra ));
4000+ inSz = BuildChannelOpenPacket ("direct-tcpip" , 9 , 0x4000 , 0x8000 ,
4001+ extra , extraSz , in , sizeof (in ));
4002+
4003+ InitChannelOpenHarness (& harness , in , inSz );
4004+ AssertIntEQ (wolfSSH_CTX_SetFwdCb (harness .ctx , RecordingRejectSetupFwdCb ,
4005+ NULL ), WS_SUCCESS );
4006+
4007+ ret = DoReceive (harness .ssh );
4008+ AssertChannelOpenFailResponse (& harness , ret );
4009+ AssertIntEQ (harness .ssh -> channelListSz , 0 );
4010+ AssertIntEQ (fwdActionCount , 1 );
4011+ AssertIntEQ (fwdActions [0 ], WOLFSSH_FWD_LOCAL_SETUP );
4012+
4013+ FreeChannelOpenHarness (& harness );
4014+ }
4015+
4016+ /* Opens a direct-tcpip forward through the recording callback and checks
4017+ * the setup and channel-id handoff went out, leaving the channel live. */
4018+ static void OpenRecordedDirectTcpip (ChannelOpenHarness * harness ,
4019+ byte * in , word32 inSz )
4020+ {
4021+ byte extra [128 ];
4022+ word32 extraSz ;
4023+
4024+ fwdActionCount = 0 ;
4025+
4026+ extraSz = BuildDirectTcpipExtra ("127.0.0.1" , 8080 , "127.0.0.1" , 2222 ,
4027+ extra , sizeof (extra ));
4028+ inSz = BuildChannelOpenPacket ("direct-tcpip" , 9 , 0x4000 , 0x8000 ,
4029+ extra , extraSz , in , inSz );
4030+
4031+ InitChannelOpenHarness (harness , in , inSz );
4032+ AssertIntEQ (wolfSSH_CTX_SetFwdCb (harness -> ctx , RecordingFwdCb , NULL ),
4033+ WS_SUCCESS );
4034+
4035+ AssertIntEQ (DoReceive (harness -> ssh ), WS_SUCCESS );
4036+ AssertIntEQ (harness -> ssh -> channelListSz , 1 );
4037+ AssertIntEQ (fwdActionCount , 2 );
4038+ AssertIntEQ (fwdActions [0 ], WOLFSSH_FWD_LOCAL_SETUP );
4039+ AssertIntEQ (fwdActions [1 ], WOLFSSH_FWD_CHANNEL_ID );
4040+ }
4041+
4042+ /* A dropped transport never delivers a CHANNEL_CLOSE, so freeing the session
4043+ * is the only chance the application gets to release a forward's socket. */
4044+ static void TestSessionFreeSendsLocalCleanup (void )
4045+ {
4046+ ChannelOpenHarness harness ;
4047+ byte in [192 ];
4048+
4049+ OpenRecordedDirectTcpip (& harness , in , sizeof (in ));
4050+
4051+ FreeChannelOpenHarness (& harness );
4052+ AssertIntEQ (fwdActionCount , 3 );
4053+ AssertIntEQ (fwdActions [2 ], WOLFSSH_FWD_LOCAL_CLEANUP );
4054+ }
4055+
4056+ /* Same for a channel the application retires itself, and only once: the
4057+ * session free that follows must not report it again. */
4058+ static void TestChannelFreeSendsLocalCleanup (void )
4059+ {
4060+ ChannelOpenHarness harness ;
4061+ byte in [192 ];
4062+
4063+ OpenRecordedDirectTcpip (& harness , in , sizeof (in ));
4064+
4065+ AssertIntEQ (wolfSSH_ChannelFree (harness .ssh -> channelList ), WS_SUCCESS );
4066+ AssertIntEQ (harness .ssh -> channelListSz , 0 );
4067+ AssertIntEQ (fwdActionCount , 3 );
4068+ AssertIntEQ (fwdActions [2 ], WOLFSSH_FWD_LOCAL_CLEANUP );
4069+
4070+ FreeChannelOpenHarness (& harness );
4071+ AssertIntEQ (fwdActionCount , 3 );
4072+ }
4073+
38094074static void TestForwardedTcpipOnServerSendsOpenFail (void )
38104075{
38114076 ChannelOpenHarness harness ;
@@ -13135,6 +13400,13 @@ int main(int argc, char** argv)
1313513400 TestDirectTcpipOpenCbRejectBeatsFwdCb ();
1313613401 TestDirectTcpipFwdCbRejectAfterOpenCbAccept ();
1313713402 TestDirectTcpipFwdCbRejectsChannelId ();
13403+ TestDirectTcpipCloseSendsLocalCleanup ();
13404+ TestLocalForwardCloseSendsNoCleanup ();
13405+ TestSessionCloseSendsNoFwdAction ();
13406+ TestFailedOpenAfterSetupSendsCleanup ();
13407+ TestRejectedSetupSendsNoCleanup ();
13408+ TestSessionFreeSendsLocalCleanup ();
13409+ TestChannelFreeSendsLocalCleanup ();
1313813410 TestForwardedTcpipOnServerSendsOpenFail ();
1313913411 TestGlobalRequestFwdNoCbSendsFailure ();
1314013412#ifndef NO_WOLFSSH_CLIENT
0 commit comments