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/10/03 17:08:24 UTC

[mina-sshd] branch master updated (871e55b -> eed4e1c)

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

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


    from 871e55b  [SSHD-945] Provide used key instance when invoking AbstractSignature#doInitSignature
     new 38ed37f  [SSHD-947] Use separate configuration to control sending client session identity and KEX-INIT message
     new eed4e1c  [SSHD-947] Added SessionListener#sessionEstablished method to allow for early session customization based on the peer's address

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 CHANGES.md                                         |  7 +++-
 docs/client-setup.md                               | 22 ++++++++++++
 docs/event-listeners.md                            | 10 ++++--
 .../java/org/apache/sshd/common/util/Invoker.java  | 40 ++++++++++++++++++----
 .../apache/sshd/client/ClientFactoryManager.java   | 13 ++++++-
 .../sshd/client/session/AbstractClientSession.java | 29 ++++++++++------
 .../sshd/client/session/ClientSessionImpl.java     |  8 +++--
 .../sshd/common/session/SessionListener.java       | 23 +++++++++++--
 .../common/session/helpers/AbstractSession.java    | 34 +++++++++++++-----
 .../sshd/common/session/helpers/SessionHelper.java | 35 +++++++++++++++++--
 10 files changed, 186 insertions(+), 35 deletions(-)


[mina-sshd] 01/02: [SSHD-947] Use separate configuration to control sending client session identity and KEX-INIT message

Posted by lg...@apache.org.
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 38ed37f910d5dba2f8a7c159e2e5fbe1f4b86fa8
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Thu Oct 3 11:47:56 2019 +0300

    [SSHD-947] Use separate configuration to control sending client session identity and KEX-INIT message
---
 CHANGES.md                                         |  3 +++
 docs/client-setup.md                               | 22 ++++++++++++++++++
 .../apache/sshd/client/ClientFactoryManager.java   | 13 ++++++++++-
 .../sshd/client/session/AbstractClientSession.java | 26 +++++++++++++++-------
 .../sshd/client/session/ClientSessionImpl.java     |  8 +++++--
 .../common/session/helpers/AbstractSession.java    |  6 +++--
 .../sshd/common/session/helpers/SessionHelper.java |  5 +++--
 7 files changed, 68 insertions(+), 15 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index fdaae65..7fa2f29 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -70,3 +70,6 @@ exchange via properties.
 
 * [SSHD-943](https://issues.apache.org/jira/browse/SSHD-943) - Provide session instance when KEX factory is invoked in order to create a KeyExchange instance.
 
+* [SSHD-947](https://issues.apache.org/jira/browse/SSHD-947) - Added configuration allowing the user to specify whether client should wait
+for the server's identification before sending KEX-INIT message.
+
diff --git a/docs/client-setup.md b/docs/client-setup.md
index ce6453e..1c8d36a 100644
--- a/docs/client-setup.md
+++ b/docs/client-setup.md
@@ -164,6 +164,28 @@ this can be modified so that the client waits for the server's identification be
     client.start();
 ```
 
+A similar configuration can be applied to sending the initial `SSH_MSG_KEXINIT` message - i.e., the client can be configured
+to wait until the server's identification is received before sending the message. This is done in order to allow clients to
+customize the KEX phase according to the parsed server identification.
+
+```java
+    SshClient client = ...setup client...
+    PropertyResolverUtils.updateProperty(
+       client, ClientFactoryManager.SEND_IMMEDIATE_KEXINIT, false);
+    client.start();
+```
+
+**Note:** if immediate sending of the client's identification is disabled, `SSH_MSG_KEXINIT` message sending is also
+automatically delayed until after the server's identification is received.
+
+A viable configuration might be to send the client's identification immediately, but delay the client's `SSH_MSG_KEXINIT`
+message sending until the server's identification is received so that the client can customize the session based on the
+server's identity. This is a more likely configuration then delaying the client's own identification in order to be able
+to cope with port multiplexors such as [sslh](http://www.rutschle.net/tech/sslh/README.html). Such multiplexors usually
+require that the client send an initial packet immediately after connection is established so that they can analyze it
+and route it to the correct server (_ssh_ in this case). If we delay the client's identification, then obviously no server
+identification will ever be received since the multiplexor does not know how to route the connection.
+
 ## Keeping the session alive while no traffic
 
 The client-side implementation has a 2 builtin mechanisms for maintaining the session alive as far as the **server** is concerned
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/ClientFactoryManager.java b/sshd-core/src/main/java/org/apache/sshd/client/ClientFactoryManager.java
index 42197f7..5c8694b 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/ClientFactoryManager.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/ClientFactoryManager.java
@@ -49,7 +49,7 @@ public interface ClientFactoryManager
 
     /**
      * Whether to send the identification string immediately upon session connection
-     * being established or wait for the peer's identification before sending our own.
+     * being established or wait for the server's identification before sending our own.
      *
      * @see <A HREF="https://tools.ietf.org/html/rfc4253#section-4.2">RFC 4253 - section 4.2 - Protocol Version Exchange</A>
      */
@@ -61,6 +61,17 @@ public interface ClientFactoryManager
     boolean DEFAULT_SEND_IMMEDIATE_IDENTIFICATION = true;
 
     /**
+     * Whether to send {@code SSH_MSG_KEXINIT} immediately after sending
+     * the client identification string or wait until the severer's one
+     * has been received.
+     *
+     * @see #SEND_IMMEDIATE_IDENTIFICATION
+     */
+    String SEND_IMMEDIATE_KEXINIT = "send-immediate-kex-init";
+
+    boolean DEFAULT_SEND_KEXINIT = true;
+
+    /**
      * Key used to set the heartbeat interval in milliseconds (0 to disable = default)
      */
     String HEARTBEAT_INTERVAL = "heartbeat-interval";
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 91bac20..285d2a1 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
@@ -78,7 +78,8 @@ import org.apache.sshd.common.util.net.SshdSocketAddress;
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
 public abstract class AbstractClientSession extends AbstractSession implements ClientSession {
-    protected final boolean sendImmediateIdentification;
+    protected final boolean sendImmediateClientIdentification;
+    protected final boolean sendImmediateKexInit;
 
     private final List<Object> identities = new CopyOnWriteArrayList<>();
     private final AuthenticationIdentitiesProvider identitiesProvider;
@@ -94,12 +95,16 @@ public abstract class AbstractClientSession extends AbstractSession implements C
 
     protected AbstractClientSession(ClientFactoryManager factoryManager, IoSession ioSession) {
         super(false, factoryManager, ioSession);
-        this.sendImmediateIdentification = PropertyResolverUtils.getBooleanProperty(
+
+        sendImmediateClientIdentification = PropertyResolverUtils.getBooleanProperty(
             factoryManager, ClientFactoryManager.SEND_IMMEDIATE_IDENTIFICATION,
             ClientFactoryManager.DEFAULT_SEND_IMMEDIATE_IDENTIFICATION);
+        sendImmediateKexInit = PropertyResolverUtils.getBooleanProperty(
+                factoryManager, ClientFactoryManager.SEND_IMMEDIATE_KEXINIT,
+                ClientFactoryManager.DEFAULT_SEND_KEXINIT);
 
         identitiesProvider = AuthenticationIdentitiesProvider.wrapIdentities(identities);
-        this.connectionContext = (AttributeRepository) ioSession.getAttribute(AttributeRepository.class);
+        connectionContext = (AttributeRepository) ioSession.getAttribute(AttributeRepository.class);
     }
 
     @Override
@@ -248,9 +253,7 @@ public abstract class AbstractClientSession extends AbstractSession implements C
         }
     }
 
-    protected void initializeKexPhase() throws Exception {
-        sendClientIdentification();
-
+    protected void initializeKeyExchangePhase() throws Exception {
         KexExtensionHandler extHandler = getKexExtensionHandler();
         if ((extHandler == null) || (!extHandler.isKexExtensionsAvailable(this, AvailabilityPhase.PREKEX))) {
             kexState.set(KexState.INIT);
@@ -464,8 +467,15 @@ public abstract class AbstractClientSession extends AbstractSession implements C
         }
 
         signalExtraServerVersionInfo(serverVersion, ident);
-        if (!sendImmediateIdentification) {
-            initializeKexPhase();
+
+        // Now that we have the server's identity reported see if have delayed any of out duties...
+        if (!sendImmediateClientIdentification) {
+            sendClientIdentification();
+            // if client identification not sent then KEX-INIT was not sent either
+            initializeKeyExchangePhase();
+        } else if (!sendImmediateKexInit) {
+            // if client identification sent, perhaps we delayed KEX-INIT
+            initializeKeyExchangePhase();
         }
 
         return true;
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
index 7abc2ea..84bb1b8 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
@@ -97,8 +97,12 @@ public class ClientSessionImpl extends AbstractClientSession {
          */
         initializeProxyConnector();
 
-        if (sendImmediateIdentification) {
-            initializeKexPhase();
+        if (sendImmediateClientIdentification) {
+            sendClientIdentification();
+
+            if (sendImmediateKexInit) {
+                initializeKeyExchangePhase();
+            }
         }
     }
 
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 4bb43e2..5144e95 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
@@ -220,7 +220,8 @@ public abstract class AbstractSession extends SessionHelper {
 
         attachSession(ioSession, this);
 
-        Factory<Random> factory = ValidateUtils.checkNotNull(factoryManager.getRandomFactory(), "No random factory for %s", ioSession);
+        Factory<Random> factory =
+            ValidateUtils.checkNotNull(factoryManager.getRandomFactory(), "No random factory for %s", ioSession);
         random = ValidateUtils.checkNotNull(factory.create(), "No randomizer instance for %s", ioSession);
 
         refreshConfiguration();
@@ -2112,7 +2113,8 @@ public abstract class AbstractSession extends SessionHelper {
      * @param session The SSH session to attach
      * @throws MultipleAttachedSessionException If a previous session already attached
      */
-    public static void attachSession(IoSession ioSession, AbstractSession session) throws MultipleAttachedSessionException {
+    public static void attachSession(IoSession ioSession, AbstractSession session)
+            throws MultipleAttachedSessionException {
         Objects.requireNonNull(ioSession, "No I/O session");
         Objects.requireNonNull(session, "No SSH session");
         Object prev = ioSession.setAttributeIfAbsent(SESSION, session);
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 f58ed6e..55324a5 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
@@ -735,12 +735,13 @@ public abstract class SessionHelper extends AbstractKexFactoryManager implements
      * @throws IOException If failed to send the packet
      */
     protected IoWriteFuture sendIdentification(String ident) throws IOException {
-        byte[] data = (ident + "\r\n").getBytes(StandardCharsets.UTF_8);
         if (log.isDebugEnabled()) {
-            log.debug("sendIdentification({}): {}", this, ident.replace('\r', '|').replace('\n', '|'));
+            log.debug("sendIdentification({}): {}",
+                this, ident.replace('\r', '|').replace('\n', '|'));
         }
 
         IoSession networkSession = getIoSession();
+        byte[] data = (ident + "\r\n").getBytes(StandardCharsets.UTF_8);
         return networkSession.writePacket(new ByteArrayBuffer(data));
     }
 


[mina-sshd] 02/02: [SSHD-947] Added SessionListener#sessionEstablished method to allow for early session customization based on the peer's address

Posted by lg...@apache.org.
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 eed4e1c55ed9e4c5efe71e1e9cb4514fd44b7789
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Thu Oct 3 12:05:39 2019 +0300

    [SSHD-947] Added SessionListener#sessionEstablished method to allow for early session customization based on the peer's address
---
 CHANGES.md                                         |  4 ++-
 docs/event-listeners.md                            | 10 ++++--
 .../java/org/apache/sshd/common/util/Invoker.java  | 40 ++++++++++++++++++----
 .../sshd/client/session/AbstractClientSession.java | 11 +++---
 .../sshd/common/session/SessionListener.java       | 23 +++++++++++--
 .../common/session/helpers/AbstractSession.java    | 28 +++++++++++----
 .../sshd/common/session/helpers/SessionHelper.java | 30 ++++++++++++++++
 7 files changed, 122 insertions(+), 24 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index 7fa2f29..02f2b7d 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -36,9 +36,11 @@ the standard does not specifically specify the behavior regarding symbolic links
 
 ## Minor code helpers
 
-* `SessionListener` supports `sessionPeerIdentificationReceived` that is invoked once successful
+* `SessionListener` supports `sessionPeerIdentificationReceived` method that is invoked once successful
 peer version data is received.
 
+* `SessionListener` supports `sessionEstablished` method that is invoked when initial constructor is executed.
+
 * `ChannelIdTrackingUnknownChannelReferenceHandler` extends the functionality of the `DefaultUnknownChannelReferenceHandler`
 by tracking the initialized channels identifiers and being lenient only if command is received for a channel that was
 initialized in the past.
diff --git a/docs/event-listeners.md b/docs/event-listeners.md
index 49a4a83..6aa59cf 100644
--- a/docs/event-listeners.md
+++ b/docs/event-listeners.md
@@ -38,8 +38,9 @@ registered listener.
 ### `SessionListener`
 
 Informs about session related events. One can modify the session - although the modification effect depends on the session's **state**. E.g., if one
-changes the ciphers *after* the key exchange (KEX) phase, then they will take effect only if the keys are re-negotiated. It is important to read the
-documentation very carefully and understand at which stage each listener method is invoked and what are the repercussions of changes at that stage.
+changes the ciphers *after* the key exchange (KEX) phase, then they will take effect only if the keys are re-negotiated. Furthermore, invoking some
+session API(s) - event `getSomeValue` at the wrong time might yield unexpected results. It is important to read the documentation very carefully and
+understand at which stage each listener method is invoked, what are the limitations and what are the repercussions of changes at that stage.
 In this context, it is worth mentioning that one can attach to sessions **arbitrary attributes** that can be retrieved by the user's code later on:
 
 
@@ -50,6 +51,11 @@ In this context, it is worth mentioning that one can attach to sessions **arbitr
 
     sshClient/Server.addSessionListener(new SessionListener() {
         @Override
+        public void sessionEstablished(Session session) {
+            // examine the peer address or the connection context and set some attributes
+        }
+
+        @Override
         public void sessionCreated(Session session) {
             session.setAttribute(STR_KEY, "Some string value");
             session.setAttribute(LONG_KEY, 3777347L);
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/Invoker.java b/sshd-common/src/main/java/org/apache/sshd/common/util/Invoker.java
index 71cbebd..1fc2115 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/util/Invoker.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/util/Invoker.java
@@ -24,7 +24,7 @@ import java.util.Map;
 
 /**
  * The complement to the {@code Callable} interface - accepts one argument
- * and possibly throws somethind
+ * and possibly throws something
  *
  * @param <ARG> Argument type
  * @param <RET> Return type
@@ -34,7 +34,19 @@ import java.util.Map;
 public interface Invoker<ARG, RET> {
     RET invoke(ARG arg) throws Throwable;
 
-    static <ARG> Invoker<ARG, Void> wrapAll(Collection<? extends Invoker<? super ARG, ?>> invokers) {
+    /**
+     * Wraps a bunch of {@link Invoker}-s that return no value into one that
+     * invokes them in the same <U>order</U> as they appear. <B>Note:</B>
+     * <U>all</U> invokers are used and any thrown exceptions are <U>accumulated</U>
+     * and thrown as a single exception at the end of invoking all of them.
+     *
+     * @param <ARG> The argument type
+     * @param invokers The invokers to wrap - ignored if {@code null}/empty
+     * @return The wrapper
+     * @see #invokeAll(Object, Collection) invokeAll
+     */
+    static <ARG> Invoker<ARG, Void> wrapAll(
+            Collection<? extends Invoker<? super ARG, ?>> invokers) {
         return arg -> {
             invokeAll(arg, invokers);
             return null;
@@ -51,7 +63,9 @@ public interface Invoker<ARG, RET> {
      * (also ignores {@code null} members)
      * @throws Throwable If invocation failed
      */
-    static <ARG> void invokeAll(ARG arg, Collection<? extends Invoker<? super ARG, ?>> invokers) throws Throwable {
+    static <ARG> void invokeAll(
+            ARG arg, Collection<? extends Invoker<? super ARG, ?>> invokers)
+                throws Throwable {
         if (GenericUtils.isEmpty(invokers)) {
             return;
         }
@@ -74,9 +88,21 @@ public interface Invoker<ARG, RET> {
         }
     }
 
-    static <ARG> Invoker<ARG, Void> wrapFirst(Collection<? extends Invoker<? super ARG, ?>> invokers) {
+    /**
+     * Wraps a bunch of {@link Invoker}-s that return no value into one that
+     * invokes them in the same <U>order</U> as they appear. <B>Note:</B>
+     * stops when <U>first</U> invoker throws an exception (otherwise invokes all)
+     *
+     * @param <ARG> The argument type
+     * @param invokers The invokers to wrap - ignored if {@code null}/empty
+     * @return The wrapper
+     * @see #invokeTillFirstFailure(Object, Collection) invokeTillFirstFailure
+     */
+    static <ARG> Invoker<ARG, Void> wrapFirst(
+            Collection<? extends Invoker<? super ARG, ?>> invokers) {
         return arg -> {
-            Map.Entry<Invoker<? super ARG, ?>, Throwable> result = invokeTillFirstFailure(arg, invokers);
+            Map.Entry<Invoker<? super ARG, ?>, Throwable> result =
+                invokeTillFirstFailure(arg, invokers);
             if (result != null) {
                 throw result.getValue();
             }
@@ -94,7 +120,9 @@ public interface Invoker<ARG, RET> {
      * @return A {@link SimpleImmutableEntry} representing the <U>first</U> failed
      * invocation - {@code null} if all were successful (or none invoked).
      */
-    static <ARG> SimpleImmutableEntry<Invoker<? super ARG, ?>, Throwable> invokeTillFirstFailure(ARG arg, Collection<? extends Invoker<? super ARG, ?>> invokers) {
+    static <ARG> SimpleImmutableEntry<Invoker<? super ARG, ?>, Throwable>
+            invokeTillFirstFailure(
+                ARG arg, Collection<? extends Invoker<? super ARG, ?>> invokers) {
         if (GenericUtils.isEmpty(invokers)) {
             return null;
         }
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 285d2a1..38d854c 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
@@ -43,7 +43,6 @@ import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
 import org.apache.sshd.common.AttributeRepository;
 import org.apache.sshd.common.FactoryManager;
 import org.apache.sshd.common.NamedResource;
-import org.apache.sshd.common.PropertyResolverUtils;
 import org.apache.sshd.common.RuntimeSshException;
 import org.apache.sshd.common.SshConstants;
 import org.apache.sshd.common.SshException;
@@ -96,12 +95,12 @@ public abstract class AbstractClientSession extends AbstractSession implements C
     protected AbstractClientSession(ClientFactoryManager factoryManager, IoSession ioSession) {
         super(false, factoryManager, ioSession);
 
-        sendImmediateClientIdentification = PropertyResolverUtils.getBooleanProperty(
-            factoryManager, ClientFactoryManager.SEND_IMMEDIATE_IDENTIFICATION,
+        sendImmediateClientIdentification = this.getBooleanProperty(
+            ClientFactoryManager.SEND_IMMEDIATE_IDENTIFICATION,
             ClientFactoryManager.DEFAULT_SEND_IMMEDIATE_IDENTIFICATION);
-        sendImmediateKexInit = PropertyResolverUtils.getBooleanProperty(
-                factoryManager, ClientFactoryManager.SEND_IMMEDIATE_KEXINIT,
-                ClientFactoryManager.DEFAULT_SEND_KEXINIT);
+        sendImmediateKexInit = this.getBooleanProperty(
+            ClientFactoryManager.SEND_IMMEDIATE_KEXINIT,
+            ClientFactoryManager.DEFAULT_SEND_KEXINIT);
 
         identitiesProvider = AuthenticationIdentitiesProvider.wrapIdentities(identities);
         connectionContext = (AttributeRepository) ioSession.getAttribute(AttributeRepository.class);
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/SessionListener.java b/sshd-core/src/main/java/org/apache/sshd/common/session/SessionListener.java
index d322f9d..e93d024 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/session/SessionListener.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/session/SessionListener.java
@@ -35,6 +35,20 @@ public interface SessionListener extends SshdEventListener {
     }
 
     /**
+     * An initial session connection has been established - <B>Caveat emptor:</B>
+     * the main difference between this callback and {@link #sessionCreated(Session)}
+     * is that when this callback is called, the session is not yet fully initialized
+     * so not all API(s) will respond as expected. The main purpose of this callback
+     * is to allow the user to customize some session properties based on the peer's
+     * address and/or any provided connection context.
+     *
+     * @param session The established {@code Session}
+     */
+    default void sessionEstablished(Session session) {
+        // ignored
+    }
+
+    /**
      * A new session just been created
      *
      * @param session The created {@link Session}
@@ -64,7 +78,8 @@ public interface SessionListener extends SshdEventListener {
      * @param serverProposal The server proposal options (un-modifiable)
      */
     default void sessionNegotiationStart(Session session,
-            Map<KexProposalOption, String> clientProposal, Map<KexProposalOption, String> serverProposal) {
+            Map<KexProposalOption, String> clientProposal,
+            Map<KexProposalOption, String> serverProposal) {
         // ignored
     }
 
@@ -79,8 +94,10 @@ public interface SessionListener extends SshdEventListener {
      * @param reason Negotiation end reason - {@code null} if successful
      */
     default void sessionNegotiationEnd(Session session,
-            Map<KexProposalOption, String> clientProposal, Map<KexProposalOption, String> serverProposal,
-            Map<KexProposalOption, String> negotiatedOptions, Throwable reason) {
+            Map<KexProposalOption, String> clientProposal,
+            Map<KexProposalOption, String> serverProposal,
+            Map<KexProposalOption, String> negotiatedOptions,
+            Throwable reason) {
         // ignored
     }
 
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 5144e95..27e87a8 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
@@ -230,6 +230,16 @@ public abstract class AbstractSession extends SessionHelper {
         sessionListenerProxy = EventListenerUtils.proxyWrapper(SessionListener.class, loader, sessionListeners);
         channelListenerProxy = EventListenerUtils.proxyWrapper(ChannelListener.class, loader, channelListeners);
         tunnelListenerProxy = EventListenerUtils.proxyWrapper(PortForwardingEventListener.class, loader, tunnelListeners);
+
+        try {
+            signalSessionEstablished(ioSession);
+        } catch (Exception e) {
+            if (e instanceof RuntimeException) {
+                throw (RuntimeException) e;
+            } else {
+                throw new RuntimeSshException(e);
+            }
+        }
     }
 
     @Override
@@ -2040,7 +2050,8 @@ public abstract class AbstractSession extends SessionHelper {
     }
 
     /**
-     * @param seed The result of the KEXINIT handshake - required for correct session key establishment
+     * @param seed The result of the KEXINIT handshake - required
+     * for correct session key establishment
      */
     protected abstract void setKexSeed(byte... seed);
 
@@ -2052,7 +2063,8 @@ public abstract class AbstractSession extends SessionHelper {
      * @see #getFactoryManager()
      * @see #resolveAvailableSignaturesProposal(FactoryManager)
      */
-    protected String resolveAvailableSignaturesProposal() throws IOException, GeneralSecurityException {
+    protected String resolveAvailableSignaturesProposal()
+            throws IOException, GeneralSecurityException {
         return resolveAvailableSignaturesProposal(getFactoryManager());
     }
 
@@ -2064,7 +2076,7 @@ public abstract class AbstractSession extends SessionHelper {
      * @throws GeneralSecurityException If failed to generate the keys
      */
     protected abstract String resolveAvailableSignaturesProposal(FactoryManager manager)
-            throws IOException, GeneralSecurityException;
+        throws IOException, GeneralSecurityException;
 
     /**
      * Indicates the the key exchange is completed and the exchanged keys
@@ -2091,7 +2103,9 @@ public abstract class AbstractSession extends SessionHelper {
         return seed;
     }
 
-    protected abstract void receiveKexInit(Map<KexProposalOption, String> proposal, byte[] seed) throws IOException;
+    protected abstract void receiveKexInit(
+        Map<KexProposalOption, String> proposal, byte[] seed)
+            throws IOException;
 
     /**
      * Retrieve the SSH session from the I/O session. If the session has not been attached,
@@ -2102,7 +2116,8 @@ public abstract class AbstractSession extends SessionHelper {
      * @see #getSession(IoSession, boolean)
      * @throws MissingAttachedSessionException if no attached SSH session
      */
-    public static AbstractSession getSession(IoSession ioSession) throws MissingAttachedSessionException {
+    public static AbstractSession getSession(IoSession ioSession)
+            throws MissingAttachedSessionException {
         return getSession(ioSession, false);
     }
 
@@ -2134,7 +2149,8 @@ public abstract class AbstractSession extends SessionHelper {
      * @return the session attached to the I/O session or {@code null}
      * @throws MissingAttachedSessionException if no attached session and <tt>allowNull=false</tt>
      */
-    public static AbstractSession getSession(IoSession ioSession, boolean allowNull) throws MissingAttachedSessionException {
+    public static AbstractSession getSession(IoSession ioSession, boolean allowNull)
+            throws MissingAttachedSessionException {
         AbstractSession session = (AbstractSession) ioSession.getAttribute(SESSION);
         if ((session == null) && (!allowNull)) {
             throw new MissingAttachedSessionException("No session attached to " + ioSession);
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 55324a5..b9ed2f1 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
@@ -520,6 +520,36 @@ public abstract class SessionHelper extends AbstractKexFactoryManager implements
         return writeFuture;
     }
 
+    protected void signalSessionEstablished(IoSession ioSession) throws Exception {
+        try {
+            invokeSessionSignaller(l -> {
+                signalSessionEstablished(l);
+                return null;
+            });
+        } catch (Throwable err) {
+            Throwable e = GenericUtils.peelException(err);
+            if (log.isDebugEnabled()) {
+                log.debug("Failed ({}) to announce session={} established: {}",
+                      e.getClass().getSimpleName(), ioSession, e.getMessage());
+            }
+            if (log.isTraceEnabled()) {
+                log.trace("Session=" + ioSession + " establish failure details", e);
+            }
+            if (e instanceof Exception) {
+                throw (Exception) e;
+            } else {
+                throw new RuntimeSshException(e);
+            }
+        }
+    }
+
+    protected void signalSessionEstablished(SessionListener listener) {
+        if (listener == null) {
+            return;
+        }
+        listener.sessionEstablished(this);
+    }
+
     protected void signalSessionCreated(IoSession ioSession) throws Exception {
         try {
             invokeSessionSignaller(l -> {