Compare commits

..

30 Commits

Author SHA1 Message Date
ae32fe2a59 Merge branch 'python23' - python 2.3 compatibility
* python23:
  Oops, missed another << operator to replace with _shl().
  socket.SHUT_RD and socket.SHUT_WR don't exist in python 2.3.
  compat/ssubprocess.py: some python versions don't have os.closerange().
  _nb_clean: don't catch EPIPE after all.
  Fix busy-waiting in two situations:
  Factor out common mainloop code between client and server.
  Implement our own left-shift operator to shut up python 2.3 warnings.
  Don't use set() since it's not in python 2.3.
  import and use subprocess.py from python 2.6.
  Remove list comprehensions for python 2.3 compatibility.
2010-10-02 16:34:35 -07:00
5070f2ffcf Oops, missed another << operator to replace with _shl().
For python 2.3, of course.
2010-10-02 15:26:29 -07:00
b219b523c2 socket.SHUT_RD and socket.SHUT_WR don't exist in python 2.3.
Mercifully, socket.socket.shutdown() still does, but it uses hardcoded
integer parameters - and the integers correspond to the SHUT_RD and SHUT_WR
definitions in later versions - so let's just hardcode them ourselves.

See the carnage for yourself:
http://docs.python.org/release/2.3.5/lib/socket-objects.html
2010-10-02 15:24:04 -07:00
52fbb2ebbe compat/ssubprocess.py: some python versions don't have os.closerange().
Like python2.5 on Debian.  It might be a MacOS extension or something.  So
much for the comment in subprocess.py that said "keep this compatible with
python 2.2."
2010-10-01 19:26:56 -07:00
76d576a375 _nb_clean: don't catch EPIPE after all.
EPIPE is a serious error from these places, so we have to actually do
something.  Otherwise the client ends up busy waiting when the server
disconnects by surprise.

Bug noticed in a log from Chetan Kunte.
2010-10-01 18:25:03 -07:00
f6e6515a3c Fix busy-waiting in two situations:
- If you tried to connect to a server that didn't exist, then disconnected
  the client during the 60-second connection timeout, the server would
  busy wait for 60 seconds.

- If you connected to a server and then sent data, but then the server
  disconnected before reading all your data, the server would busy wait.
    (example:  yes | telnet servername 80)
2010-10-01 18:22:36 -07:00
84376284db Factor out common mainloop code between client and server.
Also improve the socket message output a bit.
2010-10-01 17:36:09 -07:00
b0f061e204 Implement our own left-shift operator to shut up python 2.3 warnings.
Apparently left-shift in python 2.3 just *always* prints a warning, even if
we weren't doing anything wrong.  Or maybe it only prints the warning
sometimes.  Anyway, let's just multiply by 2**x instead of using <<x, since
we're not performance-sensitive anyway.
2010-10-01 14:46:34 -07:00
c403a83ab8 Don't use set() since it's not in python 2.3.
Just use a plain list instead.  Technically probably slightly worse
asymptotic behaviour, but it's not like we'll have a million sockets anyway.
2010-10-01 14:38:08 -07:00
da774f3f46 import and use subprocess.py from python 2.6.
This should hopefully let us run even on python 2.3 on really old servers.
2010-10-01 12:11:48 -07:00
7d3028dee2 Remove list comprehensions for python 2.3 compatibility. 2010-10-01 11:55:45 -07:00
518df41049 ssh.py: don't os.setsid().
This prevents ssh from asking for a password successfully.  Error reported
by Chetan Kunte.
2010-10-01 11:35:13 -07:00
76bbbfd67b Catch the exception thrown when ssh can't connect.
Easiest test: give it an invalid hostname.

Reported by Chetan Kunte.
2010-10-01 10:34:20 -07:00
6e336c09bf README: remove the note about MacOS not working. It works now! 2010-10-01 00:43:01 -07:00
f950a3800b BSD: sysctl net.inet.ip.forwarding=1 is not necessary.
If your machine is a firewall/router, it affects whether people behind the
router can use your sshuttle connection - in the same way that it affects
whether they can route *anything* through you.  And thus, it should be set
by the admin, not by sshuttle.

sshuttle works fine for the local user either way.

(This also affects MacOS since it's a BSD variant.)
2010-10-01 00:39:30 -07:00
8b4466b802 BSD ipfw: switch from 'established' to 'keep-state/check-state'.
It turns out 'established' doesn't work the way I expected it to from
iptables; it's not stateful.  It just checks the TCP flags to see if the
connection *thinks* it's already established, and follows the rule if so.
That caused the first packet of each new connection to set sent to our
transproxy, but not the subsequent ones, so weird stuff happened.

With this change, any (matching) connection created *after* starting sshuttle
will get forwarded, but pre-existing ones - most importantly, sshuttle's own
ssh connection - will not.

And with this (plus the previous commit), sshuttle works on MacOS, including
10.6!
2010-10-01 00:36:46 -07:00
4bf4f70c67 ssnet: recover slightly more gracefully from an infinite forwarding loop.
If you 'telnet localhost 12300' weird things happen; someday we should
probably auto-detect and avoid that altogether.  But meanwhile, catch EPIPE
if it happens (it's irrelevant) and don't barf with a %d data type for a
value that can apparently sometimes be None.
2010-10-01 00:05:49 -07:00
410b9d4229 Magic incantation to mostly fix MacOS 10.6.
It comes down to this:
   sysctl_set('net.inet.ip.scopedroute', 0)

I say "mostly" because actually it doesn't fix it; sshuttle doesn't know
what to do with the received connection, so there must be a minor bug
remaining somewhere.  I'll fix that next.

Thanks to dkf <dfortunato@gmail.com> on the sshuttle mailing list for
suggesting the magic fix.  He points at this post in particular:
  http://discussions.apple.com/thread.jspa?messageID=11558355&#11558355
that gave him the necessary clue.
2010-10-01 00:05:48 -07:00
2ef1c6a4c4 latest options.py from bup, now with tty-width guessing.
as of bup commit bup-0.19-2-gce2ace5.
2010-09-21 18:03:17 -07:00
b35cfbd022 hostwatch: add missing errno import
If the ~/.sshuttle.hosts file does not exist, it triggers the following
error:

       Traceback (most recent call last):
         File "./sshuttle", line 80, in <module>
           sys.exit(hostwatch.hw_main(extra))
         File "/home/def/p/sshuttle/hostwatch.py", line 246, in hw_main
           read_host_cache()
         File "/home/def/p/sshuttle/hostwatch.py", line 41, in read_host_cache
           if e.errno == errno.ENOENT:
       NameError: global name 'errno' is not defined

(This only happened if you run 'sshuttle --hostwatch' from the command line
directly, without passing it through assembler.py.)
2010-09-21 17:15:46 -07:00
dcba684766 If netstat -rn returns an error, make that non-fatal.
That only really stops --auto-nets from working; it's mostly harmless
otherwise.  And apparently some locked-down shared hosts don't let you get
the list of routes.
2010-09-04 11:29:11 -07:00
ee74110cff add option to allow the remote python binary's name/path to be specified 2010-09-03 23:00:26 -07:00
5bf8687ce3 Import latest options.py from bup-0.17.
This has new support for default values in square brackets, so let's use
that.
2010-09-03 23:00:26 -07:00
6bdb9517fd README: fix some out-of-date system requirements stuff.
Reported by Jason Axelson.
2010-07-25 00:16:09 -04:00
f1b33dab29 Add a --exclude option for excluding subnets from routing.
Also, add 127.0.0.0/8 to the default list of excludes.  If you want to route
0/0, you almost certainly *don't* want to route localhost to the remote ssh
server's localhost!

Thanks to Edward for the suggestion.
2010-07-15 14:13:33 -04:00
3a25f709e5 log(): don't abort if we fail to write to stderr.
Failing to write to the log sucks, but not as much as failing to clean up
just because stderr disappeared.  So let's catch any IOError exception from
log() and just ignore it.

This should fix a problem reported by Camille Moncelier, which is that
sshuttle firewall entries stick around if your tty dies strangely (eg. your
X server aborts for some reason).
2010-05-16 17:57:18 -04:00
a8b3d69856 ssh.py: try harder to find required *.py files.
Search the entire python sys.path, not just the directory that argv[0] is
in.  That way if you symlink the sshuttle binary into (for example) ~/bin,
it'll be able to work correctly.
2010-05-12 13:53:14 -04:00
2d4f6a4308 client: add a debug1() message for connecting/connected.
If the server is going to delay us, we'd at least like to know that.
2010-05-11 19:04:44 -04:00
d435ed837d Created a googlegroups.com mailing list for sshuttle. 2010-05-11 15:30:53 -04:00
2d77403a0b Don't use try/except/finally so that python 2.4 works.
Use try/(try/except)/finally instead.  There was only once case of this.

Thanks to Wayne Scott and nisc for pointing this out.
2010-05-10 13:58:52 -04:00
12 changed files with 1658 additions and 163 deletions

View File

@ -1,14 +1,10 @@
sshuttle: where transparent proxy meets VPN meets ssh 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 As far as I know, sshuttle is the only program that solves the following
common case: common case:
- Your client machine (or router) is Linux. - Your client machine (or router) is Linux, FreeBSD, or MacOS.
- You have access to a remote network via ssh. - You have access to a remote network via ssh.
@ -53,10 +49,8 @@ This is how you use it:
----------------------- -----------------------
- <tt>git clone git://github.com/apenwarr/sshuttle</tt> - <tt>git clone git://github.com/apenwarr/sshuttle</tt>
on your client and server machines. The server can be on your client machine. You'll need root or sudo
any ssh server with python available; the client must access, and python needs to be installed.
be Linux with iptables, and you'll need root or sudo
access.
- <tt>./sshuttle -r username@sshserver 0.0.0.0/0 -vv</tt> - <tt>./sshuttle -r username@sshserver 0.0.0.0/0 -vv</tt>
@ -161,3 +155,6 @@ later. You're welcome.
-- --
Avery Pennarun <apenwarr@gmail.com> 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

View File

@ -1,8 +1,10 @@
import struct, socket, select, subprocess, errno, re import struct, socket, select, errno, re
import compat.ssubprocess as ssubprocess
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):
try: try:
SO_ORIGINAL_DST = 80 SO_ORIGINAL_DST = 80
@ -20,10 +22,11 @@ def original_dst(sock):
class FirewallClient: class FirewallClient:
def __init__(self, port, subnets): def __init__(self, port, subnets_include, subnets_exclude):
self.port = port self.port = port
self.auto_nets = [] self.auto_nets = []
self.subnets = subnets self.subnets_include = subnets_include
self.subnets_exclude = subnets_exclude
argvbase = ([sys.argv[0]] + argvbase = ([sys.argv[0]] +
['-v'] * (helpers.verbose or 0) + ['-v'] * (helpers.verbose or 0) +
['--firewall', str(port)]) ['--firewall', str(port)])
@ -44,7 +47,7 @@ class FirewallClient:
e = None e = None
for argv in argv_tries: for argv in argv_tries:
try: try:
self.p = subprocess.Popen(argv, stdout=s1, preexec_fn=setup) self.p = ssubprocess.Popen(argv, stdout=s1, preexec_fn=setup)
e = None e = None
break break
except OSError, e: except OSError, e:
@ -67,8 +70,10 @@ class FirewallClient:
def start(self): def start(self):
self.pfile.write('ROUTES\n') self.pfile.write('ROUTES\n')
for (ip,width) in self.subnets+self.auto_nets: for (ip,width) in self.subnets_include+self.auto_nets:
self.pfile.write('%s,%d\n' % (ip, width)) 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.write('GO\n')
self.pfile.flush() self.pfile.flush()
line = self.pfile.readline() line = self.pfile.readline()
@ -89,14 +94,21 @@ class FirewallClient:
raise Fatal('cleanup: %r returned %d' % (self.argv, rv)) raise Fatal('cleanup: %r returned %d' % (self.argv, rv))
def _main(listener, fw, use_server, remotename, seed_hosts, auto_nets): def _main(listener, fw, use_server, remotename, python, 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: '
(serverproc, serversock) = ssh.connect(remotename) debug1('connecting to server...\n')
try:
(serverproc, serversock) = ssh.connect(remotename, python)
except socket.error, e:
if e.errno == errno.EPIPE:
raise Fatal("failed to establish ssh session")
else:
raise
mux = Mux(serversock, serversock) mux = Mux(serversock, serversock)
handlers.append(mux) handlers.append(mux)
@ -110,6 +122,7 @@ def _main(listener, fw, use_server, remotename, seed_hosts, auto_nets):
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')
def onroutes(routestr): def onroutes(routestr):
if auto_nets: if auto_nets:
@ -164,26 +177,14 @@ def _main(listener, fw, use_server, remotename, seed_hosts, auto_nets):
if rv: if rv:
raise Fatal('server died with error code %d' % rv) raise Fatal('server died with error code %d' % rv)
r = set() ssnet.runonce(handlers, mux)
w = set()
x = set()
handlers = filter(lambda s: s.ok, handlers)
for s in handlers:
s.pre_select(r,w,x)
debug2('Waiting: %d[%d,%d,%d]...\n'
% (len(handlers), len(r), len(w), len(x)))
(r,w,x) = select.select(r,w,x)
#log('r=%r w=%r x=%r\n' % (r,w,x))
ready = set(r) | set(w) | set(x)
for s in handlers:
if s.socks & ready:
s.callback()
if use_server: if use_server:
mux.callback() mux.callback()
mux.check_fullness() mux.check_fullness()
def main(listenip, use_server, remotename, seed_hosts, auto_nets, subnets): def main(listenip, use_server, remotename, python, 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)
@ -210,10 +211,10 @@ def main(listenip, use_server, remotename, seed_hosts, auto_nets, subnets):
listenip = listener.getsockname() listenip = listener.getsockname()
debug1('Listening on %r.\n' % (listenip,)) debug1('Listening on %r.\n' % (listenip,))
fw = FirewallClient(listenip[1], subnets) fw = FirewallClient(listenip[1], subnets_include, subnets_exclude)
try: try:
return _main(listener, fw, use_server, remotename, return _main(listener, fw, use_server, remotename,
seed_hosts, auto_nets) python, seed_hosts, auto_nets)
finally: finally:
fw.done() fw.done()

0
compat/__init__.py Normal file
View File

1305
compat/ssubprocess.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,12 @@
import subprocess, re, errno import re, errno
import compat.ssubprocess as ssubprocess
import helpers import helpers
from helpers import * from helpers import *
def ipt_chain_exists(name): def ipt_chain_exists(name):
argv = ['iptables', '-t', 'nat', '-nL'] argv = ['iptables', '-t', 'nat', '-nL']
p = subprocess.Popen(argv, stdout = subprocess.PIPE) p = ssubprocess.Popen(argv, stdout = ssubprocess.PIPE)
for line in p.stdout: for line in p.stdout:
if line.startswith('Chain %s ' % name): if line.startswith('Chain %s ' % name):
return True return True
@ -17,7 +18,7 @@ def ipt_chain_exists(name):
def ipt(*args): def ipt(*args):
argv = ['iptables', '-t', 'nat'] + list(args) argv = ['iptables', '-t', 'nat'] + list(args)
debug1('>> %s\n' % ' '.join(argv)) debug1('>> %s\n' % ' '.join(argv))
rv = subprocess.call(argv) rv = ssubprocess.call(argv)
if rv: if rv:
raise Fatal('%r returned %d' % (argv, rv)) raise Fatal('%r returned %d' % (argv, rv))
@ -43,32 +44,46 @@ def do_iptables(port, subnets):
ipt('-I', 'OUTPUT', '1', '-j', chain) ipt('-I', 'OUTPUT', '1', '-j', chain)
ipt('-I', 'PREROUTING', '1', '-j', chain) ipt('-I', 'PREROUTING', '1', '-j', chain)
# create new subnet entries # create new subnet entries. Note that we're sorting in a very
for snet,swidth in subnets: # particular order: we need to go from most-specific (largest swidth)
ipt('-A', chain, '-j', 'REDIRECT', # to least-specific, and at any given level of specificity, we want
'--dest', '%s/%s' % (snet,swidth), # excludes to come first. That's why the columns are in such a non-
'-p', 'tcp', # intuitive order.
'--to-ports', str(port), for swidth,sexclude,snet in sorted(subnets, reverse=True):
'-m', 'ttl', '!', '--ttl', '42' # to prevent infinite loops 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): def ipfw_rule_exists(n):
argv = ['ipfw', 'list'] argv = ['ipfw', 'list']
p = subprocess.Popen(argv, stdout = subprocess.PIPE) p = ssubprocess.Popen(argv, stdout = ssubprocess.PIPE)
found = False
for line in p.stdout: for line in p.stdout:
if line.startswith('%05d ' % n): if line.startswith('%05d ' % n):
if line.find('ipttl 42') < 0 and line.find('established') < 0: if not ('ipttl 42 setup keep-state' in line
or ('skipto %d' % (n+1)) in line
or 'check-state' in line):
log('non-sshuttle ipfw rule: %r\n' % line.strip())
raise Fatal('non-sshuttle ipfw rule #%d already exists!' % n) raise Fatal('non-sshuttle ipfw rule #%d already exists!' % n)
return True found = True
rv = p.wait() rv = p.wait()
if rv: if rv:
raise Fatal('%r returned %d' % (argv, rv)) raise Fatal('%r returned %d' % (argv, rv))
return found
def sysctl_get(name): def sysctl_get(name):
argv = ['sysctl', '-n', name] argv = ['sysctl', '-n', name]
p = subprocess.Popen(argv, stdout = subprocess.PIPE) p = ssubprocess.Popen(argv, stdout = ssubprocess.PIPE)
line = p.stdout.readline() line = p.stdout.readline()
rv = p.wait() rv = p.wait()
if rv: if rv:
@ -82,7 +97,7 @@ def sysctl_get(name):
def _sysctl_set(name, val): def _sysctl_set(name, val):
argv = ['sysctl', '-w', '%s=%s' % (name, val)] argv = ['sysctl', '-w', '%s=%s' % (name, val)]
debug1('>> %s\n' % ' '.join(argv)) debug1('>> %s\n' % ' '.join(argv))
rv = subprocess.call(argv, stdout = open('/dev/null', 'w')) rv = ssubprocess.call(argv, stdout = open('/dev/null', 'w'))
_oldctls = [] _oldctls = []
@ -96,13 +111,14 @@ def sysctl_set(name, val):
def ipfw(*args): def ipfw(*args):
argv = ['ipfw', '-q'] + list(args) argv = ['ipfw', '-q'] + list(args)
debug1('>> %s\n' % ' '.join(argv)) debug1('>> %s\n' % ' '.join(argv))
rv = subprocess.call(argv) rv = ssubprocess.call(argv)
if rv: if rv:
raise Fatal('%r returned %d' % (argv, rv)) raise Fatal('%r returned %d' % (argv, rv))
def do_ipfw(port, subnets): def do_ipfw(port, subnets):
sport = str(port) sport = str(port)
xsport = str(port+1)
# cleanup any existing rules # cleanup any existing rules
if ipfw_rule_exists(port): if ipfw_rule_exists(port):
@ -114,17 +130,22 @@ def do_ipfw(port, subnets):
if subnets: if subnets:
sysctl_set('net.inet.ip.fw.enable', 1) sysctl_set('net.inet.ip.fw.enable', 1)
sysctl_set('net.inet.ip.forwarding', 1) sysctl_set('net.inet.ip.scopedroute', 0)
ipfw('add', sport, 'accept', 'ip', ipfw('add', sport, 'check-state', 'ip',
'from', 'any', 'to', 'any', 'established') 'from', 'any', 'to', 'any')
# create new subnet entries # create new subnet entries
for snet,swidth in subnets: for swidth,sexclude,snet in sorted(subnets, reverse=True):
ipfw('add', sport, 'fwd', '127.0.0.1,%d' % port, if sexclude:
'log', 'tcp', ipfw('add', sport, 'skipto', xsport,
'from', 'any', 'to', '%s/%s' % (snet,swidth), 'log', 'tcp',
'not', 'ipttl', '42') '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', 'keep-state', 'setup')
def program_exists(name): def program_exists(name):
@ -228,10 +249,11 @@ def main(port):
elif line == 'GO\n': elif line == 'GO\n':
break break
try: try:
(ip,width) = line.strip().split(',', 1) (width,exclude,ip) = line.strip().split(',', 2)
except: except:
raise Fatal('firewall: expected route or GO but got %r' % line) raise Fatal('firewall: expected route or GO but got %r' % line)
subnets.append((ip, int(width))) subnets.append((int(width), bool(int(exclude)), ip))
try: try:
if line: if line:
debug1('firewall manager: starting transproxy.\n') debug1('firewall manager: starting transproxy.\n')
@ -258,7 +280,6 @@ def main(port):
raise Fatal('expected EOF, got %r' % line) raise Fatal('expected EOF, got %r' % line)
else: else:
break break
finally: finally:
try: try:
debug1('firewall manager: undoing changes.\n') debug1('firewall manager: undoing changes.\n')

View File

@ -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:
@ -23,3 +28,10 @@ def debug3(s):
class Fatal(Exception): class Fatal(Exception):
pass pass
def list_contains_any(l, sub):
for i in sub:
if i in l:
return True
return False

View File

@ -1,5 +1,6 @@
import subprocess, time, socket, re, select import time, socket, re, select, errno
if not globals().get('skip_imports'): if not globals().get('skip_imports'):
import compat.ssubprocess as ssubprocess
import helpers import helpers
from helpers import * from helpers import *
@ -108,7 +109,7 @@ def _check_netstat():
debug2(' > netstat\n') debug2(' > netstat\n')
argv = ['netstat', '-n'] argv = ['netstat', '-n']
try: try:
p = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=null) p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null)
content = p.stdout.read() content = p.stdout.read()
p.wait() p.wait()
except OSError, e: except OSError, e:
@ -128,7 +129,7 @@ def _check_smb(hostname):
argv = ['smbclient', '-U', '%', '-L', hostname] argv = ['smbclient', '-U', '%', '-L', hostname]
debug2(' > smb: %s\n' % hostname) debug2(' > smb: %s\n' % hostname)
try: try:
p = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=null) p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null)
lines = p.stdout.readlines() lines = p.stdout.readlines()
p.wait() p.wait()
except OSError, e: except OSError, e:
@ -185,7 +186,7 @@ def _check_nmb(hostname, is_workgroup, is_master):
argv = ['nmblookup'] + ['-M']*is_master + ['--', hostname] argv = ['nmblookup'] + ['-M']*is_master + ['--', hostname]
debug2(' > n%d%d: %s\n' % (is_workgroup, is_master, hostname)) debug2(' > n%d%d: %s\n' % (is_workgroup, is_master, hostname))
try: try:
p = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=null) p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null)
lines = p.stdout.readlines() lines = p.stdout.readlines()
rv = p.wait() rv = p.wait()
except OSError, e: except OSError, e:

19
main.py
View File

@ -49,16 +49,18 @@ sshuttle [-l [ip:]port] [-r [username@]sshserver[:port]] <subnets...>
sshuttle --firewall <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 [0.0.0.0:0]
H,auto-hosts scan for remote hostnames and update local /etc/hosts H,auto-hosts scan for remote hostnames and update local /etc/hosts
N,auto-nets automatically determine subnets to route N,auto-nets automatically determine subnets to route
python= specify the name/path of the python interpreter on the remote server [python]
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) 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)
firewall [internal use only] firewall (internal use only)
hostwatch [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:])
@ -79,6 +81,11 @@ try:
else: else:
if len(extra) < 1 and not opt.auto_nets: if len(extra) < 1 and not opt.auto_nets:
o.fatal('at least one subnet (or -N) 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
@ -93,9 +100,11 @@ try:
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,
(opt.python or "python"),
sh, sh,
opt.auto_nets, opt.auto_nets,
parse_subnets(extra))) 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)

View File

@ -1,30 +1,94 @@
import sys, textwrap, getopt, re """Command-line options parser.
With the help of an options spec string, easily parse command-line options.
"""
import sys, os, textwrap, getopt, re, struct
class OptDict: class OptDict:
def __init__(self): def __init__(self):
self._opts = {} self._opts = {}
def __setitem__(self, k, v): def __setitem__(self, k, v):
if k.startswith('no-') or k.startswith('no_'):
k = k[3:]
v = not v
self._opts[k] = v self._opts[k] = v
def __getitem__(self, k): def __getitem__(self, k):
if k.startswith('no-') or k.startswith('no_'):
return not self._opts[k[3:]]
return self._opts[k] return self._opts[k]
def __getattr__(self, k): def __getattr__(self, k):
return self[k] return self[k]
def _default_onabort(msg):
sys.exit(97)
def _intify(v):
try:
vv = int(v or '')
if str(vv) == v:
return vv
except ValueError:
pass
return v
def _atoi(v):
try:
return int(v or 0)
except ValueError:
return 0
def _remove_negative_kv(k, v):
if k.startswith('no-') or k.startswith('no_'):
return k[3:], not v
return k,v
def _remove_negative_k(k):
return _remove_negative_kv(k, None)[0]
def _tty_width():
s = struct.pack("HHHH", 0, 0, 0, 0)
try:
import fcntl, termios
s = fcntl.ioctl(sys.stderr.fileno(), termios.TIOCGWINSZ, s)
except (IOError, ImportError):
return _atoi(os.environ.get('WIDTH')) or 70
(ysize,xsize,ypix,xpix) = struct.unpack('HHHH', s)
return xsize
class Options: class Options:
def __init__(self, exe, optspec, optfunc=getopt.gnu_getopt): """Option parser.
When constructed, two strings are mandatory. The first one is the command
name showed before error messages. The second one is a string called an
optspec that specifies the synopsis and option flags and their description.
For more information about optspecs, consult the bup-options(1) man page.
Two optional arguments specify an alternative parsing function and an
alternative behaviour on abort (after having output the usage string).
By default, the parser function is getopt.gnu_getopt, and the abort
behaviour is to exit the program.
"""
def __init__(self, exe, optspec, optfunc=getopt.gnu_getopt,
onabort=_default_onabort):
self.exe = exe self.exe = exe
self.optspec = optspec self.optspec = optspec
self._onabort = onabort
self.optfunc = optfunc self.optfunc = optfunc
self._aliases = {} self._aliases = {}
self._shortopts = 'h?' self._shortopts = 'h?'
self._longopts = ['help'] self._longopts = ['help']
self._hasparms = {} self._hasparms = {}
self._defaults = {}
self._usagestr = self._gen_usage() self._usagestr = self._gen_usage()
def _gen_usage(self): def _gen_usage(self):
out = [] out = []
lines = self.optspec.strip().split('\n') lines = self.optspec.strip().split('\n')
@ -36,10 +100,13 @@ class Options:
out.append('%s: %s\n' % (first_syn and 'usage' or ' or', l)) out.append('%s: %s\n' % (first_syn and 'usage' or ' or', l))
first_syn = False first_syn = False
out.append('\n') out.append('\n')
last_was_option = False
while lines: while lines:
l = lines.pop() l = lines.pop()
if l.startswith(' '): if l.startswith(' '):
out.append('\n%s\n' % l.lstrip()) out.append('%s%s\n' % (last_was_option and '\n' or '',
l.lstrip()))
last_was_option = False
elif l: elif l:
(flags, extra) = l.split(' ', 1) (flags, extra) = l.split(' ', 1)
extra = extra.strip() extra = extra.strip()
@ -48,18 +115,24 @@ class Options:
has_parm = 1 has_parm = 1
else: else:
has_parm = 0 has_parm = 0
g = re.search(r'\[([^\]]*)\]$', extra)
if g:
defval = g.group(1)
else:
defval = None
flagl = flags.split(',') flagl = flags.split(',')
flagl_nice = [] flagl_nice = []
for f in flagl: for f in flagl:
f_nice = re.sub(r'\W', '_', f) f,dvi = _remove_negative_kv(f, _intify(defval))
self._aliases[f] = flagl[0] self._aliases[f] = _remove_negative_k(flagl[0])
self._aliases[f_nice] = flagl[0]
self._hasparms[f] = has_parm self._hasparms[f] = has_parm
self._defaults[f] = dvi
if len(f) == 1: if len(f) == 1:
self._shortopts += f + (has_parm and ':' or '') self._shortopts += f + (has_parm and ':' or '')
flagl_nice.append('-' + f) flagl_nice.append('-' + f)
else: else:
assert(not f.startswith('no-')) # supported implicitly f_nice = re.sub(r'\W', '_', f)
self._aliases[f_nice] = _remove_negative_k(flagl[0])
self._longopts.append(f + (has_parm and '=' or '')) self._longopts.append(f + (has_parm and '=' or ''))
self._longopts.append('no-' + f) self._longopts.append('no-' + f)
flagl_nice.append('--' + f) flagl_nice.append('--' + f)
@ -67,52 +140,62 @@ class Options:
if has_parm: if has_parm:
flags_nice += ' ...' flags_nice += ' ...'
prefix = ' %-20s ' % flags_nice prefix = ' %-20s ' % flags_nice
argtext = '\n'.join(textwrap.wrap(extra, width=70, argtext = '\n'.join(textwrap.wrap(extra, width=_tty_width(),
initial_indent=prefix, initial_indent=prefix,
subsequent_indent=' '*28)) subsequent_indent=' '*28))
out.append(argtext + '\n') out.append(argtext + '\n')
last_was_option = True
else: else:
out.append('\n') out.append('\n')
last_was_option = False
return ''.join(out).rstrip() + '\n' return ''.join(out).rstrip() + '\n'
def usage(self): def usage(self, msg=""):
"""Print usage string to stderr and abort."""
sys.stderr.write(self._usagestr) sys.stderr.write(self._usagestr)
sys.exit(97) e = self._onabort and self._onabort(msg) or None
if e:
raise e
def fatal(self, s): def fatal(self, s):
sys.stderr.write('error: %s\n' % s) """Print an error message to stderr and abort with usage string."""
return self.usage() msg = 'error: %s\n' % s
sys.stderr.write(msg)
return self.usage(msg)
def parse(self, args): def parse(self, args):
"""Parse a list of arguments and return (options, flags, extra).
In the returned tuple, "options" is an OptDict with known options,
"flags" is a list of option flags that were used on the command-line,
and "extra" is a list of positional arguments.
"""
try: try:
(flags,extra) = self.optfunc(args, self._shortopts, self._longopts) (flags,extra) = self.optfunc(args, self._shortopts, self._longopts)
except getopt.GetoptError, e: except getopt.GetoptError, e:
self.fatal(e) self.fatal(e)
opt = OptDict() opt = OptDict()
for f in self._aliases.values():
opt[f] = None for k,v in self._defaults.iteritems():
k = self._aliases[k]
opt[k] = v
for (k,v) in flags: for (k,v) in flags:
while k.startswith('-'): k = k.lstrip('-')
k = k[1:] if k in ('h', '?', 'help'):
if k in ['h', '?', 'help']:
self.usage() self.usage()
if k.startswith('no-'): if k.startswith('no-'):
k = self._aliases[k[3:]] k = self._aliases[k[3:]]
opt[k] = None v = 0
else: else:
k = self._aliases[k] k = self._aliases[k]
if not self._hasparms[k]: if not self._hasparms[k]:
assert(v == '') assert(v == '')
opt[k] = (opt._opts.get(k) or 0) + 1 v = (opt._opts.get(k) or 0) + 1
else: else:
try: v = _intify(v)
vv = int(v) opt[k] = v
if str(vv) == v: for (f1,f2) in self._aliases.iteritems():
v = vv opt[f1] = opt._opts.get(f2)
except ValueError:
pass
opt[k] = v
for (f1,f2) in self._aliases.items():
opt[f1] = opt[f2]
return (opt,flags,extra) return (opt,flags,extra)

View File

@ -1,6 +1,7 @@
import re, struct, socket, select, subprocess, traceback import re, struct, socket, select, traceback
if not globals().get('skip_imports'): if not globals().get('skip_imports'):
import ssnet, helpers, hostwatch import ssnet, helpers, hostwatch
import compat.ssubprocess as ssubprocess
from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
from helpers import * from helpers import *
@ -36,14 +37,18 @@ def _maskbits(netmask):
if not netmask: if not netmask:
return 32 return 32
for i in range(32): for i in range(32):
if netmask[0] & (1<<i): if netmask[0] & _shl(1, i):
return 32-i return 32-i
return 0 return 0
def _shl(n, bits):
return n * int(2**bits)
def _list_routes(): def _list_routes():
argv = ['netstat', '-rn'] argv = ['netstat', '-rn']
p = subprocess.Popen(argv, stdout=subprocess.PIPE) p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE)
routes = [] routes = []
for line in p.stdout: for line in p.stdout:
cols = re.split(r'\s+', line) cols = re.split(r'\s+', line)
@ -53,11 +58,12 @@ def _list_routes():
maskw = _ipmatch(cols[2]) # linux only maskw = _ipmatch(cols[2]) # linux only
mask = _maskbits(maskw) # returns 32 if maskw is null mask = _maskbits(maskw) # returns 32 if maskw is null
width = min(ipw[1], mask) width = min(ipw[1], mask)
ip = ipw[0] & (((1<<width)-1) << (32-width)) ip = ipw[0] & _shl(_shl(1, width) - 1, 32-width)
routes.append((socket.inet_ntoa(struct.pack('!I', ip)), width)) routes.append((socket.inet_ntoa(struct.pack('!I', ip)), width))
rv = p.wait() rv = p.wait()
if rv != 0: if rv != 0:
raise Fatal('%r returned %d' % (argv, rv)) log('WARNING: %r returned %d\n' % (argv, rv))
log('WARNING: That prevents --auto-nets from working.\n')
return routes return routes
@ -79,14 +85,15 @@ def start_hostwatch(seed_hosts):
# child # child
rv = 99 rv = 99
try: try:
s2.close() try:
os.dup2(s1.fileno(), 1) s2.close()
os.dup2(s1.fileno(), 0) os.dup2(s1.fileno(), 1)
s1.close() os.dup2(s1.fileno(), 0)
rv = hostwatch.hw_main(seed_hosts) or 0 s1.close()
except Exception, e: rv = hostwatch.hw_main(seed_hosts) or 0
log('%s\n' % _exc_dump()) except Exception, e:
rv = 98 log('%s\n' % _exc_dump())
rv = 98
finally: finally:
os._exit(rv) os._exit(rv)
s1.close() s1.close()
@ -120,8 +127,9 @@ def main():
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 routepkt = ''
for r in routes) for r in routes:
routepkt += '%s,%d\n' % r
mux.send(0, ssnet.CMD_ROUTES, routepkt) mux.send(0, ssnet.CMD_ROUTES, routepkt)
hw = Hostwatch() hw = Hostwatch()
@ -154,21 +162,6 @@ def main():
if rpid: if rpid:
raise Fatal('hostwatch exited unexpectedly: code 0x%04x\n' % rv) raise Fatal('hostwatch exited unexpectedly: code 0x%04x\n' % rv)
r = set() ssnet.runonce(handlers, mux)
w = set()
x = set()
handlers = filter(lambda s: s.ok, handlers)
for s in handlers:
s.pre_select(r,w,x)
debug2('Waiting: %d[%d,%d,%d] (fullness=%d/%d)...\n'
% (len(handlers), len(r), len(w), len(x),
mux.fullness, mux.too_full))
(r,w,x) = select.select(r,w,x)
#log('r=%r w=%r x=%r\n' % (r,w,x))
ready = set(r) | set(w) | set(x)
for s in handlers:
#debug2('check: %r: %r\n' % (s, s.socks & ready))
if s.socks & ready:
s.callback()
mux.check_fullness() mux.check_fullness()
mux.callback() mux.callback()

24
ssh.py
View File

@ -1,21 +1,27 @@
import sys, os, re, subprocess, socket, zlib import sys, os, re, socket, zlib
import compat.ssubprocess as ssubprocess
import helpers import helpers
from helpers import * from helpers import *
def readfile(name): def readfile(name):
basedir = os.path.dirname(os.path.abspath(sys.argv[0])) basedir = os.path.dirname(os.path.abspath(sys.argv[0]))
fullname = os.path.join(basedir, name) path = [basedir] + sys.path
return open(fullname, 'rb').read() 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): def empackage(z, filename):
(path,basename) = os.path.split(filename)
content = z.compress(readfile(filename)) content = z.compress(readfile(filename))
content += z.flush(zlib.Z_SYNC_FLUSH) content += z.flush(zlib.Z_SYNC_FLUSH)
return '%s\n%d\n%s' % (filename,len(content), content) return '%s\n%d\n%s' % (basename,len(content), content)
def connect(rhostport): def connect(rhostport, python):
main_exe = sys.argv[0] main_exe = sys.argv[0]
l = (rhostport or '').split(':', 1) l = (rhostport or '').split(':', 1)
rhost = l[0] rhost = l[0]
@ -29,6 +35,7 @@ def connect(rhostport):
z = zlib.compressobj(1) z = zlib.compressobj(1)
content = readfile('assembler.py') content = readfile('assembler.py')
content2 = (empackage(z, 'helpers.py') + content2 = (empackage(z, 'helpers.py') +
empackage(z, 'compat/ssubprocess.py') +
empackage(z, 'ssnet.py') + empackage(z, 'ssnet.py') +
empackage(z, 'hostwatch.py') + empackage(z, 'hostwatch.py') +
empackage(z, 'server.py') + empackage(z, 'server.py') +
@ -44,18 +51,17 @@ def connect(rhostport):
if not rhost: if not rhost:
argv = ['python', '-c', pyscript] argv = [python, '-c', pyscript]
else: else:
argv = ['ssh'] + portl + [rhost, '--', "python -c '%s'" % pyscript] argv = ['ssh'] + portl + [rhost, '--', "'%s' -c '%s'" % (python, pyscript)]
(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()
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()
debug2('executing: %r\n' % argv) debug2('executing: %r\n' % argv)
p = subprocess.Popen(argv, stdin=s1a, stdout=s1b, preexec_fn=setup, p = ssubprocess.Popen(argv, stdin=s1a, stdout=s1b, preexec_fn=setup,
close_fds=True) close_fds=True)
os.close(s1a) os.close(s1a)
os.close(s1b) os.close(s1b)

101
ssnet.py
View File

@ -1,6 +1,12 @@
import struct, socket, errno, select import struct, socket, errno, select
if not globals().get('skip_imports'): if not globals().get('skip_imports'):
from helpers import * from helpers import *
# these don't exist in the socket module in python 2.3!
SHUT_RD = 0
SHUT_WR = 1
SHUT_RDWR = 2
HDR_LEN = 8 HDR_LEN = 8
@ -31,6 +37,22 @@ cmd_to_name = {
def _add(l, elem):
if not elem in l:
l.append(elem)
def _fds(l):
out = []
for i in l:
try:
out.append(i.fileno())
except AttributeError:
out.append(i)
out.sort()
return out
def _nb_clean(func, *args): def _nb_clean(func, *args):
try: try:
return func(*args) return func(*args)
@ -38,6 +60,7 @@ def _nb_clean(func, *args):
if e.errno not in (errno.EWOULDBLOCK, errno.EAGAIN): if e.errno not in (errno.EWOULDBLOCK, errno.EAGAIN):
raise raise
else: else:
debug3('%s: err was: %s\n' % (func.__name__, e))
return None return None
@ -69,21 +92,30 @@ class SockWrapper:
debug1('%r: error was: %r\n' % (self, self.exc)) debug1('%r: error was: %r\n' % (self, self.exc))
def __repr__(self): def __repr__(self):
return 'SW:%s' % (self.peername,) if self.rsock == self.wsock:
fds = '#%d' % self.rsock.fileno()
else:
fds = '#%d,%d' % (self.rsock.fileno(), self.wsock.fileno())
return 'SW%s:%s' % (fds, self.peername)
def seterr(self, e): def seterr(self, e):
if not self.exc: if not self.exc:
self.exc = e self.exc = e
def try_connect(self): def try_connect(self):
if self.connect_to and self.shut_write:
self.noread()
self.connect_to = None
if not self.connect_to: if not self.connect_to:
return # already connected return # already connected
self.rsock.setblocking(False) self.rsock.setblocking(False)
debug3('%r: trying connect to %r\n' % (self, self.connect_to))
try: try:
self.rsock.connect(self.connect_to) self.rsock.connect(self.connect_to)
# connected successfully (Linux) # connected successfully (Linux)
self.connect_to = None self.connect_to = None
except socket.error, e: except socket.error, e:
debug3('%r: connect result: %r\n' % (self, 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] == errno.EISCONN: elif e.args[0] == errno.EISCONN:
@ -102,14 +134,14 @@ class SockWrapper:
if not self.shut_read: if not self.shut_read:
debug2('%r: done reading\n' % self) debug2('%r: done reading\n' % self)
self.shut_read = True self.shut_read = True
#self.rsock.shutdown(socket.SHUT_RD) # doesn't do anything anyway #self.rsock.shutdown(SHUT_RD) # doesn't do anything anyway
def nowrite(self): def nowrite(self):
if not self.shut_write: if not self.shut_write:
debug2('%r: done writing\n' % self) debug2('%r: done writing\n' % self)
self.shut_write = True self.shut_write = True
try: try:
self.wsock.shutdown(socket.SHUT_WR) self.wsock.shutdown(SHUT_WR)
except socket.error, e: except socket.error, e:
self.seterr(e) self.seterr(e)
@ -159,7 +191,7 @@ class SockWrapper:
wrote = outwrap.write(self.buf[0]) wrote = outwrap.write(self.buf[0])
self.buf[0] = self.buf[0][wrote:] self.buf[0] = self.buf[0][wrote:]
while self.buf and not self.buf[0]: while self.buf and not self.buf[0]:
self.buf[0:1] = [] self.buf.pop(0)
if not self.buf and self.shut_read: if not self.buf and self.shut_read:
outwrap.nowrite() outwrap.nowrite()
@ -167,12 +199,13 @@ class SockWrapper:
class Handler: class Handler:
def __init__(self, socks = None, callback = None): def __init__(self, socks = None, callback = None):
self.ok = True self.ok = True
self.socks = set(socks or []) self.socks = socks or []
if callback: if callback:
self.callback = callback self.callback = callback
def pre_select(self, r, w, x): def pre_select(self, r, w, x):
r |= self.socks for i in self.socks:
_add(r, i)
def callback(self): def callback(self):
log('--no callback defined-- %r\n' % self) log('--no callback defined-- %r\n' % self)
@ -181,7 +214,7 @@ class Handler:
v = s.recv(4096) v = s.recv(4096)
if not v: if not v:
log('--closed-- %r\n' % self) log('--closed-- %r\n' % self)
self.socks = set() self.socks = []
self.ok = False self.ok = False
@ -194,20 +227,20 @@ class Proxy(Handler):
def pre_select(self, r, w, x): def pre_select(self, r, w, x):
if self.wrap1.connect_to: if self.wrap1.connect_to:
w.add(self.wrap1.rsock) _add(w, self.wrap1.rsock)
elif self.wrap1.buf: elif self.wrap1.buf:
if not self.wrap2.too_full(): if not self.wrap2.too_full():
w.add(self.wrap2.wsock) _add(w, self.wrap2.wsock)
elif not self.wrap1.shut_read: elif not self.wrap1.shut_read:
r.add(self.wrap1.rsock) _add(r, self.wrap1.rsock)
if self.wrap2.connect_to: if self.wrap2.connect_to:
w.add(self.wrap2.rsock) _add(w, self.wrap2.rsock)
elif self.wrap2.buf: elif self.wrap2.buf:
if not self.wrap1.too_full(): if not self.wrap1.too_full():
w.add(self.wrap1.wsock) _add(w, self.wrap1.wsock)
elif not self.wrap2.shut_read: elif not self.wrap2.shut_read:
r.add(self.wrap2.rsock) _add(r, self.wrap2.rsock)
def callback(self): def callback(self):
self.wrap1.try_connect() self.wrap1.try_connect()
@ -216,6 +249,12 @@ class Proxy(Handler):
self.wrap2.fill() self.wrap2.fill()
self.wrap1.copy_to(self.wrap2) self.wrap1.copy_to(self.wrap2)
self.wrap2.copy_to(self.wrap1) self.wrap2.copy_to(self.wrap1)
if self.wrap1.buf and self.wrap2.shut_write:
self.wrap1.buf = []
self.wrap1.noread()
if self.wrap2.buf and self.wrap1.shut_write:
self.wrap2.buf = []
self.wrap2.noread()
if (self.wrap1.shut_read and self.wrap2.shut_read and if (self.wrap1.shut_read and self.wrap2.shut_read and
not self.wrap1.buf and not self.wrap2.buf): not self.wrap1.buf and not self.wrap2.buf):
self.ok = False self.ok = False
@ -247,7 +286,10 @@ class Mux(Handler):
return self.chani return self.chani
def amount_queued(self): def amount_queued(self):
return sum(len(b) for b in self.outbuf) total = 0
for b in self.outbuf:
total += len(b)
return total
def check_fullness(self): def check_fullness(self):
if self.fullness > 32768: if self.fullness > 32768:
@ -308,7 +350,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]))) debug2('mux wrote: %r/%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]:
@ -346,9 +388,9 @@ class Mux(Handler):
break break
def pre_select(self, r, w, x): def pre_select(self, r, w, x):
r.add(self.rsock) _add(r, self.rsock)
if self.outbuf: if self.outbuf:
w.add(self.wsock) _add(w, self.wsock)
def callback(self): def callback(self):
(r,w,x) = select.select([self.rsock], [self.wsock], [], 0) (r,w,x) = select.select([self.rsock], [self.wsock], [], 0)
@ -420,3 +462,28 @@ def connect_dst(ip, port):
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))
def runonce(handlers, mux):
r = []
w = []
x = []
handlers = filter(lambda s: s.ok, handlers)
for s in handlers:
s.pre_select(r,w,x)
debug2('Waiting: %d r=%r w=%r x=%r (fullness=%d/%d)\n'
% (len(handlers), _fds(r), _fds(w), _fds(x),
mux.fullness, mux.too_full))
(r,w,x) = select.select(r,w,x)
debug2(' Ready: %d r=%r w=%r x=%r\n'
% (len(handlers), _fds(r), _fds(w), _fds(x)))
ready = r+w+x
did = {}
for h in handlers:
for s in h.socks:
if s in ready:
h.callback()
did[s] = 1
for s in ready:
if not s in did:
raise Fatal('socket %r was not used by any handler' % s)