Notify channel on KeyEstablished exception 91/115491/11
authorPeter Suna <peter.suna@pantheon.tech>
Tue, 18 Feb 2025 11:16:31 +0000 (12:16 +0100)
committerIvan Hrasko <ivan.hrasko@pantheon.tech>
Wed, 26 Feb 2025 14:53:32 +0000 (14:53 +0000)
Call notifyTransportChannelFailed method when SshClient
failed on KeyEstablished with the IOException.

If IOException close the session and TransportChannel
is not ready, then it is necessary to call
notifyTransportChannelFailed which triggers the reconnection process.

JIRA: NETCONF-1423
Change-Id: I5aa4714a3d941b7ba6b1a26d95ea10b7af83840a
Signed-off-by: Peter Suna <peter.suna@pantheon.tech>
transport/transport-ssh/src/main/java/org/opendaylight/netconf/transport/ssh/SSHTransportStack.java
transport/transport-ssh/src/test/java/org/opendaylight/netconf/transport/ssh/NC1423Test.java

index 952a80afa1f459dd10339a7c9184c2c05cd8a17d..cf5cb92b042f444e00b27aac0c99651fa2a41e74 100644 (file)
@@ -97,7 +97,7 @@ public abstract sealed class SSHTransportStack extends AbstractOverlayTransportS
                         onKeyEstablished(session);
                     } catch (IOException e) {
                         LOG.error("Post-key step failed on session {}", sessionId, e);
-                        deleteSession(sessionId);
+                        transportFailed(sessionId, e);
                     }
                 }
                 case Authenticated -> {
@@ -180,13 +180,6 @@ public abstract sealed class SSHTransportStack extends AbstractOverlayTransportS
         });
     }
 
-    @NonNullByDefault
-    private void deleteSession(final Long sessionId) {
-        sessions.remove(sessionId);
-        // auth failure, close underlay if any
-        completeUnderlay(sessionId, underlay -> underlay.channel().close());
-    }
-
     @NonNullByDefault
     private void completeUnderlay(final Long sessionId, final Consumer<TransportChannel> action) {
         final var removed = underlays.remove(sessionId);
index 3a98316c8bb7cea34127c2b6226de5dce562b8f5..e02fcf6075df2dd423f0342ba84d75d04eab3c9f 100644 (file)
@@ -21,6 +21,7 @@ import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 import static org.opendaylight.netconf.shaded.sshd.common.session.SessionListener.Event.Authenticated;
+import static org.opendaylight.netconf.shaded.sshd.common.session.SessionListener.Event.KeyEstablished;
 import static org.opendaylight.netconf.transport.ssh.TestUtils.buildClientAuthWithPassword;
 import static org.opendaylight.netconf.transport.ssh.TestUtils.buildClientIdentityWithPassword;
 import static org.opendaylight.netconf.transport.ssh.TestUtils.buildServerIdentityWithKeyPair;
@@ -36,6 +37,7 @@ import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.ArgumentCaptor;
+import org.opendaylight.netconf.shaded.sshd.common.SshException;
 import org.opendaylight.netconf.shaded.sshd.common.io.IoSession;
 import org.opendaylight.netconf.shaded.sshd.common.session.Session;
 import org.opendaylight.netconf.shaded.sshd.common.session.SessionListener;
@@ -89,6 +91,56 @@ class NC1423Test extends AbstractClientServerTest {
         );
     }
 
+    @Test
+    void testKeyEstablishedEventFailure() throws Exception {
+        // Prepares a use case where KeyEstablished sessionEvent is called and fails to update current authFutureHolder
+        // in ClientUserAuthService.
+        final var authFutureUpdateExc = new SshException("Authentication already ongoing");
+        doAnswer(sessionListenerInv -> {
+            final var sessionListenerSpy = spy(sessionListenerInv.<SessionListener>getArgument(0));
+            // Capture sessionEvent on spy SessionListener and modifies its parameters to throw an IOException.
+            doAnswer(sessionEventInv -> {
+                final var sessionArg = sessionEventInv.<Session>getArgument(0);
+
+                // Prepare mocked session with correct ID.
+                final var sessionMock = mock(TransportClientSession.class);
+                final var ioSessionMock = mock(IoSession.class);
+                doReturn(sessionArg.getIoSession().getId()).when(ioSessionMock).getId();
+                doReturn(ioSessionMock).when(sessionMock).getIoSession();
+                // Throw exception when createSubsystemChannel method is called.
+                doThrow(authFutureUpdateExc).when(sessionMock).auth();
+
+                // Replace original arguments with mock session and Authenticated Event.
+                final var arguments = sessionEventInv.getArguments();
+                arguments[0] = sessionMock;
+                arguments[1] = KeyEstablished;
+                // Invoke real sessionEvent method with modified parameters.
+                return sessionEventInv.callRealMethod();
+            }).when(sessionListenerSpy).sessionEvent(any(), any());
+
+            // Replace original arguments with prepared spy Listener.
+            final var arguments = sessionListenerInv.getArguments();
+            arguments[0] = sessionListenerSpy;
+            // Invoke real addSessionListener method with modified parameters.
+            return sessionListenerInv.callRealMethod();
+        }).when(transportSshSpy).addSessionListener(any());
+
+        sshClient = SSHClient.of(SUBSYSTEM, clientListener, transportSshSpy);
+        sshServer = SSHServer.of(serviceFactory, group, SUBSYSTEM, serverListener, sshServerConfig, null);
+
+        // Execute connect.
+        sshServer.listen(serverBootstrap, tcpServerConfig).get(2, TimeUnit.SECONDS);
+        sshClient.connect(clientBootstrap, tcpClientConfig).get(2, TimeUnit.SECONDS);
+
+        // Verify thrown IOException exception.
+        final var exceptionCapture = ArgumentCaptor.forClass(IOException.class);
+        verify(clientListener, timeout(2000)).onTransportChannelFailed(exceptionCapture.capture());
+        final var receivedException = exceptionCapture.getValue();
+
+        // Verify correct exception.
+        assertEquals(authFutureUpdateExc, receivedException);
+    }
+
     @Test
     void testAuthenticationSessionEventFailure() throws Exception {
         // Prepares a use case where Authenticated sessionEvent is called and fails to register a channel