You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by lg...@apache.org on 2019/02/12 08:17:25 UTC

[mina-sshd] 01/02: [SSHD-892] Inform user about possible session disconnect prior to disconnecting and allow intervention via SessionDisconnectHandler

This is an automated email from the ASF dual-hosted git repository.

lgoldstein pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git

commit f7b04b7b9643ecf5e42fc66f53dcfb9fe5cf31ac
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Sun Feb 10 16:28:39 2019 +0200

    [SSHD-892] Inform user about possible session disconnect prior to disconnecting and allow intervention via SessionDisconnectHandler
---
 CHANGES.md                                         |   6 +
 docs/event-listeners.md                            |   9 +
 .../sshd/client/session/AbstractClientSession.java |  12 +-
 .../org/apache/sshd/common/FactoryManager.java     |   2 +
 .../common/helpers/AbstractFactoryManager.java     |  12 ++
 .../org/apache/sshd/common/session/Session.java    |   4 +-
 .../common/session/SessionDisconnectHandler.java   | 132 ++++++++++++++
 .../session/SessionDisconnectHandlerManager.java   |  31 ++++
 .../common/session/helpers/AbstractSession.java    |   2 +-
 .../sshd/common/session/helpers/SessionHelper.java |  32 ++++
 .../sshd/server/session/AbstractServerSession.java |  24 ++-
 .../sshd/server/session/ServerUserAuthService.java |  62 +++++--
 .../session/helpers/AbstractSessionTest.java       |   2 +-
 .../java/org/apache/sshd/server/ServerTest.java    | 199 ++++++++++++++++-----
 14 files changed, 457 insertions(+), 72 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index 739b760..3ae98eb 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -7,7 +7,13 @@
 * The `ChannelSession` provides a mechanism for supporting non-standard extended data (a.k.a. STDERR data)
 in a similar manner as the "regular" data. Please read the relevant section in the main documentation page.
 
+* The user can use a registered `SessionDisconnectHandler` in order be informed and also intervene in cases
+where the code decides to disconnect the session due to various protocol or configuration parameters violations.
+
 ## Behavioral changes and enhancements
 
 * [SSHD-882](https://issues.apache.org/jira/browse/SSHD-882) - Provide hooks to allow users to register a consumer
 for STDERR data sent via the `ChannelSession` - especially for the SFTP subsystem.
+
+* [SSHD=892](https://issues.apache.org/jira/browse/SSHD-882) - Inform user about possible session disconnect prior
+to disconnecting and allow intervention via `SessionDisconnectHandler`.
diff --git a/docs/event-listeners.md b/docs/event-listeners.md
index 5436763..3b9beef 100644
--- a/docs/event-listeners.md
+++ b/docs/event-listeners.md
@@ -158,6 +158,15 @@ message received in the session as well.
 rather than being accumulated. However, one can use the `EventListenerUtils` and create a cumulative listener - see how
 `SessionListener` or `ChannelListener` proxies were implemented.
 
+### `SessionDisconnectHandler`
+
+This handler can be registered in order to monitor session disconnect initiated by the internal code due to various
+protocol requirements - e.g., unknown service, idle timeout, etc.. In many cases the implementor can intervene and
+cancel the disconnect by handling the problem somehow and then signaling to the code that there is no longer any need
+to disconnect. The handler can be registered globally at the `SshClient/Server` instance or per-session (via a `SessionListener`).
+
+**NOTE:** this handler is non-cumulative - i.e., setting it replaces any existing previous handler instance.
+
 ### `SignalListener`
 
 Informs about signal requests as described in [RFC 4254 - section 6.9](https://tools.ietf.org/html/rfc4254#section-6.9), break requests
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
index 57485ad..aebdd2c 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
@@ -61,6 +61,7 @@ import org.apache.sshd.common.kex.KexState;
 import org.apache.sshd.common.keyprovider.KeyIdentityProvider;
 import org.apache.sshd.common.session.ConnectionService;
 import org.apache.sshd.common.session.SessionContext;
+import org.apache.sshd.common.session.SessionDisconnectHandler;
 import org.apache.sshd.common.session.helpers.AbstractConnectionService;
 import org.apache.sshd.common.session.helpers.AbstractSession;
 import org.apache.sshd.common.util.GenericUtils;
@@ -373,7 +374,16 @@ public abstract class AbstractClientSession extends AbstractSession implements C
     }
 
     @Override
-    public void startService(String name) throws Exception {
+    public void startService(String name, Buffer buffer) throws Exception {
+        SessionDisconnectHandler handler = getSessionDisconnectHandler();
+        if ((handler != null)
+                && handler.handleUnsupportedServiceDisconnectReason(this, SshConstants.SSH_MSG_SERVICE_REQUEST, name, buffer)) {
+            if (log.isDebugEnabled()) {
+                log.debug("startService({}) ignore unknown service={} by handler", this, name);
+            }
+            return;
+        }
+
         throw new IllegalStateException("Starting services is not supported on the client side: " + name);
     }
 
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/FactoryManager.java b/sshd-core/src/main/java/org/apache/sshd/common/FactoryManager.java
index e9fa80e..d6bb13e 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/FactoryManager.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/FactoryManager.java
@@ -37,6 +37,7 @@ import org.apache.sshd.common.kex.KexFactoryManager;
 import org.apache.sshd.common.random.Random;
 import org.apache.sshd.common.session.ConnectionService;
 import org.apache.sshd.common.session.ReservedSessionMessagesManager;
+import org.apache.sshd.common.session.SessionDisconnectHandlerManager;
 import org.apache.sshd.common.session.SessionListenerManager;
 import org.apache.sshd.common.session.UnknownChannelReferenceHandlerManager;
 import org.apache.sshd.server.forward.AgentForwardingFilter;
@@ -54,6 +55,7 @@ public interface FactoryManager
         extends KexFactoryManager,
                 SessionListenerManager,
                 ReservedSessionMessagesManager,
+                SessionDisconnectHandlerManager,
                 ChannelListenerManager,
                 ChannelStreamPacketWriterResolverManager,
                 UnknownChannelReferenceHandlerManager,
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/helpers/AbstractFactoryManager.java b/sshd-core/src/main/java/org/apache/sshd/common/helpers/AbstractFactoryManager.java
index 16c563f..6549572 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/helpers/AbstractFactoryManager.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/helpers/AbstractFactoryManager.java
@@ -56,6 +56,7 @@ import org.apache.sshd.common.kex.AbstractKexFactoryManager;
 import org.apache.sshd.common.random.Random;
 import org.apache.sshd.common.session.ConnectionService;
 import org.apache.sshd.common.session.ReservedSessionMessagesHandler;
+import org.apache.sshd.common.session.SessionDisconnectHandler;
 import org.apache.sshd.common.session.SessionListener;
 import org.apache.sshd.common.session.UnknownChannelReferenceHandler;
 import org.apache.sshd.common.session.helpers.AbstractSessionFactory;
@@ -94,6 +95,7 @@ public abstract class AbstractFactoryManager extends AbstractKexFactoryManager i
     private final Map<AttributeRepository.AttributeKey<?>, Object> attributes = new ConcurrentHashMap<>();
     private PropertyResolver parentResolver = SyspropsMapWrapper.SYSPROPS_RESOLVER;
     private ReservedSessionMessagesHandler reservedSessionMessagesHandler;
+    private SessionDisconnectHandler sessionDisconnectHandler;
     private ChannelStreamPacketWriterResolver channelStreamPacketWriterResolver;
     private UnknownChannelReferenceHandler unknownChannelReferenceHandler;
     private IoServiceEventListener eventListener;
@@ -310,6 +312,16 @@ public abstract class AbstractFactoryManager extends AbstractKexFactoryManager i
     }
 
     @Override
+    public SessionDisconnectHandler getSessionDisconnectHandler() {
+        return sessionDisconnectHandler;
+    }
+
+    @Override
+    public void setSessionDisconnectHandler(SessionDisconnectHandler sessionDisconnectHandler) {
+        this.sessionDisconnectHandler = sessionDisconnectHandler;
+    }
+
+    @Override
     public ChannelStreamPacketWriterResolver getChannelStreamPacketWriterResolver() {
         return channelStreamPacketWriterResolver;
     }
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/Session.java b/sshd-core/src/main/java/org/apache/sshd/common/session/Session.java
index 90f2885..3c7f26e 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/session/Session.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/session/Session.java
@@ -57,6 +57,7 @@ public interface Session
                 KexFactoryManager,
                 SessionListenerManager,
                 ReservedSessionMessagesManager,
+                SessionDisconnectHandlerManager,
                 ChannelListenerManager,
                 ChannelStreamPacketWriterResolverManager,
                 PortForwardingEventListenerManager,
@@ -304,9 +305,10 @@ public interface Session
 
     /**
      * @param name Service name
+     * @param buffer Extra information provided when the service start request was received
      * @throws Exception If failed to start it
      */
-    void startService(String name) throws Exception;
+    void startService(String name, Buffer buffer) throws Exception;
 
     @Override
     default <T> T resolveAttribute(AttributeRepository.AttributeKey<T> key) {
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/SessionDisconnectHandler.java b/sshd-core/src/main/java/org/apache/sshd/common/session/SessionDisconnectHandler.java
new file mode 100644
index 0000000..d1e7dab
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/common/session/SessionDisconnectHandler.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.common.session;
+
+import java.io.IOException;
+
+import org.apache.sshd.common.Service;
+import org.apache.sshd.common.session.Session.TimeoutStatus;
+import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.server.ServerFactoryManager;
+
+/**
+ * Invoked when the internal session code decides it should disconnect
+ * a session due to some consideration. Usually allows intervening in
+ * the decision and even canceling it.
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface SessionDisconnectHandler {
+    /**
+     * Invoked when an internal timeout has expired (e.g., authentication, idle).
+     *
+     * @param session The session whose timeout has expired
+     * @param timeoutStatus The expired timeout
+     * @return {@code true} if expired timeout should be reset (i.e., no disconnect).
+     * If {@code false} then session will disconnect due to the expired timeout
+     * @throws IOException If failed to handle the event
+     */
+    default boolean handleTimeoutDisconnectReason(
+            Session session, TimeoutStatus timeoutStatus)
+                throws IOException {
+        return false;
+    }
+
+    /**
+     * Called to inform that the maximum allowed concurrent sessions threshold
+     * has been exceeded. <B>Note:</B> when handler is invoked the session is
+     * not yet marked as having been authenticated, nor has the authentication
+     * success been acknowledged to the peer.
+     *
+     * @param session The session that caused the excess
+     * @param service The {@link Service} instance through which the request was received
+     * @param username The authenticated username that is associated with the session.
+     * @param currentSessionCount The current sessions count
+     * @param maxSessionCount The maximum allowed sessions count
+     * @return {@code true} if accept the exceeding session regardless of the
+     * threshold. If {@code false} then exceeding session will be disconnected
+     * @throws IOException If failed to handle the event, <B>Note:</B> choosing
+     * to ignore this disconnect reason does not reset the current concurrent sessions
+     * counter in any way - i.e., the handler will be re-invoked every time the
+     * threshold is exceeded.
+     * @see ServerFactoryManager#MAX_CONCURRENT_SESSIONS
+     */
+    default boolean handleSessionsCountDisconnectReason(
+            Session session, Service service, String username, int currentSessionCount, int maxSessionCount)
+                throws IOException {
+        return false;
+    }
+
+    /**
+     * Invoked when a request has been made related to an unknown SSH
+     * service as described in <A HREF="https://tools.ietf.org/html/rfc4253#section-10">RFC 4253 - section 10</A>.
+     *
+     * @param session The session through which the command was received
+     * @param cmd The service related command
+     * @param serviceName The service name
+     * @param buffer Any extra data received in the packet containing the request
+     * @return {@code true} if disregard the request (e.g., the handler handled it)
+     * @throws IOException If failed to handle the request
+     */
+    default boolean handleUnsupportedServiceDisconnectReason(
+            Session session, int cmd, String serviceName, Buffer buffer)
+                throws IOException {
+        return false;
+    }
+
+    /**
+     * Invoked if the number of authentication attempts exceeded the maximum allowed
+     *
+     * @param session The session being authenticated
+     * @param service The {@link Service} instance through which the request was received
+     * @param serviceName The authentication service name
+     * @param method The authentication method name
+     * @param user The authentication username
+     * @param currentAuthCount The authentication attempt count
+     * @param maxAuthCount The maximum allowed attempts
+     * @return {@code true} if OK to ignore the exceeded attempt count and
+     * allow more attempts. <B>Note:</B> choosing to ignore this disconnect reason does
+     * not reset the current count - i.e., it will be re-invoked on the next attempt.
+     * @throws IOException If failed to handle the event
+     */
+    default boolean handleAuthCountDisconnectReason(
+            Session session, Service service, String serviceName, String method, String user, int currentAuthCount, int maxAuthCount)
+                throws IOException {
+        return false;
+    }
+
+    /**
+     * Invoked if the authentication parameters changed in mid-authentication process.
+     *
+     * @param session The session being authenticated
+     * @param service The {@link Service} instance through which the request was received
+     * @param authUser The original username being authenticated
+     * @param username The requested username
+     * @param authService The original authentication service name
+     * @param serviceName The requested service name
+     * @return {@code true} if OK to ignore the change
+     * @throws IOException If failed to handle the event
+     */
+    default boolean handleAuthParamsDisconnectReason(
+            Session session, Service service, String authUser, String username, String authService, String serviceName)
+                throws IOException {
+        return false;
+    }
+}
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/SessionDisconnectHandlerManager.java b/sshd-core/src/main/java/org/apache/sshd/common/session/SessionDisconnectHandlerManager.java
new file mode 100644
index 0000000..d75fee4
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/common/session/SessionDisconnectHandlerManager.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.common.session;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface SessionDisconnectHandlerManager {
+    SessionDisconnectHandler getSessionDisconnectHandler();
+
+    void setSessionDisconnectHandler(SessionDisconnectHandler handler);
+}
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractSession.java b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractSession.java
index e582039..b949d6a 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractSession.java
@@ -483,7 +483,7 @@ public abstract class AbstractSession extends SessionHelper {
         validateKexState(SshConstants.SSH_MSG_SERVICE_REQUEST, KexState.DONE);
 
         try {
-            startService(serviceName);
+            startService(serviceName, buffer);
         } catch (Throwable e) {
             if (debugEnabled) {
                 log.debug("handleServiceRequest({}) Service {} rejected: {} = {}",
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/SessionHelper.java b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/SessionHelper.java
index b2f16dd..a996d6d 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/SessionHelper.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/SessionHelper.java
@@ -61,6 +61,7 @@ import org.apache.sshd.common.session.ConnectionService;
 import org.apache.sshd.common.session.ReservedSessionMessagesHandler;
 import org.apache.sshd.common.session.Session;
 import org.apache.sshd.common.session.SessionContext;
+import org.apache.sshd.common.session.SessionDisconnectHandler;
 import org.apache.sshd.common.session.SessionListener;
 import org.apache.sshd.common.session.UnknownChannelReferenceHandler;
 import org.apache.sshd.common.util.GenericUtils;
@@ -99,6 +100,7 @@ public abstract class SessionHelper extends AbstractKexFactoryManager implements
     private final AtomicReference<TimeoutStatus> timeoutStatus = new AtomicReference<>(TimeoutStatus.NoTimeout);
 
     private ReservedSessionMessagesHandler reservedSessionMessagesHandler;
+    private SessionDisconnectHandler sessionDisconnectHandler;
     private UnknownChannelReferenceHandler unknownChannelReferenceHandler;
     private ChannelStreamPacketWriterResolver channelStreamPacketWriterResolver;
 
@@ -238,6 +240,25 @@ public abstract class SessionHelper extends AbstractKexFactoryManager implements
             return;
         }
 
+        SessionDisconnectHandler handler = getSessionDisconnectHandler();
+        if ((handler != null) && handler.handleTimeoutDisconnectReason(this, status)) {
+            if (log.isDebugEnabled()) {
+                log.debug("checkForTimeouts({}) cancel {} due to handler intervention", this, status);
+            }
+
+            switch(status) {
+                case AuthTimeout:
+                    resetAuthTimeout();
+                    break;
+                case IdleTimeout:
+                    resetIdleTimeout();
+                    break;
+
+                default:    // ignored
+            }
+            return;
+        }
+
         if (log.isDebugEnabled()) {
             log.debug("checkForTimeouts({}) disconnect - reason={}", this, status);
         }
@@ -330,6 +351,17 @@ public abstract class SessionHelper extends AbstractKexFactoryManager implements
         reservedSessionMessagesHandler = handler;
     }
 
+    @Override
+    public SessionDisconnectHandler getSessionDisconnectHandler() {
+        return resolveEffectiveProvider(SessionDisconnectHandler.class,
+            sessionDisconnectHandler, getFactoryManager().getSessionDisconnectHandler());
+    }
+
+    @Override
+    public void setSessionDisconnectHandler(SessionDisconnectHandler sessionDisconnectHandler) {
+        this.sessionDisconnectHandler = sessionDisconnectHandler;
+    }
+
     protected void handleIgnore(Buffer buffer) throws Exception {
         // malformed ignore message - ignore (even though we don't have to, but we can be tolerant in this case)
         if (!buffer.isValidMessageStructure(byte[].class)) {
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/session/AbstractServerSession.java b/sshd-core/src/main/java/org/apache/sshd/server/session/AbstractServerSession.java
index 2a5209c..523f16a 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/session/AbstractServerSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/session/AbstractServerSession.java
@@ -46,6 +46,7 @@ import org.apache.sshd.common.kex.KexState;
 import org.apache.sshd.common.keyprovider.KeyPairProvider;
 import org.apache.sshd.common.session.ConnectionService;
 import org.apache.sshd.common.session.SessionContext;
+import org.apache.sshd.common.session.SessionDisconnectHandler;
 import org.apache.sshd.common.session.helpers.AbstractSession;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.ValidateUtils;
@@ -227,7 +228,7 @@ public abstract class AbstractServerSession extends AbstractSession implements S
     }
 
     @Override
-    public void startService(String name) throws Exception {
+    public void startService(String name, Buffer buffer) throws Exception {
         FactoryManager factoryManager = getFactoryManager();
         currentService = ServiceFactory.create(
             factoryManager.getServiceFactories(),
@@ -240,6 +241,16 @@ public abstract class AbstractServerSession extends AbstractSession implements S
          *      appropriate SSH_MSG_DISCONNECT message and MUST disconnect.
          */
         if (currentService == null) {
+            SessionDisconnectHandler handler = getSessionDisconnectHandler();
+            if ((handler != null)
+                    && handler.handleUnsupportedServiceDisconnectReason(
+                            this, SshConstants.SSH_MSG_SERVICE_REQUEST, name, buffer)) {
+                if (log.isDebugEnabled()) {
+                    log.debug("startService({}) ignore unknown service={} by handler", this, name);
+                }
+                return;
+            }
+
             throw new SshException(SshConstants.SSH2_DISCONNECT_SERVICE_NOT_AVAILABLE, "Unknown service: " + name);
         }
     }
@@ -247,6 +258,17 @@ public abstract class AbstractServerSession extends AbstractSession implements S
     @Override
     protected void handleServiceAccept(String serviceName, Buffer buffer) throws Exception {
         super.handleServiceAccept(serviceName, buffer);
+
+        SessionDisconnectHandler handler = getSessionDisconnectHandler();
+        if ((handler != null)
+                && handler.handleUnsupportedServiceDisconnectReason(
+                        this, SshConstants.SSH_MSG_SERVICE_ACCEPT, serviceName, buffer)) {
+            if (log.isDebugEnabled()) {
+                log.debug("handleServiceAccept({}) ignore unknown service={} by handler", this, serviceName);
+            }
+            return;
+        }
+
         // TODO: can services be initiated by the server-side ?
         disconnect(SshConstants.SSH2_DISCONNECT_PROTOCOL_ERROR, "Unsupported packet: SSH_MSG_SERVICE_ACCEPT for " + serviceName);
     }
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/session/ServerUserAuthService.java b/sshd-core/src/main/java/org/apache/sshd/server/session/ServerUserAuthService.java
index d6f6d51..1e45bbd 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/session/ServerUserAuthService.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/session/ServerUserAuthService.java
@@ -47,6 +47,7 @@ import org.apache.sshd.common.SshException;
 import org.apache.sshd.common.config.keys.KeyRandomArt;
 import org.apache.sshd.common.io.IoWriteFuture;
 import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.session.SessionDisconnectHandler;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.NumberUtils;
 import org.apache.sshd.common.util.ValidateUtils;
@@ -173,20 +174,35 @@ public class ServerUserAuthService extends AbstractCloseable implements Service,
                       session, username, service, method);
             }
 
-            if (this.authUserName == null || this.authService == null) {
+            if ((this.authUserName == null) || (this.authService == null)) {
                 this.authUserName = username;
                 this.authService = service;
             } else if (this.authUserName.equals(username) && this.authService.equals(service)) {
                 nbAuthRequests++;
                 if (nbAuthRequests > maxAuthRequests) {
-                    session.disconnect(SshConstants.SSH2_DISCONNECT_PROTOCOL_ERROR,
-                        "Too many authentication failures: " + nbAuthRequests);
-                    return;
+                    SessionDisconnectHandler handler = session.getSessionDisconnectHandler();
+                    if ((handler == null)
+                            || (!handler.handleAuthCountDisconnectReason(
+                                    session, this, service, method, username, nbAuthRequests, maxAuthRequests))) {
+                        session.disconnect(SshConstants.SSH2_DISCONNECT_PROTOCOL_ERROR,
+                            "Too many authentication failures: " + nbAuthRequests);
+                        return;
+                    }
                 }
             } else {
-                session.disconnect(SshConstants.SSH2_DISCONNECT_PROTOCOL_ERROR,
-                    "Change of username or service is not allowed (" + this.authUserName + ", " + this.authService + ") -> ("
-                        + username + ", " + service + ")");
+                SessionDisconnectHandler handler = session.getSessionDisconnectHandler();
+                if ((handler != null)
+                        && handler.handleAuthParamsDisconnectReason(
+                            session, this, this.authUserName, username, this.authService, service)) {
+                    if (debugEnabled) {
+                        log.debug("process({}) ignore mismatched authentication parameters: user={}/{}, service={}/{}",
+                                session, this.authUserName, username, this.authService, service);
+                    }
+                } else {
+                    session.disconnect(SshConstants.SSH2_DISCONNECT_PROTOCOL_ERROR,
+                        "Change of username or service is not allowed (" + this.authUserName + ", " + this.authService + ") -> ("
+                            + username + ", " + service + ")");
+                }
                 return;
             }
 
@@ -287,7 +303,7 @@ public class ServerUserAuthService extends AbstractCloseable implements Service,
         boolean debugEnabled = log.isDebugEnabled();
         if (debugEnabled) {
             log.debug("handleAuthenticationSuccess({}@{}) {}",
-                  username, session, SshConstants.getCommandMessageName(cmd));
+                username, session, SshConstants.getCommandMessageName(cmd));
         }
 
         boolean success = false;
@@ -303,9 +319,19 @@ public class ServerUserAuthService extends AbstractCloseable implements Service,
             if (maxSessionCount != null) {
                 int currentSessionCount = session.getActiveSessionCountForUser(username);
                 if (currentSessionCount >= maxSessionCount) {
-                    session.disconnect(SshConstants.SSH2_DISCONNECT_TOO_MANY_CONNECTIONS,
-                        "Too many concurrent connections (" + currentSessionCount + ") - max. allowed: " + maxSessionCount);
-                    return;
+                    SessionDisconnectHandler handler = session.getSessionDisconnectHandler();
+                    if ((handler == null)
+                            || (!handler.handleSessionsCountDisconnectReason(
+                                    session, this, username, currentSessionCount, maxSessionCount))) {
+                        session.disconnect(SshConstants.SSH2_DISCONNECT_TOO_MANY_CONNECTIONS,
+                            "Too many concurrent connections (" + currentSessionCount + ") - max. allowed: " + maxSessionCount);
+                        return;
+                    } else {
+                        if (debugEnabled) {
+                            log.debug("handleAuthenticationSuccess({}@{}) ignore {}/{} sessions count due to handler intervention",
+                                username, session, currentSessionCount, maxSessionCount);
+                        }
+                    }
                 }
             }
 
@@ -313,11 +339,11 @@ public class ServerUserAuthService extends AbstractCloseable implements Service,
                 sendWelcomeBanner(session);
             }
 
-            buffer = session.createBuffer(SshConstants.SSH_MSG_USERAUTH_SUCCESS, Byte.SIZE);
-            session.writePacket(buffer);
+            Buffer response = session.createBuffer(SshConstants.SSH_MSG_USERAUTH_SUCCESS, Byte.SIZE);
+            session.writePacket(response);
             session.setUsername(username);
             session.setAuthenticated();
-            session.startService(authService);
+            session.startService(authService, buffer);
             session.resetIdleTimeout();
             log.info("Session {}@{} authenticated", username, session.getIoSession().getRemoteAddress());
         } else {
@@ -330,10 +356,10 @@ public class ServerUserAuthService extends AbstractCloseable implements Service,
                 log.debug("handleAuthenticationSuccess({}@{}) remaining methods={}", username, session, remaining);
             }
 
-            buffer = session.createBuffer(SshConstants.SSH_MSG_USERAUTH_FAILURE, remaining.length() + Byte.SIZE);
-            buffer.putString(remaining);
-            buffer.putBoolean(true);    // partial success ...
-            session.writePacket(buffer);
+            Buffer response = session.createBuffer(SshConstants.SSH_MSG_USERAUTH_FAILURE, remaining.length() + Byte.SIZE);
+            response.putString(remaining);
+            response.putBoolean(true);    // partial success ...
+            session.writePacket(response);
         }
 
         try {
diff --git a/sshd-core/src/test/java/org/apache/sshd/common/session/helpers/AbstractSessionTest.java b/sshd-core/src/test/java/org/apache/sshd/common/session/helpers/AbstractSessionTest.java
index 770dca8..f06fa1e 100644
--- a/sshd-core/src/test/java/org/apache/sshd/common/session/helpers/AbstractSessionTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/common/session/helpers/AbstractSessionTest.java
@@ -460,7 +460,7 @@ public class AbstractSessionTest extends BaseTestSupport {
         }
 
         @Override
-        public void startService(String name) throws Exception {
+        public void startService(String name, Buffer buffer) throws Exception {
             // ignored
         }
 
diff --git a/sshd-core/src/test/java/org/apache/sshd/server/ServerTest.java b/sshd-core/src/test/java/org/apache/sshd/server/ServerTest.java
index c874714..fefd7b1 100644
--- a/sshd-core/src/test/java/org/apache/sshd/server/ServerTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/server/ServerTest.java
@@ -60,7 +60,9 @@ import org.apache.sshd.common.channel.WindowClosedException;
 import org.apache.sshd.common.io.IoSession;
 import org.apache.sshd.common.kex.KexProposalOption;
 import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.session.Session.TimeoutStatus;
 import org.apache.sshd.common.session.SessionContext;
+import org.apache.sshd.common.session.SessionDisconnectHandler;
 import org.apache.sshd.common.session.SessionListener;
 import org.apache.sshd.common.session.helpers.AbstractConnectionService;
 import org.apache.sshd.common.session.helpers.AbstractSession;
@@ -199,24 +201,64 @@ public class ServerTest extends BaseTestSupport {
         final long testAuthTimeout = TimeUnit.SECONDS.toMillis(5L);
         PropertyResolverUtils.updateProperty(sshd, FactoryManager.AUTH_TIMEOUT, testAuthTimeout);
 
+        AtomicReference<TimeoutStatus> timeoutHolder = new AtomicReference<>();
+        sshd.setSessionDisconnectHandler(new SessionDisconnectHandler() {
+            @Override
+            public boolean handleTimeoutDisconnectReason(Session session, TimeoutStatus timeoutStatus)
+                    throws IOException {
+                outputDebugMessage("Session %s timeout reported: %s", session, timeoutStatus);
+                TimeoutStatus prev = timeoutHolder.getAndSet(timeoutStatus);
+                if (prev != null) {
+                    throw new StreamCorruptedException("Multiple timeout disconnects: " + timeoutStatus + " / " + prev);
+                }
+                return false;
+            }
+
+            @Override
+            public String toString() {
+                return SessionDisconnectHandler.class.getSimpleName() + "[" + getCurrentTestName() + "]";
+            }
+        });
         sshd.start();
         client.start();
-        try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, sshd.getPort()).verify(7L, TimeUnit.SECONDS).getSession()) {
-            Collection<ClientSession.ClientSessionEvent> res = s.waitFor(EnumSet.of(ClientSession.ClientSessionEvent.CLOSED), 2L * testAuthTimeout);
-            assertTrue("Session should be closed: " + res,
-                       res.containsAll(EnumSet.of(ClientSession.ClientSessionEvent.CLOSED, ClientSession.ClientSessionEvent.WAIT_AUTH)));
+        Collection<ClientSession.ClientSessionEvent> res;
+        try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, sshd.getPort())
+                .verify(7L, TimeUnit.SECONDS)
+                .getSession()) {
+            res = s.waitFor(EnumSet.of(ClientSession.ClientSessionEvent.CLOSED), 2L * testAuthTimeout);
         } finally {
             client.stop();
         }
+
+        assertTrue("Session should be closed: " + res,
+            res.containsAll(EnumSet.of(ClientSession.ClientSessionEvent.CLOSED, ClientSession.ClientSessionEvent.WAIT_AUTH)));
+        assertSame("Mismatched timeout status reported", TimeoutStatus.AuthTimeout, timeoutHolder.getAndSet(null));
     }
 
     @Test
     public void testIdleTimeout() throws Exception {
-        final CountDownLatch latch = new CountDownLatch(1);
-        TestEchoShell.latch = new CountDownLatch(1);
         final long testIdleTimeout = 2500L;
         PropertyResolverUtils.updateProperty(sshd, FactoryManager.IDLE_TIMEOUT, testIdleTimeout);
+        AtomicReference<TimeoutStatus> timeoutHolder = new AtomicReference<>();
+        CountDownLatch latch = new CountDownLatch(1);
+        TestEchoShell.latch = new CountDownLatch(1);
+        sshd.setSessionDisconnectHandler(new SessionDisconnectHandler() {
+            @Override
+            public boolean handleTimeoutDisconnectReason(Session session, TimeoutStatus timeoutStatus)
+                    throws IOException {
+                outputDebugMessage("Session %s timeout reported: %s", session, timeoutStatus);
+                TimeoutStatus prev = timeoutHolder.getAndSet(timeoutStatus);
+                if (prev != null) {
+                    throw new StreamCorruptedException("Multiple timeout disconnects: " + timeoutStatus + " / " + prev);
+                }
+                return false;
+            }
 
+            @Override
+            public String toString() {
+                return SessionDisconnectHandler.class.getSimpleName() + "[" + getCurrentTestName() + "]";
+            }
+        });
         sshd.addSessionListener(new SessionListener() {
             @Override
             public void sessionCreated(Session session) {
@@ -235,7 +277,8 @@ public class ServerTest extends BaseTestSupport {
             }
 
             @Override
-            public void sessionDisconnect(Session session, int reason, String msg, String language, boolean initiator) {
+            public void sessionDisconnect(
+                    Session session, int reason, String msg, String language, boolean initiator) {
                 outputDebugMessage("Session %s disconnected (sender=%s): reason=%d, message=%s",
                     session, initiator, reason, msg);
             }
@@ -245,6 +288,11 @@ public class ServerTest extends BaseTestSupport {
                 outputDebugMessage("Session closed: %s", session);
                 latch.countDown();
             }
+
+            @Override
+            public String toString() {
+                return SessionListener.class.getSimpleName() + "[" + getCurrentTestName() + "]";
+            }
         });
 
         TestChannelListener channelListener = new TestChannelListener(getCurrentTestName());
@@ -252,6 +300,7 @@ public class ServerTest extends BaseTestSupport {
         sshd.start();
 
         client.start();
+        Collection<ClientSession.ClientSessionEvent> res;
         try (ClientSession s = createTestClientSession(sshd);
              ChannelShell shell = s.createShellChannel();
              ByteArrayOutputStream out = new ByteArrayOutputStream();
@@ -263,16 +312,16 @@ public class ServerTest extends BaseTestSupport {
             assertTrue("No changes in activated channels", channelListener.waitForActiveChannelsChange(5L, TimeUnit.SECONDS));
             assertTrue("No changes in open channels", channelListener.waitForOpenChannelsChange(5L, TimeUnit.SECONDS));
 
-            Collection<ClientSession.ClientSessionEvent> res =
-                s.waitFor(EnumSet.of(ClientSession.ClientSessionEvent.CLOSED), 2L * testIdleTimeout);
-            assertTrue("Session should be closed and authenticated: " + res,
-                res.containsAll(EnumSet.of(ClientSession.ClientSessionEvent.CLOSED, ClientSession.ClientSessionEvent.AUTHED)));
+            res = s.waitFor(EnumSet.of(ClientSession.ClientSessionEvent.CLOSED), 2L * testIdleTimeout);
         } finally {
             client.stop();
         }
 
+        assertTrue("Session should be closed and authenticated: " + res,
+            res.containsAll(EnumSet.of(ClientSession.ClientSessionEvent.CLOSED, ClientSession.ClientSessionEvent.AUTHED)));
         assertTrue("Session latch not signalled in time", latch.await(1L, TimeUnit.SECONDS));
         assertTrue("Shell latch not signalled in time", TestEchoShell.latch.await(1L, TimeUnit.SECONDS));
+        assertSame("Mismatched timeout status", TimeoutStatus.IdleTimeout, timeoutHolder.getAndSet(null));
     }
 
     /*
@@ -284,16 +333,14 @@ public class ServerTest extends BaseTestSupport {
      */
     @Test
     public void testServerIdleTimeoutWithForce() throws Exception {
-        final CountDownLatch latch = new CountDownLatch(1);
-
-        sshd.setCommandFactory(StreamCommand::new);
-
         final long idleTimeoutValue = TimeUnit.SECONDS.toMillis(5L);
         PropertyResolverUtils.updateProperty(sshd, FactoryManager.IDLE_TIMEOUT, idleTimeoutValue);
 
         final long disconnectTimeoutValue = TimeUnit.SECONDS.toMillis(2L);
         PropertyResolverUtils.updateProperty(sshd, FactoryManager.DISCONNECT_TIMEOUT, disconnectTimeoutValue);
 
+        CountDownLatch latch = new CountDownLatch(1);
+        sshd.setCommandFactory(StreamCommand::new);
         sshd.addSessionListener(new SessionListener() {
             @Override
             public void sessionCreated(Session session) {
@@ -315,6 +362,11 @@ public class ServerTest extends BaseTestSupport {
                 outputDebugMessage("Session closed: %s", session);
                 latch.countDown();
             }
+
+            @Override
+            public String toString() {
+                return SessionListener.class.getSimpleName() + "[" + getCurrentTestName() + "]";
+            }
         });
 
         TestChannelListener channelListener = new TestChannelListener(getCurrentTestName());
@@ -386,7 +438,7 @@ public class ServerTest extends BaseTestSupport {
             }
         });
 
-        final Semaphore sigSem = new Semaphore(0, true);
+        Semaphore sigSem = new Semaphore(0, true);
         client.addSessionListener(new SessionListener() {
             @Override
             public void sessionCreated(Session session) {
@@ -415,10 +467,17 @@ public class ServerTest extends BaseTestSupport {
             public void sessionClosed(Session session) {
                 outputDebugMessage("Session closed: %s", session);
             }
+
+            @Override
+            public String toString() {
+                return SessionListener.class.getSimpleName() + "[" + getCurrentTestName() + "]";
+            }
         });
 
         client.start();
-        try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, sshd.getPort()).verify(7L, TimeUnit.SECONDS).getSession()) {
+        try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, sshd.getPort())
+                .verify(7L, TimeUnit.SECONDS)
+                .getSession()) {
             assertTrue("Failed to receive signal on time", sigSem.tryAcquire(11L, TimeUnit.SECONDS));
         } finally {
             client.stop();
@@ -458,7 +517,7 @@ public class ServerTest extends BaseTestSupport {
             }
         });
 
-        final Semaphore sigSem = new Semaphore(0, true);
+        Semaphore sigSem = new Semaphore(0, true);
         client.addSessionListener(new SessionListener() {
             @Override
             public void sessionCreated(Session session) {
@@ -479,11 +538,18 @@ public class ServerTest extends BaseTestSupport {
             public void sessionClosed(Session session) {
                 sigSem.release();
             }
+
+            @Override
+            public String toString() {
+                return SessionListener.class.getSimpleName() + "[" + getCurrentTestName() + "]";
+            }
         });
 
         client.start();
         try {
-            try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, sshd.getPort()).verify(7L, TimeUnit.SECONDS).getSession()) {
+            try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, sshd.getPort())
+                    .verify(7L, TimeUnit.SECONDS)
+                    .getSession()) {
                 assertTrue("Session closing not signalled on time", sigSem.tryAcquire(5L, TimeUnit.SECONDS));
                 for (boolean incoming : new boolean[]{true, false}) {
                     assertNull("Unexpected compression information for incoming=" + incoming, s.getCompressionInformation(incoming));
@@ -497,7 +563,7 @@ public class ServerTest extends BaseTestSupport {
 
     @Test
     public void testKexCompletedEvent() throws Exception {
-        final AtomicInteger serverEventCount = new AtomicInteger(0);
+        AtomicInteger serverEventCount = new AtomicInteger(0);
         sshd.addSessionListener(new SessionListener() {
             @Override
             public void sessionEvent(Session session, Event event) {
@@ -505,10 +571,15 @@ public class ServerTest extends BaseTestSupport {
                     serverEventCount.incrementAndGet();
                 }
             }
+
+            @Override
+            public String toString() {
+                return SessionListener.class.getSimpleName() + "[" + getCurrentTestName() + "]";
+            }
         });
         sshd.start();
 
-        final AtomicInteger clientEventCount = new AtomicInteger(0);
+        AtomicInteger clientEventCount = new AtomicInteger(0);
         client.addSessionListener(new SessionListener() {
             @Override
             public void sessionEvent(Session session, Event event) {
@@ -516,6 +587,11 @@ public class ServerTest extends BaseTestSupport {
                     clientEventCount.incrementAndGet();
                 }
             }
+
+            @Override
+            public String toString() {
+                return SessionListener.class.getSimpleName() + "[" + getCurrentTestName() + "]";
+            }
         });
         client.start();
 
@@ -530,7 +606,7 @@ public class ServerTest extends BaseTestSupport {
 
     @Test   // see SSHD-645
     public void testChannelStateChangeNotifications() throws Exception {
-        final Semaphore exitSignal = new Semaphore(0);
+        Semaphore exitSignal = new Semaphore(0);
         sshd.setCommandFactory(command -> new Command() {
             private ExitCallback cb;
 
@@ -568,7 +644,7 @@ public class ServerTest extends BaseTestSupport {
         sshd.start();
         client.start();
 
-        final Collection<String> stateChangeHints = new CopyOnWriteArrayList<>();
+        Collection<String> stateChangeHints = new CopyOnWriteArrayList<>();
         try (ClientSession s = createTestClientSession(sshd);
              ChannelExec shell = s.createExecChannel(getCurrentTestName())) {
             shell.addChannelListener(new ChannelListener() {
@@ -583,7 +659,7 @@ public class ServerTest extends BaseTestSupport {
 
             assertTrue("Timeout while wait for exit signal", exitSignal.tryAcquire(15L, TimeUnit.SECONDS));
             Collection<ClientChannelEvent> result =
-                    shell.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), TimeUnit.SECONDS.toMillis(13L));
+                shell.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), TimeUnit.SECONDS.toMillis(13L));
             assertFalse("Channel close timeout", result.contains(ClientChannelEvent.TIMEOUT));
 
             Integer status = shell.getExitStatus();
@@ -599,7 +675,7 @@ public class ServerTest extends BaseTestSupport {
 
     @Test
     public void testEnvironmentVariablesPropagationToServer() throws Exception {
-        final AtomicReference<Environment> envHolder = new AtomicReference<>(null);
+        AtomicReference<Environment> envHolder = new AtomicReference<>(null);
         sshd.setCommandFactory(command -> new Command() {
             private ExitCallback cb;
 
@@ -663,7 +739,7 @@ public class ServerTest extends BaseTestSupport {
             assertTrue("No changes in open channels", channelListener.waitForOpenChannelsChange(5L, TimeUnit.SECONDS));
 
             Collection<ClientChannelEvent> result =
-                    shell.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), TimeUnit.SECONDS.toMillis(17L));
+                shell.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), TimeUnit.SECONDS.toMillis(17L));
             assertFalse("Channel close timeout", result.contains(ClientChannelEvent.TIMEOUT));
 
             Integer status = shell.getExitStatus();
@@ -691,17 +767,20 @@ public class ServerTest extends BaseTestSupport {
     public void testImmediateAuthFailureOpcode() throws Exception {
         sshd.setPasswordAuthenticator(RejectAllPasswordAuthenticator.INSTANCE);
         sshd.setPublickeyAuthenticator(RejectAllPublickeyAuthenticator.INSTANCE);
-        final AtomicInteger challengeCount = new AtomicInteger(0);
+        AtomicInteger challengeCount = new AtomicInteger(0);
         sshd.setKeyboardInteractiveAuthenticator(new KeyboardInteractiveAuthenticator() {
             @Override
-            public InteractiveChallenge generateChallenge(ServerSession session, String username, String lang, String subMethods) {
+            public InteractiveChallenge generateChallenge(
+                    ServerSession session, String username, String lang, String subMethods) {
                 challengeCount.incrementAndGet();
                 outputDebugMessage("generateChallenge(%s@%s) count=%s", username, session, challengeCount);
                 return null;
             }
 
             @Override
-            public boolean authenticate(ServerSession session, String username, List<String> responses) throws Exception {
+            public boolean authenticate(
+                    ServerSession session, String username, List<String> responses)
+                        throws Exception {
                 return false;
             }
         });
@@ -709,11 +788,13 @@ public class ServerTest extends BaseTestSupport {
 
         // order is important
         String authMethods = GenericUtils.join(
-                Arrays.asList(UserAuthMethodFactory.KB_INTERACTIVE, UserAuthMethodFactory.PUBLIC_KEY, UserAuthMethodFactory.PUBLIC_KEY), ',');
+            Arrays.asList(UserAuthMethodFactory.KB_INTERACTIVE, UserAuthMethodFactory.PUBLIC_KEY, UserAuthMethodFactory.PUBLIC_KEY), ',');
         PropertyResolverUtils.updateProperty(client, ClientAuthenticationManager.PREFERRED_AUTHS, authMethods);
 
         client.start();
-        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, sshd.getPort()).verify(7L, TimeUnit.SECONDS).getSession()) {
+        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, sshd.getPort())
+                .verify(7L, TimeUnit.SECONDS)
+                .getSession()) {
             AuthFuture auth = session.auth();
             assertTrue("Failed to complete authentication on time", auth.await(17L, TimeUnit.SECONDS));
             assertFalse("Unexpected authentication success", auth.isSuccess());
@@ -728,20 +809,24 @@ public class ServerTest extends BaseTestSupport {
         sshd.setPasswordAuthenticator(RejectAllPasswordAuthenticator.INSTANCE);
         sshd.setPublickeyAuthenticator(RejectAllPublickeyAuthenticator.INSTANCE);
 
-        final InteractiveChallenge challenge = new InteractiveChallenge();
+        InteractiveChallenge challenge = new InteractiveChallenge();
         challenge.setInteractionInstruction(getCurrentTestName());
         challenge.setInteractionName(getClass().getSimpleName());
         challenge.setLanguageTag("il-heb");
         challenge.addPrompt(new PromptEntry("Password", false));
-        final AtomicInteger serverCount = new AtomicInteger(0);
+
+        AtomicInteger serverCount = new AtomicInteger(0);
         sshd.setKeyboardInteractiveAuthenticator(new KeyboardInteractiveAuthenticator() {
             @Override
-            public InteractiveChallenge generateChallenge(ServerSession session, String username, String lang, String subMethods) {
+            public InteractiveChallenge generateChallenge(
+                    ServerSession session, String username, String lang, String subMethods) {
                 return challenge;
             }
 
             @Override
-            public boolean authenticate(ServerSession session, String username, List<String> responses) throws Exception {
+            public boolean authenticate(
+                    ServerSession session, String username, List<String> responses)
+                        throws Exception {
                 outputDebugMessage("authenticate(%s@%s) count=%s", username, session, serverCount);
                 serverCount.incrementAndGet();
                 return false;
@@ -751,10 +836,10 @@ public class ServerTest extends BaseTestSupport {
 
         // order is important
         String authMethods = GenericUtils.join(
-                Arrays.asList(UserAuthMethodFactory.KB_INTERACTIVE, UserAuthMethodFactory.PUBLIC_KEY, UserAuthMethodFactory.PUBLIC_KEY), ',');
+            Arrays.asList(UserAuthMethodFactory.KB_INTERACTIVE, UserAuthMethodFactory.PUBLIC_KEY, UserAuthMethodFactory.PUBLIC_KEY), ',');
         PropertyResolverUtils.updateProperty(client, ClientAuthenticationManager.PREFERRED_AUTHS, authMethods);
-        final AtomicInteger clientCount = new AtomicInteger(0);
-        final String[] replies = {getCurrentTestName()};
+        AtomicInteger clientCount = new AtomicInteger(0);
+        String[] replies = {getCurrentTestName()};
         client.setUserInteraction(new UserInteraction() {
             @Override
             public boolean isInteractionAllowed(ClientSession session) {
@@ -762,7 +847,8 @@ public class ServerTest extends BaseTestSupport {
             }
 
             @Override
-            public String[] interactive(ClientSession session, String name, String instruction, String lang, String[] prompt, boolean[] echo) {
+            public String[] interactive(
+                    ClientSession session, String name, String instruction, String lang, String[] prompt, boolean[] echo) {
                 clientCount.incrementAndGet();
                 return replies;
             }
@@ -774,12 +860,16 @@ public class ServerTest extends BaseTestSupport {
         });
 
         client.start();
-        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, sshd.getPort()).verify(7L, TimeUnit.SECONDS).getSession()) {
+        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, sshd.getPort())
+                .verify(7L, TimeUnit.SECONDS)
+                .getSession()) {
             AuthFuture auth = session.auth();
             assertTrue("Failed to complete authentication on time", auth.await(17L, TimeUnit.SECONDS));
             assertFalse("Unexpected authentication success", auth.isSuccess());
-            assertEquals("Mismatched interactive server challenge calls", ClientAuthenticationManager.DEFAULT_PASSWORD_PROMPTS, serverCount.get());
-            assertEquals("Mismatched interactive client challenge calls", ClientAuthenticationManager.DEFAULT_PASSWORD_PROMPTS, clientCount.get());
+            assertEquals("Mismatched interactive server challenge calls",
+                ClientAuthenticationManager.DEFAULT_PASSWORD_PROMPTS, serverCount.get());
+            assertEquals("Mismatched interactive client challenge calls",
+                ClientAuthenticationManager.DEFAULT_PASSWORD_PROMPTS, clientCount.get());
         } finally {
             client.stop();
         }
@@ -789,8 +879,7 @@ public class ServerTest extends BaseTestSupport {
     public void testIdentificationStringsOverrides() throws Exception {
         String clientIdent = getCurrentTestName() + "-client";
         PropertyResolverUtils.updateProperty(client, ClientFactoryManager.CLIENT_IDENTIFICATION, clientIdent);
-        final String expClientIdent = SessionContext.DEFAULT_SSH_VERSION_PREFIX + clientIdent;
-
+        String expClientIdent = SessionContext.DEFAULT_SSH_VERSION_PREFIX + clientIdent;
         String serverIdent = getCurrentTestName() + "-server";
         PropertyResolverUtils.updateProperty(sshd, ServerFactoryManager.SERVER_IDENTIFICATION, serverIdent);
         String expServerIdent = SessionContext.DEFAULT_SSH_VERSION_PREFIX + serverIdent;
@@ -817,6 +906,11 @@ public class ServerTest extends BaseTestSupport {
             public void sessionClosed(Session session) {
                 // ignored
             }
+
+            @Override
+            public String toString() {
+                return SessionListener.class.getSimpleName() + "[" + getCurrentTestName() + "]";
+            }
         };
 
         sshd.addSessionListener(listener);
@@ -825,7 +919,9 @@ public class ServerTest extends BaseTestSupport {
         client.addSessionListener(listener);
         client.start();
 
-        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, sshd.getPort()).verify(7L, TimeUnit.SECONDS).getSession()) {
+        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, sshd.getPort())
+                .verify(7L, TimeUnit.SECONDS)
+                .getSession()) {
             session.addPasswordIdentity(getCurrentTestName());
             session.auth().verify(9L, TimeUnit.SECONDS);
             assertEquals("Mismatched client identification", expClientIdent, session.getClientVersion());
@@ -842,7 +938,7 @@ public class ServerTest extends BaseTestSupport {
                 getClass().getSimpleName(),
                 getCurrentTestName());
         PropertyResolverUtils.updateProperty(sshd, ServerFactoryManager.SERVER_EXTRA_IDENTIFICATION_LINES,
-                GenericUtils.join(expected, ServerFactoryManager.SERVER_EXTRA_IDENT_LINES_SEPARATOR));
+            GenericUtils.join(expected, ServerFactoryManager.SERVER_EXTRA_IDENT_LINES_SEPARATOR));
         sshd.start();
 
         AtomicReference<List<String>> actualHolder = new AtomicReference<>();
@@ -860,7 +956,8 @@ public class ServerTest extends BaseTestSupport {
             }
 
             @Override
-            public String[] interactive(ClientSession session, String name, String instruction, String lang, String[] prompt, boolean[] echo) {
+            public String[] interactive(
+                    ClientSession session, String name, String instruction, String lang, String[] prompt, boolean[] echo) {
                 return null;
             }
 
@@ -871,7 +968,9 @@ public class ServerTest extends BaseTestSupport {
         });
         client.start();
 
-        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, sshd.getPort()).verify(7L, TimeUnit.SECONDS).getSession()) {
+        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, sshd.getPort())
+                .verify(7L, TimeUnit.SECONDS)
+                .getSession()) {
             session.addPasswordIdentity(getCurrentTestName());
             session.auth().verify(9L, TimeUnit.SECONDS);
             assertTrue("No signal received in time", signal.tryAcquire(11L, TimeUnit.SECONDS));
@@ -885,7 +984,9 @@ public class ServerTest extends BaseTestSupport {
     }
 
     private ClientSession createTestClientSession(SshServer server) throws Exception {
-        ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, server.getPort()).verify(7L, TimeUnit.SECONDS).getSession();
+        ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, server.getPort())
+                .verify(7L, TimeUnit.SECONDS)
+                .getSession();
         try {
             session.addPasswordIdentity(getCurrentTestName());
             session.auth().verify(5L, TimeUnit.SECONDS);