2010-10-01 21:06:56 +02:00
|
|
|
import struct, socket, select, errno, re
|
|
|
|
import compat.ssubprocess as ssubprocess
|
2010-05-02 08:14:20 +02:00
|
|
|
import helpers, ssnet, ssh
|
2010-05-02 05:14:42 +02:00
|
|
|
from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
|
2010-05-02 00:03:45 +02:00
|
|
|
from helpers import *
|
|
|
|
|
2010-10-02 02:36:09 +02:00
|
|
|
|
2010-05-02 00:03:45 +02:00
|
|
|
def original_dst(sock):
|
2010-05-05 00:24:43 +02:00
|
|
|
try:
|
|
|
|
SO_ORIGINAL_DST = 80
|
|
|
|
SOCKADDR_MIN = 16
|
|
|
|
sockaddr_in = sock.getsockopt(socket.SOL_IP,
|
|
|
|
SO_ORIGINAL_DST, SOCKADDR_MIN)
|
|
|
|
(proto, port, a,b,c,d) = struct.unpack('!HHBBBB', sockaddr_in[:8])
|
|
|
|
assert(socket.htons(proto) == socket.AF_INET)
|
|
|
|
ip = '%d.%d.%d.%d' % (a,b,c,d)
|
|
|
|
return (ip,port)
|
|
|
|
except socket.error, e:
|
|
|
|
if e.args[0] == errno.ENOPROTOOPT:
|
|
|
|
return sock.getsockname()
|
|
|
|
raise
|
2010-05-02 00:03:45 +02:00
|
|
|
|
|
|
|
|
2010-05-05 04:05:49 +02:00
|
|
|
class FirewallClient:
|
2010-07-15 20:07:01 +02:00
|
|
|
def __init__(self, port, subnets_include, subnets_exclude):
|
2010-05-03 01:29:03 +02:00
|
|
|
self.port = port
|
2010-05-08 02:02:04 +02:00
|
|
|
self.auto_nets = []
|
2010-07-15 20:07:01 +02:00
|
|
|
self.subnets_include = subnets_include
|
|
|
|
self.subnets_exclude = subnets_exclude
|
2010-05-03 02:54:10 +02:00
|
|
|
argvbase = ([sys.argv[0]] +
|
|
|
|
['-v'] * (helpers.verbose or 0) +
|
2010-05-08 02:02:04 +02:00
|
|
|
['--firewall', str(port)])
|
2010-05-03 02:54:10 +02:00
|
|
|
argv_tries = [
|
2010-11-09 08:27:02 +01:00
|
|
|
['sudo', '-p', '[local sudo] Password: '] + argvbase,
|
2010-05-03 02:54:10 +02:00
|
|
|
['su', '-c', ' '.join(argvbase)],
|
|
|
|
argvbase
|
|
|
|
]
|
|
|
|
|
|
|
|
# we can't use stdin/stdout=subprocess.PIPE here, as we normally would,
|
|
|
|
# because stupid Linux 'su' requires that stdin be attached to a tty.
|
|
|
|
# Instead, attach a *bidirectional* socket to its stdout, and use
|
|
|
|
# that for talking in both directions.
|
|
|
|
(s1,s2) = socket.socketpair()
|
|
|
|
def setup():
|
|
|
|
# run in the child process
|
|
|
|
s2.close()
|
|
|
|
e = None
|
2010-11-09 08:27:02 +01:00
|
|
|
if os.getuid() == 0:
|
|
|
|
argv_tries = argv_tries[-1:] # last entry only
|
2010-05-03 02:54:10 +02:00
|
|
|
for argv in argv_tries:
|
|
|
|
try:
|
2010-11-09 08:27:02 +01:00
|
|
|
if argv[0] == 'su':
|
|
|
|
sys.stderr.write('[local su] ')
|
2010-10-01 21:06:56 +02:00
|
|
|
self.p = ssubprocess.Popen(argv, stdout=s1, preexec_fn=setup)
|
2010-05-03 02:54:10 +02:00
|
|
|
e = None
|
|
|
|
break
|
|
|
|
except OSError, e:
|
|
|
|
pass
|
|
|
|
self.argv = argv
|
|
|
|
s1.close()
|
|
|
|
self.pfile = s2.makefile('wb+')
|
|
|
|
if e:
|
2010-05-05 04:05:49 +02:00
|
|
|
log('Spawning firewall manager: %r\n' % self.argv)
|
2010-05-03 02:54:10 +02:00
|
|
|
raise Fatal(e)
|
|
|
|
line = self.pfile.readline()
|
2010-05-03 01:29:03 +02:00
|
|
|
self.check()
|
|
|
|
if line != 'READY\n':
|
|
|
|
raise Fatal('%r expected READY, got %r' % (self.argv, line))
|
|
|
|
|
|
|
|
def check(self):
|
|
|
|
rv = self.p.poll()
|
|
|
|
if rv:
|
|
|
|
raise Fatal('%r returned %d' % (self.argv, rv))
|
|
|
|
|
|
|
|
def start(self):
|
2010-05-08 02:02:04 +02:00
|
|
|
self.pfile.write('ROUTES\n')
|
2010-07-15 20:07:01 +02:00
|
|
|
for (ip,width) in self.subnets_include+self.auto_nets:
|
|
|
|
self.pfile.write('%d,0,%s\n' % (width, ip))
|
|
|
|
for (ip,width) in self.subnets_exclude:
|
|
|
|
self.pfile.write('%d,1,%s\n' % (width, ip))
|
2010-05-03 02:54:10 +02:00
|
|
|
self.pfile.write('GO\n')
|
|
|
|
self.pfile.flush()
|
|
|
|
line = self.pfile.readline()
|
|
|
|
self.check()
|
|
|
|
if line != 'STARTED\n':
|
|
|
|
raise Fatal('%r expected STARTED, got %r' % (self.argv, line))
|
2010-05-03 01:29:03 +02:00
|
|
|
|
Added new --auto-hosts and --seed-hosts options to the client.
Now if you use --auto-hosts (-H), the client will ask the server to spawn a
hostwatcher to add names. That, in turn, will send names back to the
server, which sends them back to the client, which sends them to the
firewall subprocess, which will write them to /etc/hosts. Whew!
Only the firewall process can write to /etc/hosts, of course, because only
he's running as root.
Since the name discovery process is kind of slow, we cache the names in
~/.sshuttle.hosts on the remote server.
Right now, most of the names are discovered using nmblookup and smbclient,
as well as by reading the existing entries in /etc/hosts. What would really
be nice would be to query active directory or mdns somehow... but I don't
really know how those work, so this is what you get for now :) It's pretty
neat, at least.
2010-05-08 09:03:12 +02:00
|
|
|
def sethostip(self, hostname, ip):
|
|
|
|
assert(not re.search(r'[^-\w]', hostname))
|
|
|
|
assert(not re.search(r'[^0-9.]', ip))
|
|
|
|
self.pfile.write('HOST %s,%s\n' % (hostname, ip))
|
|
|
|
self.pfile.flush()
|
|
|
|
|
2010-05-03 01:29:03 +02:00
|
|
|
def done(self):
|
2010-05-03 02:54:10 +02:00
|
|
|
self.pfile.close()
|
2010-05-03 01:29:03 +02:00
|
|
|
rv = self.p.wait()
|
|
|
|
if rv:
|
|
|
|
raise Fatal('cleanup: %r returned %d' % (self.argv, rv))
|
2010-05-02 03:14:19 +02:00
|
|
|
|
|
|
|
|
2010-11-09 09:17:01 +01:00
|
|
|
def _main(listener, fw, ssh_cmd, remotename, python, seed_hosts, auto_nets):
|
2010-05-02 02:03:50 +02:00
|
|
|
handlers = []
|
2010-11-09 08:59:26 +01:00
|
|
|
if helpers.verbose >= 1:
|
|
|
|
helpers.logprefix = 'c : '
|
|
|
|
else:
|
|
|
|
helpers.logprefix = 'client: '
|
|
|
|
debug1('connecting to server...\n')
|
|
|
|
try:
|
2010-11-09 09:17:01 +01:00
|
|
|
(serverproc, serversock) = ssh.connect(ssh_cmd, remotename, python)
|
2010-11-09 08:59:26 +01:00
|
|
|
except socket.error, e:
|
|
|
|
if e.errno == errno.EPIPE:
|
|
|
|
raise Fatal("failed to establish ssh session")
|
2010-05-02 08:23:42 +02:00
|
|
|
else:
|
2010-11-09 08:59:26 +01:00
|
|
|
raise
|
|
|
|
mux = Mux(serversock, serversock)
|
|
|
|
handlers.append(mux)
|
|
|
|
|
|
|
|
expected = 'SSHUTTLE0001'
|
|
|
|
initstring = serversock.recv(len(expected))
|
|
|
|
|
|
|
|
rv = serverproc.poll()
|
|
|
|
if rv:
|
|
|
|
raise Fatal('server died with error code %d' % rv)
|
2010-05-02 08:23:42 +02:00
|
|
|
|
2010-11-09 08:59:26 +01:00
|
|
|
if initstring != expected:
|
|
|
|
raise Fatal('expected server init string %r; got %r'
|
|
|
|
% (expected, initstring))
|
|
|
|
debug1('connected.\n')
|
2010-05-02 08:23:42 +02:00
|
|
|
|
2010-05-08 02:02:04 +02:00
|
|
|
def onroutes(routestr):
|
|
|
|
if auto_nets:
|
|
|
|
for line in routestr.strip().split('\n'):
|
|
|
|
(ip,width) = line.split(',', 1)
|
|
|
|
fw.auto_nets.append((ip,int(width)))
|
|
|
|
|
|
|
|
# we definitely want to do this *after* starting ssh, or we might end
|
|
|
|
# up intercepting the ssh connection!
|
|
|
|
#
|
|
|
|
# Moreover, now that we have the --auto-nets option, we have to wait
|
|
|
|
# for the server to send us that message anyway. Even if we haven't
|
|
|
|
# set --auto-nets, we might as well wait for the message first, then
|
|
|
|
# ignore its contents.
|
|
|
|
mux.got_routes = None
|
|
|
|
fw.start()
|
|
|
|
mux.got_routes = onroutes
|
2010-05-02 05:14:42 +02:00
|
|
|
|
Added new --auto-hosts and --seed-hosts options to the client.
Now if you use --auto-hosts (-H), the client will ask the server to spawn a
hostwatcher to add names. That, in turn, will send names back to the
server, which sends them back to the client, which sends them to the
firewall subprocess, which will write them to /etc/hosts. Whew!
Only the firewall process can write to /etc/hosts, of course, because only
he's running as root.
Since the name discovery process is kind of slow, we cache the names in
~/.sshuttle.hosts on the remote server.
Right now, most of the names are discovered using nmblookup and smbclient,
as well as by reading the existing entries in /etc/hosts. What would really
be nice would be to query active directory or mdns somehow... but I don't
really know how those work, so this is what you get for now :) It's pretty
neat, at least.
2010-05-08 09:03:12 +02:00
|
|
|
def onhostlist(hostlist):
|
|
|
|
debug2('got host list: %r\n' % hostlist)
|
|
|
|
for line in hostlist.strip().split():
|
|
|
|
if line:
|
|
|
|
name,ip = line.split(',', 1)
|
|
|
|
fw.sethostip(name, ip)
|
|
|
|
mux.got_host_list = onhostlist
|
|
|
|
|
2010-05-02 02:03:50 +02:00
|
|
|
def onaccept():
|
|
|
|
sock,srcip = listener.accept()
|
|
|
|
dstip = original_dst(sock)
|
2011-01-01 05:53:42 +01:00
|
|
|
debug1('Accept: %s:%r -> %s:%r.\n' % (srcip[0],srcip[1],
|
|
|
|
dstip[0],dstip[1]))
|
2010-05-05 00:24:43 +02:00
|
|
|
if dstip == listener.getsockname():
|
2010-05-02 08:14:20 +02:00
|
|
|
debug1("-- ignored: that's my address!\n")
|
2010-05-02 02:20:54 +02:00
|
|
|
sock.close()
|
|
|
|
return
|
2010-11-09 08:59:26 +01:00
|
|
|
chan = mux.next_channel()
|
|
|
|
mux.send(chan, ssnet.CMD_CONNECT, '%s,%s' % dstip)
|
|
|
|
outwrap = MuxWrapper(mux, chan)
|
2010-05-02 05:32:30 +02:00
|
|
|
handlers.append(Proxy(SockWrapper(sock, sock), outwrap))
|
2010-05-02 02:03:50 +02:00
|
|
|
handlers.append(Handler([listener], onaccept))
|
Added new --auto-hosts and --seed-hosts options to the client.
Now if you use --auto-hosts (-H), the client will ask the server to spawn a
hostwatcher to add names. That, in turn, will send names back to the
server, which sends them back to the client, which sends them to the
firewall subprocess, which will write them to /etc/hosts. Whew!
Only the firewall process can write to /etc/hosts, of course, because only
he's running as root.
Since the name discovery process is kind of slow, we cache the names in
~/.sshuttle.hosts on the remote server.
Right now, most of the names are discovered using nmblookup and smbclient,
as well as by reading the existing entries in /etc/hosts. What would really
be nice would be to query active directory or mdns somehow... but I don't
really know how those work, so this is what you get for now :) It's pretty
neat, at least.
2010-05-08 09:03:12 +02:00
|
|
|
|
|
|
|
if seed_hosts != None:
|
|
|
|
debug1('seed_hosts: %r\n' % seed_hosts)
|
|
|
|
mux.send(0, ssnet.CMD_HOST_REQ, '\n'.join(seed_hosts))
|
2010-05-02 02:03:50 +02:00
|
|
|
|
|
|
|
while 1:
|
2010-11-09 08:59:26 +01:00
|
|
|
rv = serverproc.poll()
|
|
|
|
if rv:
|
|
|
|
raise Fatal('server died with error code %d' % rv)
|
2010-05-02 06:52:06 +02:00
|
|
|
|
2010-10-02 02:36:09 +02:00
|
|
|
ssnet.runonce(handlers, mux)
|
2010-11-09 08:59:26 +01:00
|
|
|
mux.callback()
|
|
|
|
mux.check_fullness()
|
2010-05-02 03:30:59 +02:00
|
|
|
|
|
|
|
|
2010-11-09 09:17:01 +01:00
|
|
|
def main(listenip, ssh_cmd, remotename, python, seed_hosts, auto_nets,
|
2010-07-15 20:07:01 +02:00
|
|
|
subnets_include, subnets_exclude):
|
2010-05-02 08:14:20 +02:00
|
|
|
debug1('Starting sshuttle proxy.\n')
|
2010-05-02 03:30:59 +02:00
|
|
|
listener = socket.socket()
|
|
|
|
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
2010-05-02 03:50:43 +02:00
|
|
|
if listenip[1]:
|
|
|
|
ports = [listenip[1]]
|
|
|
|
else:
|
2010-05-05 00:24:43 +02:00
|
|
|
ports = xrange(12300,9000,-1)
|
2010-05-02 03:50:43 +02:00
|
|
|
last_e = None
|
|
|
|
bound = False
|
2010-05-02 08:14:20 +02:00
|
|
|
debug2('Binding:')
|
2010-05-02 03:50:43 +02:00
|
|
|
for port in ports:
|
2010-05-02 08:14:20 +02:00
|
|
|
debug2(' %d' % port)
|
2010-05-02 03:50:43 +02:00
|
|
|
try:
|
|
|
|
listener.bind((listenip[0], port))
|
|
|
|
bound = True
|
|
|
|
break
|
|
|
|
except socket.error, e:
|
|
|
|
last_e = e
|
2010-05-02 08:14:20 +02:00
|
|
|
debug2('\n')
|
2010-05-02 03:50:43 +02:00
|
|
|
if not bound:
|
|
|
|
assert(last_e)
|
|
|
|
raise last_e
|
2010-05-02 03:30:59 +02:00
|
|
|
listener.listen(10)
|
2010-05-02 03:50:43 +02:00
|
|
|
listenip = listener.getsockname()
|
2010-05-02 08:14:20 +02:00
|
|
|
debug1('Listening on %r.\n' % (listenip,))
|
2010-05-02 03:30:59 +02:00
|
|
|
|
2010-07-15 20:07:01 +02:00
|
|
|
fw = FirewallClient(listenip[1], subnets_include, subnets_exclude)
|
2010-05-03 01:29:03 +02:00
|
|
|
|
2010-05-02 03:30:59 +02:00
|
|
|
try:
|
2010-11-09 09:17:01 +01:00
|
|
|
return _main(listener, fw, ssh_cmd, remotename,
|
|
|
|
python, seed_hosts, auto_nets)
|
2010-05-02 03:30:59 +02:00
|
|
|
finally:
|
2010-05-05 04:05:49 +02:00
|
|
|
fw.done()
|