##// END OF EJS Templates
ignore SIGINT in paramiko tunnel subprocesses
MinRK -
Show More
@@ -1,354 +1,356 b''
1 """Basic ssh tunnel utilities, and convenience functions for tunneling
1 """Basic ssh tunnel utilities, and convenience functions for tunneling
2 zeromq connections.
2 zeromq connections.
3
3
4 Authors
4 Authors
5 -------
5 -------
6 * Min RK
6 * Min RK
7 """
7 """
8
8
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10 # Copyright (C) 2010-2011 The IPython Development Team
10 # Copyright (C) 2010-2011 The IPython Development Team
11 #
11 #
12 # Distributed under the terms of the BSD License. The full license is in
12 # Distributed under the terms of the BSD License. The full license is in
13 # the file COPYING, distributed as part of this software.
13 # the file COPYING, distributed as part of this software.
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15
15
16
16
17
17
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19 # Imports
19 # Imports
20 #-----------------------------------------------------------------------------
20 #-----------------------------------------------------------------------------
21
21
22 from __future__ import print_function
22 from __future__ import print_function
23
23
24 import os,sys, atexit
24 import os,sys, atexit
25 import signal
25 import socket
26 import socket
26 from multiprocessing import Process
27 from multiprocessing import Process
27 from getpass import getpass, getuser
28 from getpass import getpass, getuser
28 import warnings
29 import warnings
29
30
30 try:
31 try:
31 with warnings.catch_warnings():
32 with warnings.catch_warnings():
32 warnings.simplefilter('ignore', DeprecationWarning)
33 warnings.simplefilter('ignore', DeprecationWarning)
33 import paramiko
34 import paramiko
34 except ImportError:
35 except ImportError:
35 paramiko = None
36 paramiko = None
36 else:
37 else:
37 from forward import forward_tunnel
38 from forward import forward_tunnel
38
39
39 try:
40 try:
40 from IPython.external import pexpect
41 from IPython.external import pexpect
41 except ImportError:
42 except ImportError:
42 pexpect = None
43 pexpect = None
43
44
44 #-----------------------------------------------------------------------------
45 #-----------------------------------------------------------------------------
45 # Code
46 # Code
46 #-----------------------------------------------------------------------------
47 #-----------------------------------------------------------------------------
47
48
48 # select_random_ports copied from IPython.parallel.util
49 # select_random_ports copied from IPython.parallel.util
49 _random_ports = set()
50 _random_ports = set()
50
51
51 def select_random_ports(n):
52 def select_random_ports(n):
52 """Selects and return n random ports that are available."""
53 """Selects and return n random ports that are available."""
53 ports = []
54 ports = []
54 for i in xrange(n):
55 for i in xrange(n):
55 sock = socket.socket()
56 sock = socket.socket()
56 sock.bind(('', 0))
57 sock.bind(('', 0))
57 while sock.getsockname()[1] in _random_ports:
58 while sock.getsockname()[1] in _random_ports:
58 sock.close()
59 sock.close()
59 sock = socket.socket()
60 sock = socket.socket()
60 sock.bind(('', 0))
61 sock.bind(('', 0))
61 ports.append(sock)
62 ports.append(sock)
62 for i, sock in enumerate(ports):
63 for i, sock in enumerate(ports):
63 port = sock.getsockname()[1]
64 port = sock.getsockname()[1]
64 sock.close()
65 sock.close()
65 ports[i] = port
66 ports[i] = port
66 _random_ports.add(port)
67 _random_ports.add(port)
67 return ports
68 return ports
68
69
69
70
70 #-----------------------------------------------------------------------------
71 #-----------------------------------------------------------------------------
71 # Check for passwordless login
72 # Check for passwordless login
72 #-----------------------------------------------------------------------------
73 #-----------------------------------------------------------------------------
73
74
74 def try_passwordless_ssh(server, keyfile, paramiko=None):
75 def try_passwordless_ssh(server, keyfile, paramiko=None):
75 """Attempt to make an ssh connection without a password.
76 """Attempt to make an ssh connection without a password.
76 This is mainly used for requiring password input only once
77 This is mainly used for requiring password input only once
77 when many tunnels may be connected to the same server.
78 when many tunnels may be connected to the same server.
78
79
79 If paramiko is None, the default for the platform is chosen.
80 If paramiko is None, the default for the platform is chosen.
80 """
81 """
81 if paramiko is None:
82 if paramiko is None:
82 paramiko = sys.platform == 'win32'
83 paramiko = sys.platform == 'win32'
83 if not paramiko:
84 if not paramiko:
84 f = _try_passwordless_openssh
85 f = _try_passwordless_openssh
85 else:
86 else:
86 f = _try_passwordless_paramiko
87 f = _try_passwordless_paramiko
87 return f(server, keyfile)
88 return f(server, keyfile)
88
89
89 def _try_passwordless_openssh(server, keyfile):
90 def _try_passwordless_openssh(server, keyfile):
90 """Try passwordless login with shell ssh command."""
91 """Try passwordless login with shell ssh command."""
91 if pexpect is None:
92 if pexpect is None:
92 raise ImportError("pexpect unavailable, use paramiko")
93 raise ImportError("pexpect unavailable, use paramiko")
93 cmd = 'ssh -f '+ server
94 cmd = 'ssh -f '+ server
94 if keyfile:
95 if keyfile:
95 cmd += ' -i ' + keyfile
96 cmd += ' -i ' + keyfile
96 cmd += ' exit'
97 cmd += ' exit'
97 p = pexpect.spawn(cmd)
98 p = pexpect.spawn(cmd)
98 while True:
99 while True:
99 try:
100 try:
100 p.expect('[Pp]assword:', timeout=.1)
101 p.expect('[Pp]assword:', timeout=.1)
101 except pexpect.TIMEOUT:
102 except pexpect.TIMEOUT:
102 continue
103 continue
103 except pexpect.EOF:
104 except pexpect.EOF:
104 return True
105 return True
105 else:
106 else:
106 return False
107 return False
107
108
108 def _try_passwordless_paramiko(server, keyfile):
109 def _try_passwordless_paramiko(server, keyfile):
109 """Try passwordless login with paramiko."""
110 """Try passwordless login with paramiko."""
110 if paramiko is None:
111 if paramiko is None:
111 msg = "Paramiko unavaliable, "
112 msg = "Paramiko unavaliable, "
112 if sys.platform == 'win32':
113 if sys.platform == 'win32':
113 msg += "Paramiko is required for ssh tunneled connections on Windows."
114 msg += "Paramiko is required for ssh tunneled connections on Windows."
114 else:
115 else:
115 msg += "use OpenSSH."
116 msg += "use OpenSSH."
116 raise ImportError(msg)
117 raise ImportError(msg)
117 username, server, port = _split_server(server)
118 username, server, port = _split_server(server)
118 client = paramiko.SSHClient()
119 client = paramiko.SSHClient()
119 client.load_system_host_keys()
120 client.load_system_host_keys()
120 client.set_missing_host_key_policy(paramiko.WarningPolicy())
121 client.set_missing_host_key_policy(paramiko.WarningPolicy())
121 try:
122 try:
122 client.connect(server, port, username=username, key_filename=keyfile,
123 client.connect(server, port, username=username, key_filename=keyfile,
123 look_for_keys=True)
124 look_for_keys=True)
124 except paramiko.AuthenticationException:
125 except paramiko.AuthenticationException:
125 return False
126 return False
126 else:
127 else:
127 client.close()
128 client.close()
128 return True
129 return True
129
130
130
131
131 def tunnel_connection(socket, addr, server, keyfile=None, password=None, paramiko=None, timeout=60):
132 def tunnel_connection(socket, addr, server, keyfile=None, password=None, paramiko=None, timeout=60):
132 """Connect a socket to an address via an ssh tunnel.
133 """Connect a socket to an address via an ssh tunnel.
133
134
134 This is a wrapper for socket.connect(addr), when addr is not accessible
135 This is a wrapper for socket.connect(addr), when addr is not accessible
135 from the local machine. It simply creates an ssh tunnel using the remaining args,
136 from the local machine. It simply creates an ssh tunnel using the remaining args,
136 and calls socket.connect('tcp://localhost:lport') where lport is the randomly
137 and calls socket.connect('tcp://localhost:lport') where lport is the randomly
137 selected local port of the tunnel.
138 selected local port of the tunnel.
138
139
139 """
140 """
140 new_url, tunnel = open_tunnel(addr, server, keyfile=keyfile, password=password, paramiko=paramiko, timeout=timeout)
141 new_url, tunnel = open_tunnel(addr, server, keyfile=keyfile, password=password, paramiko=paramiko, timeout=timeout)
141 socket.connect(new_url)
142 socket.connect(new_url)
142 return tunnel
143 return tunnel
143
144
144
145
145 def open_tunnel(addr, server, keyfile=None, password=None, paramiko=None, timeout=60):
146 def open_tunnel(addr, server, keyfile=None, password=None, paramiko=None, timeout=60):
146 """Open a tunneled connection from a 0MQ url.
147 """Open a tunneled connection from a 0MQ url.
147
148
148 For use inside tunnel_connection.
149 For use inside tunnel_connection.
149
150
150 Returns
151 Returns
151 -------
152 -------
152
153
153 (url, tunnel): The 0MQ url that has been forwarded, and the tunnel object
154 (url, tunnel): The 0MQ url that has been forwarded, and the tunnel object
154 """
155 """
155
156
156 lport = select_random_ports(1)[0]
157 lport = select_random_ports(1)[0]
157 transport, addr = addr.split('://')
158 transport, addr = addr.split('://')
158 ip,rport = addr.split(':')
159 ip,rport = addr.split(':')
159 rport = int(rport)
160 rport = int(rport)
160 if paramiko is None:
161 if paramiko is None:
161 paramiko = sys.platform == 'win32'
162 paramiko = sys.platform == 'win32'
162 if paramiko:
163 if paramiko:
163 tunnelf = paramiko_tunnel
164 tunnelf = paramiko_tunnel
164 else:
165 else:
165 tunnelf = openssh_tunnel
166 tunnelf = openssh_tunnel
166
167
167 tunnel = tunnelf(lport, rport, server, remoteip=ip, keyfile=keyfile, password=password, timeout=timeout)
168 tunnel = tunnelf(lport, rport, server, remoteip=ip, keyfile=keyfile, password=password, timeout=timeout)
168 return 'tcp://127.0.0.1:%i'%lport, tunnel
169 return 'tcp://127.0.0.1:%i'%lport, tunnel
169
170
170 def openssh_tunnel(lport, rport, server, remoteip='127.0.0.1', keyfile=None, password=None, timeout=60):
171 def openssh_tunnel(lport, rport, server, remoteip='127.0.0.1', keyfile=None, password=None, timeout=60):
171 """Create an ssh tunnel using command-line ssh that connects port lport
172 """Create an ssh tunnel using command-line ssh that connects port lport
172 on this machine to localhost:rport on server. The tunnel
173 on this machine to localhost:rport on server. The tunnel
173 will automatically close when not in use, remaining open
174 will automatically close when not in use, remaining open
174 for a minimum of timeout seconds for an initial connection.
175 for a minimum of timeout seconds for an initial connection.
175
176
176 This creates a tunnel redirecting `localhost:lport` to `remoteip:rport`,
177 This creates a tunnel redirecting `localhost:lport` to `remoteip:rport`,
177 as seen from `server`.
178 as seen from `server`.
178
179
179 keyfile and password may be specified, but ssh config is checked for defaults.
180 keyfile and password may be specified, but ssh config is checked for defaults.
180
181
181 Parameters
182 Parameters
182 ----------
183 ----------
183
184
184 lport : int
185 lport : int
185 local port for connecting to the tunnel from this machine.
186 local port for connecting to the tunnel from this machine.
186 rport : int
187 rport : int
187 port on the remote machine to connect to.
188 port on the remote machine to connect to.
188 server : str
189 server : str
189 The ssh server to connect to. The full ssh server string will be parsed.
190 The ssh server to connect to. The full ssh server string will be parsed.
190 user@server:port
191 user@server:port
191 remoteip : str [Default: 127.0.0.1]
192 remoteip : str [Default: 127.0.0.1]
192 The remote ip, specifying the destination of the tunnel.
193 The remote ip, specifying the destination of the tunnel.
193 Default is localhost, which means that the tunnel would redirect
194 Default is localhost, which means that the tunnel would redirect
194 localhost:lport on this machine to localhost:rport on the *server*.
195 localhost:lport on this machine to localhost:rport on the *server*.
195
196
196 keyfile : str; path to public key file
197 keyfile : str; path to public key file
197 This specifies a key to be used in ssh login, default None.
198 This specifies a key to be used in ssh login, default None.
198 Regular default ssh keys will be used without specifying this argument.
199 Regular default ssh keys will be used without specifying this argument.
199 password : str;
200 password : str;
200 Your ssh password to the ssh server. Note that if this is left None,
201 Your ssh password to the ssh server. Note that if this is left None,
201 you will be prompted for it if passwordless key based login is unavailable.
202 you will be prompted for it if passwordless key based login is unavailable.
202 timeout : int [default: 60]
203 timeout : int [default: 60]
203 The time (in seconds) after which no activity will result in the tunnel
204 The time (in seconds) after which no activity will result in the tunnel
204 closing. This prevents orphaned tunnels from running forever.
205 closing. This prevents orphaned tunnels from running forever.
205 """
206 """
206 if pexpect is None:
207 if pexpect is None:
207 raise ImportError("pexpect unavailable, use paramiko_tunnel")
208 raise ImportError("pexpect unavailable, use paramiko_tunnel")
208 ssh="ssh "
209 ssh="ssh "
209 if keyfile:
210 if keyfile:
210 ssh += "-i " + keyfile
211 ssh += "-i " + keyfile
211
212
212 if ':' in server:
213 if ':' in server:
213 server, port = server.split(':')
214 server, port = server.split(':')
214 ssh += " -p %s" % port
215 ssh += " -p %s" % port
215
216
216 cmd = "%s -f -L 127.0.0.1:%i:%s:%i %s sleep %i" % (
217 cmd = "%s -f -L 127.0.0.1:%i:%s:%i %s sleep %i" % (
217 ssh, lport, remoteip, rport, server, timeout)
218 ssh, lport, remoteip, rport, server, timeout)
218 tunnel = pexpect.spawn(cmd)
219 tunnel = pexpect.spawn(cmd)
219 failed = False
220 failed = False
220 while True:
221 while True:
221 try:
222 try:
222 tunnel.expect('[Pp]assword:', timeout=.1)
223 tunnel.expect('[Pp]assword:', timeout=.1)
223 except pexpect.TIMEOUT:
224 except pexpect.TIMEOUT:
224 continue
225 continue
225 except pexpect.EOF:
226 except pexpect.EOF:
226 if tunnel.exitstatus:
227 if tunnel.exitstatus:
227 print (tunnel.exitstatus)
228 print (tunnel.exitstatus)
228 print (tunnel.before)
229 print (tunnel.before)
229 print (tunnel.after)
230 print (tunnel.after)
230 raise RuntimeError("tunnel '%s' failed to start"%(cmd))
231 raise RuntimeError("tunnel '%s' failed to start"%(cmd))
231 else:
232 else:
232 return tunnel.pid
233 return tunnel.pid
233 else:
234 else:
234 if failed:
235 if failed:
235 print("Password rejected, try again")
236 print("Password rejected, try again")
236 password=None
237 password=None
237 if password is None:
238 if password is None:
238 password = getpass("%s's password: "%(server))
239 password = getpass("%s's password: "%(server))
239 tunnel.sendline(password)
240 tunnel.sendline(password)
240 failed = True
241 failed = True
241
242
242 def _split_server(server):
243 def _split_server(server):
243 if '@' in server:
244 if '@' in server:
244 username,server = server.split('@', 1)
245 username,server = server.split('@', 1)
245 else:
246 else:
246 username = getuser()
247 username = getuser()
247 if ':' in server:
248 if ':' in server:
248 server, port = server.split(':')
249 server, port = server.split(':')
249 port = int(port)
250 port = int(port)
250 else:
251 else:
251 port = 22
252 port = 22
252 return username, server, port
253 return username, server, port
253
254
254 def paramiko_tunnel(lport, rport, server, remoteip='127.0.0.1', keyfile=None, password=None, timeout=60):
255 def paramiko_tunnel(lport, rport, server, remoteip='127.0.0.1', keyfile=None, password=None, timeout=60):
255 """launch a tunner with paramiko in a subprocess. This should only be used
256 """launch a tunner with paramiko in a subprocess. This should only be used
256 when shell ssh is unavailable (e.g. Windows).
257 when shell ssh is unavailable (e.g. Windows).
257
258
258 This creates a tunnel redirecting `localhost:lport` to `remoteip:rport`,
259 This creates a tunnel redirecting `localhost:lport` to `remoteip:rport`,
259 as seen from `server`.
260 as seen from `server`.
260
261
261 If you are familiar with ssh tunnels, this creates the tunnel:
262 If you are familiar with ssh tunnels, this creates the tunnel:
262
263
263 ssh server -L localhost:lport:remoteip:rport
264 ssh server -L localhost:lport:remoteip:rport
264
265
265 keyfile and password may be specified, but ssh config is checked for defaults.
266 keyfile and password may be specified, but ssh config is checked for defaults.
266
267
267
268
268 Parameters
269 Parameters
269 ----------
270 ----------
270
271
271 lport : int
272 lport : int
272 local port for connecting to the tunnel from this machine.
273 local port for connecting to the tunnel from this machine.
273 rport : int
274 rport : int
274 port on the remote machine to connect to.
275 port on the remote machine to connect to.
275 server : str
276 server : str
276 The ssh server to connect to. The full ssh server string will be parsed.
277 The ssh server to connect to. The full ssh server string will be parsed.
277 user@server:port
278 user@server:port
278 remoteip : str [Default: 127.0.0.1]
279 remoteip : str [Default: 127.0.0.1]
279 The remote ip, specifying the destination of the tunnel.
280 The remote ip, specifying the destination of the tunnel.
280 Default is localhost, which means that the tunnel would redirect
281 Default is localhost, which means that the tunnel would redirect
281 localhost:lport on this machine to localhost:rport on the *server*.
282 localhost:lport on this machine to localhost:rport on the *server*.
282
283
283 keyfile : str; path to public key file
284 keyfile : str; path to public key file
284 This specifies a key to be used in ssh login, default None.
285 This specifies a key to be used in ssh login, default None.
285 Regular default ssh keys will be used without specifying this argument.
286 Regular default ssh keys will be used without specifying this argument.
286 password : str;
287 password : str;
287 Your ssh password to the ssh server. Note that if this is left None,
288 Your ssh password to the ssh server. Note that if this is left None,
288 you will be prompted for it if passwordless key based login is unavailable.
289 you will be prompted for it if passwordless key based login is unavailable.
289 timeout : int [default: 60]
290 timeout : int [default: 60]
290 The time (in seconds) after which no activity will result in the tunnel
291 The time (in seconds) after which no activity will result in the tunnel
291 closing. This prevents orphaned tunnels from running forever.
292 closing. This prevents orphaned tunnels from running forever.
292
293
293 """
294 """
294 if paramiko is None:
295 if paramiko is None:
295 raise ImportError("Paramiko not available")
296 raise ImportError("Paramiko not available")
296
297
297 if password is None:
298 if password is None:
298 if not _try_passwordless_paramiko(server, keyfile):
299 if not _try_passwordless_paramiko(server, keyfile):
299 password = getpass("%s's password: "%(server))
300 password = getpass("%s's password: "%(server))
300
301
301 p = Process(target=_paramiko_tunnel,
302 p = Process(target=_paramiko_tunnel,
302 args=(lport, rport, server, remoteip),
303 args=(lport, rport, server, remoteip),
303 kwargs=dict(keyfile=keyfile, password=password))
304 kwargs=dict(keyfile=keyfile, password=password))
304 p.daemon=False
305 p.daemon=False
305 p.start()
306 p.start()
306 atexit.register(_shutdown_process, p)
307 atexit.register(_shutdown_process, p)
307 return p
308 return p
308
309
309 def _shutdown_process(p):
310 def _shutdown_process(p):
310 if p.is_alive():
311 if p.is_alive():
311 p.terminate()
312 p.terminate()
312
313
313 def _paramiko_tunnel(lport, rport, server, remoteip, keyfile=None, password=None):
314 def _paramiko_tunnel(lport, rport, server, remoteip, keyfile=None, password=None):
314 """Function for actually starting a paramiko tunnel, to be passed
315 """Function for actually starting a paramiko tunnel, to be passed
315 to multiprocessing.Process(target=this), and not called directly.
316 to multiprocessing.Process(target=this), and not called directly.
316 """
317 """
317 username, server, port = _split_server(server)
318 username, server, port = _split_server(server)
318 client = paramiko.SSHClient()
319 client = paramiko.SSHClient()
319 client.load_system_host_keys()
320 client.load_system_host_keys()
320 client.set_missing_host_key_policy(paramiko.WarningPolicy())
321 client.set_missing_host_key_policy(paramiko.WarningPolicy())
321
322
322 try:
323 try:
323 client.connect(server, port, username=username, key_filename=keyfile,
324 client.connect(server, port, username=username, key_filename=keyfile,
324 look_for_keys=True, password=password)
325 look_for_keys=True, password=password)
325 # except paramiko.AuthenticationException:
326 # except paramiko.AuthenticationException:
326 # if password is None:
327 # if password is None:
327 # password = getpass("%s@%s's password: "%(username, server))
328 # password = getpass("%s@%s's password: "%(username, server))
328 # client.connect(server, port, username=username, password=password)
329 # client.connect(server, port, username=username, password=password)
329 # else:
330 # else:
330 # raise
331 # raise
331 except Exception as e:
332 except Exception as e:
332 print ('*** Failed to connect to %s:%d: %r' % (server, port, e))
333 print ('*** Failed to connect to %s:%d: %r' % (server, port, e))
333 sys.exit(1)
334 sys.exit(1)
334
335
335 # print ('Now forwarding port %d to %s:%d ...' % (lport, server, rport))
336 # Don't let SIGINT kill the tunnel subprocess
336
337 signal.signal(signal.SIGINT, signal.SIG_IGN)
338
337 try:
339 try:
338 forward_tunnel(lport, remoteip, rport, client.get_transport())
340 forward_tunnel(lport, remoteip, rport, client.get_transport())
339 except KeyboardInterrupt:
341 except KeyboardInterrupt:
340 print ('SIGINT: Port forwarding stopped cleanly')
342 print ('SIGINT: Port forwarding stopped cleanly')
341 sys.exit(0)
343 sys.exit(0)
342 except Exception as e:
344 except Exception as e:
343 print ("Port forwarding stopped uncleanly: %s"%e)
345 print ("Port forwarding stopped uncleanly: %s"%e)
344 sys.exit(255)
346 sys.exit(255)
345
347
346 if sys.platform == 'win32':
348 if sys.platform == 'win32':
347 ssh_tunnel = paramiko_tunnel
349 ssh_tunnel = paramiko_tunnel
348 else:
350 else:
349 ssh_tunnel = openssh_tunnel
351 ssh_tunnel = openssh_tunnel
350
352
351
353
352 __all__ = ['tunnel_connection', 'ssh_tunnel', 'openssh_tunnel', 'paramiko_tunnel', 'try_passwordless_ssh']
354 __all__ = ['tunnel_connection', 'ssh_tunnel', 'openssh_tunnel', 'paramiko_tunnel', 'try_passwordless_ssh']
353
355
354
356
General Comments 0
You need to be logged in to leave comments. Login now