Skip to content

Commit 1f330b6

Browse files
author
Dan
committed
Added direct-tcpip tunneling support to fake server. Added test for proxying an SSH connection through another SSH server using direct-tcpip. Resolves #12
1 parent 5fa7bf9 commit 1f330b6

File tree

3 files changed

+133
-17
lines changed

3 files changed

+133
-17
lines changed

fake_server/fake_server.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
paramiko repository
2626
"""
2727

28+
from gevent import monkey
29+
monkey.patch_all()
2830
import gevent
2931
import os
3032
import socket
@@ -36,8 +38,7 @@
3638
import paramiko
3739
import time
3840
from stub_sftp import StubSFTPServer
39-
from gevent import monkey
40-
monkey.patch_all()
41+
from tunnel import Tunneler
4142

4243

4344
logger = logging.getLogger("fake_server")
@@ -46,10 +47,11 @@
4647
host_key = paramiko.RSAKey(filename = os.path.sep.join([os.path.dirname(__file__), 'rsa.key']))
4748

4849
class Server (paramiko.ServerInterface):
49-
def __init__(self, cmd_req_response = {}, fail_auth=False):
50+
def __init__(self, transport, cmd_req_response = {}, fail_auth=False):
5051
self.event = Event()
5152
self.cmd_req_response = cmd_req_response
5253
self.fail_auth = fail_auth
54+
self.transport = transport
5355

5456
def check_channel_request(self, kind, chanid):
5557
return paramiko.OPEN_SUCCEEDED
@@ -72,6 +74,20 @@ def check_channel_pty_request(self, channel, term, width, height, pixelwidth,
7274
pixelheight, modes):
7375
return True
7476

77+
def check_channel_direct_tcpip_request(self, chanid, origin, destination):
78+
logger.debug("Proxy connection %s -> %s requested", origin, destination,)
79+
extra = {'username' : self.transport.get_username()}
80+
logger.debug("Starting proxy connection %s -> %s",
81+
origin, destination, extra=extra)
82+
try:
83+
tunnel = Tunneler(destination, self.transport, chanid)
84+
tunnel.start()
85+
except Exception, ex:
86+
logger.error("Error creating proxy connection to %s - %s",
87+
destination, ex,)
88+
return paramiko.OPEN_FAILED_CONNECT_FAILED
89+
return paramiko.OPEN_SUCCEEDED
90+
7591
def check_channel_forward_agent_request(self, channel):
7692
logger.debug("Forward agent key request for channel %s" % (channel,))
7793
return True
@@ -142,7 +158,8 @@ def _handle_ssh_connection(cmd_req_response, transport, fail_auth=False):
142158
return
143159
transport.add_server_key(host_key)
144160
transport.set_subsystem_handler('sftp', paramiko.SFTPServer, StubSFTPServer)
145-
server = Server(cmd_req_response = cmd_req_response, fail_auth=fail_auth)
161+
server = Server(transport, cmd_req_response=cmd_req_response,
162+
fail_auth=fail_auth)
146163
try:
147164
transport.start_server(server=server)
148165
except paramiko.SSHException, e:
@@ -157,9 +174,9 @@ def _handle_ssh_connection(cmd_req_response, transport, fail_auth=False):
157174
return
158175
while transport.is_active():
159176
logger.debug("Transport active, waiting..")
160-
time.sleep(1)
177+
gevent.sleep(1)
161178
while not channel.send_ready():
162-
time.sleep(.5)
179+
gevent.sleep(.2)
163180
channel.close()
164181

165182
def handle_ssh_connection(cmd_req_response, sock,

fake_server/tunnel.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# This file is part of parallel-ssh.
2+
3+
# Copyright (C) 2014 Panos Kittenis
4+
5+
# This library is free software; you can redistribute it and/or
6+
# modify it under the terms of the GNU Lesser General Public
7+
# License as published by the Free Software Foundation, version 2.1.
8+
9+
# This library is distributed in the hope that it will be useful,
10+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12+
# Lesser General Public License for more details.
13+
14+
# You should have received a copy of the GNU Lesser General Public
15+
# License along with this library; if not, write to the Free Software
16+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17+
18+
"""
19+
Generic 'Tunneler' module for tunneling between source <-> destination
20+
network connections asynchronously with gevent.
21+
"""
22+
23+
import gevent
24+
from gevent import socket, select
25+
import logging
26+
27+
logger = logging.getLogger("fake_server")
28+
29+
class Tunneler(gevent.Greenlet):
30+
def __init__(self, address, transport, chanid):
31+
gevent.Greenlet.__init__(self)
32+
self.socket = socket.create_connection(address)
33+
self.transport = transport
34+
self.chanid = chanid
35+
36+
def close(self):
37+
try:
38+
self.transport.close()
39+
except Exception:
40+
pass
41+
return
42+
43+
def tunnel(self, dest_socket, source_chan):
44+
try:
45+
while True:
46+
logger.debug("Tunnel waiting for data..")
47+
data = source_chan.recv(1024)
48+
dest_socket.sendall(data)
49+
response_data = dest_socket.recv(1024)
50+
source_chan.sendall(response_data)
51+
logger.debug("Tunnel sent data..")
52+
gevent.sleep(1)
53+
finally:
54+
source_chan.close()
55+
dest_socket.close()
56+
57+
def run(self):
58+
channel = self.transport.accept(20)
59+
if not channel:
60+
return
61+
if not channel.get_id() == self.chanid:
62+
return
63+
peer = self.socket.getpeername()
64+
logger.debug("Start tunneling with peer %s, user %s", peer,
65+
self.transport.get_username())
66+
try:
67+
self.tunnel(self.socket, channel)
68+
except Exception, ex:
69+
logger.exception("Got exception creating tunnel - %s", ex,)
70+
logger.debug("Finished tunneling")

tests/test_pssh_client.py

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
import unittest
2323
from pssh import ParallelSSHClient, UnknownHostException, \
24-
AuthenticationException, ConnectionErrorException
24+
AuthenticationException, ConnectionErrorException, logger as pssh_logger
2525
from fake_server.fake_server import start_server, make_socket, \
2626
logger as server_logger, paramiko_logger
2727
import random
@@ -35,6 +35,10 @@
3535
USER_KEY = paramiko.RSAKey.from_private_key_file(
3636
os.path.sep.join([os.path.dirname(__file__), 'test_client_private_key']))
3737

38+
# server_logger.setLevel(logging.DEBUG)
39+
# pssh_logger.setLevel(logging.DEBUG)
40+
# logging.basicConfig()
41+
3842
class ParallelSSHClientTest(unittest.TestCase):
3943

4044
def setUp(self):
@@ -67,7 +71,7 @@ def test_pssh_client_exec_command(self):
6771
output = client.get_stdout(cmd)
6872
expected = {'127.0.0.1' : {'exit_code' : 0}}
6973
self.assertEqual(expected, output,
70-
msg = "Got unexpected command output - %s" % (output,))
74+
msg="Got unexpected command output - %s" % (output,))
7175
del client
7276
server.join()
7377

@@ -85,15 +89,15 @@ def test_pssh_client_exec_command_get_buffers(self):
8589
stdout = list(output['127.0.0.1']['stdout'])
8690
stderr = list(output['127.0.0.1']['stderr'])
8791
self.assertEqual(expected_exit_code, exit_code,
88-
msg = "Got unexpected exit code - %s, expected %s" %
92+
msg="Got unexpected exit code - %s, expected %s" %
8993
(exit_code,
9094
expected_exit_code,))
9195
self.assertEqual(expected_stdout, stdout,
92-
msg = "Got unexpected stdout - %s, expected %s" %
96+
msg="Got unexpected stdout - %s, expected %s" %
9397
(stdout,
9498
expected_stdout,))
9599
self.assertEqual(expected_stderr, stderr,
96-
msg = "Got unexpected stderr - %s, expected %s" %
100+
msg="Got unexpected stderr - %s, expected %s" %
97101
(stderr,
98102
expected_stderr,))
99103
del client
@@ -112,15 +116,15 @@ def test_pssh_client_run_command_get_output(self):
112116
stdout = list(output['127.0.0.1']['stdout'])
113117
stderr = list(output['127.0.0.1']['stderr'])
114118
self.assertEqual(expected_exit_code, exit_code,
115-
msg = "Got unexpected exit code - %s, expected %s" %
119+
msg="Got unexpected exit code - %s, expected %s" %
116120
(exit_code,
117121
expected_exit_code,))
118122
self.assertEqual(expected_stdout, stdout,
119-
msg = "Got unexpected stdout - %s, expected %s" %
123+
msg="Got unexpected stdout - %s, expected %s" %
120124
(stdout,
121125
expected_stdout,))
122126
self.assertEqual(expected_stderr, stderr,
123-
msg = "Got unexpected stderr - %s, expected %s" %
127+
msg="Got unexpected stderr - %s, expected %s" %
124128
(stderr,
125129
expected_stderr,))
126130
del client
@@ -141,15 +145,15 @@ def test_pssh_client_run_command_get_output_explicit(self):
141145
stdout = list(output['127.0.0.1']['stdout'])
142146
stderr = list(output['127.0.0.1']['stderr'])
143147
self.assertEqual(expected_exit_code, exit_code,
144-
msg = "Got unexpected exit code - %s, expected %s" %
148+
msg="Got unexpected exit code - %s, expected %s" %
145149
(exit_code,
146150
expected_exit_code,))
147151
self.assertEqual(expected_stdout, stdout,
148-
msg = "Got unexpected stdout - %s, expected %s" %
152+
msg="Got unexpected stdout - %s, expected %s" %
149153
(stdout,
150154
expected_stdout,))
151155
self.assertEqual(expected_stderr, stderr,
152-
msg = "Got unexpected stderr - %s, expected %s" %
156+
msg="Got unexpected stderr - %s, expected %s" %
153157
(stderr,
154158
expected_stderr,))
155159
del client
@@ -298,3 +302,28 @@ def test_pssh_pool_size(self):
298302
self.assertEqual(expected, actual,
299303
msg="Expected pool size to be %s, got %s" % (
300304
expected, actual,))
305+
306+
def test_ssh_proxy(self):
307+
"""Test connecting to remote destination via SSH proxy
308+
client -> proxy -> destination
309+
Proxy SSH server accepts no commands and sends no responses, only
310+
proxies to destination. Destination accepts a command as usual."""
311+
proxy_server_socket = make_socket('127.0.0.1')
312+
proxy_server_port = proxy_server_socket.getsockname()[1]
313+
proxy_server = start_server({}, proxy_server_socket)
314+
server = start_server({ self.fake_cmd : self.fake_resp },
315+
self.listen_socket)
316+
client = ParallelSSHClient(['127.0.0.1'], port=self.listen_port,
317+
pkey=self.user_key,
318+
proxy_host='127.0.0.1',
319+
proxy_port=proxy_server_port
320+
)
321+
output = client.run_command(self.fake_cmd)
322+
stdout = list(output['127.0.0.1']['stdout'])
323+
expected_stdout = [self.fake_resp]
324+
self.assertEqual(expected_stdout, stdout,
325+
msg="Got unexpected stdout - %s, expected %s" %
326+
(stdout,
327+
expected_stdout,))
328+
server.kill()
329+
proxy_server.kill()

0 commit comments

Comments
 (0)