Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SshTunnel] WIP #992

Merged
merged 9 commits into from
Jan 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1272,8 +1272,15 @@ Start `proxy.py` as:
--tunnel-username username \
--tunnel-hostname ip.address.or.domain.name \
--tunnel-port 22 \
--tunnel-remote-host 127.0.0.1
--tunnel-remote-port 8899
--tunnel-remote-port 8899 \
--tunnel-ssh-key /path/to/ssh/private.key \
--tunnel-ssh-key-passphrase XXXXX
...[redacted]... [I] listener.setup:97 - Listening on 127.0.0.1:8899
...[redacted]... [I] pool.setup:106 - Started 16 acceptors in threadless (local) mode
...[redacted]... [I] transport._log:1873 - Connected (version 2.0, client OpenSSH_7.6p1)
...[redacted]... [I] transport._log:1873 - Authentication (publickey) successful!
...[redacted]... [I] listener.setup:116 - SSH connection established to ip.address.or.domain.name:22...
...[redacted]... [I] listener.start_port_forward:91 - :8899 forwarding successful...
```

Make a HTTP proxy request on `remote` server and
Expand Down Expand Up @@ -1312,6 +1319,13 @@ access_log:328 - remote:52067 - GET httpbin.org:80
FIREWALL
(allow tcp/22)

Not planned.

If you have a valid use case, kindly open an issue. You are always welcome to send
contributions via pull-requests to add this functionality :)

> To proxy local requests remotely, make use of [Proxy Pool Plugin](#proxypoolplugin).

# Embed proxy.py

## Blocking Mode
Expand Down
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@
(_py_class_role, '_asyncio.Task'),
(_py_class_role, 'asyncio.events.AbstractEventLoop'),
(_py_class_role, 'CacheStore'),
(_py_class_role, 'Channel'),
(_py_class_role, 'HttpParser'),
(_py_class_role, 'HttpProtocolHandlerPlugin'),
(_py_class_role, 'HttpProxyBasePlugin'),
Expand Down
1 change: 1 addition & 0 deletions proxy/common/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ def _env_threadless_compliant() -> bool:
DEFAULT_DISABLE_HEADERS: List[bytes] = []
DEFAULT_DISABLE_HTTP_PROXY = False
DEFAULT_ENABLE_DASHBOARD = False
DEFAULT_ENABLE_SSH_TUNNEL = False
DEFAULT_ENABLE_DEVTOOLS = False
DEFAULT_ENABLE_EVENTS = False
DEFAULT_EVENTS_QUEUE = None
Expand Down
8 changes: 4 additions & 4 deletions proxy/core/ssh/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@

Submodules
"""
from .client import SshClient
from .tunnel import Tunnel
from .handler import SshHttpProtocolHandler
from .listener import SshTunnelListener

__all__ = [
'SshClient',
'Tunnel',
'SshHttpProtocolHandler',
'SshTunnelListener',
]
28 changes: 0 additions & 28 deletions proxy/core/ssh/client.py

This file was deleted.

34 changes: 34 additions & 0 deletions proxy/core/ssh/handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
"""
proxy.py
~~~~~~~~
⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on
Network monitoring, controls & Application development, testing, debugging.

:copyright: (c) 2013-present by Abhinav Singh and contributors.
:license: BSD, see LICENSE for more details.
"""
import argparse

from typing import TYPE_CHECKING, Tuple

if TYPE_CHECKING:
try:
from paramiko.channel import Channel
except ImportError:
pass


class SshHttpProtocolHandler:
"""Handles incoming connections over forwarded SSH transport."""

def __init__(self, flags: argparse.Namespace) -> None:
self.flags = flags

def on_connection(
self,
chan: 'Channel',
origin: Tuple[str, int],
server: Tuple[str, int],
) -> None:
pass
135 changes: 135 additions & 0 deletions proxy/core/ssh/listener.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# -*- coding: utf-8 -*-
"""
proxy.py
~~~~~~~~
⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on
Network monitoring, controls & Application development, testing, debugging.

:copyright: (c) 2013-present by Abhinav Singh and contributors.
:license: BSD, see LICENSE for more details.
"""
import argparse
import logging

from typing import TYPE_CHECKING, Any, Callable, Optional, Set, Tuple

try:
from paramiko import SSHClient, AutoAddPolicy
from paramiko.transport import Transport
if TYPE_CHECKING:
from paramiko.channel import Channel
except ImportError:
pass

from ...common.flag import flags

logger = logging.getLogger(__name__)


flags.add_argument(
'--tunnel-hostname',
type=str,
default=None,
help='Default: None. Remote hostname or IP address to which SSH tunnel will be established.',
)

flags.add_argument(
'--tunnel-port',
type=int,
default=22,
help='Default: 22. SSH port of the remote host.',
)

flags.add_argument(
'--tunnel-username',
type=str,
default=None,
help='Default: None. Username to use for establishing SSH tunnel.',
)

flags.add_argument(
'--tunnel-ssh-key',
type=str,
default=None,
help='Default: None. Private key path in pem format',
)

flags.add_argument(
'--tunnel-ssh-key-passphrase',
type=str,
default=None,
help='Default: None. Private key passphrase',
)

flags.add_argument(
'--tunnel-remote-port',
type=int,
default=8899,
help='Default: 8899. Remote port which will be forwarded locally for proxy.',
)


class SshTunnelListener:
"""Connects over SSH and forwards a remote port to local host.

Incoming connections are delegated to provided callback."""

def __init__(
self,
flags: argparse.Namespace,
on_connection_callback: Callable[['Channel', Tuple[str, int], Tuple[str, int]], None],
) -> None:
self.flags = flags
self.on_connection_callback = on_connection_callback
self.ssh: Optional[SSHClient] = None
self.transport: Optional[Transport] = None
self.forwarded: Set[Tuple[str, int]] = set()

def start_port_forward(self, remote_addr: Tuple[str, int]) -> None:
assert self.transport is not None
self.transport.request_port_forward(
*remote_addr,
handler=self.on_connection_callback,
)
self.forwarded.add(remote_addr)
logger.info('%s:%d forwarding successful...' % remote_addr)

def stop_port_forward(self, remote_addr: Tuple[str, int]) -> None:
assert self.transport is not None
self.transport.cancel_port_forward(*remote_addr)
self.forwarded.remove(remote_addr)

def __enter__(self) -> 'SshTunnelListener':
self.setup()
return self

def __exit__(self, *args: Any) -> None:
self.shutdown()

def setup(self) -> None:
self.ssh = SSHClient()
self.ssh.load_system_host_keys()
self.ssh.set_missing_host_key_policy(AutoAddPolicy())
self.ssh.connect(
hostname=self.flags.tunnel_hostname,
port=self.flags.tunnel_port,
username=self.flags.tunnel_username,
key_filename=self.flags.tunnel_ssh_key,
passphrase=self.flags.tunnel_ssh_key_passphrase,
)
logger.info(
'SSH connection established to %s:%d...' % (
self.flags.tunnel_hostname,
self.flags.tunnel_port,
),
)
self.transport = self.ssh.get_transport()

def shutdown(self) -> None:
for remote_addr in list(self.forwarded):
self.stop_port_forward(remote_addr)
self.forwarded.clear()
if self.transport is not None:
self.transport.close()
if self.ssh is not None:
self.ssh.close()
70 changes: 0 additions & 70 deletions proxy/core/ssh/tunnel.py

This file was deleted.

7 changes: 5 additions & 2 deletions proxy/core/work/threadless.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from ...common.constants import DEFAULT_INACTIVE_CONN_CLEANUP_TIMEOUT, DEFAULT_SELECTOR_SELECT_TIMEOUT
from ...common.constants import DEFAULT_WAIT_FOR_TASKS_TIMEOUT

from ..connection import TcpClientConnection, UpstreamConnectionPool
from ..connection import TcpClientConnection
from ..event import eventNames

if TYPE_CHECKING: # pragma: no cover
Expand Down Expand Up @@ -91,7 +91,10 @@ def __init__(
self.wait_timeout: float = DEFAULT_WAIT_FOR_TASKS_TIMEOUT
self.cleanup_inactive_timeout: float = DEFAULT_INACTIVE_CONN_CLEANUP_TIMEOUT
self._total: int = 0
self._upstream_conn_pool: Optional[UpstreamConnectionPool] = None
# When put at the top, causes circular import error
# since integrated ssh tunnel was introduced.
from ..connection import UpstreamConnectionPool # pylint: disable=C0415
self._upstream_conn_pool: Optional['UpstreamConnectionPool'] = None
self._upstream_conn_filenos: Set[int] = set()
if self.flags.enable_conn_pool:
self._upstream_conn_pool = UpstreamConnectionPool()
Expand Down
Loading