From 1783572cf8104e6ceb077d7847017c482d0c0caf Mon Sep 17 00:00:00 2001 From: Thomas Wolf Date: Sun, 8 Mar 2026 21:49:00 +0100 Subject: [PATCH 1/3] GH-879: close channel gracefully in TCP/IP forwarding When an exception occurs on the forwarding socket, close the SSH channel gracefully. --- CHANGES.md | 2 ++ .../org/apache/sshd/common/forward/DefaultForwarder.java | 5 ++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index baf5f4116e..d4f4dafbf2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -31,6 +31,8 @@ ## Bug Fixes +* [GH-879](https://github.com/apache/mina-sshd/issues/879) Close SSH channel gracefully on exception in port forwarding + ## New Features ## Potential Compatibility Issues diff --git a/sshd-core/src/main/java/org/apache/sshd/common/forward/DefaultForwarder.java b/sshd-core/src/main/java/org/apache/sshd/common/forward/DefaultForwarder.java index 195e911e8e..90420c5e83 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/forward/DefaultForwarder.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/forward/DefaultForwarder.java @@ -1034,10 +1034,9 @@ public void sessionClosed(IoSession session) throws Exception { log.debug("sessionClosed({}) closing channel={} after {} messages - cause={}", session, channel, messagesCounter, (cause == null) ? null : cause.getClass().getSimpleName()); } - if (channel == null) { - return; + if (channel != null) { + channel.close(false); } - channel.close(cause != null); } @Override From 160f911b697403e0d430873686c2cf04302a8627 Mon Sep 17 00:00:00 2001 From: Thomas Wolf Date: Sun, 8 Mar 2026 23:43:00 +0100 Subject: [PATCH 2/3] Ignore flaky ServerTest.serverIdleTimoutWithForce() This test was incorrect since the channel windowing was fixed in commit 0c8f9b22 in 2022. It's actually surprising that the test "worked" somehow for so long, but lately it's been failing more often, in particular on faster machines. The basic problem is that since the channel window fix, AbstcractClientChannel would call LocalWindow.release() when data was written from the channel to the externally provided OutputStream. This would then send back a window adjustment to the server, and hence the server's RemoteWindow would not stay at zero. Apparently the test still succeeded most of the time (probably because of the very low sleep of 1ms in its busy-waiting loop waiting for the window to drop to zero), but it may not have tested what it wanted to test. The solution is to simply not provide an external OutputStream to the channel. Then the channel sets up its own stream, and calls LocalWindow.release() only once the data is really read from invertedOut. That way, the window really drops to zero, and the server will not get window adjustments. Despite this seemingly logical fix the test still was flaky. So ignore it for now. Note that the test claims to test "full TCP/IP buffers", but it actually doesn't. It just tests that an idle timeout is effective and can close the session even when the local window is fully used up. --- .../org/apache/sshd/server/ServerTest.java | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/sshd-core/src/test/java/org/apache/sshd/server/ServerTest.java b/sshd-core/src/test/java/org/apache/sshd/server/ServerTest.java index 77e583aaca..8126d52838 100644 --- a/sshd-core/src/test/java/org/apache/sshd/server/ServerTest.java +++ b/sshd-core/src/test/java/org/apache/sshd/server/ServerTest.java @@ -22,8 +22,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.io.PipedInputStream; -import java.io.PipedOutputStream; import java.io.StreamCorruptedException; import java.nio.charset.StandardCharsets; import java.time.Duration; @@ -86,6 +84,7 @@ import org.apache.sshd.util.test.EchoShell; import org.apache.sshd.util.test.EchoShellFactory; import org.apache.sshd.util.test.TestChannelListener; +import org.junit.Ignore; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.MethodOrderer.MethodName; @@ -94,14 +93,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertTrue; - /** * @author Apache MINA SSHD Project */ @@ -356,6 +347,7 @@ public String toString() { * read the data, filling the ssh window and the tcp socket - the server session becomes idle, but the ssh * disconnect message can't be written - the server session is forcibly closed */ + @Ignore("Unstable test") @Test void serverIdleTimeoutWithForce() throws Exception { final long idleTimeoutValue = TimeUnit.SECONDS.toMillis(5L); @@ -401,12 +393,8 @@ public String toString() { client.start(); try (ClientSession s = createTestClientSession(sshd); - ChannelExec shell = s.createExecChannel("normal"); - // Create a pipe that will block reading when the buffer is full - PipedInputStream pis = new PipedInputStream(); - PipedOutputStream pos = new PipedOutputStream(pis)) { + ChannelExec shell = s.createExecChannel("normal")) { - shell.setOut(pos); shell.open().verify(OPEN_TIMEOUT); assertTrue(channelListener.waitForActiveChannelsChange(5L, TimeUnit.SECONDS), @@ -424,7 +412,7 @@ public String toString() { RemoteWindow wRemote = channel.getRemoteWindow(); for (long totalNanoTime = 0L; wRemote.getSize() > 0;) { long nanoStart = System.nanoTime(); - Thread.sleep(1L); + Thread.sleep(100L); long nanoEnd = System.nanoTime(); long nanoDuration = nanoEnd - nanoStart; From 07cfbba3fc0a96476eaa19bd22cac4d6c67ff9fc Mon Sep 17 00:00:00 2001 From: Thomas Wolf Date: Fri, 13 Mar 2026 09:59:38 +0100 Subject: [PATCH 3/3] [releng] Fix maven wrapper Using dlcdn.apache.org as URL wasn't a good idea; it carries only the latest 3.8.x and 3.9.x versions. repo.maven.apache.org keeps all versions, so use that. --- .mvn/wrapper/maven-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index 2ef9d4c4ea..8dea6c227c 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1,3 +1,3 @@ wrapperVersion=3.3.4 distributionType=only-script -distributionUrl=https://dlcdn.apache.org/maven/maven-3/3.9.12/binaries/apache-maven-3.9.12-bin.zip +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip