/
tunnel
executable file
·368 lines (325 loc) · 14.4 KB
/
tunnel
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
#!/usr/bin/env python
# Copyright Anne Archibald 2011
# based on code from paramiko
# This code is licensed under the LGPL
import sys, getpass
import socket, select
import SocketServer
import threading
import time
import struct
from optparse import OptionParser
from paramiko import SSHClient, AutoAddPolicy, Transport
from paramiko.resource import ResourceManager
SSH_PORT=22
def verbose(s):
if g_verbose:
print s
def user_machine_port(m):
if '@' in m:
u, r = m.split('@')
else:
u, r = None, m
if ':' in m:
m, p = r.split(':')
else:
m, p = r, SSH_PORT
return u, m, int(p)
class SSHSockClient(SSHClient):
"""subclass paramiko.client.SSHClient that uses an existing socket-like object
Our base class (SSHClient) always creates a new socket
object. This subclass lets us re-use an existing socket-like
object."""
def connect(self, hostname,
port=SSH_PORT, username=None,
password=None, pkey=None,
key_filename=None,
timeout=None,
allow_agent=True,
look_for_keys=True,
compress=False,
sock=None):
"""
Connect to a socket connected SSH server and authenticate to the
server. The server's host key
is checked against the system host keys (see L{load_system_host_keys})
and any local host keys (L{load_host_keys}). If the server's hostname
is not found in either set of host keys, the missing host key policy
is used (see L{set_missing_host_key_policy}). The default policy is
to reject the key and raise an L{SSHException}.
Authentication is attempted in the following order of priority:
- The C{pkey} or C{key_filename} passed in (if any)
- Any key we can find through an SSH agent
- Any "id_rsa" or "id_dsa" key discoverable in C{~/.ssh/}
- Plain username/password auth, if a password was given
If a private key requires a password to unlock it, and a password is
passed in, that password will be used to attempt to unlock the key.
@param hostname: the server to connect to
@type hostname: str
@param port: the server port to connect to
@type port: int
@param username: the username to authenticate as (defaults to the
current local username)
@type username: str
@param password: a password to use for authentication or for unlocking
a private key
@type password: str
@param pkey: an optional private key to use for authentication
@type pkey: L{PKey}
@param key_filename: the filename, or list of filenames, of optional
private key(s) to try for authentication
@type key_filename: str or list(str)
@param timeout: an optional timeout (in seconds) for the TCP connect
@type timeout: float
@param allow_agent: set to False to disable connecting to the SSH agent
@type allow_agent: bool
@param look_for_keys: set to False to disable searching for discoverable
private key files in C{~/.ssh/}
@type look_for_keys: bool
@param compress: set to True to turn on compression
@type compress: bool
@param sock: the socket to connect to
@type sock: socket-like object
@raise BadHostKeyException: if the server's host key could not be
verified
@raise AuthenticationException: if authentication failed
@raise SSHException: if there was any other error connecting or
establishing an SSH session
"""
if sock is None:
for (family, socktype, proto, canonname, sockaddr) in socket.getaddrinfo(hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM):
if socktype == socket.SOCK_STREAM:
af = family
addr = sockaddr
break
else:
# some OS like AIX don't indicate SOCK_STREAM support, so just guess. :(
af, _, _, _, addr = socket.getaddrinfo(hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM)
sock = socket.socket(af, socket.SOCK_STREAM)
if timeout is not None:
try:
sock.settimeout(timeout)
except:
pass
sock.connect(addr)
t = self._transport = Transport(sock)
t.use_compression(compress=compress)
if self._log_channel is not None:
t.set_log_channel(self._log_channel)
t.start_client()
ResourceManager.register(self, t)
server_key = t.get_remote_server_key()
keytype = server_key.get_name()
if port == SSH_PORT:
server_hostkey_name = hostname
else:
server_hostkey_name = "[%s]:%d" % (hostname, port)
our_server_key = self._system_host_keys.get(server_hostkey_name, {}).get(keytype, None)
if our_server_key is None:
our_server_key = self._host_keys.get(server_hostkey_name, {}).get(keytype, None)
if our_server_key is None:
# will raise exception if the key is rejected; let that fall out
self._policy.missing_host_key(self, server_hostkey_name, server_key)
# if the callback returns, assume the key is ok
our_server_key = server_key
if server_key != our_server_key:
raise BadHostKeyException(hostname, server_key, our_server_key)
if username is None:
username = getpass.getuser()
if key_filename is None:
key_filenames = []
elif isinstance(key_filename, (str, unicode)):
key_filenames = [ key_filename ]
else:
key_filenames = key_filename
self._auth(username, password, pkey, key_filenames, allow_agent, look_for_keys)
def setup_tunnel(remote_machine_list):
clients = []
for m in remote_machine_list:
user, machine, port = user_machine_port(m)
if clients:
verbose("opening channel to %s" % m)
t = clients[-1].get_transport()
# I don't think these lport numbers necessarily mean anything
ch = t.open_channel('direct-tcpip',
dest_addr=(machine,port),
src_addr=('localhost',0))
else:
ch = None
verbose("starting tunneled connection")
c = SSHSockClient()
clients.append(c)
c.load_system_host_keys()
c.set_missing_host_key_policy(AutoAddPolicy())
kwargs=dict(sock=ch, hostname=machine, username=user, port=port,
compress=len(clients)==len(remote_machine_list))
verbose(" arguments to connect(): "+str(kwargs))
c.connect(sock=ch, hostname=machine, username=user, port=port,
compress=len(clients)==len(remote_machine_list))
verbose("Connected to %s" % m)
clients[-1].get_transport().set_keepalive(120)
return clients
class ForwardServer (SocketServer.ThreadingTCPServer):
daemon_threads = True
allow_reuse_address = True
class Handler (SocketServer.BaseRequestHandler):
def handle(self):
try:
chan = self.ssh_transport.open_channel('direct-tcpip',
(self.chain_host, self.chain_port),
self.request.getpeername())
except Exception, e:
verbose('Incoming request to %s:%d failed: %s' % (self.chain_host,
self.chain_port,
repr(e)))
return
if chan is None:
verbose('Incoming request to %s:%d was rejected by the SSH server.' %
(self.chain_host, self.chain_port))
return
verbose('Connected! Tunnel open %r -> %r -> %r' % (self.request.getpeername(),
chan.getpeername(), (self.chain_host, self.chain_port)))
while True:
r, w, x = select.select([self.request, chan], [], [])
if self.request in r:
data = self.request.recv(1024)
if len(data) == 0:
break
chan.send(data)
if chan in r:
data = chan.recv(1024)
if len(data) == 0:
break
self.request.send(data)
chan.close()
verbose('Tunnel closed from %r' % (self.request.getpeername(),))
self.request.close()
class SOCKS4aHandler (SocketServer.BaseRequestHandler):
def handle(self):
data = ""
while True:
r, w, x = select.select([self.request], [], [])
if self.request in r:
data = self.request.recv(1024)
if len(data) == 0:
continue
if data[0]!=chr(4):
# not the right version of SOCKS
verbose('Failed: Not a SOCKS4 connection')
return
if len(data)<8:
# packet too short!?
verbose('Failed: Packet too short')
return
v, request, port, ip = struct.unpack("!BBHI",data[:8])
if request!=1:
verbose('Failed: did not request outgoing connection')
return
user, rest = data[8:].split(chr(0),1)
verbose("User claims to be %s" % repr(user))
if ip<256:
host, rest = rest.split(chr(0),1)
else:
host = socket.inet_ntoa(data[4:8])
data=rest
break
try:
chan = self.ssh_transport.open_channel('direct-tcpip',
(host, port),
self.request.getpeername())
except Exception, e:
verbose('Incoming request to %s:%d failed: %s' % (host,
port,
repr(e)))
self.request.send(struct.pack('!BBHI',0,0x5b,0,0))
return
self.request.send(struct.pack('!BBHI',0,0x5a,0,0))
verbose('Connected! Tunnel open %r -> %r -> %r' %
(self.request.getpeername(),
chan.getpeername(), (host, port)))
if data:
verbose("Dropping trailing characters on SOCKS request: %s" % repr(data))
while True:
r, w, x = select.select([self.request, chan], [], [])
if self.request in r:
data = self.request.recv(1024)
if len(data) == 0:
break
chan.send(data)
if chan in r:
data = chan.recv(1024)
if len(data) == 0:
break
self.request.send(data)
chan.close()
verbose('Tunnel closed from %r' % (self.request.getpeername(),))
self.request.close()
def forward_tunnel(local_port, remote_host, remote_port, transport):
# this is a little convoluted, but lets me configure things for the Handler
# object. (SocketServer doesn't give Handlers any way to access the outer
# server normally.)
class SubHandler (Handler):
chain_host = remote_host
chain_port = remote_port
ssh_transport = transport
s = threading.Thread(target=ForwardServer(('localhost', local_port), SubHandler).serve_forever)
s.setDaemon(True)
s.start()
verbose("started forwarding from %d on localhost to %d on remote host" %
(local_port, remote_port))
def socks(port, transport):
# this is a little convoluted, but lets me configure things for the Handler
# object. (SocketServer doesn't give Handlers any way to access the outer
# server normally.)
class SOCKSSubHandler (SOCKS4aHandler):
ssh_transport = transport
s = threading.Thread(target=ForwardServer(('localhost', port), SOCKSSubHandler).serve_forever)
s.setDaemon(True)
s.start()
verbose("started SOCKS4a forwarding from %d on localhost" % port)
if __name__=='__main__':
parser = OptionParser(
usage="%prog [options] machine_1 machine_2 ... machine_n",
epilog=("Open an SSH connection to each machine_i, "
"tunneled through the connection to "
"machine_(i-1). Forwardings then transfer "
"connections to localhost to connections from "
"machine_n to itself; this way no unencrypted "
"traffic is sent across the network. SOCKS "
"forwarding allows a SOCKS-aware client to connect "
"to arbitrary machines while apparently coming from "
"machine_n. DNS requests may also be handled by the "
"SOCKS server. Note that all ports opened on the "
"local machine are accessible to any user on the "
"local machine, but to no one else."))
parser.add_option("-v", "--verbose", action="store_true",
help="Verbose output.")
parser.add_option("-T", "--taken", action="store_true",
help="Report success if the local port is already taken.")
parser.add_option("-D", "--socks",
metavar="PORT", type=int,
help="Act as a SOCKS4a proxy listening on PORT (to localhost only)")
parser.add_option("-f", "--forward",
action="append", metavar="LOCAL:REMOTE",
help="forward connections from localhost port LOCAL to final host port REMOTE")
options, args = parser.parse_args(sys.argv[1:])
# FIXME: add an option to forward stdin/stdout
global g_verbose
g_verbose = options.verbose
clients = setup_tunnel(args)
for s in options.forward:
l, r = s.split(":")
l, r = int(l), int(r)
try:
forward_tunnel(l, "localhost", r, clients[-1].get_transport())
except socket.error, e:
if not (e.errno==98 and options.taken):
raise
if options.socks:
try:
socks(options.socks, clients[-1].get_transport())
except socket.error, e:
if not (e.errno==98 and options.taken):
raise
while True:
time.sleep(1)