##// END OF EJS Templates
added preliminary ssh tunneling support for clients
Min RK -
Show More
@@ -1,7 +1,7
1 1 #!/usr/bin/env python
2 2
3 3 #
4 # This file is adapted from a paramiko demo, and thus LGPL 2.1.
4 # This file is adapted from a paramiko demo, and thus licensed under LGPL 2.1.
5 5 # Original Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com>
6 6 # Edits Copyright (C) 2010 The IPython Team
7 7 #
@@ -83,7 +83,7 class Handler (SocketServer.BaseRequestHandler):
83 83 self.request.send(data)
84 84 chan.close()
85 85 self.request.close()
86 verbose('Tunnel closed from %r' % (self.request.getpeername(),))
86 verbose('Tunnel closed ')
87 87
88 88
89 89 def forward_tunnel(local_port, remote_host, remote_port, transport):
@@ -94,7 +94,7 def forward_tunnel(local_port, remote_host, remote_port, transport):
94 94 chain_host = remote_host
95 95 chain_port = remote_port
96 96 ssh_transport = transport
97 ForwardServer(('', local_port), SubHander).serve_forever()
97 ForwardServer(('127.0.0.1', local_port), SubHander).serve_forever()
98 98
99 99
100 100 def verbose(s):
@@ -19,6 +19,7 import zmq
19 19 from zmq.eventloop import ioloop, zmqstream
20 20
21 21 from IPython.external.decorator import decorator
22 from IPython.zmq import tunnel
22 23
23 24 import streamsession as ss
24 25 # from remotenamespace import RemoteNamespace
@@ -117,7 +118,33 class Client(object):
117 118
118 119 addr : bytes; zmq url, e.g. 'tcp://127.0.0.1:10101'
119 120 The address of the controller's registration socket.
120
121 [Default: 'tcp://127.0.0.1:10101']
122 context : zmq.Context
123 Pass an existing zmq.Context instance, otherwise the client will create its own
124 username : bytes
125 set username to be passed to the Session object
126 debug : bool
127 flag for lots of message printing for debug purposes
128
129 #-------------- ssh related args ----------------
130 # These are args for configuring the ssh tunnel to be used
131 # credentials are used to forward connections over ssh to the Controller
132 # Note that the ip given in `addr` needs to be relative to sshserver
133 # The most basic case is to leave addr as pointing to localhost (127.0.0.1),
134 # and set sshserver as the same machine the Controller is on. However,
135 # the only requirement is that sshserver is able to see the Controller
136 # (i.e. is within the same trusted network).
137
138 sshserver : str
139 A string of the form passed to ssh, i.e. 'server.tld' or 'user@server.tld:port'
140 If keyfile or password is specified, and this is not, it will default to
141 the ip given in addr.
142 keyfile : str; path to public key file
143 This specifies a key to be used in ssh login, default None.
144 Regular default ssh keys will be used without specifying this argument.
145 password : str;
146 Your ssh password to sshserver. Note that if this is left None,
147 you will be prompted for it if passwordless key based login is unavailable.
121 148
122 149 Attributes
123 150 ----------
@@ -159,6 +186,7 class Client(object):
159 186
160 187
161 188 _connected=False
189 _ssh=False
162 190 _engines=None
163 191 _addr='tcp://127.0.0.1:10101'
164 192 _registration_socket=None
@@ -173,18 +201,33 class Client(object):
173 201 history = None
174 202 debug = False
175 203
176 def __init__(self, addr='tcp://127.0.0.1:10101', context=None, username=None, debug=False):
204 def __init__(self, addr='tcp://127.0.0.1:10101', context=None, username=None, debug=False,
205 sshserver=None, keyfile=None, password=None, paramiko=None):
177 206 if context is None:
178 207 context = zmq.Context()
179 208 self.context = context
180 209 self._addr = addr
210 self._ssh = bool(sshserver or keyfile or password)
211 if self._ssh and sshserver is None:
212 # default to the same
213 sshserver = addr.split('://')[1].split(':')[0]
214 if self._ssh and password is None:
215 if tunnel.try_passwordless_ssh(sshserver, keyfile, paramiko):
216 password=False
217 else:
218 password = getpass("SSH Password for %s: "%sshserver)
219 ssh_kwargs = dict(keyfile=keyfile, password=password, paramiko=paramiko)
220
181 221 if username is None:
182 222 self.session = ss.StreamSession()
183 223 else:
184 224 self.session = ss.StreamSession(username)
185 self._registration_socket = self.context.socket(zmq.PAIR)
225 self._registration_socket = self.context.socket(zmq.XREQ)
186 226 self._registration_socket.setsockopt(zmq.IDENTITY, self.session.session)
187 self._registration_socket.connect(addr)
227 if self._ssh:
228 tunnel.tunnel_connection(self._registration_socket, addr, sshserver, **ssh_kwargs)
229 else:
230 self._registration_socket.connect(addr)
188 231 self._engines = {}
189 232 self._ids = set()
190 233 self.outstanding=set()
@@ -198,7 +241,7 class Client(object):
198 241 }
199 242 self._queue_handlers = {'execute_reply' : self._handle_execute_reply,
200 243 'apply_reply' : self._handle_apply_reply}
201 self._connect()
244 self._connect(sshserver, ssh_kwargs)
202 245
203 246
204 247 @property
@@ -229,12 +272,19 class Client(object):
229 272 targets = [targets]
230 273 return [self._engines[t] for t in targets], list(targets)
231 274
232 def _connect(self):
275 def _connect(self, sshserver, ssh_kwargs):
233 276 """setup all our socket connections to the controller. This is called from
234 277 __init__."""
235 278 if self._connected:
236 279 return
237 280 self._connected=True
281
282 def connect_socket(s, addr):
283 if self._ssh:
284 return tunnel.tunnel_connection(s, addr, sshserver, **ssh_kwargs)
285 else:
286 return s.connect(addr)
287
238 288 self.session.send(self._registration_socket, 'connection_request')
239 289 idents,msg = self.session.recv(self._registration_socket,mode=0)
240 290 if self.debug:
@@ -245,23 +295,23 class Client(object):
245 295 if content.queue:
246 296 self._mux_socket = self.context.socket(zmq.PAIR)
247 297 self._mux_socket.setsockopt(zmq.IDENTITY, self.session.session)
248 self._mux_socket.connect(content.queue)
298 connect_socket(self._mux_socket, content.queue)
249 299 if content.task:
250 300 self._task_socket = self.context.socket(zmq.PAIR)
251 301 self._task_socket.setsockopt(zmq.IDENTITY, self.session.session)
252 self._task_socket.connect(content.task)
302 connect_socket(self._task_socket, content.task)
253 303 if content.notification:
254 304 self._notification_socket = self.context.socket(zmq.SUB)
255 self._notification_socket.connect(content.notification)
305 connect_socket(self._notification_socket, content.notification)
256 306 self._notification_socket.setsockopt(zmq.SUBSCRIBE, "")
257 307 if content.query:
258 308 self._query_socket = self.context.socket(zmq.PAIR)
259 309 self._query_socket.setsockopt(zmq.IDENTITY, self.session.session)
260 self._query_socket.connect(content.query)
310 connect_socket(self._query_socket, content.query)
261 311 if content.control:
262 312 self._control_socket = self.context.socket(zmq.PAIR)
263 313 self._control_socket.setsockopt(zmq.IDENTITY, self.session.session)
264 self._control_socket.connect(content.control)
314 connect_socket(self._control_socket, content.control)
265 315 self._update_engines(dict(content.engines))
266 316
267 317 else:
@@ -852,4 +902,4 class AsynClient(Client):
852 902 for stream in (self.queue_stream, self.notifier_stream,
853 903 self.task_stream, self.control_stream):
854 904 stream.flush()
855 No newline at end of file
905
@@ -1,12 +1,21
1 """Basic ssh tunneling utilities."""
1 2
3 #-----------------------------------------------------------------------------
4 # Copyright (C) 2008-2010 The IPython Development Team
5 #
6 # Distributed under the terms of the BSD License. The full license is in
7 # the file COPYING, distributed as part of this software.
8 #-----------------------------------------------------------------------------
2 9
3 #-----------------------------------------
10
11
12 #-----------------------------------------------------------------------------
4 13 # Imports
5 #-----------------------------------------
14 #-----------------------------------------------------------------------------
6 15
7 16 from __future__ import print_function
8 17
9 import os,sys
18 import os,sys, atexit
10 19 from multiprocessing import Process
11 20 from getpass import getpass, getuser
12 21
@@ -16,20 +25,137 except ImportError:
16 25 paramiko = None
17 26 else:
18 27 from forward import forward_tunnel
28
29 try:
30 from IPython.external import pexpect
31 except ImportError:
32 pexpect = None
33
34 from IPython.zmq.parallel.entry_point import select_random_ports
35
36 #-----------------------------------------------------------------------------
37 # Code
38 #-----------------------------------------------------------------------------
39
40 #-----------------------------------------------------------------------------
41 # Check for passwordless login
42 #-----------------------------------------------------------------------------
43
44 def try_passwordless_ssh(server, keyfile, paramiko=None):
45 """Attempt to make an ssh connection without a password.
46 This is mainly used for requiring password input only once
47 when many tunnels may be connected to the same server.
19 48
20 from IPython.external import pexpect
49 If paramiko is None, the default for the platform is chosen.
50 """
51 if paramiko is None:
52 paramiko = sys.platform == 'win32'
53 if not paramiko:
54 f = _try_passwordless_openssh
55 else:
56 f = _try_passwordless_paramiko
57 return f(server, keyfile)
21 58
59 def _try_passwordless_openssh(server, keyfile):
60 """Try passwordless login with shell ssh command."""
61 if pexpect is None:
62 raise ImportError("pexpect unavailable, use paramiko")
63 cmd = 'ssh -f '+ server
64 if keyfile:
65 cmd += ' -i ' + keyfile
66 cmd += ' exit'
67 p = pexpect.spawn(cmd)
68 while True:
69 try:
70 p.expect('[Ppassword]:', timeout=.1)
71 except pexpect.TIMEOUT:
72 continue
73 except pexpect.EOF:
74 return True
75 else:
76 return False
22 77
23 def launch_ssh_tunnel(lport, rport, server, remoteip='127.0.0.1', keyfile=None, timeout=15):
78 def _try_passwordless_paramiko(server, keyfile):
79 """Try passwordless login with paramiko."""
80 if paramiko is None:
81 raise ImportError("paramiko unavailable, use openssh")
82 username, server, port = _split_server(server)
83 client = paramiko.SSHClient()
84 client.load_system_host_keys()
85 client.set_missing_host_key_policy(paramiko.WarningPolicy())
86 try:
87 client.connect(server, port, username=username, key_filename=keyfile,
88 look_for_keys=True)
89 except paramiko.AuthenticationException:
90 return False
91 else:
92 client.close()
93 return True
94
95
96 def tunnel_connection(socket, addr, server, keyfile=None, password=None, paramiko=None):
97 """Connect a socket to an address via an ssh tunnel.
98
99 This is a wrapper for socket.connect(addr), when addr is not accessible
100 from the local machine. It simply creates an ssh tunnel using the remaining args,
101 and calls socket.connect('tcp://localhost:lport') where lport is the randomly
102 selected local port of the tunnel.
103
104 """
105 lport = select_random_ports(1)[0]
106 transport, addr = addr.split('://')
107 ip,rport = addr.split(':')
108 rport = int(rport)
109 if paramiko is None:
110 paramiko = sys.platform == 'win32'
111 if paramiko:
112 tunnelf = paramiko_tunnel
113 else:
114 tunnelf = openssh_tunnel
115 tunnel = tunnelf(lport, rport, server, remoteip=ip, keyfile=keyfile, password=password)
116 socket.connect('tcp://127.0.0.1:%i'%lport)
117 return tunnel
118
119 def openssh_tunnel(lport, rport, server, remoteip='127.0.0.1', keyfile=None, password=None, timeout=15):
24 120 """Create an ssh tunnel using command-line ssh that connects port lport
25 121 on this machine to localhost:rport on server. The tunnel
26 122 will automatically close when not in use, remaining open
27 123 for a minimum of timeout seconds for an initial connection.
124
125 This creates a tunnel redirecting `localhost:lport` to `remoteip:rport`,
126 as seen from `server`.
127
128 keyfile and password may be specified, but ssh config is checked for defaults.
129
130 Parameters
131 ----------
132
133 lport : int
134 local port for connecting to the tunnel from this machine.
135 rport : int
136 port on the remote machine to connect to.
137 server : str
138 The ssh server to connect to. The full ssh server string will be parsed.
139 user@server:port
140 remoteip : str [Default: 127.0.0.1]
141 The remote ip, specifying the destination of the tunnel.
142 Default is localhost, which means that the tunnel would redirect
143 localhost:lport on this machine to localhost:rport on the *server*.
144
145 keyfile : str; path to public key file
146 This specifies a key to be used in ssh login, default None.
147 Regular default ssh keys will be used without specifying this argument.
148 password : str;
149 Your ssh password to the ssh server. Note that if this is left None,
150 you will be prompted for it if passwordless key based login is unavailable.
151
28 152 """
153 if pexpect is None:
154 raise ImportError("pexpect unavailable, use paramiko_tunnel")
29 155 ssh="ssh "
30 156 if keyfile:
31 157 ssh += "-i " + keyfile
32 cmd = ssh + " -f -L %i:127.0.0.1:%i %s sleep %i"%(lport, rport, server, timeout)
158 cmd = ssh + " -f -L 127.0.0.1:%i:127.0.0.1:%i %s sleep %i"%(lport, rport, server, timeout)
33 159 tunnel = pexpect.spawn(cmd)
34 160 failed = False
35 161 while True:
@@ -48,7 +174,10 def launch_ssh_tunnel(lport, rport, server, remoteip='127.0.0.1', keyfile=None,
48 174 else:
49 175 if failed:
50 176 print("Password rejected, try again")
51 tunnel.sendline(getpass())
177 password=None
178 if password is None:
179 password = getpass("%s's password: "%(server))
180 tunnel.sendline(password)
52 181 failed = True
53 182
54 183 def _split_server(server):
@@ -63,28 +192,62 def _split_server(server):
63 192 port = 22
64 193 return username, server, port
65 194
66 def launch_paramiko_tunnel(lport, rport, server, remoteip='127.0.0.1', keyfile=None):
67 """launch a tunner with paramiko in a subprocess"""
195 def paramiko_tunnel(lport, rport, server, remoteip='127.0.0.1', keyfile=None, password=None, timeout=15):
196 """launch a tunner with paramiko in a subprocess. This should only be used
197 when shell ssh is unavailable (e.g. Windows).
198
199 This creates a tunnel redirecting `localhost:lport` to `remoteip:rport`,
200 as seen from `server`.
201
202 keyfile and password may be specified, but ssh config is checked for defaults.
203
204 Parameters
205 ----------
206
207 lport : int
208 local port for connecting to the tunnel from this machine.
209 rport : int
210 port on the remote machine to connect to.
211 server : str
212 The ssh server to connect to. The full ssh server string will be parsed.
213 user@server:port
214 remoteip : str [Default: 127.0.0.1]
215 The remote ip, specifying the destination of the tunnel.
216 Default is localhost, which means that the tunnel would redirect
217 localhost:lport on this machine to localhost:rport on the *server*.
218
219 keyfile : str; path to public key file
220 This specifies a key to be used in ssh login, default None.
221 Regular default ssh keys will be used without specifying this argument.
222 password : str;
223 Your ssh password to the ssh server. Note that if this is left None,
224 you will be prompted for it if passwordless key based login is unavailable.
225
226 """
68 227 if paramiko is None:
69 228 raise ImportError("Paramiko not available")
70 server = _split_server(server)
71 if keyfile is None:
72 passwd = getpass("%s@%s's password: "%(server[0], server[1]))
73 else:
74 passwd = None
229
230 if password is None:
231 if not _check_passwordless_paramiko(server, keyfile):
232 password = getpass("%s's password: "%(server))
233
75 234 p = Process(target=_paramiko_tunnel,
76 235 args=(lport, rport, server, remoteip),
77 kwargs=dict(keyfile=keyfile, password=passwd))
236 kwargs=dict(keyfile=keyfile, password=password))
78 237 p.daemon=False
79 238 p.start()
239 atexit.register(_shutdown_process, p)
80 240 return p
81 241
242 def _shutdown_process(p):
243 if p.isalive():
244 p.terminate()
82 245
83 246 def _paramiko_tunnel(lport, rport, server, remoteip, keyfile=None, password=None):
84 247 """function for actually starting a paramiko tunnel, to be passed
85 248 to multiprocessing.Process(target=this).
86 249 """
87 username, server, port = server
250 username, server, port = _split_server(server)
88 251 client = paramiko.SSHClient()
89 252 client.load_system_host_keys()
90 253 client.set_missing_host_key_policy(paramiko.WarningPolicy())
@@ -92,20 +255,34 def _paramiko_tunnel(lport, rport, server, remoteip, keyfile=None, password=None
92 255 try:
93 256 client.connect(server, port, username=username, key_filename=keyfile,
94 257 look_for_keys=True, password=password)
258 # except paramiko.AuthenticationException:
259 # if password is None:
260 # password = getpass("%s@%s's password: "%(username, server))
261 # client.connect(server, port, username=username, password=password)
262 # else:
263 # raise
95 264 except Exception as e:
96 265 print ('*** Failed to connect to %s:%d: %r' % (server, port, e))
97 266 sys.exit(1)
98 267
99 print ('Now forwarding port %d to %s:%d ...' % (lport, server, rport))
268 # print ('Now forwarding port %d to %s:%d ...' % (lport, server, rport))
100 269
101 270 try:
102 271 forward_tunnel(lport, remoteip, rport, client.get_transport())
103 272 except KeyboardInterrupt:
104 print ('C-c: Port forwarding stopped.')
273 print ('SIGINT: Port forwarding stopped cleanly')
105 274 sys.exit(0)
275 except Exception as e:
276 print ("Port forwarding stopped uncleanly: %s"%e)
277 sys.exit(255)
278
279 if sys.platform == 'win32':
280 ssh_tunnel = paramiko_tunnel
281 else:
282 ssh_tunnel = openssh_tunnel
106 283
107 284
108 __all__ = ['launch_ssh_tunnel', 'launch_paramiko_tunnel']
285 __all__ = ['tunnel_connection', 'ssh_tunnel', 'openssh_tunnel', 'paramiko_tunnel', 'try_passwordless_ssh']
109 286
110 287
111 288
General Comments 0
You need to be logged in to leave comments. Login now