Skip to content

Commit 865edf2

Browse files
committed
ENH: Implement Jump Host connection
Allow specification of jumphosts. Sessions opened using this configuration will connect to the remote host through the specified jump servers.
1 parent bc99ada commit 865edf2

File tree

4 files changed

+243
-3
lines changed

4 files changed

+243
-3
lines changed

src/Renci.SshNet/Channels/ChannelDirectTcpip.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ public void Open(string remoteHost, uint port, IForwardedPort forwardedPort, Soc
5252

5353
_socket = socket;
5454
_forwardedPort = forwardedPort;
55-
_forwardedPort.Closing += ForwardedPort_Closing;
55+
if (_forwardedPort != null)
56+
_forwardedPort.Closing += ForwardedPort_Closing;
5657

5758
var ep = (IPEndPoint) socket.RemoteEndPoint;
5859

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
using System;
2+
using System.Net;
3+
using System.Net.Sockets;
4+
using System.Threading;
5+
using Renci.SshNet.Abstractions;
6+
using Renci.SshNet.Common;
7+
using Renci.SshNet.Messages.Connection;
8+
9+
namespace Renci.SshNet.Channels
10+
{
11+
/// <summary>
12+
/// Implements "direct-tcpip" SSH channel.
13+
/// </summary>
14+
internal class JumpChannel
15+
{
16+
private Socket listener;
17+
private ISession _session;
18+
private EventWaitHandle _channelOpen = new AutoResetEvent(false);
19+
20+
/// <summary>
21+
/// Gets the bound host.
22+
/// </summary>
23+
public string BoundHost { get; private set; }
24+
25+
/// <summary>
26+
/// Gets the bound port.
27+
/// </summary>
28+
public uint BoundPort { get; private set; }
29+
30+
/// <summary>
31+
/// Gets the forwarded host.
32+
/// </summary>
33+
public string Host { get; private set; }
34+
35+
/// <summary>
36+
/// Gets the forwarded port.
37+
/// </summary>
38+
public uint Port { get; private set; }
39+
40+
/// <summary>
41+
/// Gets a value indicating whether port forwarding is started.
42+
/// </summary>
43+
/// <value>
44+
/// <c>true</c> if port forwarding is started; otherwise, <c>false</c>.
45+
/// </value>
46+
public bool IsStarted
47+
{ get; private set; }
48+
49+
/// <summary>
50+
/// Initializes a new instance of the <see cref="ForwardedPortLocal"/> class.
51+
/// </summary>
52+
/// <param name="session"></param>
53+
/// <param name="host">The host.</param>
54+
/// <param name="port">The port.</param>
55+
/// <exception cref="ArgumentNullException"><paramref name="host"/> is <c>null</c>.</exception>
56+
/// <exception cref="ArgumentOutOfRangeException"><paramref name="port" /> is greater than <see cref="F:System.Net.IPEndPoint.MaxPort" />.</exception>
57+
/// <example>
58+
/// <code source="..\..\src\Renci.SshNet.Tests\Classes\ForwardedPortLocalTest.cs" region="Example SshClient AddForwardedPort Start Stop ForwardedPortLocal" language="C#" title="Local port forwarding" />
59+
/// </example>
60+
public JumpChannel(ISession session, string host, uint port)
61+
{
62+
if (host == null)
63+
throw new ArgumentNullException("host");
64+
65+
port.ValidatePort("port");
66+
67+
Host = host;
68+
Port = port;
69+
70+
_session = session;
71+
}
72+
73+
public Socket Connect()
74+
{
75+
var ep = new IPEndPoint(IPAddress.Loopback, 0);
76+
listener = new Socket(ep.AddressFamily, SocketType.Stream, ProtocolType.Tcp) { NoDelay = true };
77+
listener.Bind(ep);
78+
listener.Listen(1);
79+
80+
IsStarted = true;
81+
82+
// update bound port (in case original was passed as zero)
83+
ep.Port = ((IPEndPoint)listener.LocalEndPoint).Port;
84+
85+
var e = new SocketAsyncEventArgs();
86+
e.Completed += AcceptCompleted;
87+
88+
// only accept new connections while we are started
89+
if (!listener.AcceptAsync(e))
90+
{
91+
AcceptCompleted(null, e);
92+
}
93+
94+
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
95+
socket.Connect(ep);
96+
97+
// Wait for channel to open
98+
_session.WaitOnHandle(_channelOpen);
99+
listener.Dispose();
100+
listener = null;
101+
102+
return socket;
103+
}
104+
105+
#region IDisposable Members
106+
107+
private bool _isDisposed;
108+
109+
/// <summary>
110+
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
111+
/// </summary>
112+
public void Dispose()
113+
{
114+
Dispose(true);
115+
GC.SuppressFinalize(this);
116+
}
117+
118+
/// <summary>
119+
/// Releases unmanaged and - optionally - managed resources
120+
/// </summary>
121+
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
122+
protected void Dispose(bool disposing)
123+
{
124+
if (_isDisposed)
125+
return;
126+
127+
_isDisposed = true;
128+
}
129+
130+
/// <summary>
131+
/// Releases unmanaged resources and performs other cleanup operations before the
132+
/// <see cref="ForwardedPortLocal"/> is reclaimed by garbage collection.
133+
/// </summary>
134+
~JumpChannel()
135+
{
136+
Dispose(false);
137+
}
138+
139+
#endregion
140+
141+
142+
private void AcceptCompleted(object sender, SocketAsyncEventArgs e)
143+
{
144+
if (e.SocketError == SocketError.OperationAborted || e.SocketError == SocketError.NotSocket)
145+
{
146+
// server was stopped
147+
return;
148+
}
149+
150+
// capture client socket
151+
var clientSocket = e.AcceptSocket;
152+
153+
if (e.SocketError != SocketError.Success)
154+
{
155+
// dispose broken client socket
156+
CloseClientSocket(clientSocket);
157+
return;
158+
}
159+
160+
_channelOpen.Set();
161+
162+
// process connection
163+
ProcessAccept(clientSocket);
164+
}
165+
166+
private void ProcessAccept(Socket clientSocket)
167+
{
168+
// close the client socket if we're no longer accepting new connections
169+
if (!IsStarted)
170+
{
171+
CloseClientSocket(clientSocket);
172+
return;
173+
}
174+
175+
try
176+
{
177+
var originatorEndPoint = (IPEndPoint)clientSocket.RemoteEndPoint;
178+
179+
using (var channel = _session.CreateChannelDirectTcpip())
180+
{
181+
channel.Open(Host, Port, null, clientSocket);
182+
channel.Bind();
183+
}
184+
}
185+
catch
186+
{
187+
CloseClientSocket(clientSocket);
188+
}
189+
}
190+
191+
private static void CloseClientSocket(Socket clientSocket)
192+
{
193+
if (clientSocket.Connected)
194+
{
195+
try
196+
{
197+
clientSocket.Shutdown(SocketShutdown.Send);
198+
}
199+
catch (Exception)
200+
{
201+
// ignore exception when client socket was already closed
202+
}
203+
}
204+
205+
clientSocket.Dispose();
206+
}
207+
}
208+
}

src/Renci.SshNet/ConnectionInfo.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,11 @@ public class ConnectionInfo : IConnectionInfoInternal
106106
/// </summary>
107107
public string Username { get; private set; }
108108

109+
/// <summary>
110+
/// Connection info used for connecting through a jump host.
111+
/// </summary>
112+
public ConnectionInfo JumpHost { get; set; }
113+
109114
/// <summary>
110115
/// Gets proxy type.
111116
/// </summary>

src/Renci.SshNet/Session.cs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,9 @@ public SemaphoreLight SessionSemaphore
229229

230230
private uint _nextChannelNumber;
231231

232+
private ISession _jumpSession;
233+
private JumpChannel _jumpChannel;
234+
232235
/// <summary>
233236
/// Gets the next channel number.
234237
/// </summary>
@@ -341,6 +344,7 @@ public Message ClientInitMessage
341344
/// <value>The client version.</value>
342345
public string ClientVersion { get; private set; }
343346

347+
344348
/// <summary>
345349
/// Gets or sets the connection info.
346350
/// </summary>
@@ -554,6 +558,9 @@ internal Session(ConnectionInfo connectionInfo, IServiceFactory serviceFactory,
554558
_serviceFactory = serviceFactory;
555559
_socketFactory = socketFactory;
556560
_messageListenerCompleted = new ManualResetEvent(true);
561+
562+
if (connectionInfo.JumpHost != null)
563+
_jumpSession = new Session(connectionInfo.JumpHost, _serviceFactory, _socketFactory);
557564
}
558565

559566
/// <summary>
@@ -587,8 +594,17 @@ public void Connect()
587594
// Build list of available messages while connecting
588595
_sshMessageFactory = new SshMessageFactory();
589596

590-
_socket = _serviceFactory.CreateConnector(ConnectionInfo, _socketFactory)
591-
.Connect(ConnectionInfo);
597+
if (_jumpSession != null)
598+
{
599+
_jumpSession.Connect();
600+
_jumpChannel = new JumpChannel(_jumpSession, ConnectionInfo.Host, (uint)ConnectionInfo.Port);
601+
_socket = _jumpChannel.Connect();
602+
}
603+
else
604+
{
605+
_socket = _serviceFactory.CreateConnector(ConnectionInfo, _socketFactory)
606+
.Connect(ConnectionInfo);
607+
}
592608

593609
var serverIdentification = _serviceFactory.CreateProtocolVersionExchange()
594610
.Start(ClientVersion, _socket, ConnectionInfo.Timeout);
@@ -2148,6 +2164,11 @@ protected virtual void Dispose(bool disposing)
21482164
_messageListenerCompleted = null;
21492165
}
21502166

2167+
if (_jumpChannel != null)
2168+
_jumpChannel.Dispose();
2169+
if (_jumpSession != null)
2170+
_jumpSession.Dispose();
2171+
21512172
_disposed = true;
21522173
}
21532174
}
@@ -2220,6 +2241,11 @@ IChannelForwardedTcpip ISession.CreateChannelForwardedTcpip(uint remoteChannelNu
22202241
remoteChannelDataPacketSize);
22212242
}
22222243

2244+
JumpChannel CreateJumpChannel(string host, uint port)
2245+
{
2246+
return new JumpChannel(this, host, port);
2247+
}
2248+
22232249
/// <summary>
22242250
/// Sends a message to the server.
22252251
/// </summary>

0 commit comments

Comments
 (0)