@@ -1951,6 +1951,51 @@ static int RejectDirectTcpipSetup(WS_FwdCbAction action, void* ctx,
19511951 return WS_SUCCESS ;
19521952}
19531953
1954+ #define REGRESS_FWD_ACTION_MAX 8
1955+ static WS_FwdCbAction fwdActions [REGRESS_FWD_ACTION_MAX ];
1956+ static word32 fwdActionCount ;
1957+
1958+ /* Records the actions the library asked for, so a test can assert the
1959+ * sequence a forwarding channel produces. */
1960+ static int RecordingFwdCb (WS_FwdCbAction action , void * ctx ,
1961+ const char * host , word32 port )
1962+ {
1963+ (void )ctx ;
1964+ (void )host ;
1965+ (void )port ;
1966+
1967+ if (fwdActionCount < REGRESS_FWD_ACTION_MAX )
1968+ fwdActions [fwdActionCount ] = action ;
1969+ fwdActionCount ++ ;
1970+
1971+ return WS_SUCCESS ;
1972+ }
1973+
1974+ /* As RecordingFwdCb, but refuses the channel-id handoff so the open fails
1975+ * after a successful setup. */
1976+ static int RecordingRejectChannelIdFwdCb (WS_FwdCbAction action , void * ctx ,
1977+ const char * host , word32 port )
1978+ {
1979+ int ret = RecordingFwdCb (action , ctx , host , port );
1980+
1981+ if (action == WOLFSSH_FWD_CHANNEL_ID )
1982+ return WS_FWD_NOT_AVAILABLE ;
1983+
1984+ return ret ;
1985+ }
1986+
1987+ /* As RecordingFwdCb, but refuses the setup itself. */
1988+ static int RecordingRejectSetupFwdCb (WS_FwdCbAction action , void * ctx ,
1989+ const char * host , word32 port )
1990+ {
1991+ int ret = RecordingFwdCb (action , ctx , host , port );
1992+
1993+ if (action == WOLFSSH_FWD_LOCAL_SETUP )
1994+ return WS_FWD_SETUP_E ;
1995+
1996+ return ret ;
1997+ }
1998+
19541999/* Counts every action the library asks for. File scope rather than reached
19552000 * through the callback ctx, so a zero reading means the hook did not run and
19562001 * cannot instead mean the ctx stopped being delivered. */
@@ -3249,6 +3294,226 @@ static void TestDirectTcpipFwdCbRejectsChannelId(void)
32493294 FreeChannelOpenHarness (& harness );
32503295}
32513296
3297+ /* A forwarding channel that opened with a LOCAL_SETUP has to report a
3298+ * LOCAL_CLEANUP when it closes, or an application has no hook to release what
3299+ * it set up. */
3300+ static void TestDirectTcpipCloseSendsLocalCleanup (void )
3301+ {
3302+ ChannelOpenHarness harness ;
3303+ byte extra [128 ];
3304+ byte in [192 ];
3305+ byte in2 [64 ];
3306+ word32 extraSz ;
3307+ word32 inSz ;
3308+ word32 in2Sz ;
3309+ word32 selfChannelId ;
3310+ int ret ;
3311+
3312+ fwdActionCount = 0 ;
3313+
3314+ extraSz = BuildDirectTcpipExtra ("127.0.0.1" , 8080 , "127.0.0.1" , 2222 ,
3315+ extra , sizeof (extra ));
3316+ inSz = BuildChannelOpenPacket ("direct-tcpip" , 9 , 0x4000 , 0x8000 ,
3317+ extra , extraSz , in , sizeof (in ));
3318+
3319+ InitChannelOpenHarness (& harness , in , inSz );
3320+ AssertIntEQ (wolfSSH_CTX_SetFwdCb (harness .ctx , RecordingFwdCb , NULL ),
3321+ WS_SUCCESS );
3322+
3323+ ret = DoReceive (harness .ssh );
3324+ AssertIntEQ (ret , WS_SUCCESS );
3325+ AssertIntEQ (ParseMsgId (harness .io .out , harness .io .outSz ),
3326+ MSGID_CHANNEL_OPEN_CONF );
3327+ AssertIntEQ (harness .ssh -> channelListSz , 1 );
3328+ AssertIntEQ (fwdActionCount , 2 );
3329+ AssertIntEQ (fwdActions [0 ], WOLFSSH_FWD_LOCAL_SETUP );
3330+ AssertIntEQ (fwdActions [1 ], WOLFSSH_FWD_CHANNEL_ID );
3331+
3332+ selfChannelId = harness .ssh -> channelList -> channel ;
3333+ in2Sz = BuildChannelClosePacket (selfChannelId , in2 , sizeof (in2 ));
3334+ RepointHarnessInput (& harness , in2 , in2Sz );
3335+
3336+ ret = DoReceive (harness .ssh );
3337+ AssertIntEQ (ret , WS_CHANNEL_CLOSED );
3338+ AssertIntEQ (harness .ssh -> channelListSz , 0 );
3339+ AssertIntEQ (fwdActionCount , 3 );
3340+ AssertIntEQ (fwdActions [2 ], WOLFSSH_FWD_LOCAL_CLEANUP );
3341+
3342+ FreeChannelOpenHarness (& harness );
3343+ }
3344+
3345+ /* Closes a channel this side created itself, which never saw a
3346+ * LOCAL_SETUP, and reports how many forwarding actions that produced. */
3347+ static word32 CloseSelfOpenedChannel (byte channelType )
3348+ {
3349+ ChannelOpenHarness harness ;
3350+ WOLFSSH_CHANNEL * channel ;
3351+ byte in [64 ];
3352+ word32 inSz ;
3353+ word32 selfChannelId ;
3354+
3355+ fwdActionCount = 0 ;
3356+
3357+ InitChannelOpenHarness (& harness , NULL , 0 );
3358+ AssertIntEQ (wolfSSH_CTX_SetFwdCb (harness .ctx , RecordingFwdCb , NULL ),
3359+ WS_SUCCESS );
3360+
3361+ channel = ChannelNew (harness .ssh , channelType , 1024 , 1024 );
3362+ AssertNotNull (channel );
3363+ AssertIntEQ (ChannelUpdatePeer (channel , 5 , 1024 , 1024 ), WS_SUCCESS );
3364+ AssertIntEQ (ChannelAppend (harness .ssh , channel ), WS_SUCCESS );
3365+ channel -> openConfirmed = 1 ;
3366+ selfChannelId = channel -> channel ;
3367+
3368+ inSz = BuildChannelClosePacket (selfChannelId , in , sizeof (in ));
3369+ RepointHarnessInput (& harness , in , inSz );
3370+
3371+ AssertIntEQ (DoReceive (harness .ssh ), WS_CHANNEL_CLOSED );
3372+ AssertIntEQ (harness .ssh -> channelListSz , 0 );
3373+
3374+ FreeChannelOpenHarness (& harness );
3375+
3376+ return fwdActionCount ;
3377+ }
3378+
3379+ /* wolfSSH_ChannelFwdNewLocal() and wolfSSH_ChannelFwdNewRemote() build a
3380+ * forwarding channel without a LOCAL_SETUP, so its close owes no cleanup:
3381+ * the application never armed one and would be freeing what it does not
3382+ * own. This is why the emission is gated on the channel, not its type. */
3383+ static void TestLocalForwardCloseSendsNoCleanup (void )
3384+ {
3385+ AssertIntEQ (CloseSelfOpenedChannel (ID_CHANTYPE_TCPIP_DIRECT ), 0 );
3386+ }
3387+
3388+ /* And a session channel produces no forwarding action at all. */
3389+ static void TestSessionCloseSendsNoFwdAction (void )
3390+ {
3391+ AssertIntEQ (CloseSelfOpenedChannel (ID_CHANTYPE_SESSION ), 0 );
3392+ }
3393+
3394+ /* A setup that succeeded is owed its cleanup even when the open goes on to
3395+ * fail, or the application keeps a socket for a channel that never existed.
3396+ * The channel-id handoff rejecting is the reachable way in. */
3397+ static void TestFailedOpenAfterSetupSendsCleanup (void )
3398+ {
3399+ ChannelOpenHarness harness ;
3400+ byte extra [128 ];
3401+ byte in [192 ];
3402+ word32 extraSz ;
3403+ word32 inSz ;
3404+ int ret ;
3405+
3406+ fwdActionCount = 0 ;
3407+
3408+ extraSz = BuildDirectTcpipExtra ("127.0.0.1" , 8080 , "127.0.0.1" , 2222 ,
3409+ extra , sizeof (extra ));
3410+ inSz = BuildChannelOpenPacket ("direct-tcpip" , 9 , 0x4000 , 0x8000 ,
3411+ extra , extraSz , in , sizeof (in ));
3412+
3413+ InitChannelOpenHarness (& harness , in , inSz );
3414+ AssertIntEQ (wolfSSH_CTX_SetFwdCb (harness .ctx ,
3415+ RecordingRejectChannelIdFwdCb , NULL ), WS_SUCCESS );
3416+
3417+ ret = DoReceive (harness .ssh );
3418+ AssertChannelOpenFailResponse (& harness , ret );
3419+ AssertIntEQ (harness .ssh -> channelListSz , 0 );
3420+ AssertIntEQ (fwdActionCount , 3 );
3421+ AssertIntEQ (fwdActions [0 ], WOLFSSH_FWD_LOCAL_SETUP );
3422+ AssertIntEQ (fwdActions [1 ], WOLFSSH_FWD_CHANNEL_ID );
3423+ AssertIntEQ (fwdActions [2 ], WOLFSSH_FWD_LOCAL_CLEANUP );
3424+
3425+ FreeChannelOpenHarness (& harness );
3426+ }
3427+
3428+ /* A setup that reported failure set nothing up, so it is owed no cleanup;
3429+ * one would have the application release what it never acquired. */
3430+ static void TestRejectedSetupSendsNoCleanup (void )
3431+ {
3432+ ChannelOpenHarness harness ;
3433+ byte extra [128 ];
3434+ byte in [192 ];
3435+ word32 extraSz ;
3436+ word32 inSz ;
3437+ int ret ;
3438+
3439+ fwdActionCount = 0 ;
3440+
3441+ extraSz = BuildDirectTcpipExtra ("127.0.0.1" , 8080 , "127.0.0.1" , 2222 ,
3442+ extra , sizeof (extra ));
3443+ inSz = BuildChannelOpenPacket ("direct-tcpip" , 9 , 0x4000 , 0x8000 ,
3444+ extra , extraSz , in , sizeof (in ));
3445+
3446+ InitChannelOpenHarness (& harness , in , inSz );
3447+ AssertIntEQ (wolfSSH_CTX_SetFwdCb (harness .ctx , RecordingRejectSetupFwdCb ,
3448+ NULL ), WS_SUCCESS );
3449+
3450+ ret = DoReceive (harness .ssh );
3451+ AssertChannelOpenFailResponse (& harness , ret );
3452+ AssertIntEQ (harness .ssh -> channelListSz , 0 );
3453+ AssertIntEQ (fwdActionCount , 1 );
3454+ AssertIntEQ (fwdActions [0 ], WOLFSSH_FWD_LOCAL_SETUP );
3455+
3456+ FreeChannelOpenHarness (& harness );
3457+ }
3458+
3459+ /* Opens a direct-tcpip forward through the recording callback and checks
3460+ * the setup and channel-id handoff went out, leaving the channel live. */
3461+ static void OpenRecordedDirectTcpip (ChannelOpenHarness * harness ,
3462+ byte * in , word32 inSz )
3463+ {
3464+ byte extra [128 ];
3465+ word32 extraSz ;
3466+
3467+ fwdActionCount = 0 ;
3468+
3469+ extraSz = BuildDirectTcpipExtra ("127.0.0.1" , 8080 , "127.0.0.1" , 2222 ,
3470+ extra , sizeof (extra ));
3471+ inSz = BuildChannelOpenPacket ("direct-tcpip" , 9 , 0x4000 , 0x8000 ,
3472+ extra , extraSz , in , inSz );
3473+
3474+ InitChannelOpenHarness (harness , in , inSz );
3475+ AssertIntEQ (wolfSSH_CTX_SetFwdCb (harness -> ctx , RecordingFwdCb , NULL ),
3476+ WS_SUCCESS );
3477+
3478+ AssertIntEQ (DoReceive (harness -> ssh ), WS_SUCCESS );
3479+ AssertIntEQ (harness -> ssh -> channelListSz , 1 );
3480+ AssertIntEQ (fwdActionCount , 2 );
3481+ AssertIntEQ (fwdActions [0 ], WOLFSSH_FWD_LOCAL_SETUP );
3482+ AssertIntEQ (fwdActions [1 ], WOLFSSH_FWD_CHANNEL_ID );
3483+ }
3484+
3485+ /* A dropped transport never delivers a CHANNEL_CLOSE, so freeing the session
3486+ * is the only chance the application gets to release a forward's socket. */
3487+ static void TestSessionFreeSendsLocalCleanup (void )
3488+ {
3489+ ChannelOpenHarness harness ;
3490+ byte in [192 ];
3491+
3492+ OpenRecordedDirectTcpip (& harness , in , sizeof (in ));
3493+
3494+ FreeChannelOpenHarness (& harness );
3495+ AssertIntEQ (fwdActionCount , 3 );
3496+ AssertIntEQ (fwdActions [2 ], WOLFSSH_FWD_LOCAL_CLEANUP );
3497+ }
3498+
3499+ /* Same for a channel the application retires itself, and only once: the
3500+ * session free that follows must not report it again. */
3501+ static void TestChannelFreeSendsLocalCleanup (void )
3502+ {
3503+ ChannelOpenHarness harness ;
3504+ byte in [192 ];
3505+
3506+ OpenRecordedDirectTcpip (& harness , in , sizeof (in ));
3507+
3508+ AssertIntEQ (wolfSSH_ChannelFree (harness .ssh -> channelList ), WS_SUCCESS );
3509+ AssertIntEQ (harness .ssh -> channelListSz , 0 );
3510+ AssertIntEQ (fwdActionCount , 3 );
3511+ AssertIntEQ (fwdActions [2 ], WOLFSSH_FWD_LOCAL_CLEANUP );
3512+
3513+ FreeChannelOpenHarness (& harness );
3514+ AssertIntEQ (fwdActionCount , 3 );
3515+ }
3516+
32523517static void TestForwardedTcpipOnServerSendsOpenFail (void )
32533518{
32543519 ChannelOpenHarness harness ;
@@ -12137,6 +12402,13 @@ int main(int argc, char** argv)
1213712402 TestDirectTcpipOpenCbRejectBeatsFwdCb ();
1213812403 TestDirectTcpipFwdCbRejectAfterOpenCbAccept ();
1213912404 TestDirectTcpipFwdCbRejectsChannelId ();
12405+ TestDirectTcpipCloseSendsLocalCleanup ();
12406+ TestLocalForwardCloseSendsNoCleanup ();
12407+ TestSessionCloseSendsNoFwdAction ();
12408+ TestFailedOpenAfterSetupSendsCleanup ();
12409+ TestRejectedSetupSendsNoCleanup ();
12410+ TestSessionFreeSendsLocalCleanup ();
12411+ TestChannelFreeSendsLocalCleanup ();
1214012412 TestForwardedTcpipOnServerSendsOpenFail ();
1214112413 TestGlobalRequestFwdNoCbSendsFailure ();
1214212414#ifndef NO_WOLFSSH_CLIENT
0 commit comments