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 2021/01/09 05:41:50 UTC

[mina-sshd] 04/04: [SSHD-1097] Implemented endless tarpit example in sshd-contrib

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 cbd61985ef49d17d6d1904fd7f96d42561129c7b
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Fri Jan 8 09:33:53 2021 +0200

    [SSHD-1097] Implemented endless tarpit example in sshd-contrib
---
 CHANGES.md                                         |   1 +
 README.md                                          |   3 +
 docs/event-listeners.md                            |   1 +
 docs/howto.md                                      |  25 ++
 .../org/apache/sshd/common/util/GenericUtils.java  |  10 +-
 .../org/apache/sshd/common/util/buffer/Buffer.java |  27 +-
 .../sshd/common/util/buffer/ByteArrayBuffer.java   |  18 +-
 .../sshd/contrib/common/io/EndlessWriteFuture.java |  90 +++++++
 .../contrib/common/io/ImmediateWriteFuture.java    |  32 +--
 .../EndlessTarpitSenderSupportDevelopment.java     | 280 +++++++++++++++++++++
 .../sshd/client/session/AbstractClientSession.java |  13 +-
 .../sshd/client/session/ClientSessionImpl.java     |   2 +-
 .../common/kex/extension/KexExtensionHandler.java  |  88 +++----
 .../session/ReservedSessionMessagesHandler.java    |  21 +-
 .../sshd/common/session/SessionWorkBuffer.java     |   3 +-
 .../common/session/helpers/AbstractSession.java    |  77 +++---
 .../sshd/common/session/helpers/SessionHelper.java |  22 +-
 .../sshd/server/session/AbstractServerSession.java |   2 +-
 .../sshd/server/session/ServerSessionImpl.java     |   3 +-
 .../kex/extension/KexExtensionHandlerTest.java     |   2 +-
 .../sshd/util/test/CoreTestSupportUtils.java       |  10 +-
 .../sshd/scp/client/SimpleScpClientImpl.java       |   3 +-
 22 files changed, 589 insertions(+), 144 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index 5dada51..b2bc036 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -28,6 +28,7 @@
 * [SSHD-1091](https://issues.apache.org/jira/browse/SSHD-1091) Renamed `sshd-contrib` top-level package in order to align naming convention.
 * [SSHD-1097](https://issues.apache.org/jira/browse/SSHD-1097) Added more `SessionListener` callbacks related to the initial version and key exchange
 * [SSHD-1097](https://issues.apache.org/jira/browse/SSHD-1097) Added more capability to send peer identification via `ReservedSessionMessagesHandler`
+* [SSHD-1097](https://issues.apache.org/jira/browse/SSHD-1097) Implemented [endless tarpit](https://nullprogram.com/blog/2019/03/22/) example in sshd-contrib
 * [SSHD-1109](https://issues.apache.org/jira/browse/SSHD-1109) Replace log4j with logback as the slf4j logger implementation for tests
 * [SSHD-1114](https://issues.apache.org/jira/browse/SSHD-1114) Added callbacks for client-side password authentication progress
 * [SSHD-1114](https://issues.apache.org/jira/browse/SSHD-1114) Added callbacks for client-side public key authentication progress
diff --git a/README.md b/README.md
index ab548c6..9fab4f7 100644
--- a/README.md
+++ b/README.md
@@ -52,6 +52,7 @@ based applications requiring SSH support.
     * `copy-file`, `copy-data` - [DRAFT 00 - sections 6, 7](http://tools.ietf.org/id/draft-ietf-secsh-filexfer-extensions-00.txt)
     * `space-available` - [DRAFT 09 - section 9.3](http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt)
     * Several [OpenSSH SFTP extensions](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL)
+* [Endless tarpit](https://nullprogram.com/blog/2019/03/22/) - see [HOWTO(s)](./docs/howto.md) section.
 
 ## Implemented/available support
 
@@ -172,3 +173,5 @@ implementation of the logging API can be selected from the many existing adaptor
 ## [Configuration/data files parsing support](./docs/files-parsing.md)
 
 ## [Extension modules](./docs/extensions.md)
+
+# [HOWTO(s)](./docs/howto.md)
\ No newline at end of file
diff --git a/docs/event-listeners.md b/docs/event-listeners.md
index 1f17a98..3527964 100644
--- a/docs/event-listeners.md
+++ b/docs/event-listeners.md
@@ -100,6 +100,7 @@ An **experimental** implementation example is available for the client side - se
 
 Can be used to handle the following cases:
 
+* Intervene during the initial identification and KEX
 * [SSH_MSG_IGNORE](https://tools.ietf.org/html/rfc4253#section-11.2)
 * [SSH_MSG_DEBUG](https://tools.ietf.org/html/rfc4253#section-11.3)
 * [SSH_MSG_UNIMPLEMENTED](https://tools.ietf.org/html/rfc4253#section-11.4)
diff --git a/docs/howto.md b/docs/howto.md
new file mode 100644
index 0000000..e9ea60b
--- /dev/null
+++ b/docs/howto.md
@@ -0,0 +1,25 @@
+# HOWTO(s)
+
+This section contains some useful "cookbook recipes" for getting the most out of the code. Please note that it does **not** covers code samples that already appear in the previous sections - such as creating sessions, managing authentication, 3-way SCP, mounting file systems via SFTP, etc... Instead, it focuses on more "exotic" implementations that are not usually part of the normal SSH flow.
+
+## [Endless tarpit](https://nullprogram.com/blog/2019/03/22/)
+
+In order to achieve this one needs to use a `ReservedSessionMessagesHandler` on the server side that overrides the session identification and KEX message callbacks as follows:
+
+* When `sendIdentification` callback is invoked
+
+    * Check if you wish to trap the peer into the endless tarpit - if not, then return `null`
+    
+    * Spawn a thread that will feed the peer session with periodic infinite data.
+    
+    * Return a never succeeding `IoWriteFuture` - see `EndlessWriteFuture` in *sshd-contrib* package for such an implementation
+    
+* When `sendKexInitRequest` callback is invoked
+
+    * Check if you wish to trap the peer into the endless tarpit - if not, then return `null`
+
+    * Return an `IoWriteFuture` that "succeeds" immediately - see `ImmediateWriteFuture` in *sshd-contrib* package for such an implementation.
+
+The idea is to prevent the normal session establish flow by taking over the initial handshake identification and blocking the initial KEX message from the server.
+
+A sample implementation can be found in the `EndlessTarpitSenderSupportDevelopment` class in the *sshd-contrib* package *test* section.
\ No newline at end of file
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/GenericUtils.java b/sshd-common/src/main/java/org/apache/sshd/common/util/GenericUtils.java
index 9b53eb6..181a902 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/util/GenericUtils.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/util/GenericUtils.java
@@ -928,11 +928,15 @@ public final class GenericUtils {
         return current;
     }
 
-    public static IOException toIOException(Throwable e) {
+    public static void rethrowAsIoException(Throwable e) throws IOException {
         if (e instanceof IOException) {
-            return (IOException) e;
+            throw (IOException) e;
+        } else if (e instanceof RuntimeException) {
+            throw (RuntimeException) e;
+        } else if (e instanceof Error) {
+            throw (Error) e;
         } else {
-            return new IOException(e);
+            throw new IOException(e);
         }
     }
 
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java
index a691a8f..edf5137 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java
@@ -157,18 +157,20 @@ public abstract class Buffer implements Readable {
     /**
      * Reset read/write positions to zero - <B>Note:</B> zeroes any previously existing data
      *
-     * @see #clear(boolean)
+     * @return Reference to this buffer
+     * @see    #clear(boolean)
      */
-    public void clear() {
-        clear(true);
+    public Buffer clear() {
+        return clear(true);
     }
 
     /**
      * Reset read/write positions to zero
      *
-     * @param wipeData Whether to zero any previously existing data
+     * @param  wipeData Whether to zero any previously existing data
+     * @return          Reference to this buffer
      */
-    public abstract void clear(boolean wipeData);
+    public abstract Buffer clear(boolean wipeData);
 
     public boolean isValidMessageStructure(Class<?>... fieldTypes) {
         return isValidMessageStructure(GenericUtils.isEmpty(fieldTypes) ? Collections.emptyList() : Arrays.asList(fieldTypes));
@@ -992,17 +994,18 @@ public abstract class Buffer implements Readable {
         }
     }
 
-    protected void ensureCapacity(int capacity) {
-        ensureCapacity(capacity, BufferUtils.DEFAULT_BUFFER_GROWTH_FACTOR);
+    public Buffer ensureCapacity(int capacity) {
+        return ensureCapacity(capacity, BufferUtils.DEFAULT_BUFFER_GROWTH_FACTOR);
     }
 
     /**
-     * @param capacity     The required capacity
-     * @param growthFactor An {@link IntUnaryOperator} that is invoked if the current capacity is insufficient. The
-     *                     argument is the minimum required new data length, the function result should be the effective
-     *                     new data length to be allocated - if less than minimum then an exception is thrown
+     * @param  capacity     The required capacity
+     * @param  growthFactor An {@link IntUnaryOperator} that is invoked if the current capacity is insufficient. The
+     *                      argument is the minimum required new data length, the function result should be the
+     *                      effective new data length to be allocated - if less than minimum then an exception is thrown
+     * @return              This buffer instance
      */
-    public abstract void ensureCapacity(int capacity, IntUnaryOperator growthFactor);
+    public abstract Buffer ensureCapacity(int capacity, IntUnaryOperator growthFactor);
 
     /**
      * @return Current size of underlying backing data bytes array
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/ByteArrayBuffer.java b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/ByteArrayBuffer.java
index 5de36a6..04085ed 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/ByteArrayBuffer.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/ByteArrayBuffer.java
@@ -184,13 +184,15 @@ public class ByteArrayBuffer extends Buffer {
     }
 
     @Override
-    public void clear(boolean wipeData) {
+    public Buffer clear(boolean wipeData) {
         rpos = 0;
         wpos = 0;
 
         if (wipeData) {
             Arrays.fill(data, (byte) 0);
         }
+
+        return this;
     }
 
     @Override
@@ -207,11 +209,11 @@ public class ByteArrayBuffer extends Buffer {
 
     @Override
     public int putBuffer(Readable buffer, boolean expand) {
-        int r = expand ? buffer.available() : Math.min(buffer.available(), capacity());
-        ensureCapacity(r);
-        buffer.getRawBytes(data, wpos, r);
-        wpos += r;
-        return r;
+        int required = expand ? buffer.available() : Math.min(buffer.available(), capacity());
+        ensureCapacity(required);
+        buffer.getRawBytes(data, wpos, required);
+        wpos += required;
+        return required;
     }
 
     @Override
@@ -259,7 +261,7 @@ public class ByteArrayBuffer extends Buffer {
     }
 
     @Override
-    public void ensureCapacity(int capacity, IntUnaryOperator growthFactor) {
+    public Buffer ensureCapacity(int capacity, IntUnaryOperator growthFactor) {
         ValidateUtils.checkTrue(capacity >= 0, "Negative capacity requested: %d", capacity);
 
         int maxSize = size();
@@ -276,6 +278,8 @@ public class ByteArrayBuffer extends Buffer {
             System.arraycopy(data, 0, tmp, 0, data.length);
             data = tmp;
         }
+
+        return this;
     }
 
     @Override
diff --git a/sshd-contrib/src/main/java/org/apache/sshd/contrib/common/io/EndlessWriteFuture.java b/sshd-contrib/src/main/java/org/apache/sshd/contrib/common/io/EndlessWriteFuture.java
new file mode 100644
index 0000000..612c93b
--- /dev/null
+++ b/sshd-contrib/src/main/java/org/apache/sshd/contrib/common/io/EndlessWriteFuture.java
@@ -0,0 +1,90 @@
+/*
+ * 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.contrib.common.io;
+
+import java.io.IOException;
+
+import org.apache.sshd.common.future.SshFutureListener;
+import org.apache.sshd.common.io.IoWriteFuture;
+
+/**
+ * Never signals a successful write completion and ignores all listeners
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class EndlessWriteFuture implements IoWriteFuture {
+    public static final EndlessWriteFuture INSTANCE = new EndlessWriteFuture();
+
+    public EndlessWriteFuture() {
+        super();
+    }
+
+    @Override
+    public IoWriteFuture verify(long timeoutMillis) throws IOException {
+        await(timeoutMillis);
+        return null;
+    }
+
+    @Override
+    public boolean isDone() {
+        return false;
+    }
+
+    @Override
+    public Object getId() {
+        return "ENDLESS";
+    }
+
+    @Override
+    public boolean awaitUninterruptibly(long timeoutMillis) {
+        try {
+            Thread.sleep(timeoutMillis);
+        } catch (InterruptedException e) {
+            // ignored
+        }
+
+        return false;
+    }
+
+    @Override
+    public boolean await(long timeoutMillis) throws IOException {
+        return awaitUninterruptibly(timeoutMillis);
+    }
+
+    @Override
+    public IoWriteFuture removeListener(SshFutureListener<IoWriteFuture> listener) {
+        return this;
+    }
+
+    @Override
+    public IoWriteFuture addListener(SshFutureListener<IoWriteFuture> listener) {
+        return this;
+    }
+
+    @Override
+    public boolean isWritten() {
+        return false;
+    }
+
+    @Override
+    public Throwable getException() {
+        return null;
+    }
+}
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/SessionWorkBuffer.java b/sshd-contrib/src/main/java/org/apache/sshd/contrib/common/io/ImmediateWriteFuture.java
similarity index 55%
copy from sshd-core/src/main/java/org/apache/sshd/common/session/SessionWorkBuffer.java
copy to sshd-contrib/src/main/java/org/apache/sshd/contrib/common/io/ImmediateWriteFuture.java
index 00815a2..0711e40 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/session/SessionWorkBuffer.java
+++ b/sshd-contrib/src/main/java/org/apache/sshd/contrib/common/io/ImmediateWriteFuture.java
@@ -17,33 +17,19 @@
  * under the License.
  */
 
-package org.apache.sshd.common.session;
+package org.apache.sshd.contrib.common.io;
 
-import java.util.Objects;
-
-import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
+import org.apache.sshd.common.channel.IoWriteFutureImpl;
+import org.apache.sshd.common.util.buffer.Buffer;
 
 /**
+ * Succeeds immediately upon construction
+ *
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
-public class SessionWorkBuffer extends ByteArrayBuffer implements SessionHolder<Session> {
-    private final Session session;
-
-    public SessionWorkBuffer(Session session) {
-        this.session = Objects.requireNonNull(session, "No session");
-    }
-
-    @Override
-    public Session getSession() {
-        return session;
-    }
-
-    @Override
-    public void clear(boolean wipeData) {
-        throw new UnsupportedOperationException("Not allowed to clear session work buffer of " + getSession());
-    }
-
-    public void forceClear(boolean wipeData) {
-        super.clear(wipeData);
+public class ImmediateWriteFuture extends IoWriteFutureImpl {
+    public ImmediateWriteFuture(Object id, Buffer buffer) {
+        super(id, buffer);
+        setValue(Boolean.TRUE);
     }
 }
diff --git a/sshd-contrib/src/test/java/org/apache/sshd/contrib/server/session/EndlessTarpitSenderSupportDevelopment.java b/sshd-contrib/src/test/java/org/apache/sshd/contrib/server/session/EndlessTarpitSenderSupportDevelopment.java
new file mode 100644
index 0000000..9298b3b
--- /dev/null
+++ b/sshd-contrib/src/test/java/org/apache/sshd/contrib/server/session/EndlessTarpitSenderSupportDevelopment.java
@@ -0,0 +1,280 @@
+/*
+ * 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.contrib.server.session;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Base64.Encoder;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.Factory;
+import org.apache.sshd.common.FactoryManager;
+import org.apache.sshd.common.SshConstants;
+import org.apache.sshd.common.io.IoSession;
+import org.apache.sshd.common.io.IoWriteFuture;
+import org.apache.sshd.common.kex.KexProposalOption;
+import org.apache.sshd.common.random.Random;
+import org.apache.sshd.common.session.ReservedSessionMessagesHandler;
+import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.session.SessionListener;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
+import org.apache.sshd.common.util.io.NoCloseInputStream;
+import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+import org.apache.sshd.contrib.common.io.EndlessWriteFuture;
+import org.apache.sshd.contrib.common.io.ImmediateWriteFuture;
+import org.apache.sshd.core.CoreModuleProperties;
+import org.apache.sshd.server.SshServer;
+import org.apache.sshd.server.session.ServerSession;
+import org.apache.sshd.util.test.BaseTestSupport;
+import org.apache.sshd.util.test.CoreTestSupportUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * @see    <A HREF="https://nullprogram.com/blog/2019/03/22/">Endless tarpit</A>
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public final class EndlessTarpitSenderSupportDevelopment extends AbstractLoggingBean implements Runnable, SessionListener {
+    private static final Collection<EndlessTarpitSenderSupportDevelopment> THREADS = new LinkedList<>();
+
+    private final Random randomizer;
+    private final byte[] dataBuffer;
+    private final byte[] outputBuffer;
+    private AtomicLong numSent = new AtomicLong();
+    private final ServerSession session;
+    private final AtomicBoolean okToRun = new AtomicBoolean(true);
+
+    private EndlessTarpitSenderSupportDevelopment(ServerSession session, int lineLength) {
+        this.session = session;
+        this.dataBuffer = new byte[(lineLength * 4) / 6 /* BASE64 */];
+        this.outputBuffer = new byte[lineLength + 8 /* some padding */ + 2 /* CRLF */];
+        FactoryManager manager = session.getFactoryManager();
+        Factory<Random> randomFactory = manager.getRandomFactory();
+        this.randomizer = randomFactory.create();
+        this.session.addSessionListener(this);
+    }
+
+    @Override
+    public void sessionException(Session session, Throwable t) {
+        terminate("sessionException");
+    }
+
+    @Override
+    public void sessionDisconnect(
+            Session session, int reason, String msg, String language, boolean initiator) {
+        terminate("sessionDisconnect");
+    }
+
+    @Override
+    public void sessionClosed(Session session) {
+        terminate("sessionClosed");
+    }
+
+    private IoWriteFuture sendRandomLine() throws IOException {
+        randomizer.fill(dataBuffer);
+
+        Encoder encoder = Base64.getEncoder();
+        int len = encoder.encode(dataBuffer, outputBuffer);
+        outputBuffer[len] = (byte) '\r';
+        outputBuffer[len + 1] = (byte) '\n';
+
+        byte[] packet = Arrays.copyOf(outputBuffer, len + 2);
+        String line = new String(packet, 0, packet.length - 2, StandardCharsets.US_ASCII);
+        IoSession networkSession = session.getIoSession();
+        IoWriteFuture future = networkSession.writeBuffer(new ByteArrayBuffer(packet));
+        long count = numSent.incrementAndGet();
+        log.info("sendRandomLine({}) sent line #{}: {}", session, count, line);
+        return future;
+    }
+
+    @Override
+    public void run() {
+        try {
+            synchronized (THREADS) {
+                THREADS.add(this);
+            }
+
+            while (okToRun.get()) {
+                sendRandomLine();
+
+                synchronized (okToRun) {
+                    okToRun.wait(TimeUnit.SECONDS.toMillis(5L));
+                }
+            }
+        } catch (Exception e) {
+            log.error("run(" + session + ") failure", e);
+        } finally {
+            log.info("closing({})", session);
+            try {
+                session.close(true);
+            } finally {
+                session.removeSessionListener(this);
+
+                synchronized (THREADS) {
+                    THREADS.remove(this);
+                }
+            }
+        }
+    }
+
+    private void terminate(Object logHint) {
+        boolean terminated;
+        synchronized (okToRun) {
+            terminated = okToRun.getAndSet(false);
+            okToRun.notifyAll();
+        }
+
+        if (terminated) {
+            log.info("terminate({}) terminated {}", logHint, session);
+        }
+    }
+
+    //////////////////////////////////////////////////////////////////////////////////
+
+    private static <F extends FactoryManager> F setupTimeouts(F manager) {
+        CoreModuleProperties.NIO2_READ_TIMEOUT.set(manager, Duration.ofMinutes(15L));
+        CoreModuleProperties.IDLE_TIMEOUT.set(manager, Duration.ZERO);
+        CoreModuleProperties.AUTH_TIMEOUT.set(manager, Duration.ZERO);
+        return manager;
+    }
+
+    private static void startServer(String address, int port) throws Exception {
+        try (SshServer server = CoreTestSupportUtils.setupTestServer(EndlessTarpitSenderSupportDevelopment.class);
+             BufferedReader stdin = new BufferedReader(
+                     new InputStreamReader(new NoCloseInputStream(System.in), Charset.defaultCharset()))) {
+            setupTimeouts(server);
+
+            if (GenericUtils.isNotEmpty(address)) {
+                server.setHost(address);
+            }
+            server.setPort(port);
+            server.setReservedSessionMessagesHandler(new ReservedSessionMessagesHandler() {
+                private final Logger log = LoggerFactory.getLogger(EndlessTarpitSenderSupportDevelopment.class);
+
+                @Override
+                @SuppressWarnings("synthetic-access")
+                public IoWriteFuture sendIdentification(Session session, String version, List<String> extraLines)
+                        throws Exception {
+                    EndlessTarpitSenderSupportDevelopment tarpit = new EndlessTarpitSenderSupportDevelopment(
+                            (ServerSession) session, 32);
+                    Thread thread = new Thread(tarpit, "t" + session.getIoSession().getRemoteAddress());
+                    thread.start();
+                    log.info("sendIdentification({})[{}] Started endless sender", session, version);
+                    return EndlessWriteFuture.INSTANCE;
+                }
+
+                @Override
+                public IoWriteFuture sendKexInitRequest(
+                        Session session, Map<KexProposalOption, String> proposal, Buffer packet)
+                        throws Exception {
+                    log.info("sendKexInitRequest({}) suppressed KEX sending", session);
+                    return new ImmediateWriteFuture(session, packet);
+                }
+
+            });
+            System.err.append("Starting SSHD on " + address + ":" + port);
+            server.start();
+
+            try {
+                while (true) {
+                    System.out.println("Running on port " + port + " (Q)uit: ");
+                    String line = stdin.readLine();
+                    line = GenericUtils.trimToEmpty(line);
+                    if ("q".equalsIgnoreCase(line) || "quit".equalsIgnoreCase(line)) {
+                        break;
+                    }
+                }
+            } finally {
+                System.err.append("Stopping server on port ").println(port);
+                server.stop();
+            }
+        } finally {
+            for (EndlessTarpitSenderSupportDevelopment t : THREADS) {
+                t.terminate("main");
+            }
+        }
+    }
+
+    private static void startClient(String host, int port) throws Exception {
+        try (SshClient client = CoreTestSupportUtils.setupTestClient(EndlessTarpitSenderSupportDevelopment.class)) {
+            setupTimeouts(client);
+
+            client.addSessionListener(new SessionListener() {
+                private final Logger log = LoggerFactory.getLogger(EndlessTarpitSenderSupportDevelopment.class);
+                private final AtomicInteger lastCount = new AtomicInteger();
+
+                @Override
+                public void sessionEstablished(Session session) {
+                    log.info("sessionEstablished({})", session);
+                }
+
+                @Override
+                public void sessionPeerIdentificationLine(
+                        Session session, String line, List<String> extraLines) {
+                    if (lastCount.get() < GenericUtils.size(extraLines)) {
+                        int num = lastCount.incrementAndGet();
+                        log.info("sessionPeerIdentificationLine({})[{}] {}", session, num, line);
+                    }
+                }
+
+            });
+
+            client.start();
+            Duration waitTime = Duration.ofMinutes(15L);
+            try (ClientSession session = client.connect(host, host, port)
+                    .verify(waitTime)
+                    .getSession()) {
+                session.addPasswordIdentity(host);
+                session.auth().verify(waitTime);
+            } finally {
+                client.stop();
+            }
+        }
+    }
+
+    // optional args[0]=client/server - default=server, optional args[1]=port (default 22), optional args[2]=listen/connect address (default=localhost)
+    public static void main(String[] args) throws Exception {
+        int numArgs = GenericUtils.length(args);
+        String mode = (numArgs > 0) ? args[0] : "server";
+        int port = (numArgs > 1) ? Integer.parseInt(args[1]) : SshConstants.DEFAULT_PORT;
+        if ("server".equalsIgnoreCase(mode)) {
+            startServer((numArgs > 2) ? args[2] : null, port);
+        } else {
+            startClient((numArgs > 2) ? args[2] : BaseTestSupport.TEST_LOCALHOST, port);
+        }
+    }
+}
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 3bab23a..b81ac20 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
@@ -556,7 +556,7 @@ public abstract class AbstractClientSession extends AbstractSession implements C
     }
 
     @Override
-    protected byte[] sendKexInit(Map<KexProposalOption, String> proposal) throws IOException {
+    protected byte[] sendKexInit(Map<KexProposalOption, String> proposal) throws Exception {
         mergeProposals(clientProposal, proposal);
         return super.sendKexInit(proposal);
     }
@@ -683,10 +683,13 @@ public abstract class AbstractClientSession extends AbstractSession implements C
                 proposal.put(KexProposalOption.C2SENC, BuiltinCiphers.Constants.NONE);
                 proposal.put(KexProposalOption.S2CENC, BuiltinCiphers.Constants.NONE);
 
-                byte[] seed;
-                synchronized (kexState) {
-                    seed = sendKexInit(proposal);
-                    setKexSeed(seed);
+                try {
+                    synchronized (kexState) {
+                        byte[] seed = sendKexInit(proposal);
+                        setKexSeed(seed);
+                    }
+                } catch (Exception e) {
+                    GenericUtils.rethrowAsIoException(e);
                 }
             }
 
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 72c41ae..8367583 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
@@ -225,7 +225,7 @@ public class ClientSessionImpl extends AbstractClientSession {
     }
 
     @Override
-    protected void signalSessionEvent(SessionListener.Event event) throws IOException {
+    protected void signalSessionEvent(SessionListener.Event event) throws Exception {
         if (SessionListener.Event.KeyEstablished.equals(event)) {
             sendInitialServiceRequest();
         }
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/kex/extension/KexExtensionHandler.java b/sshd-core/src/main/java/org/apache/sshd/common/kex/extension/KexExtensionHandler.java
index ae039f8..745921a 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/kex/extension/KexExtensionHandler.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/kex/extension/KexExtensionHandler.java
@@ -80,16 +80,16 @@ public interface KexExtensionHandler {
      * method is called during the negotiation phase even if {@code isKexExtensionsAvailable} returns {@code false} for
      * the session.
      *
-     * @param  session     The {@link Session} initiating or receiving the proposal
-     * @param  initiator   {@code true} if the proposal is about to be sent, {@code false} if this is a proposal
-     *                     received from the peer.
-     * @param  proposal    The proposal contents - <B>Caveat emptor:</B> the proposal is <U>modifiable</U> i.e., the
-     *                     handler can modify it before being sent or before being processed (if incoming)
-     * @throws IOException If failed to handle the request
+     * @param  session   The {@link Session} initiating or receiving the proposal
+     * @param  initiator {@code true} if the proposal is about to be sent, {@code false} if this is a proposal received
+     *                   from the peer.
+     * @param  proposal  The proposal contents - <B>Caveat emptor:</B> the proposal is <U>modifiable</U> i.e., the
+     *                   handler can modify it before being sent or before being processed (if incoming)
+     * @throws Exception If failed to handle the request
      */
     default void handleKexInitProposal(
             Session session, boolean initiator, Map<KexProposalOption, String> proposal)
-            throws IOException {
+            throws Exception {
         // ignored
     }
 
@@ -98,20 +98,20 @@ public interface KexExtensionHandler {
      * called during the negotiation phase even if {@code isKexExtensionsAvailable} returns {@code false} for the
      * session.
      *
-     * @param  session     The {@link Session} executing the negotiation
-     * @param  option      The negotiated {@link KexProposalOption}
-     * @param  nValue      The negotiated option value (may be {@code null}/empty).
-     * @param  c2sOptions  The client proposals
-     * @param  cValue      The client-side value for the option (may be {@code null}/empty).
-     * @param  s2cOptions  The server proposals
-     * @param  sValue      The server-side value for the option (may be {@code null}/empty).
-     * @throws IOException If failed to handle the invocation
+     * @param  session    The {@link Session} executing the negotiation
+     * @param  option     The negotiated {@link KexProposalOption}
+     * @param  nValue     The negotiated option value (may be {@code null}/empty).
+     * @param  c2sOptions The client proposals
+     * @param  cValue     The client-side value for the option (may be {@code null}/empty).
+     * @param  s2cOptions The server proposals
+     * @param  sValue     The server-side value for the option (may be {@code null}/empty).
+     * @throws Exception  If failed to handle the invocation
      */
     default void handleKexExtensionNegotiation(
             Session session, KexProposalOption option, String nValue,
             Map<KexProposalOption, String> c2sOptions, String cValue,
             Map<KexProposalOption, String> s2cOptions, String sValue)
-            throws IOException {
+            throws Exception {
         // do nothing
     }
 
@@ -131,12 +131,12 @@ public interface KexExtensionHandler {
      * Invoked in order to allow the handler to send an {@code SSH_MSG_EXT_INFO} message. <B>Note:</B> this method is
      * called only if {@code isKexExtensionsAvailable} returns {@code true} for the session.
      *
-     * @param  session     The {@link Session}
-     * @param  phase       The phase at which the handler is invoked
-     * @throws IOException If failed to handle the invocation
-     * @see                <A HREF="https://tools.ietf.org/html/rfc8308#section-2.4">RFC-8308 - section 2.4</A>
+     * @param  session   The {@link Session}
+     * @param  phase     The phase at which the handler is invoked
+     * @throws Exception If failed to handle the invocation
+     * @see              <A HREF="https://tools.ietf.org/html/rfc8308#section-2.4">RFC-8308 - section 2.4</A>
      */
-    default void sendKexExtensions(Session session, KexPhase phase) throws IOException {
+    default void sendKexExtensions(Session session, KexPhase phase) throws Exception {
         // do nothing
     }
 
@@ -144,15 +144,15 @@ public interface KexExtensionHandler {
      * Parses the {@code SSH_MSG_EXT_INFO} message. <B>Note:</B> this method is called regardless of whether
      * {@code isKexExtensionsAvailable} returns {@code true} for the session.
      *
-     * @param  session     The {@link Session} through which the message was received
-     * @param  buffer      The message buffer
-     * @return             {@code true} if message handled - if {@code false} then {@code SSH_MSG_UNIMPLEMENTED} will be
-     *                     generated
-     * @throws IOException If failed to handle the message
-     * @see                <A HREF="https://tools.ietf.org/html/rfc8308#section-2.3">RFC-8308 - section 2.3</A>
-     * @see                #handleKexExtensionRequest(Session, int, int, String, byte[])
+     * @param  session   The {@link Session} through which the message was received
+     * @param  buffer    The message buffer
+     * @return           {@code true} if message handled - if {@code false} then {@code SSH_MSG_UNIMPLEMENTED} will be
+     *                   generated
+     * @throws Exception If failed to handle the message
+     * @see              <A HREF="https://tools.ietf.org/html/rfc8308#section-2.3">RFC-8308 - section 2.3</A>
+     * @see              #handleKexExtensionRequest(Session, int, int, String, byte[])
      */
-    default boolean handleKexExtensionsMessage(Session session, Buffer buffer) throws IOException {
+    default boolean handleKexExtensionsMessage(Session session, Buffer buffer) throws Exception {
         int count = buffer.getInt();
         for (int index = 0; index < count; index++) {
             String name = buffer.getString();
@@ -169,31 +169,31 @@ public interface KexExtensionHandler {
      * Parses the {@code SSH_MSG_NEWCOMPRESS} message. <B>Note:</B> this method is called regardless of whether
      * {@code isKexExtensionsAvailable} returns {@code true} for the session.
      *
-     * @param  session     The {@link Session} through which the message was received
-     * @param  buffer      The message buffer
-     * @return             {@code true} if message handled - if {@code false} then {@code SSH_MSG_UNIMPLEMENTED} will be
-     *                     generated
-     * @throws IOException If failed to handle the message
-     * @see                <A HREF="https://tools.ietf.org/html/rfc8308#section-3.2">RFC-8308 - section 3.2</A>
+     * @param  session   The {@link Session} through which the message was received
+     * @param  buffer    The message buffer
+     * @return           {@code true} if message handled - if {@code false} then {@code SSH_MSG_UNIMPLEMENTED} will be
+     *                   generated
+     * @throws Exception If failed to handle the message
+     * @see              <A HREF="https://tools.ietf.org/html/rfc8308#section-3.2">RFC-8308 - section 3.2</A>
      */
-    default boolean handleKexCompressionMessage(Session session, Buffer buffer) throws IOException {
+    default boolean handleKexCompressionMessage(Session session, Buffer buffer) throws Exception {
         return true;
     }
 
     /**
      * Invoked by {@link #handleKexExtensionsMessage(Session, Buffer)} in order to handle a specific extension.
      *
-     * @param  session     The {@link Session} through which the message was received
-     * @param  index       The 0-based extension index
-     * @param  count       The total extensions in the message
-     * @param  name        The extension name
-     * @param  data        The extension data
-     * @return             {@code true} whether to proceed to the next extension or stop processing the rest
-     * @throws IOException If failed to handle the extension
+     * @param  session   The {@link Session} through which the message was received
+     * @param  index     The 0-based extension index
+     * @param  count     The total extensions in the message
+     * @param  name      The extension name
+     * @param  data      The extension data
+     * @return           {@code true} whether to proceed to the next extension or stop processing the rest
+     * @throws Exception If failed to handle the extension
      */
     default boolean handleKexExtensionRequest(
             Session session, int index, int count, String name, byte[] data)
-            throws IOException {
+            throws Exception {
         return true;
     }
 }
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/ReservedSessionMessagesHandler.java b/sshd-core/src/main/java/org/apache/sshd/common/session/ReservedSessionMessagesHandler.java
index ecc49f1..496f137 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/session/ReservedSessionMessagesHandler.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/session/ReservedSessionMessagesHandler.java
@@ -20,8 +20,10 @@
 package org.apache.sshd.common.session;
 
 import java.util.List;
+import java.util.Map;
 
 import org.apache.sshd.common.io.IoWriteFuture;
+import org.apache.sshd.common.kex.KexProposalOption;
 import org.apache.sshd.common.util.SshdEventListener;
 import org.apache.sshd.common.util.buffer.Buffer;
 
@@ -45,7 +47,7 @@ public interface ReservedSessionMessagesHandler extends SshdEventListener {
      * @return            A {@link IoWriteFuture} that can be used to wait for the data to be sent successfully. If
      *                    {@code null} then the session will send the identification, otherwise it is assumed that the
      *                    handler has sent it.
-     * @throws Exception
+     * @throws Exception  if failed to handle the callback
      * @see               <A HREF="https://tools.ietf.org/html/rfc4253#section-4.2">RFC 4253 - section 4.2 - Protocol
      *                    Version Exchange</A>
      */
@@ -56,6 +58,23 @@ public interface ReservedSessionMessagesHandler extends SshdEventListener {
     }
 
     /**
+     * Invoked before sending the {@code SSH_MSG_KEXINIT} packet
+     *
+     * @param  session   The {@code Session} through which the key exchange is being managed
+     * @param  proposal  The KEX proposal that was used to build the packet
+     * @param  packet    The packet containing the fully encoded message - <B>Caveat:</B> this packet later serves as
+     *                   part of the key generation, so care must be taken if manipulating it.
+     * @return           A non-{@code null} {@link IoWriteFuture} to signal that handler took care of the KEX packet
+     *                   delivery.
+     * @throws Exception if failed to handle the callback
+     */
+    default IoWriteFuture sendKexInitRequest(
+            Session session, Map<KexProposalOption, String> proposal, Buffer packet)
+            throws Exception {
+        return null;
+    }
+
+    /**
      * Invoked when an {@code SSH_MSG_IGNORE} packet is received
      *
      * @param  session   The {@code Session} through which the message was received
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/SessionWorkBuffer.java b/sshd-core/src/main/java/org/apache/sshd/common/session/SessionWorkBuffer.java
index 00815a2..5abca14 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/session/SessionWorkBuffer.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/session/SessionWorkBuffer.java
@@ -21,6 +21,7 @@ package org.apache.sshd.common.session;
 
 import java.util.Objects;
 
+import org.apache.sshd.common.util.buffer.Buffer;
 import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
 
 /**
@@ -39,7 +40,7 @@ public class SessionWorkBuffer extends ByteArrayBuffer implements SessionHolder<
     }
 
     @Override
-    public void clear(boolean wipeData) {
+    public Buffer clear(boolean wipeData) {
         throw new UnsupportedOperationException("Not allowed to clear session work buffer of " + getSession());
     }
 
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 13bb2f9..6a2b146 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
@@ -376,7 +376,7 @@ public abstract class AbstractSession extends SessionHelper {
     public void messageReceived(Readable buffer) throws Exception {
         synchronized (decodeLock) {
             decoderBuffer.putBuffer(buffer);
-            // One of those property will be set by the constructor and the other
+            // One of those properties will be set by the constructor and the other
             // one should be set by the readIdentification method
             if ((clientVersion == null) || (serverVersion == null)) {
                 if (readIdentification(decoderBuffer)) {
@@ -568,10 +568,10 @@ public abstract class AbstractSession extends SessionHelper {
     /**
      * Send a message to put new keys into use.
      *
-     * @return             An {@link IoWriteFuture} that can be used to wait and check the result of sending the packet
-     * @throws IOException if an error occurs sending the message
+     * @return           An {@link IoWriteFuture} that can be used to wait and check the result of sending the packet
+     * @throws Exception if an error occurs sending the message
      */
-    protected IoWriteFuture sendNewKeys() throws IOException {
+    protected IoWriteFuture sendNewKeys() throws Exception {
         if (log.isDebugEnabled()) {
             log.debug("sendNewKeys({}) Send SSH_MSG_NEWKEYS", this);
         }
@@ -897,6 +897,8 @@ public abstract class AbstractSession extends SessionHelper {
                                 "Failed (" + e.getClass().getSimpleName() + ")"
                                               + " to check re-key necessity: " + e.getMessage()),
                         e);
+            } catch (Exception e) {
+                GenericUtils.rethrowAsIoException(e);
             }
         }
     }
@@ -1507,14 +1509,16 @@ public abstract class AbstractSession extends SessionHelper {
     /**
      * Send the key exchange initialization packet. This packet contains random data along with our proposal.
      *
-     * @param  proposal    our proposal for key exchange negotiation
-     * @return             the sent packet data which must be kept for later use when deriving the session keys
-     * @throws IOException if an error occurred sending the packet
+     * @param  proposal  our proposal for key exchange negotiation
+     * @return           the sent packet data which must be kept for later use when deriving the session keys
+     * @throws Exception if an error occurred sending the packet
      */
-    protected byte[] sendKexInit(Map<KexProposalOption, String> proposal) throws IOException {
-        if (log.isDebugEnabled()) {
+    protected byte[] sendKexInit(Map<KexProposalOption, String> proposal) throws Exception {
+        boolean debugEnabled = log.isDebugEnabled();
+        if (debugEnabled) {
             log.debug("sendKexInit({}) Send SSH_MSG_KEXINIT", this);
         }
+
         Buffer buffer = createBuffer(SshConstants.SSH_MSG_KEXINIT);
         int p = buffer.wpos();
         buffer.wpos(p + SshConstants.MSG_KEX_COOKIE_SIZE);
@@ -1538,20 +1542,30 @@ public abstract class AbstractSession extends SessionHelper {
 
         buffer.putBoolean(false); // first kex packet follows
         buffer.putInt(0); // reserved (FFU)
+
+        ReservedSessionMessagesHandler handler = getReservedSessionMessagesHandler();
+        IoWriteFuture future = (handler == null) ? null : handler.sendKexInitRequest(this, proposal, buffer);
         byte[] data = buffer.getCompactData();
-        writePacket(buffer);
+        if (future == null) {
+            future = writePacket(buffer);
+        } else {
+            if (debugEnabled) {
+                log.debug("sendKexInit({}) KEX handled by reserved messages handler", this);
+            }
+        }
+
         return data;
     }
 
     /**
      * Receive the remote key exchange init message. The packet data is returned for later use.
      *
-     * @param  buffer      the {@link Buffer} containing the key exchange init packet
-     * @param  proposal    the remote proposal to fill
-     * @return             the packet data
-     * @throws IOException If failed to handle the message
+     * @param  buffer    the {@link Buffer} containing the key exchange init packet
+     * @param  proposal  the remote proposal to fill
+     * @return           the packet data
+     * @throws Exception If failed to handle the message
      */
-    protected byte[] receiveKexInit(Buffer buffer, Map<KexProposalOption, String> proposal) throws IOException {
+    protected byte[] receiveKexInit(Buffer buffer, Map<KexProposalOption, String> proposal) throws Exception {
         // Recreate the packet payload which will be needed at a later time
         byte[] d = buffer.array();
         byte[] data = new byte[buffer.available() + 1 /* the opcode */];
@@ -1791,10 +1805,10 @@ public abstract class AbstractSession extends SessionHelper {
      * Compute the negotiated proposals by merging the client and server proposal. The negotiated proposal will also be
      * stored in the {@link #negotiationResult} property.
      *
-     * @return             The negotiated options {@link Map}
-     * @throws IOException If negotiation failed
+     * @return           The negotiated options {@link Map}
+     * @throws Exception If negotiation failed
      */
-    protected Map<KexProposalOption, String> negotiate() throws IOException {
+    protected Map<KexProposalOption, String> negotiate() throws Exception {
         Map<KexProposalOption, String> c2sOptions = getClientKexProposals();
         Map<KexProposalOption, String> s2cOptions = getServerKexProposals();
         signalNegotiationStart(c2sOptions, s2cOptions);
@@ -2104,6 +2118,9 @@ public abstract class AbstractSession extends SessionHelper {
                             "Failed (" + e.getClass().getSimpleName() + ")"
                                           + " to generate keys for exchange: " + e.getMessage()),
                     e);
+        } catch (Exception e) {
+            GenericUtils.rethrowAsIoException(e);
+            return null;    // actually dead code
         }
 
         return ValidateUtils.checkNotNull(
@@ -2113,26 +2130,24 @@ public abstract class AbstractSession extends SessionHelper {
     /**
      * Checks if a re-keying is required and if so initiates it
      *
-     * @return                          A {@link KeyExchangeFuture} to wait for the initiated exchange or {@code null}
-     *                                  if no need to re-key or an exchange is already in progress
-     * @throws IOException              If failed load the keys or send the request
-     * @throws GeneralSecurityException If failed to generate the necessary keys
-     * @see                             #isRekeyRequired()
-     * @see                             #requestNewKeysExchange()
+     * @return           A {@link KeyExchangeFuture} to wait for the initiated exchange or {@code null} if no need to
+     *                   re-key or an exchange is already in progress
+     * @throws Exception If failed load/generate the keys or send the request
+     * @see              #isRekeyRequired()
+     * @see              #requestNewKeysExchange()
      */
-    protected KeyExchangeFuture checkRekey() throws IOException, GeneralSecurityException {
+    protected KeyExchangeFuture checkRekey() throws Exception {
         return isRekeyRequired() ? requestNewKeysExchange() : null;
     }
 
     /**
      * Initiates a new keys exchange if one not already in progress
      *
-     * @return                          A {@link KeyExchangeFuture} to wait for the initiated exchange or {@code null}
-     *                                  if an exchange is already in progress
-     * @throws IOException              If failed to load the keys or send the request
-     * @throws GeneralSecurityException If failed to generate the keys
+     * @return           A {@link KeyExchangeFuture} to wait for the initiated exchange or {@code null} if an exchange
+     *                   is already in progress
+     * @throws Exception If failed to load/generate the keys or send the request
      */
-    protected KeyExchangeFuture requestNewKeysExchange() throws IOException, GeneralSecurityException {
+    protected KeyExchangeFuture requestNewKeysExchange() throws Exception {
         if (!kexState.compareAndSet(KexState.DONE, KexState.INIT)) {
             if (log.isDebugEnabled()) {
                 log.debug("requestNewKeysExchange({}) KEX state not DONE: {}", this, kexState);
@@ -2259,7 +2274,7 @@ public abstract class AbstractSession extends SessionHelper {
         }
     }
 
-    protected byte[] sendKexInit() throws IOException, GeneralSecurityException {
+    protected byte[] sendKexInit() throws Exception {
         String resolvedAlgorithms = resolveAvailableSignaturesProposal();
         if (GenericUtils.isEmpty(resolvedAlgorithms)) {
             throw new SshException(
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 a5d7542..4de89bf 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
@@ -214,7 +214,11 @@ public abstract class SessionHelper extends AbstractKexFactoryManager implements
     @Override
     public void setAuthenticated() throws IOException {
         this.authed = true;
-        signalSessionEvent(SessionListener.Event.Authenticated);
+        try {
+            signalSessionEvent(SessionListener.Event.Authenticated);
+        } catch (Exception e) {
+            GenericUtils.rethrowAsIoException(e);
+        }
     }
 
     /**
@@ -691,10 +695,10 @@ public abstract class SessionHelper extends AbstractKexFactoryManager implements
     /**
      * Sends a session event to all currently registered session listeners
      *
-     * @param  event       The event to send
-     * @throws IOException If any of the registered listeners threw an exception.
+     * @param  event     The event to send
+     * @throws Exception If any of the registered listeners threw an exception.
      */
-    protected void signalSessionEvent(SessionListener.Event event) throws IOException {
+    protected void signalSessionEvent(SessionListener.Event event) throws Exception {
         try {
             invokeSessionSignaller(l -> {
                 signalSessionEvent(l, event);
@@ -704,13 +708,10 @@ public abstract class SessionHelper extends AbstractKexFactoryManager implements
             Throwable t = GenericUtils.peelException(err);
             debug("sendSessionEvent({})[{}] failed ({}) to inform listeners: {}",
                     this, event, t.getClass().getSimpleName(), t.getMessage(), t);
-            if (t instanceof IOException) {
-                throw (IOException) t;
-            } else if (t instanceof RuntimeException) {
-                throw (RuntimeException) t;
+            if (t instanceof Exception) {
+                throw (Exception) t;
             } else {
-                throw new IOException(
-                        "Failed (" + t.getClass().getSimpleName() + ") to send session event: " + t.getMessage(), t);
+                throw new RuntimeSshException(t);
             }
         }
     }
@@ -1279,6 +1280,7 @@ public abstract class SessionHelper extends AbstractKexFactoryManager implements
             Throwable e = GenericUtils.peelException(err);
             debug("signalSessionClosed({}) {} while signal session closed: {}",
                     this, e.getClass().getSimpleName(), e.getMessage(), e);
+            // Do not re-throw since session closed anyway
         }
     }
 
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 b388a6c..5cff5f9 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
@@ -354,7 +354,7 @@ public abstract class AbstractServerSession extends AbstractSession implements S
     }
 
     @Override
-    protected byte[] sendKexInit(Map<KexProposalOption, String> proposal) throws IOException {
+    protected byte[] sendKexInit(Map<KexProposalOption, String> proposal) throws Exception {
         mergeProposals(serverProposal, proposal);
         return super.sendKexInit(proposal);
     }
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/session/ServerSessionImpl.java b/sshd-core/src/main/java/org/apache/sshd/server/session/ServerSessionImpl.java
index 0b4a588..ea99c9b 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/session/ServerSessionImpl.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/session/ServerSessionImpl.java
@@ -39,7 +39,8 @@ public class ServerSessionImpl extends AbstractServerSession {
 
         String headerConfig = CoreModuleProperties.SERVER_EXTRA_IDENTIFICATION_LINES.getOrNull(this);
         String[] headers = GenericUtils.split(headerConfig, CoreModuleProperties.SERVER_EXTRA_IDENT_LINES_SEPARATOR);
-        // We intentionally create a modifiable array so as to allow users to modify it via SessionListener or ReservedSessionMessagesHandler
+        // We intentionally create a modifiable array so as to allow users to
+        // modify it via SessionListener or ReservedSessionMessagesHandler
         List<String> extraLines = GenericUtils.isEmpty(headers)
                 ? new ArrayList<>()
                 : new ArrayList<>(Arrays.asList(headers));
diff --git a/sshd-core/src/test/java/org/apache/sshd/common/kex/extension/KexExtensionHandlerTest.java b/sshd-core/src/test/java/org/apache/sshd/common/kex/extension/KexExtensionHandlerTest.java
index d38c817..307cccc 100644
--- a/sshd-core/src/test/java/org/apache/sshd/common/kex/extension/KexExtensionHandlerTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/common/kex/extension/KexExtensionHandlerTest.java
@@ -53,7 +53,7 @@ public class KexExtensionHandlerTest extends JUnitTestSupport {
     }
 
     @Test
-    public void testEncodeDecodeExtensionMessage() throws IOException {
+    public void testEncodeDecodeExtensionMessage() throws Exception {
         List<Map.Entry<String, ?>> expected = Arrays.asList(
                 new SimpleImmutableEntry<>(
                         DelayCompression.NAME,
diff --git a/sshd-core/src/test/java/org/apache/sshd/util/test/CoreTestSupportUtils.java b/sshd-core/src/test/java/org/apache/sshd/util/test/CoreTestSupportUtils.java
index 6e52784..686d2de 100644
--- a/sshd-core/src/test/java/org/apache/sshd/util/test/CoreTestSupportUtils.java
+++ b/sshd-core/src/test/java/org/apache/sshd/util/test/CoreTestSupportUtils.java
@@ -56,7 +56,10 @@ public final class CoreTestSupportUtils {
     }
 
     public static SshClient setupTestClient(Class<?> anchor) {
-        SshClient client = SshClient.setUpDefaultClient();
+        return setupTestClient(SshClient.setUpDefaultClient(), anchor);
+    }
+
+    public static <C extends SshClient> C setupTestClient(C client, Class<?> anchor) {
         client.setServerKeyVerifier(AcceptAllServerKeyVerifier.INSTANCE);
         client.setHostConfigEntryResolver(HostConfigEntryResolver.EMPTY);
         client.setKeyIdentityProvider(KeyIdentityProvider.EMPTY_KEYS_PROVIDER);
@@ -77,7 +80,10 @@ public final class CoreTestSupportUtils {
     }
 
     public static SshServer setupTestServer(Class<?> anchor) {
-        SshServer sshd = SshServer.setUpDefaultServer();
+        return setupTestServer(SshServer.setUpDefaultServer(), anchor);
+    }
+
+    public static <S extends SshServer> S setupTestServer(S sshd, Class<?> anchor) {
         sshd.setKeyPairProvider(CommonTestSupportUtils.createTestHostKeyProvider(anchor));
         sshd.setPasswordAuthenticator(BogusPasswordAuthenticator.INSTANCE);
         sshd.setPublickeyAuthenticator(AcceptAllPublickeyAuthenticator.INSTANCE);
diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/client/SimpleScpClientImpl.java b/sshd-scp/src/main/java/org/apache/sshd/scp/client/SimpleScpClientImpl.java
index 7b8e474..54b865f 100644
--- a/sshd-scp/src/main/java/org/apache/sshd/scp/client/SimpleScpClientImpl.java
+++ b/sshd-scp/src/main/java/org/apache/sshd/scp/client/SimpleScpClientImpl.java
@@ -109,7 +109,8 @@ public class SimpleScpClientImpl extends AbstractLoggingBean implements SimpleSc
                 e.addSuppressed(t);
             }
 
-            throw GenericUtils.toIOException(e);
+            GenericUtils.rethrowAsIoException(e);
+            return null;    // actually dead code...
         }
     }