diff --git a/src/Renci.SshNet/Channels/ChannelForwardedTcpip.cs b/src/Renci.SshNet/Channels/ChannelForwardedTcpip.cs
index 1f536ff27..55be94a43 100644
--- a/src/Renci.SshNet/Channels/ChannelForwardedTcpip.cs
+++ b/src/Renci.SshNet/Channels/ChannelForwardedTcpip.cs
@@ -98,6 +98,18 @@ public void Bind(IPEndPoint remoteEndpoint, IForwardedPort forwardedPort)
SocketAbstraction.ReadContinuous(_socket, buffer, 0, buffer.Length, SendData);
}
+ ///
+ /// Called when channel has no more data to receive.
+ ///
+ protected override void OnEof()
+ {
+ base.OnEof();
+
+ // the server will not send any more data; shut down the send direction of the socket so the
+ // connected host receives end-of-stream, while it can still send data back over the channel
+ ShutdownSocket(SocketShutdown.Send);
+ }
+
protected override void OnErrorOccurred(Exception exp)
{
base.OnErrorOccurred(exp);
diff --git a/test/Renci.SshNet.Tests/Classes/Channels/ChannelForwardedTcpipTest_ChannelEofReceived.cs b/test/Renci.SshNet.Tests/Classes/Channels/ChannelForwardedTcpipTest_ChannelEofReceived.cs
new file mode 100644
index 000000000..bc81fcfea
--- /dev/null
+++ b/test/Renci.SshNet.Tests/Classes/Channels/ChannelForwardedTcpipTest_ChannelEofReceived.cs
@@ -0,0 +1,182 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading;
+
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+using Moq;
+
+using Renci.SshNet.Channels;
+using Renci.SshNet.Messages;
+using Renci.SshNet.Messages.Connection;
+
+namespace Renci.SshNet.Tests.Classes.Channels
+{
+ [TestClass]
+ public class ChannelForwardedTcpipTest_ChannelEofReceived
+ {
+ private Mock _sessionMock;
+ private Mock _forwardedPortMock;
+ private Mock _connectionInfoMock;
+ private ChannelForwardedTcpip _channel;
+ private uint _localChannelNumber;
+ private uint _localWindowSize;
+ private uint _localPacketSize;
+ private uint _remoteWindowSize;
+ private uint _remotePacketSize;
+ private uint _remoteChannelNumber;
+ private TimeSpan _channelCloseTimeout;
+ private TcpListener _remoteListener;
+ private Socket _remoteSocket;
+ private Exception _channelException;
+ private Thread _channelThread;
+ private List _dataSentToServer;
+ private ManualResetEvent _dataSentToServerWaitHandle;
+ private int _bytesReceivedAfterEof;
+ private byte[] _dataToSendAfterEof;
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ Arrange();
+ Act();
+ }
+
+ [TestCleanup]
+ public void CleanUp()
+ {
+ _remoteSocket?.Dispose();
+ _remoteSocket = null;
+
+ _channelThread?.Join(TimeSpan.FromSeconds(5));
+ _channelThread = null;
+
+ _channel?.Dispose();
+ _channel = null;
+
+ _remoteListener?.Stop();
+ _remoteListener = null;
+
+ _dataSentToServerWaitHandle?.Dispose();
+ _dataSentToServerWaitHandle = null;
+ }
+
+ private void Arrange()
+ {
+ var random = new Random();
+ _localChannelNumber = (uint)random.Next(0, int.MaxValue);
+ _localWindowSize = (uint)random.Next(2000, 3000);
+ _localPacketSize = (uint)random.Next(1000, 2000);
+ _remoteChannelNumber = (uint)random.Next(0, int.MaxValue);
+ _remoteWindowSize = (uint)random.Next(1000, int.MaxValue);
+ _remotePacketSize = (uint)random.Next(100, 200);
+ _channelCloseTimeout = TimeSpan.FromSeconds(random.Next(10, 20));
+ _channelException = null;
+ _dataSentToServer = new List();
+ _dataSentToServerWaitHandle = new ManualResetEvent(false);
+ _dataToSendAfterEof = new byte[] { 0x0a, 0x0b, 0x0c };
+
+ _sessionMock = new Mock(MockBehavior.Strict);
+ _connectionInfoMock = new Mock(MockBehavior.Strict);
+ _forwardedPortMock = new Mock(MockBehavior.Strict);
+
+ _ = _sessionMock.Setup(p => p.SessionLoggerFactory).Returns(NullLoggerFactory.Instance);
+ _ = _sessionMock.Setup(p => p.IsConnected).Returns(true);
+ _ = _sessionMock.Setup(p => p.ConnectionInfo).Returns(_connectionInfoMock.Object);
+ _ = _connectionInfoMock.Setup(p => p.Timeout).Returns(TimeSpan.FromSeconds(5));
+ _ = _connectionInfoMock.Setup(p => p.ChannelCloseTimeout).Returns(_channelCloseTimeout);
+ _ = _sessionMock.Setup(p => p.SendMessage(It.IsAny()));
+ _ = _sessionMock.Setup(p => p.SendMessage(It.IsAny()))
+ .Callback(m =>
+ {
+ var dataMessage = (ChannelDataMessage)m;
+ lock (_dataSentToServer)
+ {
+ _dataSentToServer.AddRange(dataMessage.Data.Skip(dataMessage.Offset).Take(dataMessage.Size));
+ }
+
+ _ = _dataSentToServerWaitHandle.Set();
+ });
+ _ = _sessionMock.Setup(p => p.TrySendMessage(It.IsAny()))
+ .Returns(true);
+ _ = _sessionMock.Setup(p => p.TryWait(It.IsAny(), _channelCloseTimeout))
+ .Callback((waitHandle, channelCloseTimeout) =>
+ {
+ _sessionMock.Raise(
+ s => s.ChannelCloseReceived += null,
+ new MessageEventArgs(new ChannelCloseMessage(_localChannelNumber)));
+ _ = waitHandle.WaitOne();
+ })
+ .Returns(WaitResult.Success);
+
+ _remoteListener = new TcpListener(IPAddress.Loopback, 0);
+ _remoteListener.Start();
+ var remoteEndpoint = (IPEndPoint)_remoteListener.LocalEndpoint;
+
+ _channel = new ChannelForwardedTcpip(_sessionMock.Object,
+ _localChannelNumber,
+ _localWindowSize,
+ _localPacketSize,
+ _remoteChannelNumber,
+ _remoteWindowSize,
+ _remotePacketSize);
+
+ _channelThread = new Thread(() =>
+ {
+ try
+ {
+ _channel.Bind(remoteEndpoint, _forwardedPortMock.Object);
+ }
+ catch (Exception ex)
+ {
+ _channelException = ex;
+ }
+ });
+ _channelThread.Start();
+
+ _remoteSocket = _remoteListener.AcceptSocket();
+ _remoteSocket.ReceiveTimeout = (int)TimeSpan.FromSeconds(5).TotalMilliseconds;
+ }
+
+ private void Act()
+ {
+ _sessionMock.Raise(s => s.ChannelEofReceived += null,
+ new MessageEventArgs(new ChannelEofMessage(_localChannelNumber)));
+
+ var buffer = new byte[16];
+ _bytesReceivedAfterEof = _remoteSocket.Receive(buffer);
+
+ // only the send direction of the socket should be shut down, so the connected host can still send data
+ _ = _remoteSocket.Send(_dataToSendAfterEof);
+ _ = _dataSentToServerWaitHandle.WaitOne(TimeSpan.FromSeconds(5));
+
+ _remoteSocket.Shutdown(SocketShutdown.Send);
+ }
+
+ [TestMethod]
+ public void ConnectedHostShouldReceiveEndOfStream()
+ {
+ Assert.AreEqual(0, _bytesReceivedAfterEof);
+ }
+
+ [TestMethod]
+ public void DataSentByConnectedHostAfterEofShouldBeForwardedToServer()
+ {
+ lock (_dataSentToServer)
+ {
+ CollectionAssert.AreEqual(_dataToSendAfterEof, _dataSentToServer);
+ }
+ }
+
+ [TestMethod]
+ public void BindShouldFinishWithoutExceptionWhenConnectedHostShutsDownSocket()
+ {
+ Assert.IsTrue(_channelThread.Join(TimeSpan.FromSeconds(5)));
+ Assert.IsNull(_channelException, _channelException?.ToString());
+ }
+ }
+}