Skip to content

Commit d5aef6c

Browse files
committed
Add forwarding between TCP ports and UNIX domain sockets
This commit adds new methods to forward local and remote TCP ports to UNIX domain paths and vice-versa over an SSH connection. Thanks go to Alex Rogozhnikov for suggesting this use case. The new SSHClientConnection methods to support this are named forward_local_port_to_path, forward_local_path_to_port, forward_remote_port_to_path, and forward_remote_path_to_port.
1 parent b391005 commit d5aef6c

File tree

3 files changed

+332
-22
lines changed

3 files changed

+332
-22
lines changed

asyncssh/connection.py

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3043,6 +3043,12 @@ class SSHClientConnection(SSHConnection):
30433043
UNIX domain socket forwarding can be set up by calling
30443044
:meth:`forward_local_path` or :meth:`forward_remote_path`.
30453045
3046+
Mixed forwarding from a TCP port to a UNIX domain socket or
3047+
vice-versa can be set up by calling :meth:`forward_local_port_to_path`,
3048+
:meth:`forward_local_path_to_port`,
3049+
:meth:`forward_remote_port_to_path`, or
3050+
:meth:`forward_remote_path_to_port`.
3051+
30463052
"""
30473053

30483054
_options: 'SSHClientConnectionOptions'
@@ -4646,6 +4652,113 @@ async def listen_reverse_ssh(self, host: str = '',
46464652
return await listen_reverse(host, port, tunnel=self,
46474653
**kwargs) # type: ignore
46484654

4655+
@async_context_manager
4656+
async def forward_local_port_to_path(self, listen_host: str,
4657+
listen_port: int,
4658+
dest_path: str) -> SSHListener:
4659+
"""Set up local TCP port forwarding to a remote UNIX domain socket
4660+
4661+
This method is a coroutine which attempts to set up port
4662+
forwarding from a local TCP listening port to a remote UNIX
4663+
domain path via the SSH connection. If the request is successful,
4664+
the return value is an :class:`SSHListener` object which can be
4665+
used later to shut down the port forwarding.
4666+
4667+
:param listen_host:
4668+
The hostname or address on the local host to listen on
4669+
:param listen_port:
4670+
The port number on the local host to listen on
4671+
:param dest_path:
4672+
The path on the remote host to forward the connections to
4673+
:type listen_host: `str`
4674+
:type listen_port: `int`
4675+
:type dest_path: `str`
4676+
4677+
:returns: :class:`SSHListener`
4678+
4679+
:raises: :exc:`OSError` if the listener can't be opened
4680+
4681+
"""
4682+
4683+
async def tunnel_connection(
4684+
session_factory: SSHUNIXSessionFactory[bytes],
4685+
_orig_host: str, _orig_port: int) -> \
4686+
Tuple[SSHUNIXChannel[bytes], SSHUNIXSession[bytes]]:
4687+
"""Forward a local connection over SSH"""
4688+
4689+
return (await self.create_unix_connection(session_factory,
4690+
dest_path))
4691+
4692+
self.logger.info('Creating local TCP forwarder from %s to %s',
4693+
(listen_host, listen_port), dest_path)
4694+
4695+
try:
4696+
listener = await create_tcp_forward_listener(self, self._loop,
4697+
tunnel_connection,
4698+
listen_host,
4699+
listen_port)
4700+
except OSError as exc:
4701+
self.logger.debug1('Failed to create local TCP listener: %s', exc)
4702+
raise
4703+
4704+
if listen_port == 0:
4705+
listen_port = listener.get_port()
4706+
4707+
self._local_listeners[listen_host, listen_port] = listener
4708+
4709+
return listener
4710+
4711+
@async_context_manager
4712+
async def forward_local_path_to_port(self, listen_path: str,
4713+
dest_host: str,
4714+
dest_port: int) -> SSHListener:
4715+
"""Set up local UNIX domain socket forwarding to a remote TCP port
4716+
4717+
This method is a coroutine which attempts to set up UNIX domain
4718+
socket forwarding from a local listening path to a remote host
4719+
and port via the SSH connection. If the request is successful,
4720+
the return value is an :class:`SSHListener` object which can
4721+
be used later to shut down the UNIX domain socket forwarding.
4722+
4723+
:param listen_path:
4724+
The path on the local host to listen on
4725+
:param dest_host:
4726+
The hostname or address to forward the connections to
4727+
:param dest_port:
4728+
The port number to forward the connections to
4729+
:type listen_path: `str`
4730+
:type dest_host: `str`
4731+
:type dest_port: `int`
4732+
4733+
:returns: :class:`SSHListener`
4734+
4735+
:raises: :exc:`OSError` if the listener can't be opened
4736+
4737+
"""
4738+
4739+
async def tunnel_connection(
4740+
session_factory: SSHTCPSessionFactory[bytes]) -> \
4741+
Tuple[SSHTCPChannel[bytes], SSHTCPSession[bytes]]:
4742+
"""Forward a local connection over SSH"""
4743+
4744+
return await self.create_connection(session_factory, dest_host,
4745+
dest_port, '', 0)
4746+
4747+
self.logger.info('Creating local UNIX forwarder from %s to %s',
4748+
listen_path, (dest_host, dest_port))
4749+
4750+
try:
4751+
listener = await create_unix_forward_listener(self, self._loop,
4752+
tunnel_connection,
4753+
listen_path)
4754+
except OSError as exc:
4755+
self.logger.debug1('Failed to create local UNIX listener: %s', exc)
4756+
raise
4757+
4758+
self._local_listeners[listen_path] = listener
4759+
4760+
return listener
4761+
46494762
@async_context_manager
46504763
async def forward_remote_port(self, listen_host: str,
46514764
listen_port: int, dest_host: str,
@@ -4727,6 +4840,88 @@ def session_factory() -> Awaitable[SSHUNIXSession[bytes]]:
47274840

47284841
return await self.create_unix_server(session_factory, listen_path)
47294842

4843+
@async_context_manager
4844+
async def forward_remote_port_to_path(self, listen_host: str,
4845+
listen_port: int,
4846+
dest_path: str) -> SSHListener:
4847+
"""Set up remote TCP port forwarding to a local UNIX domain socket
4848+
4849+
This method is a coroutine which attempts to set up port
4850+
forwarding from a remote TCP listening port to a local UNIX
4851+
domain socket path via the SSH connection. If the request is
4852+
successful, the return value is an :class:`SSHListener` object
4853+
which can be used later to shut down the port forwarding. If
4854+
the request fails, `None` is returned.
4855+
4856+
:param listen_host:
4857+
The hostname or address on the remote host to listen on
4858+
:param listen_port:
4859+
The port number on the remote host to listen on
4860+
:param dest_path:
4861+
The path on the local host to forward connections to
4862+
:type listen_host: `str`
4863+
:type listen_port: `int`
4864+
:type dest_path: `str`
4865+
4866+
:returns: :class:`SSHListener`
4867+
4868+
:raises: :class:`ChannelListenError` if the listener can't be opened
4869+
4870+
"""
4871+
4872+
def session_factory(_orig_host: str,
4873+
_orig_port: int) -> Awaitable[SSHUNIXSession]:
4874+
"""Return an SSHTCPSession used to do remote port forwarding"""
4875+
4876+
return cast(Awaitable[SSHUNIXSession],
4877+
self.forward_unix_connection(dest_path))
4878+
4879+
self.logger.info('Creating remote TCP forwarder from %s to %s',
4880+
(listen_host, listen_port), dest_path)
4881+
4882+
return await self.create_server(session_factory, listen_host,
4883+
listen_port)
4884+
4885+
@async_context_manager
4886+
async def forward_remote_path_to_port(self, listen_path: str,
4887+
dest_host: str,
4888+
dest_port: int) -> SSHListener:
4889+
"""Set up remote UNIX domain socket forwarding to a local TCP port
4890+
4891+
This method is a coroutine which attempts to set up UNIX domain
4892+
socket forwarding from a remote listening path to a local TCP
4893+
host and port via the SSH connection. If the request is
4894+
successful, the return value is an :class:`SSHListener` object
4895+
which can be used later to shut down the port forwarding. If
4896+
the request fails, `None` is returned.
4897+
4898+
:param listen_path:
4899+
The path on the remote host to listen on
4900+
:param dest_host:
4901+
The hostname or address to forward connections to
4902+
:param dest_port:
4903+
The port number to forward connections to
4904+
:type listen_path: `str`
4905+
:type dest_host: `str`
4906+
:type dest_port: `int`
4907+
4908+
:returns: :class:`SSHListener`
4909+
4910+
:raises: :class:`ChannelListenError` if the listener can't be opened
4911+
4912+
"""
4913+
4914+
def session_factory() -> Awaitable[SSHTCPSession[bytes]]:
4915+
"""Return an SSHUNIXSession used to do remote path forwarding"""
4916+
4917+
return cast(Awaitable[SSHTCPSession[bytes]],
4918+
self.forward_connection(dest_host, dest_port))
4919+
4920+
self.logger.info('Creating remote UNIX forwarder from %s to %s',
4921+
listen_path, (dest_host, dest_port))
4922+
4923+
return await self.create_unix_server(session_factory, listen_path)
4924+
47304925
@async_context_manager
47314926
async def forward_socks(self, listen_host: str,
47324927
listen_port: int) -> SSHListener:

docs/api.rst

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,18 @@ The client can also set up TCP port forwarding by calling
7575
:meth:`forward_remote_port() <SSHClientConnection.forward_remote_port>` and
7676
UNIX domain socket forwarding by calling :meth:`forward_local_path()
7777
<SSHClientConnection.forward_local_path>` or :meth:`forward_remote_path()
78-
<SSHClientConnection.forward_remote_path>`. In these cases, data transfer on
78+
<SSHClientConnection.forward_remote_path>`. Mixed forwarding from a TCP port
79+
to a UNIX domain socket or vice-versa can be set up using the functions
80+
:meth:`forward_local_port_to_path()
81+
<SSHClientConnection.forward_local_port_to_path>`,
82+
:meth:`forward_local_path_to_port()
83+
<SSHClientConnection.forward_local_path_to_port>`,
84+
:meth:`forward_remote_port_to_path()
85+
<SSHClientConnection.forward_remote_port_to_path>`, and
86+
:meth:`forward_remote_path_to_port()
87+
<SSHClientConnection.forward_remote_path_to_port>`.
88+
89+
In these cases, data transfer on
7990
the channels is managed automatically by AsyncSSH whenever new connections
8091
are opened, so custom session objects are not required.
8192

@@ -354,16 +365,20 @@ SSHClientConnection
354365
.. automethod:: start_unix_server
355366
====================================== =
356367

357-
=================================== =
368+
=========================================== =
358369
Client forwarding methods
359-
=================================== =
370+
=========================================== =
360371
.. automethod:: forward_connection
361372
.. automethod:: forward_local_port
362373
.. automethod:: forward_local_path
374+
.. automethod:: forward_local_port_to_path
375+
.. automethod:: forward_local_path_to_port
363376
.. automethod:: forward_remote_port
364377
.. automethod:: forward_remote_path
378+
.. automethod:: forward_remote_port_to_path
379+
.. automethod:: forward_remote_path_to_port
365380
.. automethod:: forward_socks
366-
=================================== =
381+
=========================================== =
367382

368383
=========================== =
369384
Connection close methods

0 commit comments

Comments
 (0)