@@ -1952,6 +1952,51 @@ static int RejectDirectTcpipSetup(WS_FwdCbAction action, void* ctx,
19521952 return WS_SUCCESS ;
19531953}
19541954
1955+ #define REGRESS_FWD_ACTION_MAX 8
1956+ static WS_FwdCbAction fwdActions [REGRESS_FWD_ACTION_MAX ];
1957+ static word32 fwdActionCount ;
1958+
1959+ /* Records the actions the library asked for, so a test can assert the
1960+ * sequence a forwarding channel produces. */
1961+ static int RecordingFwdCb (WS_FwdCbAction action , void * ctx ,
1962+ const char * host , word32 port )
1963+ {
1964+ (void )ctx ;
1965+ (void )host ;
1966+ (void )port ;
1967+
1968+ if (fwdActionCount < REGRESS_FWD_ACTION_MAX )
1969+ fwdActions [fwdActionCount ] = action ;
1970+ fwdActionCount ++ ;
1971+
1972+ return WS_SUCCESS ;
1973+ }
1974+
1975+ /* As RecordingFwdCb, but refuses the channel-id handoff so the open fails
1976+ * after a successful setup. */
1977+ static int RecordingRejectChannelIdFwdCb (WS_FwdCbAction action , void * ctx ,
1978+ const char * host , word32 port )
1979+ {
1980+ int ret = RecordingFwdCb (action , ctx , host , port );
1981+
1982+ if (action == WOLFSSH_FWD_CHANNEL_ID )
1983+ return WS_FWD_NOT_AVAILABLE ;
1984+
1985+ return ret ;
1986+ }
1987+
1988+ /* As RecordingFwdCb, but refuses the setup itself. */
1989+ static int RecordingRejectSetupFwdCb (WS_FwdCbAction action , void * ctx ,
1990+ const char * host , word32 port )
1991+ {
1992+ int ret = RecordingFwdCb (action , ctx , host , port );
1993+
1994+ if (action == WOLFSSH_FWD_LOCAL_SETUP )
1995+ return WS_FWD_SETUP_E ;
1996+
1997+ return ret ;
1998+ }
1999+
19552000/* Counts every action the library asks for. File scope rather than reached
19562001 * through the callback ctx, so a zero reading means the hook did not run and
19572002 * cannot instead mean the ctx stopped being delivered. */
@@ -3327,6 +3372,226 @@ static void TestDirectTcpipFwdCbRejectsChannelId(void)
33273372 FreeChannelOpenHarness (& harness );
33283373}
33293374
3375+ /* A forwarding channel that opened with a LOCAL_SETUP has to report a
3376+ * LOCAL_CLEANUP when it closes, or an application has no hook to release what
3377+ * it set up. */
3378+ static void TestDirectTcpipCloseSendsLocalCleanup (void )
3379+ {
3380+ ChannelOpenHarness harness ;
3381+ byte extra [128 ];
3382+ byte in [192 ];
3383+ byte in2 [64 ];
3384+ word32 extraSz ;
3385+ word32 inSz ;
3386+ word32 in2Sz ;
3387+ word32 selfChannelId ;
3388+ int ret ;
3389+
3390+ fwdActionCount = 0 ;
3391+
3392+ extraSz = BuildDirectTcpipExtra ("127.0.0.1" , 8080 , "127.0.0.1" , 2222 ,
3393+ extra , sizeof (extra ));
3394+ inSz = BuildChannelOpenPacket ("direct-tcpip" , 9 , 0x4000 , 0x8000 ,
3395+ extra , extraSz , in , sizeof (in ));
3396+
3397+ InitChannelOpenHarness (& harness , in , inSz );
3398+ AssertIntEQ (wolfSSH_CTX_SetFwdCb (harness .ctx , RecordingFwdCb , NULL ),
3399+ WS_SUCCESS );
3400+
3401+ ret = DoReceive (harness .ssh );
3402+ AssertIntEQ (ret , WS_SUCCESS );
3403+ AssertIntEQ (ParseMsgId (harness .io .out , harness .io .outSz ),
3404+ MSGID_CHANNEL_OPEN_CONF );
3405+ AssertIntEQ (harness .ssh -> channelListSz , 1 );
3406+ AssertIntEQ (fwdActionCount , 2 );
3407+ AssertIntEQ (fwdActions [0 ], WOLFSSH_FWD_LOCAL_SETUP );
3408+ AssertIntEQ (fwdActions [1 ], WOLFSSH_FWD_CHANNEL_ID );
3409+
3410+ selfChannelId = harness .ssh -> channelList -> channel ;
3411+ in2Sz = BuildChannelClosePacket (selfChannelId , in2 , sizeof (in2 ));
3412+ RepointHarnessInput (& harness , in2 , in2Sz );
3413+
3414+ ret = DoReceive (harness .ssh );
3415+ AssertIntEQ (ret , WS_CHANNEL_CLOSED );
3416+ AssertIntEQ (harness .ssh -> channelListSz , 0 );
3417+ AssertIntEQ (fwdActionCount , 3 );
3418+ AssertIntEQ (fwdActions [2 ], WOLFSSH_FWD_LOCAL_CLEANUP );
3419+
3420+ FreeChannelOpenHarness (& harness );
3421+ }
3422+
3423+ /* Closes a channel this side created itself, which never saw a
3424+ * LOCAL_SETUP, and reports how many forwarding actions that produced. */
3425+ static word32 CloseSelfOpenedChannel (byte channelType )
3426+ {
3427+ ChannelOpenHarness harness ;
3428+ WOLFSSH_CHANNEL * channel ;
3429+ byte in [64 ];
3430+ word32 inSz ;
3431+ word32 selfChannelId ;
3432+
3433+ fwdActionCount = 0 ;
3434+
3435+ InitChannelOpenHarness (& harness , NULL , 0 );
3436+ AssertIntEQ (wolfSSH_CTX_SetFwdCb (harness .ctx , RecordingFwdCb , NULL ),
3437+ WS_SUCCESS );
3438+
3439+ channel = ChannelNew (harness .ssh , channelType , 1024 , 1024 );
3440+ AssertNotNull (channel );
3441+ AssertIntEQ (ChannelUpdatePeer (channel , 5 , 1024 , 1024 ), WS_SUCCESS );
3442+ AssertIntEQ (ChannelAppend (harness .ssh , channel ), WS_SUCCESS );
3443+ channel -> openConfirmed = 1 ;
3444+ selfChannelId = channel -> channel ;
3445+
3446+ inSz = BuildChannelClosePacket (selfChannelId , in , sizeof (in ));
3447+ RepointHarnessInput (& harness , in , inSz );
3448+
3449+ AssertIntEQ (DoReceive (harness .ssh ), WS_CHANNEL_CLOSED );
3450+ AssertIntEQ (harness .ssh -> channelListSz , 0 );
3451+
3452+ FreeChannelOpenHarness (& harness );
3453+
3454+ return fwdActionCount ;
3455+ }
3456+
3457+ /* wolfSSH_ChannelFwdNewLocal() and wolfSSH_ChannelFwdNewRemote() build a
3458+ * forwarding channel without a LOCAL_SETUP, so its close owes no cleanup:
3459+ * the application never armed one and would be freeing what it does not
3460+ * own. This is why the emission is gated on the channel, not its type. */
3461+ static void TestLocalForwardCloseSendsNoCleanup (void )
3462+ {
3463+ AssertIntEQ (CloseSelfOpenedChannel (ID_CHANTYPE_TCPIP_DIRECT ), 0 );
3464+ }
3465+
3466+ /* And a session channel produces no forwarding action at all. */
3467+ static void TestSessionCloseSendsNoFwdAction (void )
3468+ {
3469+ AssertIntEQ (CloseSelfOpenedChannel (ID_CHANTYPE_SESSION ), 0 );
3470+ }
3471+
3472+ /* A setup that succeeded is owed its cleanup even when the open goes on to
3473+ * fail, or the application keeps a socket for a channel that never existed.
3474+ * The channel-id handoff rejecting is the reachable way in. */
3475+ static void TestFailedOpenAfterSetupSendsCleanup (void )
3476+ {
3477+ ChannelOpenHarness harness ;
3478+ byte extra [128 ];
3479+ byte in [192 ];
3480+ word32 extraSz ;
3481+ word32 inSz ;
3482+ int ret ;
3483+
3484+ fwdActionCount = 0 ;
3485+
3486+ extraSz = BuildDirectTcpipExtra ("127.0.0.1" , 8080 , "127.0.0.1" , 2222 ,
3487+ extra , sizeof (extra ));
3488+ inSz = BuildChannelOpenPacket ("direct-tcpip" , 9 , 0x4000 , 0x8000 ,
3489+ extra , extraSz , in , sizeof (in ));
3490+
3491+ InitChannelOpenHarness (& harness , in , inSz );
3492+ AssertIntEQ (wolfSSH_CTX_SetFwdCb (harness .ctx ,
3493+ RecordingRejectChannelIdFwdCb , NULL ), WS_SUCCESS );
3494+
3495+ ret = DoReceive (harness .ssh );
3496+ AssertChannelOpenFailResponse (& harness , ret );
3497+ AssertIntEQ (harness .ssh -> channelListSz , 0 );
3498+ AssertIntEQ (fwdActionCount , 3 );
3499+ AssertIntEQ (fwdActions [0 ], WOLFSSH_FWD_LOCAL_SETUP );
3500+ AssertIntEQ (fwdActions [1 ], WOLFSSH_FWD_CHANNEL_ID );
3501+ AssertIntEQ (fwdActions [2 ], WOLFSSH_FWD_LOCAL_CLEANUP );
3502+
3503+ FreeChannelOpenHarness (& harness );
3504+ }
3505+
3506+ /* A setup that reported failure set nothing up, so it is owed no cleanup;
3507+ * one would have the application release what it never acquired. */
3508+ static void TestRejectedSetupSendsNoCleanup (void )
3509+ {
3510+ ChannelOpenHarness harness ;
3511+ byte extra [128 ];
3512+ byte in [192 ];
3513+ word32 extraSz ;
3514+ word32 inSz ;
3515+ int ret ;
3516+
3517+ fwdActionCount = 0 ;
3518+
3519+ extraSz = BuildDirectTcpipExtra ("127.0.0.1" , 8080 , "127.0.0.1" , 2222 ,
3520+ extra , sizeof (extra ));
3521+ inSz = BuildChannelOpenPacket ("direct-tcpip" , 9 , 0x4000 , 0x8000 ,
3522+ extra , extraSz , in , sizeof (in ));
3523+
3524+ InitChannelOpenHarness (& harness , in , inSz );
3525+ AssertIntEQ (wolfSSH_CTX_SetFwdCb (harness .ctx , RecordingRejectSetupFwdCb ,
3526+ NULL ), WS_SUCCESS );
3527+
3528+ ret = DoReceive (harness .ssh );
3529+ AssertChannelOpenFailResponse (& harness , ret );
3530+ AssertIntEQ (harness .ssh -> channelListSz , 0 );
3531+ AssertIntEQ (fwdActionCount , 1 );
3532+ AssertIntEQ (fwdActions [0 ], WOLFSSH_FWD_LOCAL_SETUP );
3533+
3534+ FreeChannelOpenHarness (& harness );
3535+ }
3536+
3537+ /* Opens a direct-tcpip forward through the recording callback and checks
3538+ * the setup and channel-id handoff went out, leaving the channel live. */
3539+ static void OpenRecordedDirectTcpip (ChannelOpenHarness * harness ,
3540+ byte * in , word32 inSz )
3541+ {
3542+ byte extra [128 ];
3543+ word32 extraSz ;
3544+
3545+ fwdActionCount = 0 ;
3546+
3547+ extraSz = BuildDirectTcpipExtra ("127.0.0.1" , 8080 , "127.0.0.1" , 2222 ,
3548+ extra , sizeof (extra ));
3549+ inSz = BuildChannelOpenPacket ("direct-tcpip" , 9 , 0x4000 , 0x8000 ,
3550+ extra , extraSz , in , inSz );
3551+
3552+ InitChannelOpenHarness (harness , in , inSz );
3553+ AssertIntEQ (wolfSSH_CTX_SetFwdCb (harness -> ctx , RecordingFwdCb , NULL ),
3554+ WS_SUCCESS );
3555+
3556+ AssertIntEQ (DoReceive (harness -> ssh ), WS_SUCCESS );
3557+ AssertIntEQ (harness -> ssh -> channelListSz , 1 );
3558+ AssertIntEQ (fwdActionCount , 2 );
3559+ AssertIntEQ (fwdActions [0 ], WOLFSSH_FWD_LOCAL_SETUP );
3560+ AssertIntEQ (fwdActions [1 ], WOLFSSH_FWD_CHANNEL_ID );
3561+ }
3562+
3563+ /* A dropped transport never delivers a CHANNEL_CLOSE, so freeing the session
3564+ * is the only chance the application gets to release a forward's socket. */
3565+ static void TestSessionFreeSendsLocalCleanup (void )
3566+ {
3567+ ChannelOpenHarness harness ;
3568+ byte in [192 ];
3569+
3570+ OpenRecordedDirectTcpip (& harness , in , sizeof (in ));
3571+
3572+ FreeChannelOpenHarness (& harness );
3573+ AssertIntEQ (fwdActionCount , 3 );
3574+ AssertIntEQ (fwdActions [2 ], WOLFSSH_FWD_LOCAL_CLEANUP );
3575+ }
3576+
3577+ /* Same for a channel the application retires itself, and only once: the
3578+ * session free that follows must not report it again. */
3579+ static void TestChannelFreeSendsLocalCleanup (void )
3580+ {
3581+ ChannelOpenHarness harness ;
3582+ byte in [192 ];
3583+
3584+ OpenRecordedDirectTcpip (& harness , in , sizeof (in ));
3585+
3586+ AssertIntEQ (wolfSSH_ChannelFree (harness .ssh -> channelList ), WS_SUCCESS );
3587+ AssertIntEQ (harness .ssh -> channelListSz , 0 );
3588+ AssertIntEQ (fwdActionCount , 3 );
3589+ AssertIntEQ (fwdActions [2 ], WOLFSSH_FWD_LOCAL_CLEANUP );
3590+
3591+ FreeChannelOpenHarness (& harness );
3592+ AssertIntEQ (fwdActionCount , 3 );
3593+ }
3594+
33303595static void TestForwardedTcpipOnServerSendsOpenFail (void )
33313596{
33323597 ChannelOpenHarness harness ;
@@ -12219,6 +12484,13 @@ int main(int argc, char** argv)
1221912484 TestDirectTcpipOpenCbRejectBeatsFwdCb ();
1222012485 TestDirectTcpipFwdCbRejectAfterOpenCbAccept ();
1222112486 TestDirectTcpipFwdCbRejectsChannelId ();
12487+ TestDirectTcpipCloseSendsLocalCleanup ();
12488+ TestLocalForwardCloseSendsNoCleanup ();
12489+ TestSessionCloseSendsNoFwdAction ();
12490+ TestFailedOpenAfterSetupSendsCleanup ();
12491+ TestRejectedSetupSendsNoCleanup ();
12492+ TestSessionFreeSendsLocalCleanup ();
12493+ TestChannelFreeSendsLocalCleanup ();
1222212494 TestForwardedTcpipOnServerSendsOpenFail ();
1222312495 TestGlobalRequestFwdNoCbSendsFailure ();
1222412496#ifndef NO_WOLFSSH_CLIENT
0 commit comments