Skip to content

Commit 7134d68

Browse files
committed
add ssh tunnel feature
1 parent bf54e96 commit 7134d68

File tree

4 files changed

+57
-7
lines changed

4 files changed

+57
-7
lines changed

README.rst

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ python-proxy
1212
.. |Downloads| image:: https://pepy.tech/badge/pproxy
1313
:target: https://pepy.tech/project/pproxy
1414

15-
HTTP/Socks4/Socks5/Shadowsocks/ShadowsocksR/Redirect/Pf TCP/UDP asynchronous tunnel proxy implemented in Python3 asyncio.
15+
HTTP/Socks4/Socks5/Shadowsocks/ShadowsocksR/SSH/Redirect/Pf TCP/UDP asynchronous tunnel proxy implemented in Python3 asyncio.
1616

1717
QuickStart
1818
----------
@@ -112,6 +112,8 @@ Protocols
112112
+-------------------+------------+------------+------------+------------+--------------+
113113
| shadowsocksR ||| | | ssr:// |
114114
+-------------------+------------+------------+------------+------------+--------------+
115+
| ssh tunnel | || | | ssh:// |
116+
+-------------------+------------+------------+------------+------------+--------------+
115117
| iptables nat || | | | redir:// |
116118
+-------------------+------------+------------+------------+------------+--------------+
117119
| pfctl nat (macos) || | | | pf:// |
@@ -149,6 +151,8 @@ Requirement
149151

150152
pycryptodome_ is an optional library to enable faster (C version) cipher. **pproxy** has many built-in pure python ciphers. They are lightweight and stable, but slower than C ciphers. After speedup with PyPy_, pure python ciphers can get similar performance as C version. If the performance is important and don't have PyPy_, install pycryptodome_ instead.
151153

154+
asyncssh_ is an optional library to enable ssh tunnel client support.
155+
152156
These are some performance benchmarks between Python and C ciphers (dataset: 8M):
153157

154158
+---------------------+----------------+
@@ -167,6 +171,7 @@ PyPy3 Quickstart:
167171
$ pypy3 -m pip install asyncio pproxy
168172
169173
.. _pycryptodome: https://pycryptodome.readthedocs.io/en/latest/src/introduction.html
174+
.. _asyncssh: https://asyncssh.readthedocs.io/en/latest/
170175
.. _PyPy: http://pypy.org
171176

172177
Usage
@@ -225,6 +230,8 @@ URI Syntax
225230
+----------+-----------------------------+
226231
| ssr | shadowsocksr (SSR) protocol |
227232
+----------+-----------------------------+
233+
| ssh | ssh client tunnel |
234+
+----------+-----------------------------+
228235
| redir | redirect (iptables nat) |
229236
+----------+-----------------------------+
230237
| pf | pfctl (macos pf nat) |
@@ -644,7 +651,24 @@ Examples
644651
645652
$ pproxy -l http+in://client_ip:8081
646653
647-
Server connects to client_ip:8081 and waits for client proxy requests. The protocol http specified is just an example. It can be any protocol and cipher **pproxy** supports. The scheme **in** should exist in URI to inform **pproxy** that it is a backward proxy.
654+
Server connects to client_ip:8081 and waits for client proxy requests. The protocol http specified is just an example. It can be any protocol and cipher **pproxy** supports. The scheme "**in**" should exist in URI to inform **pproxy** that it is a backward proxy.
655+
656+
- SSH client tunnel
657+
658+
SSH client tunnel support is enabled by installing additional library asyncssh_. After "pip3 install asyncssh", you can specify "**ssh**" as scheme to proxy via ssh client tunnel.
659+
660+
.. code:: rst
661+
662+
$ pproxy -l http://:8080 -r ssh://remote_server.com/#login:password
663+
664+
If a client private key is used to authenticate, put double colon "::" between login and private key path.
665+
666+
.. code:: rst
667+
668+
$ pproxy -l http://:8080 -r ssh://remote_server.com/#login::private_key_path
669+
670+
SSH connection known_hosts feature is disabled by default.
671+
648672

649673
Projects
650674
--------

pproxy/__doc__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
__title__ = "pproxy"
2-
__version__ = "2.0.9"
2+
__version__ = "2.1.0"
33
__license__ = "MIT"
44
__description__ = "Proxy server that can tunnel among remote servers by regex rules."
55
__keywords__ = "proxy socks http shadowsocks shadowsocksr ssr redirect pf tunnel cipher ssl udp"

pproxy/proto.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,10 @@ def write(data, o=writer_remote.write):
308308
return o(data)
309309
writer_remote.write = write
310310

311+
class SSH(BaseProtocol):
312+
async def connect(self, reader_remote, writer_remote, rauth, host_name, port, myhost, **kw):
313+
pass
314+
311315
class Transparent(BaseProtocol):
312316
def correct_header(self, header, auth, sock, **kw):
313317
remote = self.query_remote(sock)
@@ -558,7 +562,7 @@ def udp_parse(protos, data, **kw):
558562
return (proto,) + ret
559563
raise Exception(f'Unsupported protocol {data[:10]}')
560564

561-
MAPPINGS = dict(direct=Direct, http=HTTP, httponly=HTTPOnly, socks5=Socks5, socks4=Socks4, socks=Socks5, ss=SS, ssr=SSR, redir=Redir, pf=Pf, tunnel=Tunnel, echo=Echo, pack=Pack, ws=WS, ssl='', secure='')
565+
MAPPINGS = dict(direct=Direct, http=HTTP, httponly=HTTPOnly, ssh=SSH, socks5=Socks5, socks4=Socks4, socks=Socks5, ss=SS, ssr=SSR, redir=Redir, pf=Pf, tunnel=Tunnel, echo=Echo, pack=Pack, ws=WS, ssl='', secure='')
562566
MAPPINGS['in'] = ''
563567

564568
def get_protos(rawprotos):

pproxy/server.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ def datagram_received(prot, data, addr):
309309
asyncio.ensure_future(datagram_handler(prot.transport, data, addr, **vars(self), **args))
310310
return asyncio.get_event_loop().create_datagram_endpoint(Protocol, local_addr=(self.host_name, self.port))
311311
async def open_connection(self, host, port, local_addr, lbind):
312-
if self.reuse:
312+
if self.reuse or self.ssh:
313313
if self.streams is None or self.streams.done() and not self.handler:
314314
self.streams = asyncio.get_event_loop().create_future()
315315
else:
@@ -323,6 +323,25 @@ async def open_connection(self, host, port, local_addr, lbind):
323323
local_addr = local_addr if lbind == 'in' else (lbind, 0) if lbind else None
324324
family = 0 if local_addr is None else 30 if ':' in local_addr[0] else 2
325325
wait = asyncio.open_connection(host=host, port=port, local_addr=local_addr, family=family)
326+
elif self.ssh:
327+
try:
328+
import asyncssh
329+
for s in ('read_', 'read_n', 'read_until'):
330+
setattr(asyncssh.SSHReader, s, getattr(asyncio.StreamReader, s))
331+
except Exception:
332+
raise Exception('Missing library: "pip3 install asyncssh"')
333+
username, password = self.auth.decode().split(':', 1)
334+
if password.startswith(':'):
335+
client_keys = [password[1:]]
336+
password = None
337+
else:
338+
client_keys = None
339+
local_addr = local_addr if self.lbind == 'in' else (self.lbind, 0) if self.lbind else None
340+
family = 0 if local_addr is None else 30 if ':' in local_addr[0] else 2
341+
conn = await asyncssh.connect(host=self.host_name, port=self.port, local_addr=local_addr, family=family, x509_trusted_certs=None, known_hosts=None, username=username, password=password, client_keys=client_keys)
342+
if not self.streams.done():
343+
self.streams.set_result((conn, None))
344+
return conn, None
326345
elif self.backward:
327346
wait = self.backward.open_connection()
328347
elif self.unix:
@@ -353,6 +372,8 @@ async def prepare_ciphers_and_headers(self, reader_remote, writer_remote, host,
353372
if not self.streams.done():
354373
self.streams.set_result((reader_remote, writer_remote))
355374
reader_remote, writer_remote = handler.connect(whost, wport)
375+
elif self.ssh:
376+
reader_remote, writer_remote = await reader_remote.open_connection(whost, wport)
356377
else:
357378
await self.rproto.connect(reader_remote=reader_remote, writer_remote=writer_remote, rauth=self.auth, host_name=whost, port=wport, writer_cipher_r=writer_cipher_r, myhost=self.host_name, sock=writer_remote.get_extra_info('socket'))
358379
return await self.relay.prepare_ciphers_and_headers(reader_remote, writer_remote, host, port, handler)
@@ -435,14 +456,15 @@ def compile(cls, uri, relay=None):
435456
match = cls.compile_rule(url.query) if url.query else None
436457
if loc:
437458
host_name, _, port = loc.partition(':')
438-
port = int(port) if port else 8080
459+
port = int(port) if port else (22 if 'ssh' in rawprotos else 8080)
439460
else:
440461
host_name = port = None
441462
return ProxyURI(protos=protos, rproto=protos[0], cipher=cipher, auth=url.fragment.encode(), \
442463
match=match, bind=loc or urlpath, host_name=host_name, port=port, \
443464
unix=not loc, lbind=lbind, sslclient=sslclient, sslserver=sslserver, \
444465
alive=True, direct='direct' in protonames, tunnel='tunnel' in protonames, \
445-
reuse='pack' in protonames or relay and relay.reuse, backward='in' in rawprotos, relay=relay)
466+
reuse='pack' in protonames or relay and relay.reuse, backward='in' in rawprotos, \
467+
ssh='ssh' in rawprotos, relay=relay)
446468
ProxyURI.DIRECT = ProxyURI(direct=True, tunnel=False, reuse=False, relay=None, alive=True, match=None, cipher=None, backward=None)
447469

448470
async def test_url(url, rserver):

0 commit comments

Comments
 (0)