mirror of
https://github.com/sshuttle/sshuttle.git
synced 2025-07-04 16:50:34 +02:00
Compare commits
34 Commits
sshuttle-0
...
sshuttle-0
Author | SHA1 | Date | |
---|---|---|---|
f1b33dab29 | |||
3a25f709e5 | |||
a8b3d69856 | |||
2d4f6a4308 | |||
d435ed837d | |||
2d77403a0b | |||
77cf37e0fa | |||
384d0e7c1d | |||
5a4a2ab7f9 | |||
33efa5ac62 | |||
a2ea5ab455 | |||
680941cb0c | |||
7043195043 | |||
77935bd110 | |||
8fe3592be3 | |||
ba19d9c72d | |||
096bbcc576 | |||
7bd0efd57b | |||
8173925bcd | |||
0cdd72c830 | |||
403a088e07 | |||
4a93d3362d | |||
33a73056ee | |||
4a462258f5 | |||
a5fc93c841 | |||
ea6bb5c255 | |||
2c2bea80bc | |||
7d674e9e37 | |||
a21e8c7a3c | |||
ca14231aae | |||
6c2dc54b9e | |||
5d1390927d | |||
da06286427 | |||
616d0680d1 |
166
README.md
Normal file
166
README.md
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
sshuttle: where transparent proxy meets VPN meets ssh
|
||||||
|
=====================================================
|
||||||
|
|
||||||
|
I just spent an afternoon working on a new kind of VPN. You can get
|
||||||
|
the first release, <a href="http://github.com/apenwarr/sshuttle">sshuttle
|
||||||
|
0.10, on github</a>.
|
||||||
|
|
||||||
|
As far as I know, sshuttle is the only program that solves the following
|
||||||
|
common case:
|
||||||
|
|
||||||
|
- Your client machine (or router) is Linux.
|
||||||
|
|
||||||
|
- You have access to a remote network via ssh.
|
||||||
|
|
||||||
|
- You don't necessarily have admin access on the remote network.
|
||||||
|
|
||||||
|
- The remote network has no VPN, or only stupid/complex VPN
|
||||||
|
protocols (IPsec, PPTP, etc). Or maybe you <i>are</i> the
|
||||||
|
admin and you just got frustrated with the awful state of
|
||||||
|
VPN tools.
|
||||||
|
|
||||||
|
- You don't want to create an ssh port forward for every
|
||||||
|
single host/port on the remote network.
|
||||||
|
|
||||||
|
- You hate openssh's port forwarding because it's randomly
|
||||||
|
slow and/or stupid.
|
||||||
|
|
||||||
|
- You can't use openssh's PermitTunnel feature because
|
||||||
|
it's disabled by default on openssh servers; plus it does
|
||||||
|
TCP-over-TCP, which has terrible performance (see below).
|
||||||
|
|
||||||
|
|
||||||
|
Prerequisites
|
||||||
|
-------------
|
||||||
|
|
||||||
|
- sudo, su, or logged in as root on your client machine.
|
||||||
|
(The server doesn't need admin access.)
|
||||||
|
|
||||||
|
- If you use Linux on your client machine:
|
||||||
|
iptables installed on the client, including at
|
||||||
|
least the iptables DNAT, REDIRECT, and ttl modules.
|
||||||
|
These are installed by default on most Linux distributions.
|
||||||
|
(The server doesn't need iptables and doesn't need to be
|
||||||
|
Linux.)
|
||||||
|
|
||||||
|
- If you use MacOS or BSD on your client machine:
|
||||||
|
Your kernel needs to be compiled with IPFIREWALL_FORWARD
|
||||||
|
(MacOS has this by default) and you need to have ipfw
|
||||||
|
available. (The server doesn't need to be MacOS or BSD.)
|
||||||
|
|
||||||
|
|
||||||
|
This is how you use it:
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
- <tt>git clone git://github.com/apenwarr/sshuttle</tt>
|
||||||
|
on your client and server machines. The server can be
|
||||||
|
any ssh server with python available; the client must
|
||||||
|
be Linux with iptables, and you'll need root or sudo
|
||||||
|
access.
|
||||||
|
|
||||||
|
- <tt>./sshuttle -r username@sshserver 0.0.0.0/0 -vv</tt>
|
||||||
|
|
||||||
|
That's it! Now your local machine can access the remote network as if you
|
||||||
|
were right there! And if your "client" machine is a router, everyone on
|
||||||
|
your local network can make connections to your remote network.
|
||||||
|
|
||||||
|
You don't need to install sshuttle on the remote server;
|
||||||
|
the remote server just needs to have python available.
|
||||||
|
sshuttle will automatically upload and run its source code
|
||||||
|
to the remote python interpreter.
|
||||||
|
|
||||||
|
This creates a transparent proxy server on your local machine for all IP
|
||||||
|
addresses that match 0.0.0.0/0. (You can use more specific IP addresses if
|
||||||
|
you want; use any number of IP addresses or subnets to change which
|
||||||
|
addresses get proxied. Using 0.0.0.0/0 proxies <i>everything</i>, which is
|
||||||
|
interesting if you don't trust the people on your local network.)
|
||||||
|
|
||||||
|
Any TCP session you initiate to one of the proxied IP addresses will be
|
||||||
|
captured by sshuttle and sent over an ssh session to the remote copy of
|
||||||
|
sshuttle, which will then regenerate the connection on that end, and funnel
|
||||||
|
the data back and forth through ssh.
|
||||||
|
|
||||||
|
Fun, right? A poor man's instant VPN, and you don't even have to have
|
||||||
|
admin access on the server.
|
||||||
|
|
||||||
|
|
||||||
|
Theory of Operation
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
sshuttle is not exactly a VPN, and not exactly port forwarding. It's kind
|
||||||
|
of both, and kind of neither.
|
||||||
|
|
||||||
|
It's like a VPN, since it can forward every port on an entire network, not
|
||||||
|
just ports you specify. Conveniently, it lets you use the "real" IP
|
||||||
|
addresses of each host rather than faking port numbers on localhost.
|
||||||
|
|
||||||
|
On the other hand, the way it *works* is more like ssh port forwarding than
|
||||||
|
a VPN. Normally, a VPN forwards your data one packet at a time, and
|
||||||
|
doesn't care about individual connections; ie. it's "stateless" with respect
|
||||||
|
to the traffic. sshuttle is the opposite of stateless; it tracks every
|
||||||
|
single connection.
|
||||||
|
|
||||||
|
You could compare sshuttle to something like the old <a
|
||||||
|
href="http://en.wikipedia.org/wiki/Slirp">Slirp</a> program, which was a
|
||||||
|
userspace TCP/IP implementation that did something similar. But it
|
||||||
|
operated on a packet-by-packet basis on the client side, reassembling the
|
||||||
|
packets on the server side. That worked okay back in the "real live serial
|
||||||
|
port" days, because serial ports had predictable latency and buffering.
|
||||||
|
|
||||||
|
But you can't safely just forward TCP packets over a TCP session (like ssh),
|
||||||
|
because TCP's performance depends fundamentally on packet loss; it
|
||||||
|
<i>must</i> experience packet loss in order to know when to slow down! At
|
||||||
|
the same time, the outer TCP session (ssh, in this case) is a reliable
|
||||||
|
transport, which means that what you forward through the tunnel <i>never</i>
|
||||||
|
experiences packet loss. The ssh session itself experiences packet loss, of
|
||||||
|
course, but TCP fixes it up and ssh (and thus you) never know the
|
||||||
|
difference. But neither does your inner TCP session, and extremely screwy
|
||||||
|
performance ensues.
|
||||||
|
|
||||||
|
sshuttle assembles the TCP stream locally, multiplexes it statefully over
|
||||||
|
an ssh session, and disassembles it back into packets at the other end. So
|
||||||
|
it never ends up doing TCP-over-TCP. It's just data-over-TCP, which is
|
||||||
|
safe.
|
||||||
|
|
||||||
|
|
||||||
|
Useless Trivia
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Back in 1998 (12 years ago! Yikes!), I released the first version of <a
|
||||||
|
href="http://alumnit.ca/wiki/?TunnelVisionReadMe">Tunnel Vision</a>, a
|
||||||
|
semi-intelligent VPN client for Linux. Unfortunately, I made two big mistakes:
|
||||||
|
I implemented the key exchange myself (oops), and I ended up doing
|
||||||
|
TCP-over-TCP (double oops). The resulting program worked okay - and people
|
||||||
|
used it for years - but the performance was always a bit funny. And nobody
|
||||||
|
ever found any security flaws in my key exchange, either, but that doesn't
|
||||||
|
mean anything. :)
|
||||||
|
|
||||||
|
The same year, dcoombs and I also released Fast Forward, a proxy server
|
||||||
|
supporting transparent proxying. Among other things, we used it for
|
||||||
|
automatically splitting traffic across more than one Internet connection (a
|
||||||
|
tool we called "Double Vision").
|
||||||
|
|
||||||
|
I was still in university at the time. A couple years after that, one of my
|
||||||
|
professors was working with some graduate students on the technology that
|
||||||
|
would eventually become <a href="http://www.slipstream.com/">Slipstream
|
||||||
|
Internet Acceleration</a>. He asked me to do a contract for him to build an
|
||||||
|
initial prototype of a transparent proxy server for mobile networks. The
|
||||||
|
idea was similar to sshuttle: if you reassemble and then disassemble the TCP
|
||||||
|
packets, you can reduce latency and improve performance vs. just forwarding
|
||||||
|
the packets over a plain VPN or mobile network. (It's unlikely that any of
|
||||||
|
my code has persisted in the Slipstream product today, but the concept is
|
||||||
|
still pretty cool. I'm still horrified that people use plain TCP on
|
||||||
|
complex mobile networks with crazily variable latency, for which it was
|
||||||
|
never really intended.)
|
||||||
|
|
||||||
|
That project I did for Slipstream was what first gave me the idea to merge
|
||||||
|
the concepts of Fast Forward, Double Vision, and Tunnel Vision into a single
|
||||||
|
program that was the best of all worlds. And here we are, at last, 10 years
|
||||||
|
later. You're welcome.
|
||||||
|
|
||||||
|
--
|
||||||
|
Avery Pennarun <apenwarr@gmail.com>
|
||||||
|
|
||||||
|
Mailing list:
|
||||||
|
Subscribe by sending a message to <sshuttle+subscribe@googlegroups.com>
|
||||||
|
List archives are at: http://groups.google.com/group/sshuttle
|
26
assembler.py
Normal file
26
assembler.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import sys, zlib
|
||||||
|
|
||||||
|
z = zlib.decompressobj()
|
||||||
|
mainmod = sys.modules[__name__]
|
||||||
|
while 1:
|
||||||
|
name = sys.stdin.readline().strip()
|
||||||
|
if name:
|
||||||
|
nbytes = int(sys.stdin.readline())
|
||||||
|
if verbosity >= 2:
|
||||||
|
sys.stderr.write('server: assembling %r (%d bytes)\n'
|
||||||
|
% (name, nbytes))
|
||||||
|
content = z.decompress(sys.stdin.read(nbytes))
|
||||||
|
exec compile(content, name, "exec")
|
||||||
|
|
||||||
|
# FIXME: this crushes everything into a single module namespace,
|
||||||
|
# then makes each of the module names point at this one. Gross.
|
||||||
|
assert(name.endswith('.py'))
|
||||||
|
modname = name[:-3]
|
||||||
|
mainmod.__dict__[modname] = mainmod
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
verbose = verbosity
|
||||||
|
sys.stderr.flush()
|
||||||
|
sys.stdout.flush()
|
||||||
|
main()
|
153
client.py
153
client.py
@ -1,35 +1,105 @@
|
|||||||
import struct, socket, select, subprocess, errno
|
import struct, socket, select, subprocess, errno, re
|
||||||
import helpers, ssnet, ssh
|
import helpers, ssnet, ssh
|
||||||
from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
|
from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
|
||||||
from helpers import *
|
from helpers import *
|
||||||
|
|
||||||
def original_dst(sock):
|
def original_dst(sock):
|
||||||
SO_ORIGINAL_DST = 80
|
try:
|
||||||
SOCKADDR_MIN = 16
|
SO_ORIGINAL_DST = 80
|
||||||
sockaddr_in = sock.getsockopt(socket.SOL_IP, SO_ORIGINAL_DST, SOCKADDR_MIN)
|
SOCKADDR_MIN = 16
|
||||||
(proto, port, a,b,c,d) = struct.unpack('!HHBBBB', sockaddr_in[:8])
|
sockaddr_in = sock.getsockopt(socket.SOL_IP,
|
||||||
assert(socket.htons(proto) == socket.AF_INET)
|
SO_ORIGINAL_DST, SOCKADDR_MIN)
|
||||||
ip = '%d.%d.%d.%d' % (a,b,c,d)
|
(proto, port, a,b,c,d) = struct.unpack('!HHBBBB', sockaddr_in[:8])
|
||||||
return (ip,port)
|
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
|
||||||
|
|
||||||
|
|
||||||
def iptables_setup(port, subnets):
|
class FirewallClient:
|
||||||
subnets_str = ['%s/%d' % (ip,width) for ip,width in subnets]
|
def __init__(self, port, subnets_include, subnets_exclude):
|
||||||
argv = (['sudo', sys.argv[0]] +
|
self.port = port
|
||||||
['-v'] * (helpers.verbose or 0) +
|
self.auto_nets = []
|
||||||
['--iptables', str(port)] + subnets_str)
|
self.subnets_include = subnets_include
|
||||||
rv = subprocess.call(argv)
|
self.subnets_exclude = subnets_exclude
|
||||||
if rv != 0:
|
argvbase = ([sys.argv[0]] +
|
||||||
raise Fatal('%r returned %d' % (argv, rv))
|
['-v'] * (helpers.verbose or 0) +
|
||||||
|
['--firewall', str(port)])
|
||||||
|
argv_tries = [
|
||||||
|
['sudo'] + argvbase,
|
||||||
|
['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
|
||||||
|
for argv in argv_tries:
|
||||||
|
try:
|
||||||
|
self.p = subprocess.Popen(argv, stdout=s1, preexec_fn=setup)
|
||||||
|
e = None
|
||||||
|
break
|
||||||
|
except OSError, e:
|
||||||
|
pass
|
||||||
|
self.argv = argv
|
||||||
|
s1.close()
|
||||||
|
self.pfile = s2.makefile('wb+')
|
||||||
|
if e:
|
||||||
|
log('Spawning firewall manager: %r\n' % self.argv)
|
||||||
|
raise Fatal(e)
|
||||||
|
line = self.pfile.readline()
|
||||||
|
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):
|
||||||
|
self.pfile.write('ROUTES\n')
|
||||||
|
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))
|
||||||
|
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))
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
def done(self):
|
||||||
|
self.pfile.close()
|
||||||
|
rv = self.p.wait()
|
||||||
|
if rv:
|
||||||
|
raise Fatal('cleanup: %r returned %d' % (self.argv, rv))
|
||||||
|
|
||||||
|
|
||||||
def _main(listener, listenport, use_server, remotename, subnets):
|
def _main(listener, fw, use_server, remotename, seed_hosts, auto_nets):
|
||||||
handlers = []
|
handlers = []
|
||||||
if use_server:
|
if use_server:
|
||||||
if helpers.verbose >= 1:
|
if helpers.verbose >= 1:
|
||||||
helpers.logprefix = 'c : '
|
helpers.logprefix = 'c : '
|
||||||
else:
|
else:
|
||||||
helpers.logprefix = 'client: '
|
helpers.logprefix = 'client: '
|
||||||
|
debug1('connecting to server...\n')
|
||||||
(serverproc, serversock) = ssh.connect(remotename)
|
(serverproc, serversock) = ssh.connect(remotename)
|
||||||
mux = Mux(serversock, serversock)
|
mux = Mux(serversock, serversock)
|
||||||
handlers.append(mux)
|
handlers.append(mux)
|
||||||
@ -44,17 +114,39 @@ def _main(listener, listenport, use_server, remotename, subnets):
|
|||||||
if initstring != expected:
|
if initstring != expected:
|
||||||
raise Fatal('expected server init string %r; got %r'
|
raise Fatal('expected server init string %r; got %r'
|
||||||
% (expected, initstring))
|
% (expected, initstring))
|
||||||
|
debug1('connected.\n')
|
||||||
|
|
||||||
# we definitely want to do this *after* starting ssh, or we might end
|
def onroutes(routestr):
|
||||||
# up intercepting the ssh connection!
|
if auto_nets:
|
||||||
iptables_setup(listenport, subnets)
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
def onaccept():
|
def onaccept():
|
||||||
sock,srcip = listener.accept()
|
sock,srcip = listener.accept()
|
||||||
dstip = original_dst(sock)
|
dstip = original_dst(sock)
|
||||||
debug1('Accept: %r:%r -> %r:%r.\n' % (srcip[0],srcip[1],
|
debug1('Accept: %r:%r -> %r:%r.\n' % (srcip[0],srcip[1],
|
||||||
dstip[0],dstip[1]))
|
dstip[0],dstip[1]))
|
||||||
if dstip == sock.getsockname():
|
if dstip == listener.getsockname():
|
||||||
debug1("-- ignored: that's my address!\n")
|
debug1("-- ignored: that's my address!\n")
|
||||||
sock.close()
|
sock.close()
|
||||||
return
|
return
|
||||||
@ -67,6 +159,10 @@ def _main(listener, listenport, use_server, remotename, subnets):
|
|||||||
handlers.append(Proxy(SockWrapper(sock, sock), outwrap))
|
handlers.append(Proxy(SockWrapper(sock, sock), outwrap))
|
||||||
handlers.append(Handler([listener], onaccept))
|
handlers.append(Handler([listener], onaccept))
|
||||||
|
|
||||||
|
if seed_hosts != None:
|
||||||
|
debug1('seed_hosts: %r\n' % seed_hosts)
|
||||||
|
mux.send(0, ssnet.CMD_HOST_REQ, '\n'.join(seed_hosts))
|
||||||
|
|
||||||
while 1:
|
while 1:
|
||||||
if use_server:
|
if use_server:
|
||||||
rv = serverproc.poll()
|
rv = serverproc.poll()
|
||||||
@ -87,16 +183,20 @@ def _main(listener, listenport, use_server, remotename, subnets):
|
|||||||
for s in handlers:
|
for s in handlers:
|
||||||
if s.socks & ready:
|
if s.socks & ready:
|
||||||
s.callback()
|
s.callback()
|
||||||
|
if use_server:
|
||||||
|
mux.callback()
|
||||||
|
mux.check_fullness()
|
||||||
|
|
||||||
|
|
||||||
def main(listenip, use_server, remotename, subnets):
|
def main(listenip, use_server, remotename, seed_hosts, auto_nets,
|
||||||
|
subnets_include, subnets_exclude):
|
||||||
debug1('Starting sshuttle proxy.\n')
|
debug1('Starting sshuttle proxy.\n')
|
||||||
listener = socket.socket()
|
listener = socket.socket()
|
||||||
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
if listenip[1]:
|
if listenip[1]:
|
||||||
ports = [listenip[1]]
|
ports = [listenip[1]]
|
||||||
else:
|
else:
|
||||||
ports = xrange(12300,65536)
|
ports = xrange(12300,9000,-1)
|
||||||
last_e = None
|
last_e = None
|
||||||
bound = False
|
bound = False
|
||||||
debug2('Binding:')
|
debug2('Binding:')
|
||||||
@ -116,7 +216,10 @@ def main(listenip, use_server, remotename, subnets):
|
|||||||
listenip = listener.getsockname()
|
listenip = listener.getsockname()
|
||||||
debug1('Listening on %r.\n' % (listenip,))
|
debug1('Listening on %r.\n' % (listenip,))
|
||||||
|
|
||||||
|
fw = FirewallClient(listenip[1], subnets_include, subnets_exclude)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return _main(listener, listenip[1], use_server, remotename, subnets)
|
return _main(listener, fw, use_server, remotename,
|
||||||
|
seed_hosts, auto_nets)
|
||||||
finally:
|
finally:
|
||||||
iptables_setup(listenip[1], [])
|
fw.done()
|
||||||
|
283
firewall.py
Normal file
283
firewall.py
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
import subprocess, re, errno
|
||||||
|
import helpers
|
||||||
|
from helpers import *
|
||||||
|
|
||||||
|
|
||||||
|
def ipt_chain_exists(name):
|
||||||
|
argv = ['iptables', '-t', 'nat', '-nL']
|
||||||
|
p = subprocess.Popen(argv, stdout = subprocess.PIPE)
|
||||||
|
for line in p.stdout:
|
||||||
|
if line.startswith('Chain %s ' % name):
|
||||||
|
return True
|
||||||
|
rv = p.wait()
|
||||||
|
if rv:
|
||||||
|
raise Fatal('%r returned %d' % (argv, rv))
|
||||||
|
|
||||||
|
|
||||||
|
def ipt(*args):
|
||||||
|
argv = ['iptables', '-t', 'nat'] + list(args)
|
||||||
|
debug1('>> %s\n' % ' '.join(argv))
|
||||||
|
rv = subprocess.call(argv)
|
||||||
|
if rv:
|
||||||
|
raise Fatal('%r returned %d' % (argv, rv))
|
||||||
|
|
||||||
|
|
||||||
|
# We name the chain based on the transproxy port number so that it's possible
|
||||||
|
# to run multiple copies of sshuttle at the same time. Of course, the
|
||||||
|
# multiple copies shouldn't have overlapping subnets, or only the most-
|
||||||
|
# recently-started one will win (because we use "-I OUTPUT 1" instead of
|
||||||
|
# "-A OUTPUT").
|
||||||
|
def do_iptables(port, subnets):
|
||||||
|
chain = 'sshuttle-%s' % port
|
||||||
|
|
||||||
|
# basic cleanup/setup of chains
|
||||||
|
if ipt_chain_exists(chain):
|
||||||
|
ipt('-D', 'OUTPUT', '-j', chain)
|
||||||
|
ipt('-D', 'PREROUTING', '-j', chain)
|
||||||
|
ipt('-F', chain)
|
||||||
|
ipt('-X', chain)
|
||||||
|
|
||||||
|
if subnets:
|
||||||
|
ipt('-N', chain)
|
||||||
|
ipt('-F', chain)
|
||||||
|
ipt('-I', 'OUTPUT', '1', '-j', chain)
|
||||||
|
ipt('-I', 'PREROUTING', '1', '-j', chain)
|
||||||
|
|
||||||
|
# create new subnet entries. Note that we're sorting in a very
|
||||||
|
# particular order: we need to go from most-specific (largest swidth)
|
||||||
|
# to least-specific, and at any given level of specificity, we want
|
||||||
|
# excludes to come first. That's why the columns are in such a non-
|
||||||
|
# intuitive order.
|
||||||
|
for swidth,sexclude,snet in sorted(subnets, reverse=True):
|
||||||
|
if sexclude:
|
||||||
|
ipt('-A', chain, '-j', 'RETURN',
|
||||||
|
'--dest', '%s/%s' % (snet,swidth),
|
||||||
|
'-p', 'tcp')
|
||||||
|
else:
|
||||||
|
ipt('-A', chain, '-j', 'REDIRECT',
|
||||||
|
'--dest', '%s/%s' % (snet,swidth),
|
||||||
|
'-p', 'tcp',
|
||||||
|
'--to-ports', str(port),
|
||||||
|
'-m', 'ttl', '!', '--ttl', '42' # to prevent infinite loops
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def ipfw_rule_exists(n):
|
||||||
|
argv = ['ipfw', 'list']
|
||||||
|
p = subprocess.Popen(argv, stdout = subprocess.PIPE)
|
||||||
|
for line in p.stdout:
|
||||||
|
if line.startswith('%05d ' % n):
|
||||||
|
if line.find('ipttl 42') < 0 and line.find('established') < 0:
|
||||||
|
raise Fatal('non-sshuttle ipfw rule #%d already exists!' % n)
|
||||||
|
return True
|
||||||
|
rv = p.wait()
|
||||||
|
if rv:
|
||||||
|
raise Fatal('%r returned %d' % (argv, rv))
|
||||||
|
|
||||||
|
|
||||||
|
def sysctl_get(name):
|
||||||
|
argv = ['sysctl', '-n', name]
|
||||||
|
p = subprocess.Popen(argv, stdout = subprocess.PIPE)
|
||||||
|
line = p.stdout.readline()
|
||||||
|
rv = p.wait()
|
||||||
|
if rv:
|
||||||
|
raise Fatal('%r returned %d' % (argv, rv))
|
||||||
|
if not line:
|
||||||
|
raise Fatal('%r returned no data' % (argv,))
|
||||||
|
assert(line[-1] == '\n')
|
||||||
|
return line[:-1]
|
||||||
|
|
||||||
|
|
||||||
|
def _sysctl_set(name, val):
|
||||||
|
argv = ['sysctl', '-w', '%s=%s' % (name, val)]
|
||||||
|
debug1('>> %s\n' % ' '.join(argv))
|
||||||
|
rv = subprocess.call(argv, stdout = open('/dev/null', 'w'))
|
||||||
|
|
||||||
|
|
||||||
|
_oldctls = []
|
||||||
|
def sysctl_set(name, val):
|
||||||
|
oldval = sysctl_get(name)
|
||||||
|
if str(val) != str(oldval):
|
||||||
|
_oldctls.append((name, oldval))
|
||||||
|
return _sysctl_set(name, val)
|
||||||
|
|
||||||
|
|
||||||
|
def ipfw(*args):
|
||||||
|
argv = ['ipfw', '-q'] + list(args)
|
||||||
|
debug1('>> %s\n' % ' '.join(argv))
|
||||||
|
rv = subprocess.call(argv)
|
||||||
|
if rv:
|
||||||
|
raise Fatal('%r returned %d' % (argv, rv))
|
||||||
|
|
||||||
|
|
||||||
|
def do_ipfw(port, subnets):
|
||||||
|
sport = str(port)
|
||||||
|
xsport = str(port+1)
|
||||||
|
|
||||||
|
# cleanup any existing rules
|
||||||
|
if ipfw_rule_exists(port):
|
||||||
|
ipfw('del', sport)
|
||||||
|
|
||||||
|
while _oldctls:
|
||||||
|
(name,oldval) = _oldctls.pop()
|
||||||
|
_sysctl_set(name, oldval)
|
||||||
|
|
||||||
|
if subnets:
|
||||||
|
sysctl_set('net.inet.ip.fw.enable', 1)
|
||||||
|
sysctl_set('net.inet.ip.forwarding', 1)
|
||||||
|
|
||||||
|
ipfw('add', sport, 'accept', 'ip',
|
||||||
|
'from', 'any', 'to', 'any', 'established')
|
||||||
|
|
||||||
|
# create new subnet entries
|
||||||
|
for swidth,sexclude,snet in sorted(subnets, reverse=True):
|
||||||
|
if sexclude:
|
||||||
|
ipfw('add', sport, 'skipto', xsport,
|
||||||
|
'log', 'tcp',
|
||||||
|
'from', 'any', 'to', '%s/%s' % (snet,swidth))
|
||||||
|
else:
|
||||||
|
ipfw('add', sport, 'fwd', '127.0.0.1,%d' % port,
|
||||||
|
'log', 'tcp',
|
||||||
|
'from', 'any', 'to', '%s/%s' % (snet,swidth),
|
||||||
|
'not', 'ipttl', '42')
|
||||||
|
|
||||||
|
|
||||||
|
def program_exists(name):
|
||||||
|
paths = (os.getenv('PATH') or os.defpath).split(os.pathsep)
|
||||||
|
for p in paths:
|
||||||
|
fn = '%s/%s' % (p, name)
|
||||||
|
if os.path.exists(fn):
|
||||||
|
return not os.path.isdir(fn) and os.access(fn, os.X_OK)
|
||||||
|
|
||||||
|
hostmap = {}
|
||||||
|
def rewrite_etc_hosts(port):
|
||||||
|
HOSTSFILE='/etc/hosts'
|
||||||
|
BAKFILE='%s.sbak' % HOSTSFILE
|
||||||
|
APPEND='# sshuttle-firewall-%d AUTOCREATED' % port
|
||||||
|
old_content = ''
|
||||||
|
st = None
|
||||||
|
try:
|
||||||
|
old_content = open(HOSTSFILE).read()
|
||||||
|
st = os.stat(HOSTSFILE)
|
||||||
|
except IOError, e:
|
||||||
|
if e.errno == errno.ENOENT:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
if old_content.strip() and not os.path.exists(BAKFILE):
|
||||||
|
os.link(HOSTSFILE, BAKFILE)
|
||||||
|
tmpname = "%s.%d.tmp" % (HOSTSFILE, port)
|
||||||
|
f = open(tmpname, 'w')
|
||||||
|
for line in old_content.rstrip().split('\n'):
|
||||||
|
if line.find(APPEND) >= 0:
|
||||||
|
continue
|
||||||
|
f.write('%s\n' % line)
|
||||||
|
for (name,ip) in sorted(hostmap.items()):
|
||||||
|
f.write('%-30s %s\n' % ('%s %s' % (ip,name), APPEND))
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
if st:
|
||||||
|
os.chown(tmpname, st.st_uid, st.st_gid)
|
||||||
|
os.chmod(tmpname, st.st_mode)
|
||||||
|
else:
|
||||||
|
os.chown(tmpname, 0, 0)
|
||||||
|
os.chmod(tmpname, 0644)
|
||||||
|
os.rename(tmpname, HOSTSFILE)
|
||||||
|
|
||||||
|
|
||||||
|
def restore_etc_hosts(port):
|
||||||
|
global hostmap
|
||||||
|
hostmap = {}
|
||||||
|
rewrite_etc_hosts(port)
|
||||||
|
|
||||||
|
|
||||||
|
# This is some voodoo for setting up the kernel's transparent
|
||||||
|
# proxying stuff. If subnets is empty, we just delete our sshuttle rules;
|
||||||
|
# otherwise we delete it, then make them from scratch.
|
||||||
|
#
|
||||||
|
# This code is supposed to clean up after itself by deleting its rules on
|
||||||
|
# exit. In case that fails, it's not the end of the world; future runs will
|
||||||
|
# supercede it in the transproxy list, at least, so the leftover rules
|
||||||
|
# are hopefully harmless.
|
||||||
|
def main(port):
|
||||||
|
assert(port > 0)
|
||||||
|
assert(port <= 65535)
|
||||||
|
|
||||||
|
if os.getuid() != 0:
|
||||||
|
raise Fatal('you must be root (or enable su/sudo) to set the firewall')
|
||||||
|
|
||||||
|
if program_exists('ipfw'):
|
||||||
|
do_it = do_ipfw
|
||||||
|
elif program_exists('iptables'):
|
||||||
|
do_it = do_iptables
|
||||||
|
else:
|
||||||
|
raise Fatal("can't find either ipfw or iptables; check your PATH")
|
||||||
|
|
||||||
|
# because of limitations of the 'su' command, the *real* stdin/stdout
|
||||||
|
# are both attached to stdout initially. Clone stdout into stdin so we
|
||||||
|
# can read from it.
|
||||||
|
os.dup2(1, 0)
|
||||||
|
|
||||||
|
debug1('firewall manager ready.\n')
|
||||||
|
sys.stdout.write('READY\n')
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
# ctrl-c shouldn't be passed along to me. When the main sshuttle dies,
|
||||||
|
# I'll die automatically.
|
||||||
|
os.setsid()
|
||||||
|
|
||||||
|
# we wait until we get some input before creating the rules. That way,
|
||||||
|
# sshuttle can launch us as early as possible (and get sudo password
|
||||||
|
# authentication as early in the startup process as possible).
|
||||||
|
line = sys.stdin.readline(128)
|
||||||
|
if not line:
|
||||||
|
return # parent died; nothing to do
|
||||||
|
|
||||||
|
subnets = []
|
||||||
|
if line != 'ROUTES\n':
|
||||||
|
raise Fatal('firewall: expected ROUTES but got %r' % line)
|
||||||
|
while 1:
|
||||||
|
line = sys.stdin.readline(128)
|
||||||
|
if not line:
|
||||||
|
raise Fatal('firewall: expected route but got %r' % line)
|
||||||
|
elif line == 'GO\n':
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
(width,exclude,ip) = line.strip().split(',', 2)
|
||||||
|
except:
|
||||||
|
raise Fatal('firewall: expected route or GO but got %r' % line)
|
||||||
|
subnets.append((int(width), bool(int(exclude)), ip))
|
||||||
|
|
||||||
|
try:
|
||||||
|
if line:
|
||||||
|
debug1('firewall manager: starting transproxy.\n')
|
||||||
|
do_it(port, subnets)
|
||||||
|
sys.stdout.write('STARTED\n')
|
||||||
|
|
||||||
|
try:
|
||||||
|
sys.stdout.flush()
|
||||||
|
except IOError:
|
||||||
|
# the parent process died for some reason; he's surely been loud
|
||||||
|
# enough, so no reason to report another error
|
||||||
|
return
|
||||||
|
|
||||||
|
# Now we wait until EOF or any other kind of exception. We need
|
||||||
|
# to stay running so that we don't need a *second* password
|
||||||
|
# authentication at shutdown time - that cleanup is important!
|
||||||
|
while 1:
|
||||||
|
line = sys.stdin.readline(128)
|
||||||
|
if line.startswith('HOST '):
|
||||||
|
(name,ip) = line[5:].strip().split(',', 1)
|
||||||
|
hostmap[name] = ip
|
||||||
|
rewrite_etc_hosts(port)
|
||||||
|
elif line:
|
||||||
|
raise Fatal('expected EOF, got %r' % line)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
debug1('firewall manager: undoing changes.\n')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
do_it(port, [])
|
||||||
|
restore_etc_hosts(port)
|
15
helpers.py
15
helpers.py
@ -4,9 +4,14 @@ logprefix = ''
|
|||||||
verbose = 0
|
verbose = 0
|
||||||
|
|
||||||
def log(s):
|
def log(s):
|
||||||
sys.stdout.flush()
|
try:
|
||||||
sys.stderr.write(logprefix + s)
|
sys.stdout.flush()
|
||||||
sys.stderr.flush()
|
sys.stderr.write(logprefix + s)
|
||||||
|
sys.stderr.flush()
|
||||||
|
except IOError:
|
||||||
|
# this could happen if stderr gets forcibly disconnected, eg. because
|
||||||
|
# our tty closes. That sucks, but it's no reason to abort the program.
|
||||||
|
pass
|
||||||
|
|
||||||
def debug1(s):
|
def debug1(s):
|
||||||
if verbose >= 1:
|
if verbose >= 1:
|
||||||
@ -16,6 +21,10 @@ def debug2(s):
|
|||||||
if verbose >= 2:
|
if verbose >= 2:
|
||||||
log(s)
|
log(s)
|
||||||
|
|
||||||
|
def debug3(s):
|
||||||
|
if verbose >= 3:
|
||||||
|
log(s)
|
||||||
|
|
||||||
|
|
||||||
class Fatal(Exception):
|
class Fatal(Exception):
|
||||||
pass
|
pass
|
||||||
|
276
hostwatch.py
Normal file
276
hostwatch.py
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
import subprocess, time, socket, re, select
|
||||||
|
if not globals().get('skip_imports'):
|
||||||
|
import helpers
|
||||||
|
from helpers import *
|
||||||
|
|
||||||
|
POLL_TIME = 60*15
|
||||||
|
NETSTAT_POLL_TIME = 30
|
||||||
|
CACHEFILE=os.path.expanduser('~/.sshuttle.hosts')
|
||||||
|
|
||||||
|
|
||||||
|
_nmb_ok = True
|
||||||
|
_smb_ok = True
|
||||||
|
hostnames = {}
|
||||||
|
queue = {}
|
||||||
|
null = open('/dev/null', 'rb+')
|
||||||
|
|
||||||
|
|
||||||
|
def _is_ip(s):
|
||||||
|
return re.match(r'\d+\.\d+\.\d+\.\d+$', s)
|
||||||
|
|
||||||
|
|
||||||
|
def write_host_cache():
|
||||||
|
tmpname = '%s.%d.tmp' % (CACHEFILE, os.getpid())
|
||||||
|
try:
|
||||||
|
f = open(tmpname, 'wb')
|
||||||
|
for name,ip in sorted(hostnames.items()):
|
||||||
|
f.write('%s,%s\n' % (name, ip))
|
||||||
|
f.close()
|
||||||
|
os.rename(tmpname, CACHEFILE)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
os.unlink(tmpname)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def read_host_cache():
|
||||||
|
try:
|
||||||
|
f = open(CACHEFILE)
|
||||||
|
except IOError, e:
|
||||||
|
if e.errno == errno.ENOENT:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
for line in f:
|
||||||
|
words = line.strip().split(',')
|
||||||
|
if len(words) == 2:
|
||||||
|
(name,ip) = words
|
||||||
|
name = re.sub(r'[^-\w]', '-', name).strip()
|
||||||
|
ip = re.sub(r'[^0-9.]', '', ip).strip()
|
||||||
|
if name and ip:
|
||||||
|
found_host(name, ip)
|
||||||
|
|
||||||
|
|
||||||
|
def found_host(hostname, ip):
|
||||||
|
hostname = re.sub(r'\..*', '', hostname)
|
||||||
|
hostname = re.sub(r'[^-\w]', '_', hostname)
|
||||||
|
if (ip.startswith('127.') or ip.startswith('255.')
|
||||||
|
or hostname == 'localhost'):
|
||||||
|
return
|
||||||
|
oldip = hostnames.get(hostname)
|
||||||
|
if oldip != ip:
|
||||||
|
hostnames[hostname] = ip
|
||||||
|
debug1('Found: %s: %s\n' % (hostname, ip))
|
||||||
|
sys.stdout.write('%s,%s\n' % (hostname, ip))
|
||||||
|
write_host_cache()
|
||||||
|
|
||||||
|
|
||||||
|
def _check_etc_hosts():
|
||||||
|
debug2(' > hosts\n')
|
||||||
|
for line in open('/etc/hosts'):
|
||||||
|
line = re.sub(r'#.*', '', line)
|
||||||
|
words = line.strip().split()
|
||||||
|
if not words:
|
||||||
|
continue
|
||||||
|
ip = words[0]
|
||||||
|
names = words[1:]
|
||||||
|
if _is_ip(ip):
|
||||||
|
debug3('< %s %r\n' % (ip, names))
|
||||||
|
for n in names:
|
||||||
|
check_host(n)
|
||||||
|
found_host(n, ip)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_revdns(ip):
|
||||||
|
debug2(' > rev: %s\n' % ip)
|
||||||
|
try:
|
||||||
|
r = socket.gethostbyaddr(ip)
|
||||||
|
debug3('< %s\n' % r[0])
|
||||||
|
check_host(r[0])
|
||||||
|
found_host(r[0], ip)
|
||||||
|
except socket.herror, e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _check_dns(hostname):
|
||||||
|
debug2(' > dns: %s\n' % hostname)
|
||||||
|
try:
|
||||||
|
ip = socket.gethostbyname(hostname)
|
||||||
|
debug3('< %s\n' % ip)
|
||||||
|
check_host(ip)
|
||||||
|
found_host(hostname, ip)
|
||||||
|
except socket.gaierror, e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _check_netstat():
|
||||||
|
debug2(' > netstat\n')
|
||||||
|
argv = ['netstat', '-n']
|
||||||
|
try:
|
||||||
|
p = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=null)
|
||||||
|
content = p.stdout.read()
|
||||||
|
p.wait()
|
||||||
|
except OSError, e:
|
||||||
|
log('%r failed: %r\n' % (argv, e))
|
||||||
|
return
|
||||||
|
|
||||||
|
for ip in re.findall(r'\d+\.\d+\.\d+\.\d+', content):
|
||||||
|
debug3('< %s\n' % ip)
|
||||||
|
check_host(ip)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_smb(hostname):
|
||||||
|
return
|
||||||
|
global _smb_ok
|
||||||
|
if not _smb_ok:
|
||||||
|
return
|
||||||
|
argv = ['smbclient', '-U', '%', '-L', hostname]
|
||||||
|
debug2(' > smb: %s\n' % hostname)
|
||||||
|
try:
|
||||||
|
p = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=null)
|
||||||
|
lines = p.stdout.readlines()
|
||||||
|
p.wait()
|
||||||
|
except OSError, e:
|
||||||
|
log('%r failed: %r\n' % (argv, e))
|
||||||
|
_smb_ok = False
|
||||||
|
return
|
||||||
|
|
||||||
|
lines.reverse()
|
||||||
|
|
||||||
|
# junk at top
|
||||||
|
while lines:
|
||||||
|
line = lines.pop().strip()
|
||||||
|
if re.match(r'Server\s+', line):
|
||||||
|
break
|
||||||
|
|
||||||
|
# server list section:
|
||||||
|
# Server Comment
|
||||||
|
# ------ -------
|
||||||
|
while lines:
|
||||||
|
line = lines.pop().strip()
|
||||||
|
if not line or re.match(r'-+\s+-+', line):
|
||||||
|
continue
|
||||||
|
if re.match(r'Workgroup\s+Master', line):
|
||||||
|
break
|
||||||
|
words = line.split()
|
||||||
|
hostname = words[0].lower()
|
||||||
|
debug3('< %s\n' % hostname)
|
||||||
|
check_host(hostname)
|
||||||
|
|
||||||
|
# workgroup list section:
|
||||||
|
# Workgroup Master
|
||||||
|
# --------- ------
|
||||||
|
while lines:
|
||||||
|
line = lines.pop().strip()
|
||||||
|
if re.match(r'-+\s+', line):
|
||||||
|
continue
|
||||||
|
if not line:
|
||||||
|
break
|
||||||
|
words = line.split()
|
||||||
|
(workgroup, hostname) = (words[0].lower(), words[1].lower())
|
||||||
|
debug3('< group(%s) -> %s\n' % (workgroup, hostname))
|
||||||
|
check_host(hostname)
|
||||||
|
check_workgroup(workgroup)
|
||||||
|
|
||||||
|
if lines:
|
||||||
|
assert(0)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_nmb(hostname, is_workgroup, is_master):
|
||||||
|
return
|
||||||
|
global _nmb_ok
|
||||||
|
if not _nmb_ok:
|
||||||
|
return
|
||||||
|
argv = ['nmblookup'] + ['-M']*is_master + ['--', hostname]
|
||||||
|
debug2(' > n%d%d: %s\n' % (is_workgroup, is_master, hostname))
|
||||||
|
try:
|
||||||
|
p = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=null)
|
||||||
|
lines = p.stdout.readlines()
|
||||||
|
rv = p.wait()
|
||||||
|
except OSError, e:
|
||||||
|
log('%r failed: %r\n' % (argv, e))
|
||||||
|
_nmb_ok = False
|
||||||
|
return
|
||||||
|
if rv:
|
||||||
|
log('%r returned %d\n' % (argv, rv))
|
||||||
|
return
|
||||||
|
for line in lines:
|
||||||
|
m = re.match(r'(\d+\.\d+\.\d+\.\d+) (\w+)<\w\w>\n', line)
|
||||||
|
if m:
|
||||||
|
g = m.groups()
|
||||||
|
(ip, name) = (g[0], g[1].lower())
|
||||||
|
debug3('< %s -> %s\n' % (name, ip))
|
||||||
|
if is_workgroup:
|
||||||
|
_enqueue(_check_smb, ip)
|
||||||
|
else:
|
||||||
|
found_host(name, ip)
|
||||||
|
check_host(name)
|
||||||
|
|
||||||
|
|
||||||
|
def check_host(hostname):
|
||||||
|
if _is_ip(hostname):
|
||||||
|
_enqueue(_check_revdns, hostname)
|
||||||
|
else:
|
||||||
|
_enqueue(_check_dns, hostname)
|
||||||
|
_enqueue(_check_smb, hostname)
|
||||||
|
_enqueue(_check_nmb, hostname, False, False)
|
||||||
|
|
||||||
|
|
||||||
|
def check_workgroup(hostname):
|
||||||
|
_enqueue(_check_nmb, hostname, True, False)
|
||||||
|
_enqueue(_check_nmb, hostname, True, True)
|
||||||
|
|
||||||
|
|
||||||
|
def _enqueue(op, *args):
|
||||||
|
t = (op,args)
|
||||||
|
if queue.get(t) == None:
|
||||||
|
queue[t] = 0
|
||||||
|
|
||||||
|
|
||||||
|
def _stdin_still_ok(timeout):
|
||||||
|
r,w,x = select.select([sys.stdin.fileno()], [], [], timeout)
|
||||||
|
if r:
|
||||||
|
b = os.read(sys.stdin.fileno(), 4096)
|
||||||
|
if not b:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def hw_main(seed_hosts):
|
||||||
|
if helpers.verbose >= 2:
|
||||||
|
helpers.logprefix = 'HH: '
|
||||||
|
else:
|
||||||
|
helpers.logprefix = 'hostwatch: '
|
||||||
|
|
||||||
|
read_host_cache()
|
||||||
|
|
||||||
|
_enqueue(_check_etc_hosts)
|
||||||
|
_enqueue(_check_netstat)
|
||||||
|
check_host('localhost')
|
||||||
|
check_host(socket.gethostname())
|
||||||
|
check_workgroup('workgroup')
|
||||||
|
check_workgroup('-')
|
||||||
|
for h in seed_hosts:
|
||||||
|
check_host(h)
|
||||||
|
|
||||||
|
while 1:
|
||||||
|
now = time.time()
|
||||||
|
for t,last_polled in queue.items():
|
||||||
|
(op,args) = t
|
||||||
|
if not _stdin_still_ok(0):
|
||||||
|
break
|
||||||
|
maxtime = POLL_TIME
|
||||||
|
if op == _check_netstat:
|
||||||
|
maxtime = NETSTAT_POLL_TIME
|
||||||
|
if now - last_polled > maxtime:
|
||||||
|
queue[t] = time.time()
|
||||||
|
op(*args)
|
||||||
|
try:
|
||||||
|
sys.stdout.flush()
|
||||||
|
except IOError:
|
||||||
|
break
|
||||||
|
|
||||||
|
# FIXME: use a smarter timeout based on oldest last_polled
|
||||||
|
if not _stdin_still_ok(1):
|
||||||
|
break
|
66
iptables.py
66
iptables.py
@ -1,66 +0,0 @@
|
|||||||
import subprocess, re
|
|
||||||
import helpers
|
|
||||||
from helpers import *
|
|
||||||
|
|
||||||
|
|
||||||
def chain_exists(name):
|
|
||||||
argv = ['iptables', '-t', 'nat', '-nL']
|
|
||||||
p = subprocess.Popen(argv, stdout = subprocess.PIPE)
|
|
||||||
for line in p.stdout:
|
|
||||||
if line.startswith('Chain %s ' % name):
|
|
||||||
return True
|
|
||||||
rv = p.wait()
|
|
||||||
if rv:
|
|
||||||
raise Exception('%r returned %d' % (argv, rv))
|
|
||||||
|
|
||||||
|
|
||||||
def ipt(*args):
|
|
||||||
argv = ['iptables', '-t', 'nat'] + list(args)
|
|
||||||
debug1('>> %s\n' % ' '.join(argv))
|
|
||||||
rv = subprocess.call(argv)
|
|
||||||
if rv:
|
|
||||||
raise Exception('%r returned %d' % (argv, rv))
|
|
||||||
|
|
||||||
|
|
||||||
# This is some iptables voodoo for setting up the Linux kernel's transparent
|
|
||||||
# proxying stuff. If subnets is empty, we just delete our sshuttle chain;
|
|
||||||
# otherwise we delete it, then make it from scratch.
|
|
||||||
#
|
|
||||||
# We name the chain based on the transproxy port number so that it's possible
|
|
||||||
# to run multiple copies of sshuttle at the same time. Of course, the
|
|
||||||
# multiple copies shouldn't have overlapping subnets, or only the most-
|
|
||||||
# recently-started one will win (because we use "-I OUTPUT 1" instead of
|
|
||||||
# "-A OUTPUT").
|
|
||||||
#
|
|
||||||
# sshuttle is supposed to clean up after itself by deleting extra chains on
|
|
||||||
# exit. In case that fails, it's not the end of the world; future runs will
|
|
||||||
# supercede it in the transproxy list, at least, so the leftover iptables
|
|
||||||
# chains are mostly harmless.
|
|
||||||
def main(port, subnets):
|
|
||||||
assert(port > 0)
|
|
||||||
assert(port <= 65535)
|
|
||||||
|
|
||||||
chain = 'sshuttle-%s' % port
|
|
||||||
|
|
||||||
# basic cleanup/setup of chains
|
|
||||||
if chain_exists(chain):
|
|
||||||
ipt('-D', 'OUTPUT', '-j', chain)
|
|
||||||
ipt('-D', 'PREROUTING', '-j', chain)
|
|
||||||
ipt('-F', chain)
|
|
||||||
ipt('-X', chain)
|
|
||||||
|
|
||||||
if subnets:
|
|
||||||
ipt('-N', chain)
|
|
||||||
ipt('-F', chain)
|
|
||||||
ipt('-I', 'OUTPUT', '1', '-j', chain)
|
|
||||||
ipt('-I', 'PREROUTING', '1', '-j', chain)
|
|
||||||
|
|
||||||
# create new subnet entries
|
|
||||||
for snet,swidth in subnets:
|
|
||||||
ipt('-A', chain, '-j', 'REDIRECT',
|
|
||||||
'--dest', '%s/%s' % (snet,swidth),
|
|
||||||
'-p', 'tcp',
|
|
||||||
'--to-ports', str(port),
|
|
||||||
'-m', 'ttl', '!', '--ttl', '42' # to prevent infinite loops
|
|
||||||
)
|
|
||||||
subnets_str = ['%s/%d' % (ip,width) for ip,width in subnets]
|
|
51
main.py
51
main.py
@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
import sys, os, re
|
import sys, os, re
|
||||||
import helpers, options, client, server, iptables
|
import helpers, options, client, server, firewall, hostwatch
|
||||||
from helpers import *
|
from helpers import *
|
||||||
|
|
||||||
|
|
||||||
@ -45,16 +45,21 @@ def parse_ipport(s):
|
|||||||
|
|
||||||
|
|
||||||
optspec = """
|
optspec = """
|
||||||
sshuttle [-l [ip:]port] [-r [username@]sshserver] <subnets...>
|
sshuttle [-l [ip:]port] [-r [username@]sshserver[:port]] <subnets...>
|
||||||
sshuttle --iptables <port> <subnets...>
|
sshuttle --firewall <port> <subnets...>
|
||||||
sshuttle --server
|
sshuttle --server
|
||||||
--
|
--
|
||||||
l,listen= transproxy to this ip address and port number [default=0]
|
l,listen= transproxy to this ip address and port number [default=0]
|
||||||
|
H,auto-hosts scan for remote hostnames and update local /etc/hosts
|
||||||
|
N,auto-nets automatically determine subnets to route
|
||||||
r,remote= ssh hostname (and optional username) of remote sshuttle server
|
r,remote= ssh hostname (and optional username) of remote sshuttle server
|
||||||
|
x,exclude= exclude this subnet (can be used more than once)
|
||||||
v,verbose increase debug message verbosity
|
v,verbose increase debug message verbosity
|
||||||
|
seed-hosts= with -H, use these hostnames for initial scan (comma-separated)
|
||||||
noserver don't use a separate server process (mostly for debugging)
|
noserver don't use a separate server process (mostly for debugging)
|
||||||
server [internal use only]
|
server [internal use only]
|
||||||
iptables [internal use only]
|
firewall [internal use only]
|
||||||
|
hostwatch [internal use only]
|
||||||
"""
|
"""
|
||||||
o = options.Options('sshuttle', optspec)
|
o = options.Options('sshuttle', optspec)
|
||||||
(opt, flags, extra) = o.parse(sys.argv[1:])
|
(opt, flags, extra) = o.parse(sys.argv[1:])
|
||||||
@ -63,25 +68,45 @@ helpers.verbose = opt.verbose
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if opt.server:
|
if opt.server:
|
||||||
|
if len(extra) != 0:
|
||||||
|
o.fatal('no arguments expected')
|
||||||
sys.exit(server.main())
|
sys.exit(server.main())
|
||||||
elif opt.iptables:
|
elif opt.firewall:
|
||||||
if len(extra) < 1:
|
if len(extra) != 1:
|
||||||
o.fatal('at least one argument expected')
|
o.fatal('exactly one argument expected')
|
||||||
sys.exit(iptables.main(int(extra[0]),
|
sys.exit(firewall.main(int(extra[0])))
|
||||||
parse_subnets(extra[1:])))
|
elif opt.hostwatch:
|
||||||
|
sys.exit(hostwatch.hw_main(extra))
|
||||||
else:
|
else:
|
||||||
if len(extra) < 1:
|
if len(extra) < 1 and not opt.auto_nets:
|
||||||
o.fatal('at least one subnet expected')
|
o.fatal('at least one subnet (or -N) expected')
|
||||||
|
includes = extra
|
||||||
|
excludes = ['127.0.0.0/8']
|
||||||
|
for k,v in flags:
|
||||||
|
if k in ('-x','--exclude'):
|
||||||
|
excludes.append(v)
|
||||||
remotename = opt.remote
|
remotename = opt.remote
|
||||||
if remotename == '' or remotename == '-':
|
if remotename == '' or remotename == '-':
|
||||||
remotename = None
|
remotename = None
|
||||||
|
if opt.seed_hosts and not opt.auto_hosts:
|
||||||
|
o.fatal('--seed-hosts only works if you also use -H')
|
||||||
|
if opt.seed_hosts:
|
||||||
|
sh = re.split(r'[\s,]+', (opt.seed_hosts or "").strip())
|
||||||
|
elif opt.auto_hosts:
|
||||||
|
sh = []
|
||||||
|
else:
|
||||||
|
sh = None
|
||||||
sys.exit(client.main(parse_ipport(opt.listen or '0.0.0.0:0'),
|
sys.exit(client.main(parse_ipport(opt.listen or '0.0.0.0:0'),
|
||||||
not opt.noserver,
|
not opt.noserver,
|
||||||
remotename,
|
remotename,
|
||||||
parse_subnets(extra)))
|
sh,
|
||||||
|
opt.auto_nets,
|
||||||
|
parse_subnets(includes),
|
||||||
|
parse_subnets(excludes)))
|
||||||
except Fatal, e:
|
except Fatal, e:
|
||||||
log('fatal: %s\n' % e)
|
log('fatal: %s\n' % e)
|
||||||
sys.exit(99)
|
sys.exit(99)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
log('\nKeyboard interrupt: exiting.\n')
|
log('\n')
|
||||||
|
log('Keyboard interrupt: exiting.\n')
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
152
server.py
152
server.py
@ -1,45 +1,175 @@
|
|||||||
import struct, socket, select
|
import re, struct, socket, select, subprocess, traceback
|
||||||
import ssnet, helpers
|
if not globals().get('skip_imports'):
|
||||||
from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
|
import ssnet, helpers, hostwatch
|
||||||
from helpers import *
|
from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
|
||||||
|
from helpers import *
|
||||||
|
|
||||||
|
|
||||||
|
def _ipmatch(ipstr):
|
||||||
|
if ipstr == 'default':
|
||||||
|
ipstr = '0.0.0.0/0'
|
||||||
|
m = re.match(r'^(\d+(\.\d+(\.\d+(\.\d+)?)?)?)(?:/(\d+))?$', ipstr)
|
||||||
|
if m:
|
||||||
|
g = m.groups()
|
||||||
|
ips = g[0]
|
||||||
|
width = int(g[4] or 32)
|
||||||
|
if g[1] == None:
|
||||||
|
ips += '.0.0.0'
|
||||||
|
width = min(width, 8)
|
||||||
|
elif g[2] == None:
|
||||||
|
ips += '.0.0'
|
||||||
|
width = min(width, 16)
|
||||||
|
elif g[3] == None:
|
||||||
|
ips += '.0'
|
||||||
|
width = min(width, 24)
|
||||||
|
return (struct.unpack('!I', socket.inet_aton(ips))[0], width)
|
||||||
|
|
||||||
|
|
||||||
|
def _ipstr(ip, width):
|
||||||
|
if width >= 32:
|
||||||
|
return ip
|
||||||
|
else:
|
||||||
|
return "%s/%d" % (ip, width)
|
||||||
|
|
||||||
|
|
||||||
|
def _maskbits(netmask):
|
||||||
|
if not netmask:
|
||||||
|
return 32
|
||||||
|
for i in range(32):
|
||||||
|
if netmask[0] & (1<<i):
|
||||||
|
return 32-i
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _list_routes():
|
||||||
|
argv = ['netstat', '-rn']
|
||||||
|
p = subprocess.Popen(argv, stdout=subprocess.PIPE)
|
||||||
|
routes = []
|
||||||
|
for line in p.stdout:
|
||||||
|
cols = re.split(r'\s+', line)
|
||||||
|
ipw = _ipmatch(cols[0])
|
||||||
|
if not ipw:
|
||||||
|
continue # some lines won't be parseable; never mind
|
||||||
|
maskw = _ipmatch(cols[2]) # linux only
|
||||||
|
mask = _maskbits(maskw) # returns 32 if maskw is null
|
||||||
|
width = min(ipw[1], mask)
|
||||||
|
ip = ipw[0] & (((1<<width)-1) << (32-width))
|
||||||
|
routes.append((socket.inet_ntoa(struct.pack('!I', ip)), width))
|
||||||
|
rv = p.wait()
|
||||||
|
if rv != 0:
|
||||||
|
raise Fatal('%r returned %d' % (argv, rv))
|
||||||
|
return routes
|
||||||
|
|
||||||
|
|
||||||
|
def list_routes():
|
||||||
|
for (ip,width) in _list_routes():
|
||||||
|
if not ip.startswith('0.') and not ip.startswith('127.'):
|
||||||
|
yield (ip,width)
|
||||||
|
|
||||||
|
|
||||||
|
def _exc_dump():
|
||||||
|
exc_info = sys.exc_info()
|
||||||
|
return ''.join(traceback.format_exception(*exc_info))
|
||||||
|
|
||||||
|
|
||||||
|
def start_hostwatch(seed_hosts):
|
||||||
|
s1,s2 = socket.socketpair()
|
||||||
|
pid = os.fork()
|
||||||
|
if not pid:
|
||||||
|
# child
|
||||||
|
rv = 99
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
s2.close()
|
||||||
|
os.dup2(s1.fileno(), 1)
|
||||||
|
os.dup2(s1.fileno(), 0)
|
||||||
|
s1.close()
|
||||||
|
rv = hostwatch.hw_main(seed_hosts) or 0
|
||||||
|
except Exception, e:
|
||||||
|
log('%s\n' % _exc_dump())
|
||||||
|
rv = 98
|
||||||
|
finally:
|
||||||
|
os._exit(rv)
|
||||||
|
s1.close()
|
||||||
|
return pid,s2
|
||||||
|
|
||||||
|
|
||||||
|
class Hostwatch:
|
||||||
|
def __init__(self):
|
||||||
|
self.pid = 0
|
||||||
|
self.sock = None
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
# synchronization header
|
|
||||||
sys.stdout.write('SSHUTTLE0001')
|
|
||||||
sys.stdout.flush()
|
|
||||||
|
|
||||||
if helpers.verbose >= 1:
|
if helpers.verbose >= 1:
|
||||||
helpers.logprefix = ' s: '
|
helpers.logprefix = ' s: '
|
||||||
else:
|
else:
|
||||||
helpers.logprefix = 'server: '
|
helpers.logprefix = 'server: '
|
||||||
|
|
||||||
|
routes = list(list_routes())
|
||||||
|
debug1('available routes:\n')
|
||||||
|
for r in routes:
|
||||||
|
debug1(' %s/%d\n' % r)
|
||||||
|
|
||||||
|
# synchronization header
|
||||||
|
sys.stdout.write('SSHUTTLE0001')
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
handlers = []
|
handlers = []
|
||||||
mux = Mux(socket.fromfd(sys.stdin.fileno(),
|
mux = Mux(socket.fromfd(sys.stdin.fileno(),
|
||||||
socket.AF_INET, socket.SOCK_STREAM),
|
socket.AF_INET, socket.SOCK_STREAM),
|
||||||
socket.fromfd(sys.stdout.fileno(),
|
socket.fromfd(sys.stdout.fileno(),
|
||||||
socket.AF_INET, socket.SOCK_STREAM))
|
socket.AF_INET, socket.SOCK_STREAM))
|
||||||
handlers.append(mux)
|
handlers.append(mux)
|
||||||
|
routepkt = ''.join('%s,%d\n' % r
|
||||||
|
for r in routes)
|
||||||
|
mux.send(0, ssnet.CMD_ROUTES, routepkt)
|
||||||
|
|
||||||
|
hw = Hostwatch()
|
||||||
|
|
||||||
|
def hostwatch_ready():
|
||||||
|
assert(hw.pid)
|
||||||
|
content = hw.sock.recv(4096)
|
||||||
|
if content:
|
||||||
|
mux.send(0, ssnet.CMD_HOST_LIST, content)
|
||||||
|
else:
|
||||||
|
raise Fatal('hostwatch process died')
|
||||||
|
|
||||||
|
def got_host_req(data):
|
||||||
|
if not hw.pid:
|
||||||
|
(hw.pid,hw.sock) = start_hostwatch(data.strip().split())
|
||||||
|
handlers.append(Handler(socks = [hw.sock],
|
||||||
|
callback = hostwatch_ready))
|
||||||
|
mux.got_host_req = got_host_req
|
||||||
|
|
||||||
def new_channel(channel, data):
|
def new_channel(channel, data):
|
||||||
(dstip,dstport) = data.split(',', 1)
|
(dstip,dstport) = data.split(',', 1)
|
||||||
dstport = int(dstport)
|
dstport = int(dstport)
|
||||||
outwrap = ssnet.connect_dst(dstip,dstport)
|
outwrap = ssnet.connect_dst(dstip,dstport)
|
||||||
handlers.append(Proxy(MuxWrapper(mux, channel), outwrap))
|
handlers.append(Proxy(MuxWrapper(mux, channel), outwrap))
|
||||||
|
|
||||||
mux.new_channel = new_channel
|
mux.new_channel = new_channel
|
||||||
|
|
||||||
while mux.ok:
|
while mux.ok:
|
||||||
|
if hw.pid:
|
||||||
|
(rpid, rv) = os.waitpid(hw.pid, os.WNOHANG)
|
||||||
|
if rpid:
|
||||||
|
raise Fatal('hostwatch exited unexpectedly: code 0x%04x\n' % rv)
|
||||||
|
|
||||||
r = set()
|
r = set()
|
||||||
w = set()
|
w = set()
|
||||||
x = set()
|
x = set()
|
||||||
handlers = filter(lambda s: s.ok, handlers)
|
handlers = filter(lambda s: s.ok, handlers)
|
||||||
for s in handlers:
|
for s in handlers:
|
||||||
s.pre_select(r,w,x)
|
s.pre_select(r,w,x)
|
||||||
debug2('Waiting: %d[%d,%d,%d]...\n'
|
debug2('Waiting: %d[%d,%d,%d] (fullness=%d/%d)...\n'
|
||||||
% (len(handlers), len(r), len(w), len(x)))
|
% (len(handlers), len(r), len(w), len(x),
|
||||||
|
mux.fullness, mux.too_full))
|
||||||
(r,w,x) = select.select(r,w,x)
|
(r,w,x) = select.select(r,w,x)
|
||||||
#log('r=%r w=%r x=%r\n' % (r,w,x))
|
#log('r=%r w=%r x=%r\n' % (r,w,x))
|
||||||
ready = set(r) | set(w) | set(x)
|
ready = set(r) | set(w) | set(x)
|
||||||
for s in handlers:
|
for s in handlers:
|
||||||
|
#debug2('check: %r: %r\n' % (s, s.socks & ready))
|
||||||
if s.socks & ready:
|
if s.socks & ready:
|
||||||
s.callback()
|
s.callback()
|
||||||
|
mux.check_fullness()
|
||||||
|
mux.callback()
|
||||||
|
72
ssh.py
72
ssh.py
@ -1,38 +1,68 @@
|
|||||||
import sys, os, re, subprocess, socket
|
import sys, os, re, subprocess, socket, zlib
|
||||||
import helpers
|
import helpers
|
||||||
|
from helpers import *
|
||||||
|
|
||||||
def connect(rhost):
|
|
||||||
|
def readfile(name):
|
||||||
|
basedir = os.path.dirname(os.path.abspath(sys.argv[0]))
|
||||||
|
path = [basedir] + sys.path
|
||||||
|
for d in path:
|
||||||
|
fullname = os.path.join(d, name)
|
||||||
|
if os.path.exists(fullname):
|
||||||
|
return open(fullname, 'rb').read()
|
||||||
|
raise Exception("can't find file %r in any of %r" % (name, path))
|
||||||
|
|
||||||
|
|
||||||
|
def empackage(z, filename):
|
||||||
|
content = z.compress(readfile(filename))
|
||||||
|
content += z.flush(zlib.Z_SYNC_FLUSH)
|
||||||
|
return '%s\n%d\n%s' % (filename,len(content), content)
|
||||||
|
|
||||||
|
|
||||||
|
def connect(rhostport):
|
||||||
main_exe = sys.argv[0]
|
main_exe = sys.argv[0]
|
||||||
nicedir = os.path.split(os.path.abspath(main_exe))[0]
|
l = (rhostport or '').split(':', 1)
|
||||||
nicedir = re.sub(r':', "_", nicedir)
|
rhost = l[0]
|
||||||
|
portl = []
|
||||||
|
if len(l) > 1:
|
||||||
|
portl = ['-p', str(int(l[1]))]
|
||||||
|
|
||||||
if rhost == '-':
|
if rhost == '-':
|
||||||
rhost = None
|
rhost = None
|
||||||
|
|
||||||
|
z = zlib.compressobj(1)
|
||||||
|
content = readfile('assembler.py')
|
||||||
|
content2 = (empackage(z, 'helpers.py') +
|
||||||
|
empackage(z, 'ssnet.py') +
|
||||||
|
empackage(z, 'hostwatch.py') +
|
||||||
|
empackage(z, 'server.py') +
|
||||||
|
"\n")
|
||||||
|
|
||||||
|
pyscript = r"""
|
||||||
|
import sys;
|
||||||
|
skip_imports=1;
|
||||||
|
verbosity=%d;
|
||||||
|
exec compile(sys.stdin.read(%d), "assembler.py", "exec")
|
||||||
|
""" % (helpers.verbose or 0, len(content))
|
||||||
|
pyscript = re.sub(r'\s+', ' ', pyscript.strip())
|
||||||
|
|
||||||
|
|
||||||
if not rhost:
|
if not rhost:
|
||||||
argv = ['sshuttle', '--server'] + ['-v']*(helpers.verbose or 0)
|
argv = ['python', '-c', pyscript]
|
||||||
else:
|
else:
|
||||||
# WARNING: shell quoting security holes are possible here, so we
|
argv = ['ssh'] + portl + [rhost, '--', "python -c '%s'" % pyscript]
|
||||||
# have to be super careful. We have to use 'sh -c' because
|
|
||||||
# csh-derived shells can't handle PATH= notation. We can't
|
|
||||||
# set PATH in advance, because ssh probably replaces it. We
|
|
||||||
# can't exec *safely* using argv, because *both* ssh and 'sh -c'
|
|
||||||
# allow shellquoting. So we end up having to double-shellquote
|
|
||||||
# stuff here.
|
|
||||||
escapedir = re.sub(r'([^\w/])', r'\\\\\\\1', nicedir)
|
|
||||||
cmd = r"""
|
|
||||||
sh -c PATH=%s:'$PATH exec sshuttle --server%s'
|
|
||||||
""" % (escapedir, ' -v' * (helpers.verbose or 0))
|
|
||||||
argv = ['ssh', rhost, '--', cmd.strip()]
|
|
||||||
(s1,s2) = socket.socketpair()
|
(s1,s2) = socket.socketpair()
|
||||||
def setup():
|
def setup():
|
||||||
# runs in the child process
|
# runs in the child process
|
||||||
s2.close()
|
s2.close()
|
||||||
if not rhost:
|
|
||||||
os.environ['PATH'] = ':'.join([nicedir,
|
|
||||||
os.environ.get('PATH', '')])
|
|
||||||
os.setsid()
|
os.setsid()
|
||||||
s1a,s1b = os.dup(s1.fileno()), os.dup(s1.fileno())
|
s1a,s1b = os.dup(s1.fileno()), os.dup(s1.fileno())
|
||||||
s1.close()
|
s1.close()
|
||||||
p = subprocess.Popen(argv, stdin=s1a, stdout=s1b, preexec_fn=setup)
|
debug2('executing: %r\n' % argv)
|
||||||
|
p = subprocess.Popen(argv, stdin=s1a, stdout=s1b, preexec_fn=setup,
|
||||||
|
close_fds=True)
|
||||||
os.close(s1a)
|
os.close(s1a)
|
||||||
os.close(s1b)
|
os.close(s1b)
|
||||||
|
s2.sendall(content)
|
||||||
|
s2.sendall(content2)
|
||||||
return p, s2
|
return p, s2
|
||||||
|
91
ssnet.py
91
ssnet.py
@ -1,5 +1,6 @@
|
|||||||
import struct, socket, errno, select
|
import struct, socket, errno, select
|
||||||
from helpers import *
|
if not globals().get('skip_imports'):
|
||||||
|
from helpers import *
|
||||||
|
|
||||||
HDR_LEN = 8
|
HDR_LEN = 8
|
||||||
|
|
||||||
@ -11,6 +12,9 @@ CMD_CONNECT = 0x4203
|
|||||||
CMD_CLOSE = 0x4204
|
CMD_CLOSE = 0x4204
|
||||||
CMD_EOF = 0x4205
|
CMD_EOF = 0x4205
|
||||||
CMD_DATA = 0x4206
|
CMD_DATA = 0x4206
|
||||||
|
CMD_ROUTES = 0x4207
|
||||||
|
CMD_HOST_REQ = 0x4208
|
||||||
|
CMD_HOST_LIST = 0x4209
|
||||||
|
|
||||||
cmd_to_name = {
|
cmd_to_name = {
|
||||||
CMD_EXIT: 'EXIT',
|
CMD_EXIT: 'EXIT',
|
||||||
@ -20,6 +24,9 @@ cmd_to_name = {
|
|||||||
CMD_CLOSE: 'CLOSE',
|
CMD_CLOSE: 'CLOSE',
|
||||||
CMD_EOF: 'EOF',
|
CMD_EOF: 'EOF',
|
||||||
CMD_DATA: 'DATA',
|
CMD_DATA: 'DATA',
|
||||||
|
CMD_ROUTES: 'ROUTES',
|
||||||
|
CMD_HOST_REQ: 'HOST_REQ',
|
||||||
|
CMD_HOST_LIST: 'HOST_LIST',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -71,16 +78,22 @@ class SockWrapper:
|
|||||||
def try_connect(self):
|
def try_connect(self):
|
||||||
if not self.connect_to:
|
if not self.connect_to:
|
||||||
return # already connected
|
return # already connected
|
||||||
self.rsock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
|
|
||||||
self.rsock.setblocking(False)
|
self.rsock.setblocking(False)
|
||||||
try:
|
try:
|
||||||
self.rsock.connect(self.connect_to)
|
self.rsock.connect(self.connect_to)
|
||||||
|
# connected successfully (Linux)
|
||||||
self.connect_to = None
|
self.connect_to = None
|
||||||
except socket.error, e:
|
except socket.error, e:
|
||||||
if e.args[0] in [errno.EINPROGRESS, errno.EALREADY]:
|
if e.args[0] in [errno.EINPROGRESS, errno.EALREADY]:
|
||||||
pass # not connected yet
|
pass # not connected yet
|
||||||
elif e.args[0] in [errno.ECONNREFUSED, errno.ETIMEDOUT]:
|
elif e.args[0] == errno.EISCONN:
|
||||||
|
# connected successfully (BSD)
|
||||||
|
self.connect_to = None
|
||||||
|
elif e.args[0] in [errno.ECONNREFUSED, errno.ETIMEDOUT,
|
||||||
|
errno.EHOSTUNREACH, errno.ENETUNREACH,
|
||||||
|
errno.EACCES, errno.EPERM]:
|
||||||
# a "normal" kind of error
|
# a "normal" kind of error
|
||||||
|
self.connect_to = None
|
||||||
self.seterr(e)
|
self.seterr(e)
|
||||||
else:
|
else:
|
||||||
raise # error we've never heard of?! barf completely.
|
raise # error we've never heard of?! barf completely.
|
||||||
@ -100,6 +113,9 @@ class SockWrapper:
|
|||||||
except socket.error, e:
|
except socket.error, e:
|
||||||
self.seterr(e)
|
self.seterr(e)
|
||||||
|
|
||||||
|
def too_full(self):
|
||||||
|
return False # fullness is determined by the socket's select() state
|
||||||
|
|
||||||
def uwrite(self, buf):
|
def uwrite(self, buf):
|
||||||
if self.connect_to:
|
if self.connect_to:
|
||||||
return 0 # still connecting
|
return 0 # still connecting
|
||||||
@ -180,14 +196,16 @@ class Proxy(Handler):
|
|||||||
if self.wrap1.connect_to:
|
if self.wrap1.connect_to:
|
||||||
w.add(self.wrap1.rsock)
|
w.add(self.wrap1.rsock)
|
||||||
elif self.wrap1.buf:
|
elif self.wrap1.buf:
|
||||||
w.add(self.wrap2.wsock)
|
if not self.wrap2.too_full():
|
||||||
|
w.add(self.wrap2.wsock)
|
||||||
elif not self.wrap1.shut_read:
|
elif not self.wrap1.shut_read:
|
||||||
r.add(self.wrap1.rsock)
|
r.add(self.wrap1.rsock)
|
||||||
|
|
||||||
if self.wrap2.connect_to:
|
if self.wrap2.connect_to:
|
||||||
w.add(self.wrap2.rsock)
|
w.add(self.wrap2.rsock)
|
||||||
elif self.wrap2.buf:
|
elif self.wrap2.buf:
|
||||||
w.add(self.wrap1.wsock)
|
if not self.wrap1.too_full():
|
||||||
|
w.add(self.wrap1.wsock)
|
||||||
elif not self.wrap2.shut_read:
|
elif not self.wrap2.shut_read:
|
||||||
r.add(self.wrap2.rsock)
|
r.add(self.wrap2.rsock)
|
||||||
|
|
||||||
@ -208,12 +226,15 @@ class Mux(Handler):
|
|||||||
Handler.__init__(self, [rsock, wsock])
|
Handler.__init__(self, [rsock, wsock])
|
||||||
self.rsock = rsock
|
self.rsock = rsock
|
||||||
self.wsock = wsock
|
self.wsock = wsock
|
||||||
self.new_channel = None
|
self.new_channel = self.got_routes = None
|
||||||
|
self.got_host_req = self.got_host_list = None
|
||||||
self.channels = {}
|
self.channels = {}
|
||||||
self.chani = 0
|
self.chani = 0
|
||||||
self.want = 0
|
self.want = 0
|
||||||
self.inbuf = ''
|
self.inbuf = ''
|
||||||
self.outbuf = []
|
self.outbuf = []
|
||||||
|
self.fullness = 0
|
||||||
|
self.too_full = False
|
||||||
self.send(0, CMD_PING, 'chicken')
|
self.send(0, CMD_PING, 'chicken')
|
||||||
|
|
||||||
def next_channel(self):
|
def next_channel(self):
|
||||||
@ -225,29 +246,60 @@ class Mux(Handler):
|
|||||||
if not self.channels.get(self.chani):
|
if not self.channels.get(self.chani):
|
||||||
return self.chani
|
return self.chani
|
||||||
|
|
||||||
|
def amount_queued(self):
|
||||||
|
return sum(len(b) for b in self.outbuf)
|
||||||
|
|
||||||
|
def check_fullness(self):
|
||||||
|
if self.fullness > 32768:
|
||||||
|
if not self.too_full:
|
||||||
|
self.send(0, CMD_PING, 'rttest')
|
||||||
|
self.too_full = True
|
||||||
|
#ob = []
|
||||||
|
#for b in self.outbuf:
|
||||||
|
# (s1,s2,c) = struct.unpack('!ccH', b[:4])
|
||||||
|
# ob.append(c)
|
||||||
|
#log('outbuf: %d %r\n' % (self.amount_queued(), ob))
|
||||||
|
|
||||||
def send(self, channel, cmd, data):
|
def send(self, channel, cmd, data):
|
||||||
data = str(data)
|
data = str(data)
|
||||||
assert(len(data) <= 65535)
|
assert(len(data) <= 65535)
|
||||||
p = struct.pack('!ccHHH', 'S', 'S', channel, cmd, len(data)) + data
|
p = struct.pack('!ccHHH', 'S', 'S', channel, cmd, len(data)) + data
|
||||||
self.outbuf.append(p)
|
self.outbuf.append(p)
|
||||||
debug2(' > channel=%d cmd=%s len=%d\n'
|
debug2(' > channel=%d cmd=%s len=%d (fullness=%d)\n'
|
||||||
% (channel, cmd_to_name[cmd], len(data)))
|
% (channel, cmd_to_name.get(cmd,hex(cmd)),
|
||||||
#log('Mux: send queue is %d/%d\n'
|
len(data), self.fullness))
|
||||||
# % (len(self.outbuf), sum(len(b) for b in self.outbuf)))
|
self.fullness += len(data)
|
||||||
|
|
||||||
def got_packet(self, channel, cmd, data):
|
def got_packet(self, channel, cmd, data):
|
||||||
debug2('< channel=%d cmd=%s len=%d\n'
|
debug2('< channel=%d cmd=%s len=%d\n'
|
||||||
% (channel, cmd_to_name[cmd], len(data)))
|
% (channel, cmd_to_name.get(cmd,hex(cmd)), len(data)))
|
||||||
if cmd == CMD_PING:
|
if cmd == CMD_PING:
|
||||||
self.send(0, CMD_PONG, data)
|
self.send(0, CMD_PONG, data)
|
||||||
elif cmd == CMD_PONG:
|
elif cmd == CMD_PONG:
|
||||||
debug2('received PING response\n')
|
debug2('received PING response\n')
|
||||||
|
self.too_full = False
|
||||||
|
self.fullness = 0
|
||||||
elif cmd == CMD_EXIT:
|
elif cmd == CMD_EXIT:
|
||||||
self.ok = False
|
self.ok = False
|
||||||
elif cmd == CMD_CONNECT:
|
elif cmd == CMD_CONNECT:
|
||||||
assert(not self.channels.get(channel))
|
assert(not self.channels.get(channel))
|
||||||
if self.new_channel:
|
if self.new_channel:
|
||||||
self.new_channel(channel, data)
|
self.new_channel(channel, data)
|
||||||
|
elif cmd == CMD_ROUTES:
|
||||||
|
if self.got_routes:
|
||||||
|
self.got_routes(data)
|
||||||
|
else:
|
||||||
|
raise Exception('got CMD_ROUTES without got_routes?')
|
||||||
|
elif cmd == CMD_HOST_REQ:
|
||||||
|
if self.got_host_req:
|
||||||
|
self.got_host_req(data)
|
||||||
|
else:
|
||||||
|
raise Exception('got CMD_HOST_REQ without got_host_req?')
|
||||||
|
elif cmd == CMD_HOST_LIST:
|
||||||
|
if self.got_host_list:
|
||||||
|
self.got_host_list(data)
|
||||||
|
else:
|
||||||
|
raise Exception('got CMD_HOST_LIST without got_host_list?')
|
||||||
else:
|
else:
|
||||||
callback = self.channels[channel]
|
callback = self.channels[channel]
|
||||||
callback(cmd, data)
|
callback(cmd, data)
|
||||||
@ -256,6 +308,7 @@ class Mux(Handler):
|
|||||||
self.wsock.setblocking(False)
|
self.wsock.setblocking(False)
|
||||||
if self.outbuf and self.outbuf[0]:
|
if self.outbuf and self.outbuf[0]:
|
||||||
wrote = _nb_clean(os.write, self.wsock.fileno(), self.outbuf[0])
|
wrote = _nb_clean(os.write, self.wsock.fileno(), self.outbuf[0])
|
||||||
|
debug2('mux wrote: %d/%d\n' % (wrote, len(self.outbuf[0])))
|
||||||
if wrote:
|
if wrote:
|
||||||
self.outbuf[0] = self.outbuf[0][wrote:]
|
self.outbuf[0] = self.outbuf[0][wrote:]
|
||||||
while self.outbuf and not self.outbuf[0]:
|
while self.outbuf and not self.outbuf[0]:
|
||||||
@ -263,7 +316,10 @@ class Mux(Handler):
|
|||||||
|
|
||||||
def fill(self):
|
def fill(self):
|
||||||
self.rsock.setblocking(False)
|
self.rsock.setblocking(False)
|
||||||
b = _nb_clean(os.read, self.rsock.fileno(), 32768)
|
try:
|
||||||
|
b = _nb_clean(os.read, self.rsock.fileno(), 32768)
|
||||||
|
except OSError, e:
|
||||||
|
raise Fatal('other end: %r' % e)
|
||||||
#log('<<< %r\n' % b)
|
#log('<<< %r\n' % b)
|
||||||
if b == '': # EOF
|
if b == '': # EOF
|
||||||
self.ok = False
|
self.ok = False
|
||||||
@ -308,6 +364,7 @@ class MuxWrapper(SockWrapper):
|
|||||||
self.mux = mux
|
self.mux = mux
|
||||||
self.channel = channel
|
self.channel = channel
|
||||||
self.mux.channels[channel] = self.got_packet
|
self.mux.channels[channel] = self.got_packet
|
||||||
|
self.socks = []
|
||||||
debug2('new channel: %d\n' % channel)
|
debug2('new channel: %d\n' % channel)
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
@ -326,9 +383,14 @@ class MuxWrapper(SockWrapper):
|
|||||||
self.shut_write = True
|
self.shut_write = True
|
||||||
self.mux.send(self.channel, CMD_EOF, '')
|
self.mux.send(self.channel, CMD_EOF, '')
|
||||||
|
|
||||||
|
def too_full(self):
|
||||||
|
return self.mux.too_full
|
||||||
|
|
||||||
def uwrite(self, buf):
|
def uwrite(self, buf):
|
||||||
if len(buf) > 65535:
|
if self.mux.too_full:
|
||||||
buf = buf[:32768]
|
return 0 # too much already enqueued
|
||||||
|
if len(buf) > 2048:
|
||||||
|
buf = buf[:2048]
|
||||||
self.mux.send(self.channel, CMD_DATA, buf)
|
self.mux.send(self.channel, CMD_DATA, buf)
|
||||||
return len(buf)
|
return len(buf)
|
||||||
|
|
||||||
@ -354,6 +416,7 @@ class MuxWrapper(SockWrapper):
|
|||||||
def connect_dst(ip, port):
|
def connect_dst(ip, port):
|
||||||
debug2('Connecting to %s:%d\n' % (ip, port))
|
debug2('Connecting to %s:%d\n' % (ip, port))
|
||||||
outsock = socket.socket()
|
outsock = socket.socket()
|
||||||
|
outsock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
|
||||||
return SockWrapper(outsock, outsock,
|
return SockWrapper(outsock, outsock,
|
||||||
connect_to = (ip,port),
|
connect_to = (ip,port),
|
||||||
peername = '%s:%d' % (ip,port))
|
peername = '%s:%d' % (ip,port))
|
||||||
|
Reference in New Issue
Block a user