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