Skip to content

Commit d17dc9e

Browse files
authored
[SshTunnel] WIP (#992)
[SshTunnel] WIP
1 parent 4222172 commit d17dc9e

File tree

11 files changed

+231
-108
lines changed

11 files changed

+231
-108
lines changed

README.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1272,8 +1272,15 @@ Start `proxy.py` as:
12721272
--tunnel-username username \
12731273
--tunnel-hostname ip.address.or.domain.name \
12741274
--tunnel-port 22 \
1275-
--tunnel-remote-host 127.0.0.1
1276-
--tunnel-remote-port 8899
1275+
--tunnel-remote-port 8899 \
1276+
--tunnel-ssh-key /path/to/ssh/private.key \
1277+
--tunnel-ssh-key-passphrase XXXXX
1278+
...[redacted]... [I] listener.setup:97 - Listening on 127.0.0.1:8899
1279+
...[redacted]... [I] pool.setup:106 - Started 16 acceptors in threadless (local) mode
1280+
...[redacted]... [I] transport._log:1873 - Connected (version 2.0, client OpenSSH_7.6p1)
1281+
...[redacted]... [I] transport._log:1873 - Authentication (publickey) successful!
1282+
...[redacted]... [I] listener.setup:116 - SSH connection established to ip.address.or.domain.name:22...
1283+
...[redacted]... [I] listener.start_port_forward:91 - :8899 forwarding successful...
12771284
```
12781285

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

1322+
Not planned.
1323+
1324+
If you have a valid use case, kindly open an issue. You are always welcome to send
1325+
contributions via pull-requests to add this functionality :)
1326+
1327+
> To proxy local requests remotely, make use of [Proxy Pool Plugin](#proxypoolplugin).
1328+
13151329
# Embed proxy.py
13161330

13171331
## Blocking Mode

docs/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@
284284
(_py_class_role, '_asyncio.Task'),
285285
(_py_class_role, 'asyncio.events.AbstractEventLoop'),
286286
(_py_class_role, 'CacheStore'),
287+
(_py_class_role, 'Channel'),
287288
(_py_class_role, 'HttpParser'),
288289
(_py_class_role, 'HttpProtocolHandlerPlugin'),
289290
(_py_class_role, 'HttpProxyBasePlugin'),

proxy/common/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ def _env_threadless_compliant() -> bool:
8888
DEFAULT_DISABLE_HEADERS: List[bytes] = []
8989
DEFAULT_DISABLE_HTTP_PROXY = False
9090
DEFAULT_ENABLE_DASHBOARD = False
91+
DEFAULT_ENABLE_SSH_TUNNEL = False
9192
DEFAULT_ENABLE_DEVTOOLS = False
9293
DEFAULT_ENABLE_EVENTS = False
9394
DEFAULT_EVENTS_QUEUE = None

proxy/core/ssh/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@
1212
1313
Submodules
1414
"""
15-
from .client import SshClient
16-
from .tunnel import Tunnel
15+
from .handler import SshHttpProtocolHandler
16+
from .listener import SshTunnelListener
1717

1818
__all__ = [
19-
'SshClient',
20-
'Tunnel',
19+
'SshHttpProtocolHandler',
20+
'SshTunnelListener',
2121
]

proxy/core/ssh/client.py

Lines changed: 0 additions & 28 deletions
This file was deleted.

proxy/core/ssh/handler.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
proxy.py
4+
~~~~~~~~
5+
⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on
6+
Network monitoring, controls & Application development, testing, debugging.
7+
8+
:copyright: (c) 2013-present by Abhinav Singh and contributors.
9+
:license: BSD, see LICENSE for more details.
10+
"""
11+
import argparse
12+
13+
from typing import TYPE_CHECKING, Tuple
14+
15+
if TYPE_CHECKING:
16+
try:
17+
from paramiko.channel import Channel
18+
except ImportError:
19+
pass
20+
21+
22+
class SshHttpProtocolHandler:
23+
"""Handles incoming connections over forwarded SSH transport."""
24+
25+
def __init__(self, flags: argparse.Namespace) -> None:
26+
self.flags = flags
27+
28+
def on_connection(
29+
self,
30+
chan: 'Channel',
31+
origin: Tuple[str, int],
32+
server: Tuple[str, int],
33+
) -> None:
34+
pass

proxy/core/ssh/listener.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
proxy.py
4+
~~~~~~~~
5+
⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on
6+
Network monitoring, controls & Application development, testing, debugging.
7+
8+
:copyright: (c) 2013-present by Abhinav Singh and contributors.
9+
:license: BSD, see LICENSE for more details.
10+
"""
11+
import argparse
12+
import logging
13+
14+
from typing import TYPE_CHECKING, Any, Callable, Optional, Set, Tuple
15+
16+
try:
17+
from paramiko import SSHClient, AutoAddPolicy
18+
from paramiko.transport import Transport
19+
if TYPE_CHECKING:
20+
from paramiko.channel import Channel
21+
except ImportError:
22+
pass
23+
24+
from ...common.flag import flags
25+
26+
logger = logging.getLogger(__name__)
27+
28+
29+
flags.add_argument(
30+
'--tunnel-hostname',
31+
type=str,
32+
default=None,
33+
help='Default: None. Remote hostname or IP address to which SSH tunnel will be established.',
34+
)
35+
36+
flags.add_argument(
37+
'--tunnel-port',
38+
type=int,
39+
default=22,
40+
help='Default: 22. SSH port of the remote host.',
41+
)
42+
43+
flags.add_argument(
44+
'--tunnel-username',
45+
type=str,
46+
default=None,
47+
help='Default: None. Username to use for establishing SSH tunnel.',
48+
)
49+
50+
flags.add_argument(
51+
'--tunnel-ssh-key',
52+
type=str,
53+
default=None,
54+
help='Default: None. Private key path in pem format',
55+
)
56+
57+
flags.add_argument(
58+
'--tunnel-ssh-key-passphrase',
59+
type=str,
60+
default=None,
61+
help='Default: None. Private key passphrase',
62+
)
63+
64+
flags.add_argument(
65+
'--tunnel-remote-port',
66+
type=int,
67+
default=8899,
68+
help='Default: 8899. Remote port which will be forwarded locally for proxy.',
69+
)
70+
71+
72+
class SshTunnelListener:
73+
"""Connects over SSH and forwards a remote port to local host.
74+
75+
Incoming connections are delegated to provided callback."""
76+
77+
def __init__(
78+
self,
79+
flags: argparse.Namespace,
80+
on_connection_callback: Callable[['Channel', Tuple[str, int], Tuple[str, int]], None],
81+
) -> None:
82+
self.flags = flags
83+
self.on_connection_callback = on_connection_callback
84+
self.ssh: Optional[SSHClient] = None
85+
self.transport: Optional[Transport] = None
86+
self.forwarded: Set[Tuple[str, int]] = set()
87+
88+
def start_port_forward(self, remote_addr: Tuple[str, int]) -> None:
89+
assert self.transport is not None
90+
self.transport.request_port_forward(
91+
*remote_addr,
92+
handler=self.on_connection_callback,
93+
)
94+
self.forwarded.add(remote_addr)
95+
logger.info('%s:%d forwarding successful...' % remote_addr)
96+
97+
def stop_port_forward(self, remote_addr: Tuple[str, int]) -> None:
98+
assert self.transport is not None
99+
self.transport.cancel_port_forward(*remote_addr)
100+
self.forwarded.remove(remote_addr)
101+
102+
def __enter__(self) -> 'SshTunnelListener':
103+
self.setup()
104+
return self
105+
106+
def __exit__(self, *args: Any) -> None:
107+
self.shutdown()
108+
109+
def setup(self) -> None:
110+
self.ssh = SSHClient()
111+
self.ssh.load_system_host_keys()
112+
self.ssh.set_missing_host_key_policy(AutoAddPolicy())
113+
self.ssh.connect(
114+
hostname=self.flags.tunnel_hostname,
115+
port=self.flags.tunnel_port,
116+
username=self.flags.tunnel_username,
117+
key_filename=self.flags.tunnel_ssh_key,
118+
passphrase=self.flags.tunnel_ssh_key_passphrase,
119+
)
120+
logger.info(
121+
'SSH connection established to %s:%d...' % (
122+
self.flags.tunnel_hostname,
123+
self.flags.tunnel_port,
124+
),
125+
)
126+
self.transport = self.ssh.get_transport()
127+
128+
def shutdown(self) -> None:
129+
for remote_addr in list(self.forwarded):
130+
self.stop_port_forward(remote_addr)
131+
self.forwarded.clear()
132+
if self.transport is not None:
133+
self.transport.close()
134+
if self.ssh is not None:
135+
self.ssh.close()

proxy/core/ssh/tunnel.py

Lines changed: 0 additions & 70 deletions
This file was deleted.

proxy/core/work/threadless.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from ...common.constants import DEFAULT_INACTIVE_CONN_CLEANUP_TIMEOUT, DEFAULT_SELECTOR_SELECT_TIMEOUT
2626
from ...common.constants import DEFAULT_WAIT_FOR_TASKS_TIMEOUT
2727

28-
from ..connection import TcpClientConnection, UpstreamConnectionPool
28+
from ..connection import TcpClientConnection
2929
from ..event import eventNames
3030

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

0 commit comments

Comments
 (0)