Compare commits

..

14 Commits

Author SHA1 Message Date
41fd0348eb Fix a bug when packets are received on a channel after it closes.
Reported by cbowns.
2010-12-11 17:27:12 -08:00
1907048dad Remove the never-used and misleading CMD_CLOSE. 2010-12-09 19:20:09 -08:00
82e1d1c166 Fix memory leak of MuxWrapper object.
(Note by apenwarr: I used Roger's original patch as the basis for this one,
but implemented it a different way.  All errors are thus my fault, but Roger
gets the credit for actually tracking down the circular reference that
caused the memory leak.)
2010-12-09 19:20:07 -08:00
a497132c01 Add debug messages for counting SockWrapper objects.
You can use this to confirm that a memory leak exists.
2010-12-09 19:20:06 -08:00
7354600849 Fix a socket leak: delete object after close on both direction.
(Note by apenwarr: seems to still work for me.  The reason the
problem occurred is that reassigning 'handlers' doesn't change it in its
parent; it creates a whole new list, and the caller still owns the old one
with all the dead sockets in it.  The problem seems to have been introduced
in commit 84376284db when I factored the
runonce() functionality out of the client and server but didn't notice this
reassignment.)
2010-12-09 19:20:01 -08:00
918725c485 Oops, earlier ipv6 patch didn't work if no -r option is specified. 2010-12-09 19:20:01 -08:00
95c9b788a0 Add support for IPv6 remote hosts.
Supported sshuttle commands for IPv6:

./sshuttle -r "IPv6:addr" 0.0.0.0/0 -vv
./sshuttle -r "[IPv6:addr]" 0.0.0.0/0 -vv
./sshuttle -r "[IPv6:addr]:22" 0.0.0.0/0 -vv

Technically "invalid" address/port formats, but they can still be parsed because they’re unambiguous, so these also work:

./sshuttle -r "IPv6:addr]" 0.0.0.0/0 -vv
./sshuttle -r "IPv6:addr]:" 0.0.0.0/0 -vv
./sshuttle -r "IPv6:addr]:22" 0.0.0.0/0 -vv
./sshuttle -r "[IPv6:addr" 0.0.0.0/0 -vv

(If you have a Mac with Back To My Mac, use dns-sd to discover the remote host's IPv6 address:
dns-sd -G v4v6 <machine name>.<member name>.members.mac.com )
2010-11-19 15:13:35 -08:00
ef71751846 Add a sshuttle.8 manpage.
You need to have 'pandoc' installed in order to render it from sshuttle.md.
2010-11-09 01:59:51 -08:00
32b4defa9b Add a new --ssh-cmd= option to let you override the ssh command.
Requested by Axel Beckert.
2010-11-09 00:17:01 -08:00
8b7605cc5d Remove the --noserver option.
It didn't work anyway.  Obviously it hasn't been tested (or apparently
needed) in a long time.
2010-11-08 23:59:26 -08:00
bcf1892305 Make password prompting more clear.
Based on suggestions by Jason Grossman and Ed Maste on the mailing list.

We now add a [local su] prefix to the 'su' password prompt (by cheating and
printing it before calling su), and we replace the 'sudo' password prompt
with '[local sudo] Password: ' (by using the little-known and
hopefully-portable -p option).

We no longer call sudo or su if the uid is already 0; otherwise the prefix
on the 'su' prompt would look weird, since su wouldn't ask for a password in
that case.

We don't add a prefix to the ssh password prompt, because it's too hard to
tell if there will *be* an ssh password prompt.  But people will probably
assume that the password request is for the server anyway; few people are
likely to think that 'sshuttle -r myhost.com' is going to prompt for the
*local* password.

Of course none of this is a problem on a modern OS, like Debian, that would
say something like "Password for apenwarr@myhost.com:" instead of just
"Password:".  MacOS doesn't do that, however, so I assume many other OSes
also don't.  Let's try to help them out.
2010-11-08 23:35:16 -08:00
fe742c928d firewall.py: don't die if a given sysctl doesn't exist.
Instead, get a list of known sysctls in the interesting prefix (net.inet.ip)
and check if there's an entry in the list for each sysctl we want to change.
If there isn't, then don't try to change it.

This fixes a problem with FreeBSD, which doesn't have
net.inet.ip.scopedroute but also doesn't need it.  Probably also fixes MacOS
10.5, which probably didn't have that either, but I don't know for sure.

Reported by Ed Maste.
2010-10-16 20:11:30 -06:00
10ce1ee5d4 ipfw: use 'delete' instead of 'del' to avoid a warning on freebsd.
'del' is an abbreviation that happened to work because of substring matching
in earlier versions of ipfw, but apparently they're planning to remove the
substring matching eventually.  In any case, 'delete' has always worked, so
there's no downside to using that.

Reported by Ed Maste.
2010-10-05 13:29:12 -04:00
a32305a275 server.py: don't send partial hostwatch lists.
If hostwatch has a lot of stuff to say all at once, it would come in more
than one recv() packet, and server.py would send each packet individually as
a CMD_HOST_LIST message.  Unfortunately, client.py (rightly) expects each
CMD_HOST_LIST message to be complete, ie. a correct sequence of rows.

So now server.py makes sure of this.  If there's a leftover bit (ie. an
unterminated line), it saves it for later.

Bug reported by user "Duke" on the mailing list.
2010-10-04 02:47:43 -07:00
10 changed files with 385 additions and 75 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
*.pyc
*~
*.8

19
Makefile Normal file
View File

@ -0,0 +1,19 @@
PANDOC:=$(shell \
if pandoc </dev/null 2>/dev/null; then \
echo pandoc; \
else \
echo "Warning: pandoc not installed; can't generate manpages." >&2; \
echo '@echo Skipping: pandoc'; \
fi)
default: all
all: sshuttle.8
sshuttle.8: sshuttle.md
%.8: %.md
$(PANDOC) -s -r markdown -w man -o $@ $<
clean:
rm -f *~ */*~ .*~ */.*~ *.8 *.tmp */*.tmp *.pyc */*.pyc

View File

@ -54,8 +54,14 @@ This is how you use it:
- <tt>./sshuttle -r username@sshserver 0.0.0.0/0 -vv</tt>
(You may be prompted for one or more passwords; first, the
local password to become root using either sudo or su, and
then the remote ssh password. Or you might have sudo and ssh set
up to not require passwords, in which case you won't be
prompted at all.)
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
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;

View File

@ -31,7 +31,7 @@ class FirewallClient:
['-v'] * (helpers.verbose or 0) +
['--firewall', str(port)])
argv_tries = [
['sudo'] + argvbase,
['sudo', '-p', '[local sudo] Password: '] + argvbase,
['su', '-c', ' '.join(argvbase)],
argvbase
]
@ -45,8 +45,12 @@ class FirewallClient:
# run in the child process
s2.close()
e = None
if os.getuid() == 0:
argv_tries = argv_tries[-1:] # last entry only
for argv in argv_tries:
try:
if argv[0] == 'su':
sys.stderr.write('[local su] ')
self.p = ssubprocess.Popen(argv, stdout=s1, preexec_fn=setup)
e = None
break
@ -94,16 +98,15 @@ class FirewallClient:
raise Fatal('cleanup: %r returned %d' % (self.argv, rv))
def _main(listener, fw, use_server, remotename, python, seed_hosts, auto_nets):
def _main(listener, fw, ssh_cmd, remotename, python, seed_hosts, auto_nets):
handlers = []
if use_server:
if helpers.verbose >= 1:
helpers.logprefix = 'c : '
else:
helpers.logprefix = 'client: '
debug1('connecting to server...\n')
try:
(serverproc, serversock) = ssh.connect(remotename, python)
(serverproc, serversock) = ssh.connect(ssh_cmd, remotename, python)
except socket.error, e:
if e.errno == errno.EPIPE:
raise Fatal("failed to establish ssh session")
@ -158,12 +161,9 @@ def _main(listener, fw, use_server, remotename, python, seed_hosts, auto_nets):
debug1("-- ignored: that's my address!\n")
sock.close()
return
if use_server:
chan = mux.next_channel()
mux.send(chan, ssnet.CMD_CONNECT, '%s,%s' % dstip)
outwrap = MuxWrapper(mux, chan)
else:
outwrap = ssnet.connect_dst(dstip[0], dstip[1])
handlers.append(Proxy(SockWrapper(sock, sock), outwrap))
handlers.append(Handler([listener], onaccept))
@ -172,18 +172,16 @@ def _main(listener, fw, use_server, remotename, python, seed_hosts, auto_nets):
mux.send(0, ssnet.CMD_HOST_REQ, '\n'.join(seed_hosts))
while 1:
if use_server:
rv = serverproc.poll()
if rv:
raise Fatal('server died with error code %d' % rv)
ssnet.runonce(handlers, mux)
if use_server:
mux.callback()
mux.check_fullness()
def main(listenip, use_server, remotename, python, seed_hosts, auto_nets,
def main(listenip, ssh_cmd, remotename, python, seed_hosts, auto_nets,
subnets_include, subnets_exclude):
debug1('Starting sshuttle proxy.\n')
listener = socket.socket()
@ -214,7 +212,7 @@ def main(listenip, use_server, remotename, python, seed_hosts, auto_nets,
fw = FirewallClient(listenip[1], subnets_include, subnets_exclude)
try:
return _main(listener, fw, use_server, remotename,
return _main(listener, fw, ssh_cmd, remotename,
python, seed_hosts, auto_nets)
finally:
fw.done()

View File

@ -81,17 +81,19 @@ def ipfw_rule_exists(n):
return found
def sysctl_get(name):
argv = ['sysctl', '-n', name]
_oldctls = {}
def _fill_oldctls(prefix):
argv = ['sysctl', prefix]
p = ssubprocess.Popen(argv, stdout = ssubprocess.PIPE)
line = p.stdout.readline()
for line in p.stdout:
assert(line[-1] == '\n')
(k,v) = line[:-1].split(': ', 1)
_oldctls[k] = v
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):
@ -100,11 +102,19 @@ def _sysctl_set(name, val):
rv = ssubprocess.call(argv, stdout = open('/dev/null', 'w'))
_oldctls = []
_changedctls = []
def sysctl_set(name, val):
oldval = sysctl_get(name)
if str(val) != str(oldval):
_oldctls.append((name, oldval))
PREFIX = 'net.inet.ip'
assert(name.startswith(PREFIX + '.'))
val = str(val)
if not _oldctls:
_fill_oldctls(PREFIX)
if not (name in _oldctls):
debug1('>> No such sysctl: %r\n' % name)
return
oldval = _oldctls[name]
if val != oldval:
_changedctls.append(name)
return _sysctl_set(name, val)
@ -122,10 +132,11 @@ def do_ipfw(port, subnets):
# cleanup any existing rules
if ipfw_rule_exists(port):
ipfw('del', sport)
ipfw('delete', sport)
while _oldctls:
(name,oldval) = _oldctls.pop()
while _changedctls:
name = _changedctls.pop()
oldval = _oldctls[name]
_sysctl_set(name, oldval)
if subnets:

View File

@ -56,8 +56,8 @@ python= specify the name/path of the python interpreter on the remote server [py
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
e,ssh-cmd= the command to use to connect to the remote [ssh]
seed-hosts= with -H, use these hostnames for initial scan (comma-separated)
noserver don't use a separate server process (mostly for debugging)
server (internal use only)
firewall (internal use only)
hostwatch (internal use only)
@ -98,9 +98,9 @@ try:
else:
sh = None
sys.exit(client.main(parse_ipport(opt.listen or '0.0.0.0:0'),
not opt.noserver,
opt.ssh_cmd,
remotename,
(opt.python or "python"),
opt.python,
sh,
opt.auto_nets,
parse_subnets(includes),

View File

@ -133,12 +133,20 @@ def main():
mux.send(0, ssnet.CMD_ROUTES, routepkt)
hw = Hostwatch()
hw.leftover = ''
def hostwatch_ready():
assert(hw.pid)
content = hw.sock.recv(4096)
if content:
mux.send(0, ssnet.CMD_HOST_LIST, content)
lines = (hw.leftover + content).split('\n')
if lines[-1]:
# no terminating newline: entry isn't complete yet!
hw.leftover = lines.pop()
lines.append('')
else:
hw.leftover = ''
mux.send(0, ssnet.CMD_HOST_LIST, '\n'.join(lines))
else:
raise Fatal('hostwatch process died')

31
ssh.py
View File

@ -21,17 +21,35 @@ def empackage(z, filename):
return '%s\n%d\n%s' % (basename,len(content), content)
def connect(rhostport, python):
def connect(ssh_cmd, rhostport, python):
main_exe = sys.argv[0]
portl = []
rhostIsIPv6 = False
if (rhostport or '').count(':') > 1:
rhostIsIPv6 = True
if rhostport.count(']') or rhostport.count('['):
result = rhostport.split(']')
rhost = result[0].strip('[')
if len(result) > 1:
result[1] = result[1].strip(':')
if result[1] is not '':
portl = ['-p', str(int(result[1]))]
else: # can't disambiguate IPv6 colons and a port number. pass the hostname through.
rhost = rhostport
else: # IPv4
l = (rhostport or '').split(':', 1)
rhost = l[0]
portl = []
if len(l) > 1:
portl = ['-p', str(int(l[1]))]
if rhost == '-':
rhost = None
ipv6flag = []
if rhostIsIPv6:
ipv6flag = ['-6']
z = zlib.compressobj(1)
content = readfile('assembler.py')
content2 = (empackage(z, 'helpers.py') +
@ -53,7 +71,14 @@ def connect(rhostport, python):
if not rhost:
argv = [python, '-c', pyscript]
else:
argv = ['ssh'] + portl + [rhost, '--', "'%s' -c '%s'" % (python, pyscript)]
if ssh_cmd:
sshl = ssh_cmd.split(' ')
else:
sshl = ['ssh']
argv = (sshl +
portl +
ipv6flag +
[rhost, '--', "'%s' -c '%s'" % (python, pyscript)])
(s1,s2) = socket.socketpair()
def setup():
# runs in the child process

223
sshuttle.md Normal file
View File

@ -0,0 +1,223 @@
% sshuttle(8) Sshuttle 0.42
% Avery Pennarun <apenwarr@gmail.com>
% 2010-11-09
# NAME
sshuttle - a transparent proxy-based VPN using ssh
# SYNOPSIS
sshuttle [options...] [-r [username@]sshserver[:port]] \<subnets...\>
# DESCRIPTION
sshuttle allows you to create a VPN connection from your
machine to any remote server that you can connect to via
ssh, as long as that server has python 2.3 or higher.
To work, you must have root access on the local machine,
but you can have a normal account on the server.
It's valid to run sshuttle more than once simultaneously on
a single client machine, connecting to a different server
every time, so you can be on more than one VPN at once.
If run on a router, sshuttle can forward traffic for your
entire subnet to the VPN.
# OPTIONS
\<subnets...\>
: a list of subnets to route over the VPN, in the form
`a.b.c.d[/width]`. Valid examples are 1.2.3.4 (a
single IP address), 1.2.3.4/32 (equivalent to 1.2.3.4),
1.2.3.0/24 (a 24-bit subnet, ie. with a 255.255.255.0
netmask), and 0/0 ('just route everything through the
VPN').
-l, --listen=*[ip:]port*
: use this ip address and port number as the transparent
proxy port. By default sshuttle finds an available
port automatically, so you don't need to override it.
-H, --auto-hosts
: scan for remote hostnames and update the local /etc/hosts
file with matching entries for as long as the VPN is
open. This is nicer than changing your system's DNS
(/etc/resolv.conf) settings, for several reasons. First,
hostnames are added without domain names attached, so
you can `ssh thatserver` without worrying if your local
domain matches the remote one. Second, if you sshuttle
into more than one VPN at a time, it's impossible to
use more than one DNS server at once anyway, but
sshuttle correctly merges /etc/hosts entries between
all running copies. Third, if you're only routing a
few subnets over the VPN, you probably would prefer to
keep using your local DNS server for everything else.
-N, --auto-nets
: in addition to the subnets provided on the command
line, ask the server which subnets it thinks we should
route, and route those automatically. The suggestions
are taken automatically from the server's routing
table.
--python
: specify the name/path of the remote python interpreter.
The default is just `python`, which means to use the
default python interpreter on the remote system's PATH.
-r, --remote=*[username@]sshserver[:port]*
: the remote hostname and optional username and ssh
port number to use for connecting to the remote server.
For example, example.com, testuser@example.com,
testuser@example.com:2222, or example.com:2244.
-x, --exclude=*subnet*
: explicitly exclude this subnet from forwarding. The
format of this option is the same as the `<subnets>`
option. To exclude more than one subnet, specify the
`-x` option more than once. You can say something like
`0/0 -x 1.2.3.0/24` to forward everything except the
local subnet over the VPN, for example.
-v, --verbose
: print more information about the session. This option
can be used more than once for increased verbosity. By
default, sshuttle prints only error messages.
-e, --ssh-cmd
: the command to use to connect to the remote server. The
default is just `ssh`. Use this if your ssh client is
in a non-standard location or you want to provide extra
options to the ssh command, for example, `-e 'ssh -v'`.
--seed-hosts
: a comma-separated list of hostnames to use to
initialize the `--auto-hosts` scan algorithm.
`--auto-hosts` does things like poll local SMB servers
for lists of local hostnames, but can speed things up
if you use this option to give it a few names to start
from.
--server
: (internal use only) run the sshuttle server on
stdin/stdout. This is what the client runs on
the remote end.
--firewall
: (internal use only) run the firewall manager. This is
the only part of sshuttle that must run as root. If
you start sshuttle as a non-root user, it will
automatically run `sudo` or `su` to start the firewall
manager, but the core of sshuttle still runs as a
normal user.
--hostwatch
: (internal use only) run the hostwatch daemon. This
process runs on the server side and collects hostnames for
the `--auto-hosts` option. Using this option by itself
makes it a lot easier to debug and test the `--auto-hosts`
feature.
# EXAMPLES
Test locally by proxying all local connections, without using ssh:
$ sshuttle -v 0/0
Starting sshuttle proxy.
Listening on ('0.0.0.0', 12300).
[local sudo] Password:
firewall manager ready.
c : connecting to server...
s: available routes:
s: 192.168.42.0/24
c : connected.
firewall manager: starting transproxy.
c : Accept: '192.168.42.106':50035 -> '192.168.42.121':139.
c : Accept: '192.168.42.121':47523 -> '77.141.99.22':443.
...etc...
^C
firewall manager: undoing changes.
KeyboardInterrupt
c : Keyboard interrupt: exiting.
c : SW#8:192.168.42.121:47523: deleting
c : SW#6:192.168.42.106:50035: deleting
Test connection to a remote server, with automatic hostname
and subnet guessing:
$ sshuttle -vNHr example.org
Starting sshuttle proxy.
Listening on ('0.0.0.0', 12300).
firewall manager ready.
c : connecting to server...
s: available routes:
s: 77.141.99.0/24
c : connected.
c : seed_hosts: []
firewall manager: starting transproxy.
hostwatch: Found: testbox1: 1.2.3.4
hostwatch: Found: mytest2: 5.6.7.8
hostwatch: Found: domaincontroller: 99.1.2.3
c : Accept: '192.168.42.121':60554 -> '77.141.99.22':22.
^C
firewall manager: undoing changes.
c : Keyboard interrupt: exiting.
c : SW#6:192.168.42.121:60554: deleting
# DISCUSSION
When it starts, sshuttle creates an ssh session to the
server specified by the `-r` option. If `-r` is omitted,
it will start both its client and server locally, which is
sometimes useful for testing.
After connecting to the remote server, sshuttle uploads its
(python) source code to the remote end and executes it
there. Thus, you don't need to install sshuttle on the
remote server, and there are never sshuttle version
conflicts between client and server.
Unlike most VPNs, sshuttle forwards sessions, not packets.
That is, it uses kernel transparent proxying (`iptables
REDIRECT` rules on Linux, or `ipfw fwd` rules on BSD) to
capture outgoing TCP sessions, then creates entirely
separate TCP sessions out to the original destination at
the other end of the tunnel.
Packet-level forwarding (eg. using the tun/tap devices on
Linux) seems elegant at first, but it results in
several problems, notably the 'tcp over tcp' problem. The
tcp protocol depends fundamentally on packets being dropped
in order to implement its congestion control agorithm; if
you pass tcp packets through a tcp-based tunnel (such as
ssh), the inner tcp packets will never be dropped, and so
the inner tcp stream's congestion control will be
completely broken, and performance will be terrible. Thus,
packet-based VPNs (such as IPsec and openvpn) cannot use
tcp-based encrypted streams like ssh or ssl, and have to
implement their own encryption from scratch, which is very
complex and error prone.
sshuttle's simplicity comes from the fact that it can
safely use the existing ssh encrypted tunnel without
incurring a performance penalty. It does this by letting
the client-side kernel manage the incoming tcp stream, and
the server-side kernel manage the outgoing tcp stream;
there is no need for congestion control to be shared
between the two separate streams, so a tcp-based tunnel is
fine.
# SEE ALSO
`ssh`(1), `python`(1)

View File

@ -15,7 +15,7 @@ CMD_EXIT = 0x4200
CMD_PING = 0x4201
CMD_PONG = 0x4202
CMD_CONNECT = 0x4203
CMD_CLOSE = 0x4204
# CMD_CLOSE = 0x4204 # never used - removed
CMD_EOF = 0x4205
CMD_DATA = 0x4206
CMD_ROUTES = 0x4207
@ -27,7 +27,6 @@ cmd_to_name = {
CMD_PING: 'PING',
CMD_PONG: 'PONG',
CMD_CONNECT: 'CONNECT',
CMD_CLOSE: 'CLOSE',
CMD_EOF: 'EOF',
CMD_DATA: 'DATA',
CMD_ROUTES: 'ROUTES',
@ -75,8 +74,12 @@ def _try_peername(sock):
return 'unknown'
_swcount = 0
class SockWrapper:
def __init__(self, rsock, wsock, connect_to=None, peername=None):
global _swcount
_swcount += 1
debug3('creating new SockWrapper (%d now exist\n)' % _swcount)
self.exc = None
self.rsock = rsock
self.wsock = wsock
@ -87,7 +90,9 @@ class SockWrapper:
self.try_connect()
def __del__(self):
debug1('%r: deleting\n' % self)
global _swcount
_swcount -= 1
debug1('%r: deleting (%d remain)\n' % (self, _swcount))
if self.exc:
debug1('%r: error was: %r\n' % (self, self.exc))
@ -258,6 +263,8 @@ class Proxy(Handler):
if (self.wrap1.shut_read and self.wrap2.shut_read and
not self.wrap1.buf and not self.wrap2.buf):
self.ok = False
self.wrap1.nowrite()
self.wrap2.nowrite()
class Mux(Handler):
@ -343,7 +350,11 @@ class Mux(Handler):
else:
raise Exception('got CMD_HOST_LIST without got_host_list?')
else:
callback = self.channels[channel]
callback = self.channels.get(channel)
if not callback:
log('warning: closed channel %d got cmd=%s len=%d\n'
% (channel, cmd_to_name.get(cmd,hex(cmd)), len(data)))
else:
callback(cmd, data)
def flush(self):
@ -419,11 +430,19 @@ class MuxWrapper(SockWrapper):
def noread(self):
if not self.shut_read:
self.shut_read = True
self.maybe_close()
def nowrite(self):
if not self.shut_write:
self.shut_write = True
self.mux.send(self.channel, CMD_EOF, '')
self.maybe_close()
def maybe_close(self):
if self.shut_read and self.shut_write:
# remove the mux's reference to us. The python garbage collector
# will then be able to reap our object.
self.mux.channels[self.channel] = None
def too_full(self):
return self.mux.too_full
@ -443,10 +462,7 @@ class MuxWrapper(SockWrapper):
return None # no data available right now
def got_packet(self, cmd, data):
if cmd == CMD_CLOSE:
self.noread()
self.nowrite()
elif cmd == CMD_EOF:
if cmd == CMD_EOF:
self.noread()
elif cmd == CMD_DATA:
self.buf.append(data)
@ -468,7 +484,10 @@ def runonce(handlers, mux):
r = []
w = []
x = []
handlers = filter(lambda s: s.ok, handlers)
to_remove = filter(lambda s: not s.ok, handlers)
for h in to_remove:
handlers.remove(h)
for s in handlers:
s.pre_select(r,w,x)
debug2('Waiting: %d r=%r w=%r x=%r (fullness=%d/%d)\n'