Compare commits

..

50 Commits

Author SHA1 Message Date
6121a6dca3 sshuttle.md: fix whitespace issues. 2014-12-16 14:06:13 +11:00
c576682caf sshuttle.md: document Internet Sharing incompatibility 2014-12-16 14:04:25 +11:00
343905784b Added --exclude-from feature.
(Slightly modified by apenwarr)
2014-10-06 13:04:33 +11:00
91d705c24f Document missing --dns option in sshuttle manpage 2014-10-06 13:01:31 +11:00
e5251969b0 firewall.py: catch SIGINT and SIGTERM too.
There were still a few conditions under some OSes that would cause
firewall.py to terminate without cleaning up the firewall settings.  'pkill
sshuttle' was one of them.  Ignore a couple more signals to further ensure a
correct cleanup.

(This only affects sshuttle --firewall, which is a subprocess of the main
sshuttle process.  The firewall is supposed to exit automatically whenever
the client exits, and so far that part seems to work reliably.)
2014-10-06 13:00:57 +11:00
b8e150fc4d Use python-config to compile with latest Python version.
For OS X systems without Python 2.5, runpython.c does not compile.
Use python-config to get the paths for the latest version.
2014-10-03 14:58:26 -07:00
36378efe5e Revert Debian package specific change.
This is required so sshuttle can be run from git repository.

The way the Debian package is created is non-standard, and probably
needs redoing anyway.
2014-09-23 11:14:56 +10:00
cba8b261c6 Use the new arguments from redo v0.10.
(apenwarr: also updates to the matching, latest minimal/do)
2014-09-23 10:14:59 +10:00
39425a03c5 firewall: catch SIGHUP and SIGPIPE.
Not sure if this will fix anything, but it might stop the problem reported
on some MacOS versions where the firewall doesn't get cleaned up correctly.
2014-09-23 10:14:27 +10:00
5a39341d50 ui-macos/main.py: fix wait() to avoid deadlock.
If the subprocess was trying to write to its stdout/stderr, its process
would never actually finish because it was blocked waiting for us to read
it, but we were blocked on waitpid().  Instead, use waitpid(WNOHANG) and
continually read from the subprocess (which should be a blocking operation)
until it exits.
2014-09-23 10:11:13 +10:00
3eef3635ac ipfw: don't use 'log' parameter.
I guess we were causing the kernel to syslog on every single packet on
MacOS.  Oops.
2014-09-23 10:09:16 +10:00
f1c79c7e92 PEP8 fixes. 2014-09-16 10:24:16 +10:00
5529a04cc9 Fix whitespace. 2014-09-15 14:46:45 +10:00
035c5ad7a6 Fix: Use sock for consistency. 2014-09-15 14:44:07 +10:00
c013386ecb If IPv4 bind but IPv6 succeeds don't error. 2014-09-15 14:32:59 +10:00
a33f6199c4 Remove broken IPv6 code. 2014-09-15 14:23:09 +10:00
0f2c249e4d Remove dodgy code. 2014-09-15 14:14:52 +10:00
192e5b36e8 Added some Ubuntu notes 2014-09-15 14:14:52 +10:00
4036b7dfcf Added some requirements 2014-09-15 14:14:52 +10:00
8ec6daf02a Added a shell script to make a .deb package 2014-09-15 14:14:52 +10:00
e2507f86d5 Added a control file for the Debian package 2014-09-15 14:14:52 +10:00
e4fe62de3c Added a sample prefixes file 2014-09-15 14:14:52 +10:00
734f32d112 Sample tunnel configuration 2014-09-15 14:14:52 +10:00
a34e106b55 Changed the sshuttle binary to point to install 2014-09-15 14:14:52 +10:00
e6e80f1f04 Changed the file to be more "canonical" 2014-09-15 14:14:52 +10:00
32865bd2dd Added the PyXAPI requirement to the readme 2014-09-15 14:14:52 +10:00
2f11f50bc2 Adding more robust exit codes 2014-09-15 14:14:52 +10:00
a95491765d Added -s to accept subnets from a config file 2014-09-15 14:14:52 +10:00
d8754dc3a0 First version; still has debugging 2014-09-15 14:14:52 +10:00
3956a5df94 Moved docs out of the src directory 2014-09-15 14:14:52 +10:00
7442eb61e9 Mass relocation of files to their own subdirectory 2014-09-15 14:14:51 +10:00
6107abf10f Fixed a bug where lack of IPv6 destination = fatal
There was a problem where trying to bind .v4 and .v6 listeners would set them
to None if there was nothing to bind (if, say, you weren't binding an IPv6
listener).  Later, the code then would try to call a member function of the
listener.  The member function would not do anything if there was no listener,
but trying to dereference None yielded the broken behavior.
2014-09-15 14:14:51 +10:00
5e8ad544ee TProxy UDP support, including DNS. 2011-08-26 09:53:59 +10:00
20254bab57 TProxy IPv6 support. 2011-07-11 11:20:52 +10:00
f41c6b62e5 TProxy support as well as NAT support. 2011-07-11 11:20:51 +10:00
9a7412c08f More changes to simplify the upcomming IPv6 patch. 2011-07-11 11:20:50 +10:00
c6200eecdc Choose which method to use for intercepting traffic. 2011-07-11 11:16:51 +10:00
55f86a8b3f Rewrite binding code. DNS port may now be different from TCP port. 2011-07-11 11:16:50 +10:00
e7caae8126 Make it clear ports are for IPv4. 2011-07-11 11:16:49 +10:00
4db9b372c2 Make iptables functions work with any table, not just nat. 2011-07-11 11:16:48 +10:00
061e6a0933 Keep track of address family address belongs too. 2011-06-16 14:51:34 +10:00
50849b86b0 This hack is IPv4 specific, ensure it doesn't get used for other
address families.
2011-06-16 14:51:34 +10:00
6b7cf770f5 Improve debugging. 2011-06-16 14:42:15 +10:00
b26e1ee652 Introduce independent_listener, will be used for both IPv4 and IPv6
connections.
2011-06-06 11:14:28 +10:00
6500067905 When DNS response received, MUX channel no longer required. Delete it. 2011-06-06 11:12:23 +10:00
50c2b86f15 Rename onaccept to onaccept_tcp as it is tcp specific. 2011-06-06 10:54:57 +10:00
97dca42291 Rename dnslistener to dns_listener for consistency with tcp_listener. 2011-06-06 10:44:38 +10:00
6e53b07002 Rename listener to tcp_listener, as it is TCP specific. 2011-06-06 10:43:39 +10:00
08bd1dca46 Rename TCP specific commands to clarify they are TCP specific. 2011-06-06 10:39:50 +10:00
94566b5efc Split expiration into handling into another function. 2011-06-06 10:23:04 +10:00
74 changed files with 1897 additions and 1336 deletions

View File

@ -1,3 +0,0 @@
*.8
/md-to-man
/*.md.tmp

View File

@ -1,5 +0,0 @@
/bin/ls *.md |
sed 's/\.md/.8/' |
xargs redo-ifchange
redo-always

View File

@ -1 +0,0 @@
rm -f *~ .*~ *.8 t/*.8 md-to-man *.tmp t/*.tmp

View File

@ -1,2 +0,0 @@
redo-ifchange md-to-man $2.md.tmp
. ./md-to-man $1 $2 $3

View File

@ -1,3 +0,0 @@
redo-ifchange ../version/vars $2.md
. ../version/vars
sed -e "s/%VERSION%/$TAG/" -e "s/%DATE%/$DATE/" $2.md

View File

@ -1,8 +0,0 @@
redo-ifchange md2man.py
if ./md2man.py </dev/null >/dev/null; then
echo './md2man.py $2.md.tmp'
else
echo "Warning: md2man.py missing modules; can't generate manpages." >&2
echo "Warning: try this: sudo easy_install markdown BeautifulSoup" >&2
echo 'echo Skipping: $2.1 >&2'
fi

View File

@ -1,278 +0,0 @@
#!/usr/bin/env python
import sys, os, markdown, re
from BeautifulSoup import BeautifulSoup
def _split_lines(s):
return re.findall(r'([^\n]*\n?)', s)
class Writer:
def __init__(self):
self.started = False
self.indent = 0
self.last_wrote = '\n'
def _write(self, s):
if s:
self.last_wrote = s
sys.stdout.write(s)
def writeln(self, s):
if s:
self.linebreak()
self._write('%s\n' % s)
def write(self, s):
if s:
self.para()
for line in _split_lines(s):
if line.startswith('.'):
self._write('\\&' + line)
else:
self._write(line)
def linebreak(self):
if not self.last_wrote.endswith('\n'):
self._write('\n')
def para(self, bullet=None):
if not self.started:
if not bullet:
bullet = ' '
if not self.indent:
self.writeln(_macro('.PP'))
else:
assert(self.indent >= 2)
prefix = ' '*(self.indent-2) + bullet + ' '
self.writeln('.IP "%s" %d' % (prefix, self.indent))
self.started = True
def end_para(self):
self.linebreak()
self.started = False
def start_bullet(self):
self.indent += 3
self.para(bullet='\\[bu]')
def end_bullet(self):
self.indent -= 3
self.end_para()
w = Writer()
def _macro(name, *args):
if not name.startswith('.'):
raise ValueError('macro names must start with "."')
fixargs = []
for i in args:
i = str(i)
i = i.replace('\\', '')
i = i.replace('"', "'")
if (' ' in i) or not i:
i = '"%s"' % i
fixargs.append(i)
return ' '.join([name] + list(fixargs))
def macro(name, *args):
w.writeln(_macro(name, *args))
def _force_string(owner, tag):
if tag.string:
return tag.string
else:
out = ''
for i in tag:
if not (i.string or i.name in ['a', 'br']):
raise ValueError('"%s" tags must contain only strings: '
'got %r: %r' % (owner.name, tag.name, tag))
out += _force_string(owner, i)
return out
def _clean(s):
s = s.replace('\\', '\\\\')
return s
def _bitlist(tag):
if getattr(tag, 'contents', None) == None:
for i in _split_lines(str(tag)):
yield None,_clean(i)
else:
for e in tag:
name = getattr(e, 'name', None)
if name in ['a', 'br']:
name = None # just treat as simple text
s = _force_string(tag, e)
if name:
yield name,_clean(s)
else:
for i in _split_lines(s):
yield None,_clean(i)
def _bitlist_simple(tag):
for typ,text in _bitlist(tag):
if typ and not typ in ['em', 'strong', 'code']:
raise ValueError('unexpected tag %r inside %r' % (typ, tag.name))
yield text
def _text(bitlist):
out = ''
for typ,text in bitlist:
if not typ:
out += text
elif typ == 'em':
out += '\\fI%s\\fR' % text
elif typ in ['strong', 'code']:
out += '\\fB%s\\fR' % text
else:
raise ValueError('unexpected tag %r inside %r' % (typ, tag.name))
out = out.strip()
out = re.sub(re.compile(r'^\s+', re.M), '', out)
return out
def text(tag):
w.write(_text(_bitlist(tag)))
# This is needed because .BI (and .BR, .RB, etc) are weird little state
# machines that alternate between two fonts. So if someone says something
# like foo<b>chicken</b><b>wicken</b>dicken we have to convert that to
# .BI foo chickenwicken dicken
def _boldline(l):
out = ['']
last_bold = False
for typ,text in l:
nonzero = not not typ
if nonzero != last_bold:
last_bold = not last_bold
out.append('')
out[-1] += re.sub(r'\s+', ' ', text)
macro('.BI', *out)
def do_definition(tag):
w.end_para()
macro('.TP')
w.started = True
split = 0
pre = []
post = []
for typ,text in _bitlist(tag):
if split:
post.append((typ,text))
elif text.lstrip().startswith(': '):
split = 1
post.append((typ,text.lstrip()[2:].lstrip()))
else:
pre.append((typ,text))
_boldline(pre)
w.write(_text(post))
def do_list(tag):
for i in tag:
name = getattr(i, 'name', '').lower()
if not name and not str(i).strip():
pass
elif name != 'li':
raise ValueError('only <li> is allowed inside <ul>: got %r' % i)
else:
w.start_bullet()
for xi in i:
do(xi)
w.end_para()
w.end_bullet()
def do(tag):
name = getattr(tag, 'name', '').lower()
if not name:
text(tag)
elif name == 'h1':
macro('.SH', _force_string(tag, tag).upper())
w.started = True
elif name == 'h2':
macro('.SS', _force_string(tag, tag))
w.started = True
elif name.startswith('h') and len(name)==2:
raise ValueError('%r invalid - man page headers must be h1 or h2'
% name)
elif name == 'pre':
t = _force_string(tag.code, tag.code)
if t.strip():
macro('.RS', '+4n')
macro('.nf')
w.write(_clean(t).rstrip())
macro('.fi')
macro('.RE')
w.end_para()
elif name == 'p' or name == 'br':
g = re.match(re.compile(r'([^\n]*)\n +: +(.*)', re.S), str(tag))
if g:
# it's a definition list (which some versions of python-markdown
# don't support, including the one in Debian-lenny, so we can't
# enable that markdown extension). Fake it up.
do_definition(tag)
else:
text(tag)
w.end_para()
elif name == 'ul':
do_list(tag)
else:
raise ValueError('non-man-compatible html tag %r' % name)
PROD='Untitled'
VENDOR='Vendor Name'
SECTION='9'
GROUPNAME='User Commands'
DATE=''
AUTHOR=''
lines = []
if len(sys.argv) > 1:
for n in sys.argv[1:]:
lines += open(n).read().decode('utf8').split('\n')
else:
lines += sys.stdin.read().decode('utf8').split('\n')
# parse pandoc-style document headers (not part of markdown)
g = re.match(r'^%\s+(.*?)\((.*?)\)\s+(.*)$', lines[0])
if g:
PROD = g.group(1)
SECTION = g.group(2)
VENDOR = g.group(3)
lines.pop(0)
g = re.match(r'^%\s+(.*?)$', lines[0])
if g:
AUTHOR = g.group(1)
lines.pop(0)
g = re.match(r'^%\s+(.*?)$', lines[0])
if g:
DATE = g.group(1)
lines.pop(0)
g = re.match(r'^%\s+(.*?)$', lines[0])
if g:
GROUPNAME = g.group(1)
lines.pop(0)
inp = '\n'.join(lines)
if AUTHOR:
inp += ('\n# AUTHOR\n\n%s\n' % AUTHOR).replace('<', '\\<')
html = markdown.markdown(inp)
soup = BeautifulSoup(html, convertEntities=BeautifulSoup.HTML_ENTITIES)
macro('.TH', PROD.upper(), SECTION, DATE, VENDOR, GROUPNAME)
macro('.ad', 'l') # left justified
macro('.nh') # disable hyphenation
for e in soup:
do(e)

View File

@ -8,6 +8,14 @@ just switch your wireless off and on. Sshuttle makes the
kernel setting it changes permanent, so this won't happen kernel setting it changes permanent, so this won't happen
again, even after a reboot. again, even after a reboot.
Required Software
=================
- You need PyXAPI, available here:
http://www.pps.univ-paris-diderot.fr/~ylg/PyXAPI/
- You also need autossh, available in various package management systems
- Python 2.x, both locally and the remote system
sshuttle: where transparent proxy meets VPN meets ssh sshuttle: where transparent proxy meets VPN meets ssh
===================================================== =====================================================
@ -51,19 +59,34 @@ Prerequisites
Linux.) Linux.)
- If you use MacOS or BSD on your client machine: - If you use MacOS or BSD on your client machine:
Your kernel needs to be compiled with IPFIREWALL_FORWARD Your kernel needs to be compiled with `IPFIREWALL_FORWARD`
(MacOS has this by default) and you need to have ipfw (MacOS has this by default) and you need to have ipfw
available. (The server doesn't need to be MacOS or BSD.) available. (The server doesn't need to be MacOS or BSD.)
This is how you use it: Obtaining sshuttle
------------------
- First, go get PyXAPI from the link above
- Clone github.com/jwyllie83/sshuttle/tree/local
Usage on (Ubuntu) Linux
----------------------- -----------------------
- <tt>git clone git://github.com/apenwarr/sshuttle</tt> - `cd packaging; ./make_deb`
on your client machine. You'll need root or sudo
access, and python needs to be installed. - `sudo dpkg -i ./sshuttle-VERSION.deb`
- Check out the files in `/etc/sshuttle`; configure them so your tunnel works
- `sudo service sshuttle start`
Usage on other Linuxes and OSes
-------------------------------
- The most basic use of sshuttle looks like:
<tt>./sshuttle -r username@sshserver 0.0.0.0/0 -vv</tt> <tt>./sshuttle -r username@sshserver 0.0.0.0/0 -vv</tt>
- There is a shortcut for 0.0.0.0/0 for those that value - There is a shortcut for 0.0.0.0/0 for those that value
@ -83,6 +106,9 @@ 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 up to not require passwords, in which case you won't be
prompted at all.) prompted at all.)
Usage Notes
-----------
That's it! Now your local machine can access the remote network as if you 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. your local network can make connections to your remote network.

402
client.py
View File

@ -1,402 +0,0 @@
import struct, socket, select, errno, re, signal, time
import compat.ssubprocess as ssubprocess
import helpers, ssnet, ssh, ssyslog
from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
from helpers import *
_extra_fd = os.open('/dev/null', os.O_RDONLY)
def got_signal(signum, frame):
log('exiting on signal %d\n' % signum)
sys.exit(1)
_pidname = None
def check_daemon(pidfile):
global _pidname
_pidname = os.path.abspath(pidfile)
try:
oldpid = open(_pidname).read(1024)
except IOError, e:
if e.errno == errno.ENOENT:
return # no pidfile, ok
else:
raise Fatal("can't read %s: %s" % (_pidname, e))
if not oldpid:
os.unlink(_pidname)
return # invalid pidfile, ok
oldpid = int(oldpid.strip() or 0)
if oldpid <= 0:
os.unlink(_pidname)
return # invalid pidfile, ok
try:
os.kill(oldpid, 0)
except OSError, e:
if e.errno == errno.ESRCH:
os.unlink(_pidname)
return # outdated pidfile, ok
elif e.errno == errno.EPERM:
pass
else:
raise
raise Fatal("%s: sshuttle is already running (pid=%d)"
% (_pidname, oldpid))
def daemonize():
if os.fork():
os._exit(0)
os.setsid()
if os.fork():
os._exit(0)
outfd = os.open(_pidname, os.O_WRONLY|os.O_CREAT|os.O_EXCL, 0666)
try:
os.write(outfd, '%d\n' % os.getpid())
finally:
os.close(outfd)
os.chdir("/")
# Normal exit when killed, or try/finally won't work and the pidfile won't
# be deleted.
signal.signal(signal.SIGTERM, got_signal)
si = open('/dev/null', 'r+')
os.dup2(si.fileno(), 0)
os.dup2(si.fileno(), 1)
si.close()
ssyslog.stderr_to_syslog()
def daemon_cleanup():
try:
os.unlink(_pidname)
except OSError, e:
if e.errno == errno.ENOENT:
pass
else:
raise
def original_dst(sock):
try:
SO_ORIGINAL_DST = 80
SOCKADDR_MIN = 16
sockaddr_in = sock.getsockopt(socket.SOL_IP,
SO_ORIGINAL_DST, SOCKADDR_MIN)
(proto, port, a,b,c,d) = struct.unpack('!HHBBBB', sockaddr_in[:8])
assert(socket.htons(proto) == socket.AF_INET)
ip = '%d.%d.%d.%d' % (a,b,c,d)
return (ip,port)
except socket.error, e:
if e.args[0] == errno.ENOPROTOOPT:
return sock.getsockname()
raise
class FirewallClient:
def __init__(self, port, subnets_include, subnets_exclude, dnsport):
self.port = port
self.auto_nets = []
self.subnets_include = subnets_include
self.subnets_exclude = subnets_exclude
self.dnsport = dnsport
argvbase = ([sys.argv[1], sys.argv[0], sys.argv[1]] +
['-v'] * (helpers.verbose or 0) +
['--firewall', str(port), str(dnsport)])
if ssyslog._p:
argvbase += ['--syslog']
argv_tries = [
['sudo', '-p', '[local sudo] Password: '] + 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
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
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 == EXITCODE_NEEDS_REBOOT:
raise FatalNeedsReboot()
elif rv:
raise Fatal('cleanup: %r returned %d' % (self.argv, rv))
def onaccept(listener, mux, handlers):
global _extra_fd
try:
sock,srcip = listener.accept()
except socket.error, e:
if e.args[0] in [errno.EMFILE, errno.ENFILE]:
debug1('Rejected incoming connection: too many open files!\n')
# free up an fd so we can eat the connection
os.close(_extra_fd)
try:
sock,srcip = listener.accept()
sock.close()
finally:
_extra_fd = os.open('/dev/null', os.O_RDONLY)
return
else:
raise
dstip = original_dst(sock)
debug1('Accept: %s:%r -> %s:%r.\n' % (srcip[0],srcip[1],
dstip[0],dstip[1]))
if dstip[1] == listener.getsockname()[1] and islocal(dstip[0]):
debug1("-- ignored: that's my address!\n")
sock.close()
return
chan = mux.next_channel()
if not chan:
log('warning: too many open channels. Discarded connection.\n')
sock.close()
return
mux.send(chan, ssnet.CMD_CONNECT, '%s,%s' % dstip)
outwrap = MuxWrapper(mux, chan)
handlers.append(Proxy(SockWrapper(sock, sock), outwrap))
dnsreqs = {}
def dns_done(chan, data):
peer,sock,timeout = dnsreqs.get(chan) or (None,None,None)
debug3('dns_done: channel=%r peer=%r\n' % (chan, peer))
if peer:
del dnsreqs[chan]
debug3('doing sendto %r\n' % (peer,))
sock.sendto(data, peer)
def ondns(listener, mux, handlers):
pkt,peer = listener.recvfrom(4096)
now = time.time()
if pkt:
debug1('DNS request from %r: %d bytes\n' % (peer, len(pkt)))
chan = mux.next_channel()
dnsreqs[chan] = peer,listener,now+30
mux.send(chan, ssnet.CMD_DNS_REQ, pkt)
mux.channels[chan] = lambda cmd,data: dns_done(chan,data)
for chan,(peer,sock,timeout) in dnsreqs.items():
if timeout < now:
del dnsreqs[chan]
debug3('Remaining DNS requests: %d\n' % len(dnsreqs))
def _main(listener, fw, ssh_cmd, remotename, python, latency_control,
dnslistener, seed_hosts, auto_nets,
syslog, daemon):
handlers = []
if helpers.verbose >= 1:
helpers.logprefix = 'c : '
else:
helpers.logprefix = 'client: '
debug1('connecting to server...\n')
try:
(serverproc, serversock) = ssh.connect(ssh_cmd, remotename, python,
stderr=ssyslog._p and ssyslog._p.stdin,
options=dict(latency_control=latency_control))
except socket.error, e:
if e.args[0] == errno.EPIPE:
raise Fatal("failed to establish ssh session (1)")
else:
raise
mux = Mux(serversock, serversock)
handlers.append(mux)
expected = 'SSHUTTLE0001'
try:
v = 'x'
while v and v != '\0':
v = serversock.recv(1)
v = 'x'
while v and v != '\0':
v = serversock.recv(1)
initstring = serversock.recv(len(expected))
except socket.error, e:
if e.args[0] == errno.ECONNRESET:
raise Fatal("failed to establish ssh session (2)")
else:
raise
rv = serverproc.poll()
if rv:
raise Fatal('server died with error code %d' % rv)
if initstring != expected:
raise Fatal('expected server init string %r; got %r'
% (expected, initstring))
debug1('connected.\n')
print 'Connected.'
sys.stdout.flush()
if daemon:
daemonize()
log('daemonizing (%s).\n' % _pidname)
elif syslog:
debug1('switching to syslog.\n')
ssyslog.stderr_to_syslog()
def onroutes(routestr):
if auto_nets:
for line in routestr.strip().split('\n'):
(ip,width) = line.split(',', 1)
fw.auto_nets.append((ip,int(width)))
# we definitely want to do this *after* starting ssh, or we might end
# up intercepting the ssh connection!
#
# Moreover, now that we have the --auto-nets option, we have to wait
# for the server to send us that message anyway. Even if we haven't
# set --auto-nets, we might as well wait for the message first, then
# ignore its contents.
mux.got_routes = None
fw.start()
mux.got_routes = onroutes
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
handlers.append(Handler([listener], lambda: onaccept(listener, mux, handlers)))
if dnslistener:
handlers.append(Handler([dnslistener], lambda: ondns(dnslistener, mux, handlers)))
if seed_hosts != None:
debug1('seed_hosts: %r\n' % seed_hosts)
mux.send(0, ssnet.CMD_HOST_REQ, '\n'.join(seed_hosts))
while 1:
rv = serverproc.poll()
if rv:
raise Fatal('server died with error code %d' % rv)
ssnet.runonce(handlers, mux)
if latency_control:
mux.check_fullness()
mux.callback()
def main(listenip, ssh_cmd, remotename, python, latency_control, dns,
seed_hosts, auto_nets,
subnets_include, subnets_exclude, syslog, daemon, pidfile):
if syslog:
ssyslog.start_syslog()
if daemon:
try:
check_daemon(pidfile)
except Fatal, e:
log("%s\n" % e)
return 5
debug1('Starting sshuttle proxy.\n')
if listenip[1]:
ports = [listenip[1]]
else:
ports = xrange(12300,9000,-1)
last_e = None
bound = False
debug2('Binding:')
for port in ports:
debug2(' %d' % port)
listener = socket.socket()
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
dnslistener = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
dnslistener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
listener.bind((listenip[0], port))
dnslistener.bind((listenip[0], port))
bound = True
break
except socket.error, e:
last_e = e
debug2('\n')
if not bound:
assert(last_e)
raise last_e
listener.listen(10)
listenip = listener.getsockname()
debug1('Listening on %r.\n' % (listenip,))
if dns:
dnsip = dnslistener.getsockname()
debug1('DNS listening on %r.\n' % (dnsip,))
dnsport = dnsip[1]
else:
dnsport = 0
dnslistener = None
fw = FirewallClient(listenip[1], subnets_include, subnets_exclude, dnsport)
try:
return _main(listener, fw, ssh_cmd, remotename,
python, latency_control, dnslistener,
seed_hosts, auto_nets, syslog, daemon)
finally:
try:
if daemon:
# it's not our child anymore; can't waitpid
fw.p.returncode = 0
fw.done()
finally:
if daemon:
daemon_cleanup()

141
main.py
View File

@ -1,141 +0,0 @@
import sys, os, re
import helpers, options, client, server, firewall, hostwatch
import compat.ssubprocess as ssubprocess
from helpers import *
# list of:
# 1.2.3.4/5 or just 1.2.3.4
def parse_subnets(subnets_str):
subnets = []
for s in subnets_str:
m = re.match(r'(\d+)(?:\.(\d+)\.(\d+)\.(\d+))?(?:/(\d+))?$', s)
if not m:
raise Fatal('%r is not a valid IP subnet format' % s)
(a,b,c,d,width) = m.groups()
(a,b,c,d) = (int(a or 0), int(b or 0), int(c or 0), int(d or 0))
if width == None:
width = 32
else:
width = int(width)
if a > 255 or b > 255 or c > 255 or d > 255:
raise Fatal('%d.%d.%d.%d has numbers > 255' % (a,b,c,d))
if width > 32:
raise Fatal('*/%d is greater than the maximum of 32' % width)
subnets.append(('%d.%d.%d.%d' % (a,b,c,d), width))
return subnets
# 1.2.3.4:567 or just 1.2.3.4 or just 567
def parse_ipport(s):
s = str(s)
m = re.match(r'(?:(\d+)\.(\d+)\.(\d+)\.(\d+))?(?::)?(?:(\d+))?$', s)
if not m:
raise Fatal('%r is not a valid IP:port format' % s)
(a,b,c,d,port) = m.groups()
(a,b,c,d,port) = (int(a or 0), int(b or 0), int(c or 0), int(d or 0),
int(port or 0))
if a > 255 or b > 255 or c > 255 or d > 255:
raise Fatal('%d.%d.%d.%d has numbers > 255' % (a,b,c,d))
if port > 65535:
raise Fatal('*:%d is greater than the maximum of 65535' % port)
if a == None:
a = b = c = d = 0
return ('%d.%d.%d.%d' % (a,b,c,d), port)
optspec = """
sshuttle [-l [ip:]port] [-r [username@]sshserver[:port]] <subnets...>
sshuttle --server
sshuttle --firewall <port> <subnets...>
sshuttle --hostwatch
--
l,listen= transproxy to this ip address and port number [127.0.0.1:0]
H,auto-hosts scan for remote hostnames and update local /etc/hosts
N,auto-nets automatically determine subnets to route
dns capture local DNS requests and forward to the remote DNS server
python= path to python interpreter on the remote server
r,remote= ssh hostname (and optional username) of remote sshuttle server
x,exclude= exclude this subnet (can be used more than once)
exclude-from= exclude the subnets in a file (whitespace separated)
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)
no-latency-control sacrifice latency to improve bandwidth benchmarks
wrap= restart counting channel numbers after this number (for testing)
D,daemon run in the background as a daemon
V,version print sshuttle's version number
syslog send log messages to syslog (default if you use --daemon)
pidfile= pidfile name (only if using --daemon) [./sshuttle.pid]
server (internal use only)
firewall (internal use only)
hostwatch (internal use only)
"""
o = options.Options(optspec)
(opt, flags, extra) = o.parse(sys.argv[2:])
if opt.version:
import version
print version.TAG
sys.exit(0)
if opt.daemon:
opt.syslog = 1
if opt.wrap:
import ssnet
ssnet.MAX_CHANNEL = int(opt.wrap)
helpers.verbose = opt.verbose
try:
if opt.server:
if len(extra) != 0:
o.fatal('no arguments expected')
server.latency_control = opt.latency_control
sys.exit(server.main())
elif opt.firewall:
if len(extra) != 2:
o.fatal('exactly two arguments expected')
sys.exit(firewall.main(int(extra[0]), int(extra[1]), opt.syslog))
elif opt.hostwatch:
sys.exit(hostwatch.hw_main(extra))
else:
if len(extra) < 1 and not opt.auto_nets:
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)
if k in ('-X', '--exclude-from'):
excludes += open(v).read().split()
remotename = opt.remote
if remotename == '' or remotename == '-':
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'),
opt.ssh_cmd,
remotename,
opt.python,
opt.latency_control,
opt.dns,
sh,
opt.auto_nets,
parse_subnets(includes),
parse_subnets(excludes),
opt.syslog, opt.daemon, opt.pidfile))
except FatalNeedsReboot, e:
log('You must reboot before using sshuttle.\n')
sys.exit(EXITCODE_NEEDS_REBOOT)
except Fatal, e:
log('fatal: %s\n' % e)
sys.exit(99)
except KeyboardInterrupt:
log('\n')
log('Keyboard interrupt: exiting.\n')
sys.exit(1)

26
packaging/control Normal file
View File

@ -0,0 +1,26 @@
Package: sshuttle
Version: 0.2
Architecture: i386
Maintainer: Jim Wyllie <jwyllie83@gmail.com>
Depends: autossh, upstart, python (>=2.6)
Section: utils
Priority: optional
Homepage: http://github.com/jwyllie83/sshuttle.udp
Description: "Full-featured" VPN over an SSH tunnel, allowing full remote
access somewhere where all you have is an SSH connection. It works well if
you generally find yourself in the following situation:
.
- Your client machine (or router) is Linux, FreeBSD, or MacOS.
- You have access to a remote network via ssh.
- You don't necessarily have admin access on the remote network.
- You do not wish to, or can't, use other VPN software
- 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 suboptimal performance
.
It also has hooks for more complicated setups (VPN-in-a-SSH-VPN, etc) to allow
you to set it up as you like.

41
packaging/make_deb Executable file
View File

@ -0,0 +1,41 @@
#!/bin/bash
#
# This script puts together a .deb package suitable for installing on an Ubuntu
# system
B="/tmp/sshuttle/build"
if [ ! -x /usr/bin/dpkg ]; then
echo 'Unable to build: dpkg not found on system'
exit 1
fi
# Create the new directory structure
mkdir -p ${B}/etc/sshuttle/pre-start.d
mkdir -p ${B}/etc/sshuttle/post-stop.d
mkdir -p ${B}/usr/share/sshuttle
mkdir -p ${B}/usr/bin
mkdir -p ${B}/etc/init
mkdir -p ${B}/DEBIAN
# Copy over all of the files
cp -r ../src/* ${B}/usr/share/sshuttle
cp ../src/sshuttle ${B}/usr/bin
cp -r sshuttle.conf ${B}/etc/init
cp prefixes.conf ${B}/etc/sshuttle
cp tunnel.conf ${B}/etc/sshuttle
# Copy the control file over, as well
cp control ${B}/DEBIAN
# Create the md5sum manifest
if [ -x /usr/bin/md5sum ]; then
cd ${B}
find . -type f | egrep -v DEBIAN | sed -re 's/^..//' | xargs md5sum > ${B}/DEBIAN/md5sums
cd ${OLDPWD}
fi
# Build the debian package
VERSION=$(egrep -e '^Version' control | sed -re 's/^[^:]*: //')
dpkg --build ${B} ./sshuttle-${VERSION}.deb
rm -rf ${B}

5
packaging/prefixes.conf Normal file
View File

@ -0,0 +1,5 @@
# Output prefixes here, one per line. Prefix is in:
# prefix/netmask format
# Like this:
# 192.168.0.0/16
# 192.0.43.10/32

90
packaging/sshuttle.conf Normal file
View File

@ -0,0 +1,90 @@
description "Create a transparent proxy over SSH"
author "Jim Wyllie <jwyllie83@gmail.com>"
manual
nice -5
# Edit this file with network prefixes that should be loaded through the SSH
# tunnel.
env PREFIX_LOCATION=/etc/sshuttle/prefixes.conf
# Routing table; defaults to 100
env ROUTE_TABLE=100
# fwmark; defaults to 1
env FWMARK=1
# SSH tunnel configuration file
env SSHUTTLE_TUNNEL_FILE=/etc/sshuttle/tunnel.conf
# File containing the tunnel proxy name / host / whatever
env TUNNEL_PROXY="/etc/sshuttle/tunnel.conf"
# Any other commands needed to run before or after loading the SSH tunnel.
# This is where you can put any of your hacks to set up tunnels-in-tunnels,
# etc. Scripts in this directory are executed in order.
env MISC_START_DIR=/etc/sshuttle/pre-start.d
env MISC_STOP_DIR=/etc/sshuttle/post-stop.d
start on (local-filesystems and net-device-up IFACE!=lo)
stop on stopping network-services
#respawn
pre-start script
# Make sure we have created the routes
sudo ip rule add fwmark ${FWMARK} lookup ${ROUTE_TABLE}
logger "Starting sshuttle..."
if [ -f "${PREFIX_LOCATION}" ]; then
cat "${PREFIX_LOCATION}" | while read ROUTE; do
# Skip comments
if [ -n "$(echo ${ROUTE} | egrep "^[ ]*#")" ]; then
continue
fi
# Skip empty lines
if [ -z "${ROUTE}" ]; then
continue
fi
logger "Adding route: ${ROUTE}"
ip route add local ${ROUTE} dev lo table ${ROUTE_TABLE}
done
fi
for RUNFILE in ${MISC_START_DIR}/*; do
logger "Executing ${RUNFILE}"
/bin/sh -c "${RUNFILE}"
done
end script
post-stop script
if [ -f "${PREFIX_LOCATION}" ]; then
cat "${PREFIX_LOCATION}" | while read ROUTE; do
# Skip comments
if [ -n "$(echo ${ROUTE} | egrep "^[ ]*#")" ]; then
continue
fi
# Skip empty lines
if [ -z "${ROUTE}" ]; then
continue
fi
logger "Deleting route: ${ROUTE}"
ip route del local ${ROUTE} dev lo table ${ROUTE_TABLE}
done
fi
ip rule del fwmark ${FWMARK}
for RUNFILE in "${MISC_STOP_DIR}/*"; do
logger "Executing ${RUNFILE}"
/bin/sh -c "${RUNFILE}"
done
end script
exec /usr/bin/sshuttle --dns --method=tproxy --listen 0.0.0.0 --remote sshuttle_tunnel -s /etc/sshuttle/prefixes.conf -e "ssh -F ${TUNNEL_PROXY}"

19
packaging/tunnel.conf Normal file
View File

@ -0,0 +1,19 @@
# Here is where you can specify any SSH tunnel options See ssh_config(5) for
# details. You need to leave the Host line intact, but everything else can
# specify whatever you want
Host sshuttle_tunnel
# REQUIRED: Set this to be the host to which you would like to connect your
# tunnel
#Hostname localhost
# REQUIRED: Set this to be the target SSH user on the remote system
#User foo
# ---------------------------------------------------------------------------
# The rest are all optional; see ssh_config(5) for the full list of what can
# be specified. Some very commonly needed ones are below.
# ---------------------------------------------------------------------------
# SSH key used for connecting
#IdentityFile /path/to/key

View File

@ -1,11 +1,11 @@
exec >&2 exec >&2
UI= UI=
[ "$(uname)" = "Darwin" ] && UI=ui-macos/all [ "$(uname)" = "Darwin" ] && UI=ui-macos/all
redo-ifchange Documentation/all version/all $UI redo-ifchange sshuttle.8 $UI
echo echo
echo "What now?" echo "What now?"
[ -z "$UI" ] || echo "- Try the MacOS GUI: open ui-macos/Sshuttle*.app" [ -z "$UI" ] || echo "- Try the MacOS GUI: open ui-macos/Sshuttle*.app"
echo "- Run sshuttle: ./sshuttle --dns -r HOSTNAME 0/0" echo "- Run sshuttle: ./sshuttle --dns -r HOSTNAME 0/0"
echo "- Read the README: less README.md" echo "- Read the README: less README.md"
echo "- Read the man page: less Documentation/sshuttle.md" echo "- Read the man page: less sshuttle.md"

View File

@ -1,4 +1,5 @@
import sys, zlib import sys
import zlib
z = zlib.decompressobj() z = zlib.decompressobj()
mainmod = sys.modules[__name__] mainmod = sys.modules[__name__]

View File

@ -1,2 +1,2 @@
redo ui-macos/clean Documentation/clean version/clean redo ui-macos/clean
rm -f *~ */*~ .*~ */.*~ *.8 *.tmp */*.tmp *.pyc */*.pyc rm -f *~ */*~ .*~ */.*~ *.8 *.tmp */*.tmp *.pyc */*.pyc

754
src/client.py Normal file
View File

@ -0,0 +1,754 @@
import struct
import errno
import re
import signal
import time
import compat.ssubprocess as ssubprocess
import helpers
import os
import ssnet
import ssh
import ssyslog
import sys
from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
from helpers import log, debug1, debug2, debug3, Fatal, islocal
recvmsg = None
try:
# try getting recvmsg from python
import socket as pythonsocket
getattr(pythonsocket.socket, "recvmsg")
socket = pythonsocket
recvmsg = "python"
except AttributeError:
# try getting recvmsg from socket_ext library
try:
import socket_ext
getattr(socket_ext.socket, "recvmsg")
socket = socket_ext
recvmsg = "socket_ext"
except ImportError:
import socket
_extra_fd = os.open('/dev/null', os.O_RDONLY)
def got_signal(signum, frame):
log('exiting on signal %d\n' % signum)
sys.exit(1)
_pidname = None
IP_TRANSPARENT = 19
IP_ORIGDSTADDR = 20
IP_RECVORIGDSTADDR = IP_ORIGDSTADDR
SOL_IPV6 = 41
IPV6_ORIGDSTADDR = 74
IPV6_RECVORIGDSTADDR = IPV6_ORIGDSTADDR
if recvmsg == "python":
def recv_udp(listener, bufsize):
debug3('Accept UDP python using recvmsg.\n')
data, ancdata, msg_flags, srcip = listener.recvmsg(
4096, socket.CMSG_SPACE(24))
dstip = None
family = None
for cmsg_level, cmsg_type, cmsg_data in ancdata:
if cmsg_level == socket.SOL_IP and cmsg_type == IP_ORIGDSTADDR:
family, port = struct.unpack('=HH', cmsg_data[0:4])
port = socket.htons(port)
if family == socket.AF_INET:
start = 4
length = 4
else:
raise Fatal("Unsupported socket type '%s'" % family)
ip = socket.inet_ntop(family, cmsg_data[start:start + length])
dstip = (ip, port)
break
elif cmsg_level == SOL_IPV6 and cmsg_type == IPV6_ORIGDSTADDR:
family, port = struct.unpack('=HH', cmsg_data[0:4])
port = socket.htons(port)
if family == socket.AF_INET6:
start = 8
length = 16
else:
raise Fatal("Unsupported socket type '%s'" % family)
ip = socket.inet_ntop(family, cmsg_data[start:start + length])
dstip = (ip, port)
break
return (srcip, dstip, data)
elif recvmsg == "socket_ext":
def recv_udp(listener, bufsize):
debug3('Accept UDP using socket_ext recvmsg.\n')
srcip, data, adata, flags = listener.recvmsg(
(bufsize,), socket.CMSG_SPACE(24))
dstip = None
family = None
for a in adata:
if a.cmsg_level == socket.SOL_IP and a.cmsg_type == IP_ORIGDSTADDR:
family, port = struct.unpack('=HH', a.cmsg_data[0:4])
port = socket.htons(port)
if family == socket.AF_INET:
start = 4
length = 4
else:
raise Fatal("Unsupported socket type '%s'" % family)
ip = socket.inet_ntop(
family, a.cmsg_data[start:start + length])
dstip = (ip, port)
break
elif a.cmsg_level == SOL_IPV6 and a.cmsg_type == IPV6_ORIGDSTADDR:
family, port = struct.unpack('=HH', a.cmsg_data[0:4])
port = socket.htons(port)
if family == socket.AF_INET6:
start = 8
length = 16
else:
raise Fatal("Unsupported socket type '%s'" % family)
ip = socket.inet_ntop(
family, a.cmsg_data[start:start + length])
dstip = (ip, port)
break
return (srcip, dstip, data[0])
else:
def recv_udp(listener, bufsize):
debug3('Accept UDP using recvfrom.\n')
data, srcip = listener.recvfrom(bufsize)
return (srcip, None, data)
def check_daemon(pidfile):
global _pidname
_pidname = os.path.abspath(pidfile)
try:
oldpid = open(_pidname).read(1024)
except IOError, e:
if e.errno == errno.ENOENT:
return # no pidfile, ok
else:
raise Fatal("can't read %s: %s" % (_pidname, e))
if not oldpid:
os.unlink(_pidname)
return # invalid pidfile, ok
oldpid = int(oldpid.strip() or 0)
if oldpid <= 0:
os.unlink(_pidname)
return # invalid pidfile, ok
try:
os.kill(oldpid, 0)
except OSError, e:
if e.errno == errno.ESRCH:
os.unlink(_pidname)
return # outdated pidfile, ok
elif e.errno == errno.EPERM:
pass
else:
raise
raise Fatal("%s: sshuttle is already running (pid=%d)"
% (_pidname, oldpid))
def daemonize():
if os.fork():
os._exit(0)
os.setsid()
if os.fork():
os._exit(0)
outfd = os.open(_pidname, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0666)
try:
os.write(outfd, '%d\n' % os.getpid())
finally:
os.close(outfd)
os.chdir("/")
# Normal exit when killed, or try/finally won't work and the pidfile won't
# be deleted.
signal.signal(signal.SIGTERM, got_signal)
si = open('/dev/null', 'r+')
os.dup2(si.fileno(), 0)
os.dup2(si.fileno(), 1)
si.close()
ssyslog.stderr_to_syslog()
def daemon_cleanup():
try:
os.unlink(_pidname)
except OSError, e:
if e.errno == errno.ENOENT:
pass
else:
raise
def original_dst(sock):
try:
SO_ORIGINAL_DST = 80
SOCKADDR_MIN = 16
sockaddr_in = sock.getsockopt(socket.SOL_IP,
SO_ORIGINAL_DST, SOCKADDR_MIN)
(proto, port, a, b, c, d) = struct.unpack('!HHBBBB', sockaddr_in[:8])
assert(socket.htons(proto) == socket.AF_INET)
ip = '%d.%d.%d.%d' % (a, b, c, d)
return (ip, port)
except socket.error, e:
if e.args[0] == errno.ENOPROTOOPT:
return sock.getsockname()
raise
class MultiListener:
def __init__(self, type=socket.SOCK_STREAM, proto=0):
self.v6 = socket.socket(socket.AF_INET6, type, proto)
self.v4 = socket.socket(socket.AF_INET, type, proto)
def setsockopt(self, level, optname, value):
if self.v6:
self.v6.setsockopt(level, optname, value)
if self.v4:
self.v4.setsockopt(level, optname, value)
def add_handler(self, handlers, callback, method, mux):
if self.v6:
handlers.append(
Handler(
[self.v6],
lambda: callback(self.v6, method, mux, handlers)))
if self.v4:
handlers.append(
Handler(
[self.v4],
lambda: callback(self.v4, method, mux, handlers)))
def listen(self, backlog):
if self.v6:
self.v6.listen(backlog)
if self.v4:
try:
self.v4.listen(backlog)
except socket.error, e:
# on some systems v4 bind will fail if the v6 suceeded,
# in this case the v6 socket will receive v4 too.
if e.errno == errno.EADDRINUSE and self.v6:
self.v4 = None
else:
raise e
def bind(self, address_v6, address_v4):
if address_v6 and self.v6:
self.v6.bind(address_v6)
else:
self.v6 = None
if address_v4 and self.v4:
self.v4.bind(address_v4)
else:
self.v4 = None
def print_listening(self, what):
if self.v6:
listenip = self.v6.getsockname()
debug1('%s listening on %r.\n' % (what, listenip))
if self.v4:
listenip = self.v4.getsockname()
debug1('%s listening on %r.\n' % (what, listenip))
class FirewallClient:
def __init__(self, port_v6, port_v4, subnets_include, subnets_exclude,
dnsport_v6, dnsport_v4, method, udp):
self.auto_nets = []
self.subnets_include = subnets_include
self.subnets_exclude = subnets_exclude
argvbase = ([sys.argv[1], sys.argv[0], sys.argv[1]] +
['-v'] * (helpers.verbose or 0) +
['--firewall', str(port_v6), str(port_v4),
str(dnsport_v6), str(dnsport_v4),
method, str(int(udp))])
if ssyslog._p:
argvbase += ['--syslog']
argv_tries = [
['sudo', '-p', '[local sudo] Password: '] + 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
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
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[0:5] != 'READY':
raise Fatal('%r expected READY, got %r' % (self.argv, line))
self.method = line[6:-1]
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 (family, ip, width) in self.subnets_include + self.auto_nets:
self.pfile.write('%d,%d,0,%s\n' % (family, width, ip))
for (family, ip, width) in self.subnets_exclude:
self.pfile.write('%d,%d,1,%s\n' % (family, 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))
dnsreqs = {}
udp_by_src = {}
def expire_connections(now, mux):
for chan, timeout in dnsreqs.items():
if timeout < now:
debug3('expiring dnsreqs channel=%d\n' % chan)
del mux.channels[chan]
del dnsreqs[chan]
debug3('Remaining DNS requests: %d\n' % len(dnsreqs))
for peer, (chan, timeout) in udp_by_src.items():
if timeout < now:
debug3('expiring UDP channel channel=%d peer=%r\n' % (chan, peer))
mux.send(chan, ssnet.CMD_UDP_CLOSE, '')
del mux.channels[chan]
del udp_by_src[peer]
debug3('Remaining UDP channels: %d\n' % len(udp_by_src))
def onaccept_tcp(listener, method, mux, handlers):
global _extra_fd
try:
sock, srcip = listener.accept()
except socket.error, e:
if e.args[0] in [errno.EMFILE, errno.ENFILE]:
debug1('Rejected incoming connection: too many open files!\n')
# free up an fd so we can eat the connection
os.close(_extra_fd)
try:
sock, srcip = listener.accept()
sock.close()
finally:
_extra_fd = os.open('/dev/null', os.O_RDONLY)
return
else:
raise
if method == "tproxy":
dstip = sock.getsockname()
else:
dstip = original_dst(sock)
debug1('Accept TCP: %s:%r -> %s:%r.\n' % (srcip[0], srcip[1],
dstip[0], dstip[1]))
if dstip[1] == sock.getsockname()[1] and islocal(dstip[0], sock.family):
debug1("-- ignored: that's my address!\n")
sock.close()
return
chan = mux.next_channel()
if not chan:
log('warning: too many open channels. Discarded connection.\n')
sock.close()
return
mux.send(chan, ssnet.CMD_TCP_CONNECT, '%d,%s,%s' %
(sock.family, dstip[0], dstip[1]))
outwrap = MuxWrapper(mux, chan)
handlers.append(Proxy(SockWrapper(sock, sock), outwrap))
expire_connections(time.time(), mux)
def udp_done(chan, data, method, family, dstip):
(src, srcport, data) = data.split(",", 2)
srcip = (src, int(srcport))
debug3('doing send from %r to %r\n' % (srcip, dstip,))
try:
sender = socket.socket(family, socket.SOCK_DGRAM)
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sender.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
sender.bind(srcip)
sender.sendto(data, dstip)
sender.close()
except socket.error, e:
debug1('-- ignored socket error sending UDP data: %r\n' % e)
def onaccept_udp(listener, method, mux, handlers):
now = time.time()
srcip, dstip, data = recv_udp(listener, 4096)
if not dstip:
debug1(
"-- ignored UDP from %r: "
"couldn't determine destination IP address\n" % (srcip,))
return
debug1('Accept UDP: %r -> %r.\n' % (srcip, dstip,))
if srcip in udp_by_src:
chan, timeout = udp_by_src[srcip]
else:
chan = mux.next_channel()
mux.channels[chan] = lambda cmd, data: udp_done(
chan, data, method, listener.family, dstip=srcip)
mux.send(chan, ssnet.CMD_UDP_OPEN, listener.family)
udp_by_src[srcip] = chan, now + 30
hdr = "%s,%r," % (dstip[0], dstip[1])
mux.send(chan, ssnet.CMD_UDP_DATA, hdr + data)
expire_connections(now, mux)
def dns_done(chan, data, method, sock, srcip, dstip, mux):
debug3('dns_done: channel=%d src=%r dst=%r\n' % (chan, srcip, dstip))
del mux.channels[chan]
del dnsreqs[chan]
if method == "tproxy":
debug3('doing send from %r to %r\n' % (srcip, dstip,))
sender = socket.socket(sock.family, socket.SOCK_DGRAM)
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sender.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
sender.bind(srcip)
sender.sendto(data, dstip)
sender.close()
else:
debug3('doing sendto %r\n' % (dstip,))
sock.sendto(data, dstip)
def ondns(listener, method, mux, handlers):
now = time.time()
srcip, dstip, data = recv_udp(listener, 4096)
if method == "tproxy" and not dstip:
debug1(
"-- ignored UDP from %r: "
"couldn't determine destination IP address\n" % (srcip,))
return
debug1('DNS request from %r to %r: %d bytes\n' % (srcip, dstip, len(data)))
chan = mux.next_channel()
dnsreqs[chan] = now + 30
mux.send(chan, ssnet.CMD_DNS_REQ, data)
mux.channels[chan] = lambda cmd, data: dns_done(
chan, data, method, listener, srcip=dstip, dstip=srcip, mux=mux)
expire_connections(now, mux)
def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
python, latency_control,
dns_listener, method, seed_hosts, auto_nets,
syslog, daemon):
handlers = []
if helpers.verbose >= 1:
helpers.logprefix = 'c : '
else:
helpers.logprefix = 'client: '
debug1('connecting to server...\n')
try:
(serverproc, serversock) = ssh.connect(
ssh_cmd, remotename, python,
stderr=ssyslog._p and ssyslog._p.stdin,
options=dict(latency_control=latency_control, method=method))
except socket.error, e:
if e.args[0] == errno.EPIPE:
raise Fatal("failed to establish ssh session (1)")
else:
raise
mux = Mux(serversock, serversock)
handlers.append(mux)
expected = 'SSHUTTLE0001'
try:
v = 'x'
while v and v != '\0':
v = serversock.recv(1)
v = 'x'
while v and v != '\0':
v = serversock.recv(1)
initstring = serversock.recv(len(expected))
except socket.error, e:
if e.args[0] == errno.ECONNRESET:
raise Fatal("failed to establish ssh session (2)")
else:
raise
rv = serverproc.poll()
if rv:
raise Fatal('server died with error code %d' % rv)
if initstring != expected:
raise Fatal('expected server init string %r; got %r'
% (expected, initstring))
debug1('connected.\n')
print 'Connected.'
sys.stdout.flush()
if daemon:
daemonize()
log('daemonizing (%s).\n' % _pidname)
elif syslog:
debug1('switching to syslog.\n')
ssyslog.stderr_to_syslog()
def onroutes(routestr):
if auto_nets:
for line in routestr.strip().split('\n'):
(family, ip, width) = line.split(',', 2)
fw.auto_nets.append((family, 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
tcp_listener.add_handler(handlers, onaccept_tcp, method, mux)
if udp_listener:
udp_listener.add_handler(handlers, onaccept_udp, method, mux)
if dns_listener:
dns_listener.add_handler(handlers, ondns, method, mux)
if seed_hosts is not None:
debug1('seed_hosts: %r\n' % seed_hosts)
mux.send(0, ssnet.CMD_HOST_REQ, '\n'.join(seed_hosts))
while 1:
rv = serverproc.poll()
if rv:
raise Fatal('server died with error code %d' % rv)
ssnet.runonce(handlers, mux)
if latency_control:
mux.check_fullness()
mux.callback()
def main(listenip_v6, listenip_v4,
ssh_cmd, remotename, python, latency_control, dns,
method, seed_hosts, auto_nets,
subnets_include, subnets_exclude, syslog, daemon, pidfile):
if syslog:
ssyslog.start_syslog()
if daemon:
try:
check_daemon(pidfile)
except Fatal, e:
log("%s\n" % e)
return 5
debug1('Starting sshuttle proxy.\n')
if recvmsg is not None:
debug1("recvmsg %s support enabled.\n" % recvmsg)
if method == "tproxy":
if recvmsg is not None:
debug1("tproxy UDP support enabled.\n")
udp = True
else:
debug1("tproxy UDP support requires recvmsg function.\n")
udp = False
if dns and recvmsg is None:
debug1("tproxy DNS support requires recvmsg function.\n")
dns = False
else:
debug1("UDP support requires tproxy; disabling UDP.\n")
udp = False
if listenip_v6 and listenip_v6[1] and listenip_v4 and listenip_v4[1]:
# if both ports given, no need to search for a spare port
ports = [0, ]
else:
# if at least one port missing, we have to search
ports = xrange(12300, 9000, -1)
# search for free ports and try to bind
last_e = None
redirectport_v6 = 0
redirectport_v4 = 0
bound = False
debug2('Binding redirector:')
for port in ports:
debug2(' %d' % port)
tcp_listener = MultiListener()
tcp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
if udp:
udp_listener = MultiListener(socket.SOCK_DGRAM)
udp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
else:
udp_listener = None
if listenip_v6 and listenip_v6[1]:
lv6 = listenip_v6
redirectport_v6 = lv6[1]
elif listenip_v6:
lv6 = (listenip_v6[0], port)
redirectport_v6 = port
else:
lv6 = None
redirectport_v6 = 0
if listenip_v4 and listenip_v4[1]:
lv4 = listenip_v4
redirectport_v4 = lv4[1]
elif listenip_v4:
lv4 = (listenip_v4[0], port)
redirectport_v4 = port
else:
lv4 = None
redirectport_v4 = 0
try:
tcp_listener.bind(lv6, lv4)
if udp_listener:
udp_listener.bind(lv6, lv4)
bound = True
break
except socket.error, e:
if e.errno == errno.EADDRINUSE:
last_e = e
else:
raise e
debug2('\n')
if not bound:
assert(last_e)
raise last_e
tcp_listener.listen(10)
tcp_listener.print_listening("TCP redirector")
if udp_listener:
udp_listener.print_listening("UDP redirector")
bound = False
if dns:
# search for spare port for DNS
debug2('Binding DNS:')
ports = xrange(12300, 9000, -1)
for port in ports:
debug2(' %d' % port)
dns_listener = MultiListener(socket.SOCK_DGRAM)
if listenip_v6:
lv6 = (listenip_v6[0], port)
dnsport_v6 = port
else:
lv6 = None
dnsport_v6 = 0
if listenip_v4:
lv4 = (listenip_v4[0], port)
dnsport_v4 = port
else:
lv4 = None
dnsport_v4 = 0
try:
dns_listener.bind(lv6, lv4)
bound = True
break
except socket.error, e:
if e.errno == errno.EADDRINUSE:
last_e = e
else:
raise e
debug2('\n')
dns_listener.print_listening("DNS")
if not bound:
assert(last_e)
raise last_e
else:
dnsport_v6 = 0
dnsport_v4 = 0
dns_listener = None
fw = FirewallClient(redirectport_v6, redirectport_v4, subnets_include,
subnets_exclude, dnsport_v6, dnsport_v4, method, udp)
if fw.method == "tproxy":
tcp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
if udp_listener:
udp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
if udp_listener.v4 is not None:
udp_listener.v4.setsockopt(
socket.SOL_IP, IP_RECVORIGDSTADDR, 1)
if udp_listener.v6 is not None:
udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1)
if dns_listener:
dns_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
if dns_listener.v4 is not None:
dns_listener.v4.setsockopt(
socket.SOL_IP, IP_RECVORIGDSTADDR, 1)
if dns_listener.v6 is not None:
dns_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1)
try:
return _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
python, latency_control, dns_listener,
fw.method, seed_hosts, auto_nets, syslog,
daemon)
finally:
try:
if daemon:
# it's not our child anymore; can't waitpid
fw.p.returncode = 0
fw.done()
finally:
if daemon:
daemon_cleanup()

View File

View File

@ -1,17 +1,18 @@
import re, errno, socket, select, signal, struct import errno
import socket
import select
import signal
import struct
import compat.ssubprocess as ssubprocess import compat.ssubprocess as ssubprocess
import helpers, ssyslog import ssyslog
from helpers import * import sys
import os
from helpers import log, debug1, debug3, islocal, Fatal, family_to_string, \
resolvconf_nameservers
# python doesn't have a definition for this # python doesn't have a definition for this
IPPROTO_DIVERT = 254 IPPROTO_DIVERT = 254
# return values from sysctl_set
SUCCESS = 0
SAME = 1
FAILED = -1
NONEXIST = -2
def nonfatal(func, *args): def nonfatal(func, *args):
try: try:
@ -20,17 +21,15 @@ def nonfatal(func, *args):
log('error: %s\n' % e) log('error: %s\n' % e)
def _call(argv): def ipt_chain_exists(family, table, name):
debug1('>> %s\n' % ' '.join(argv)) if family == socket.AF_INET6:
rv = ssubprocess.call(argv) cmd = 'ip6tables'
if rv: elif family == socket.AF_INET:
raise Fatal('%r returned %d' % (argv, rv)) cmd = 'iptables'
return rv else:
raise Exception('Unsupported family "%s"' % family_to_string(family))
argv = [cmd, '-t', table, '-nL']
def ipt_chain_exists(name): p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE)
argv = ['iptables', '-t', 'nat', '-nL']
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
@ -39,13 +38,23 @@ def ipt_chain_exists(name):
raise Fatal('%r returned %d' % (argv, rv)) raise Fatal('%r returned %d' % (argv, rv))
def ipt(*args): def _ipt(family, table, *args):
argv = ['iptables', '-t', 'nat'] + list(args) if family == socket.AF_INET6:
_call(argv) argv = ['ip6tables', '-t', table] + list(args)
elif family == socket.AF_INET:
argv = ['iptables', '-t', table] + list(args)
else:
raise Exception('Unsupported family "%s"' % family_to_string(family))
debug1('>> %s\n' % ' '.join(argv))
rv = ssubprocess.call(argv)
if rv:
raise Fatal('%r returned %d' % (argv, rv))
_no_ttl_module = False _no_ttl_module = False
def ipt_ttl(*args):
def _ipt_ttl(family, *args):
global _no_ttl_module global _no_ttl_module
if not _no_ttl_module: if not _no_ttl_module:
# we avoid infinite loops by generating server-side connections # we avoid infinite loops by generating server-side connections
@ -53,16 +62,15 @@ def ipt_ttl(*args):
# connections, in case client == server. # connections, in case client == server.
try: try:
argsplus = list(args) + ['-m', 'ttl', '!', '--ttl', '42'] argsplus = list(args) + ['-m', 'ttl', '!', '--ttl', '42']
ipt(*argsplus) _ipt(family, *argsplus)
except Fatal: except Fatal:
ipt(*args) _ipt(family, *args)
# we only get here if the non-ttl attempt succeeds # we only get here if the non-ttl attempt succeeds
log('sshuttle: warning: your iptables is missing ' log('sshuttle: warning: your iptables is missing '
'the ttl module.\n') 'the ttl module.\n')
_no_ttl_module = True _no_ttl_module = True
else: else:
ipt(*args) _ipt(family, *args)
# We name the chain based on the transproxy port number so that it's possible # We name the chain based on the transproxy port number so that it's possible
@ -70,11 +78,27 @@ def ipt_ttl(*args):
# multiple copies shouldn't have overlapping subnets, or only the most- # multiple copies shouldn't have overlapping subnets, or only the most-
# recently-started one will win (because we use "-I OUTPUT 1" instead of # recently-started one will win (because we use "-I OUTPUT 1" instead of
# "-A OUTPUT"). # "-A OUTPUT").
def do_iptables(port, dnsport, subnets): def do_iptables_nat(port, dnsport, family, subnets, udp):
# only ipv4 supported with NAT
if family != socket.AF_INET:
raise Exception(
'Address family "%s" unsupported by nat method'
% family_to_string(family))
if udp:
raise Exception("UDP not supported by nat method")
table = "nat"
def ipt(*args):
return _ipt(family, table, *args)
def ipt_ttl(*args):
return _ipt_ttl(family, table, *args)
chain = 'sshuttle-%s' % port chain = 'sshuttle-%s' % port
# basic cleanup/setup of chains # basic cleanup/setup of chains
if ipt_chain_exists(chain): if ipt_chain_exists(family, table, chain):
nonfatal(ipt, '-D', 'OUTPUT', '-j', chain) nonfatal(ipt, '-D', 'OUTPUT', '-j', chain)
nonfatal(ipt, '-D', 'PREROUTING', '-j', chain) nonfatal(ipt, '-D', 'PREROUTING', '-j', chain)
nonfatal(ipt, '-F', chain) nonfatal(ipt, '-F', chain)
@ -92,20 +116,21 @@ def do_iptables(port, dnsport, subnets):
# to least-specific, and at any given level of specificity, we want # 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- # excludes to come first. That's why the columns are in such a non-
# intuitive order. # intuitive order.
for swidth,sexclude,snet in sorted(subnets, reverse=True): for f, swidth, sexclude, snet \
in sorted(subnets, key=lambda s: s[1], reverse=True):
if sexclude: if sexclude:
ipt('-A', chain, '-j', 'RETURN', ipt('-A', chain, '-j', 'RETURN',
'--dest', '%s/%s' % (snet,swidth), '--dest', '%s/%s' % (snet, swidth),
'-p', 'tcp') '-p', 'tcp')
else: else:
ipt_ttl('-A', chain, '-j', 'REDIRECT', ipt_ttl('-A', chain, '-j', 'REDIRECT',
'--dest', '%s/%s' % (snet,swidth), '--dest', '%s/%s' % (snet, swidth),
'-p', 'tcp', '-p', 'tcp',
'--to-ports', str(port)) '--to-ports', str(port))
if dnsport: if dnsport:
nslist = resolvconf_nameservers() nslist = resolvconf_nameservers()
for ip in nslist: for f, ip in filter(lambda i: i[0] == family, nslist):
ipt_ttl('-A', chain, '-j', 'REDIRECT', ipt_ttl('-A', chain, '-j', 'REDIRECT',
'--dest', '%s/32' % ip, '--dest', '%s/32' % ip,
'-p', 'udp', '-p', 'udp',
@ -113,14 +138,115 @@ def do_iptables(port, dnsport, subnets):
'--to-ports', str(dnsport)) '--to-ports', str(dnsport))
def do_iptables_tproxy(port, dnsport, family, subnets, udp):
if family not in [socket.AF_INET, socket.AF_INET6]:
raise Exception(
'Address family "%s" unsupported by tproxy method'
% family_to_string(family))
table = "mangle"
def ipt(*args):
return _ipt(family, table, *args)
def ipt_ttl(*args):
return _ipt_ttl(family, table, *args)
mark_chain = 'sshuttle-m-%s' % port
tproxy_chain = 'sshuttle-t-%s' % port
divert_chain = 'sshuttle-d-%s' % port
# basic cleanup/setup of chains
if ipt_chain_exists(family, table, mark_chain):
ipt('-D', 'OUTPUT', '-j', mark_chain)
ipt('-F', mark_chain)
ipt('-X', mark_chain)
if ipt_chain_exists(family, table, tproxy_chain):
ipt('-D', 'PREROUTING', '-j', tproxy_chain)
ipt('-F', tproxy_chain)
ipt('-X', tproxy_chain)
if ipt_chain_exists(family, table, divert_chain):
ipt('-F', divert_chain)
ipt('-X', divert_chain)
if subnets or dnsport:
ipt('-N', mark_chain)
ipt('-F', mark_chain)
ipt('-N', divert_chain)
ipt('-F', divert_chain)
ipt('-N', tproxy_chain)
ipt('-F', tproxy_chain)
ipt('-I', 'OUTPUT', '1', '-j', mark_chain)
ipt('-I', 'PREROUTING', '1', '-j', tproxy_chain)
ipt('-A', divert_chain, '-j', 'MARK', '--set-mark', '1')
ipt('-A', divert_chain, '-j', 'ACCEPT')
ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain,
'-m', 'tcp', '-p', 'tcp')
if subnets and udp:
ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain,
'-m', 'udp', '-p', 'udp')
if dnsport:
nslist = resolvconf_nameservers()
for f, ip in filter(lambda i: i[0] == family, nslist):
ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
'--dest', '%s/32' % ip,
'-m', 'udp', '-p', 'udp', '--dport', '53')
ipt('-A', tproxy_chain, '-j', 'TPROXY', '--tproxy-mark', '0x1/0x1',
'--dest', '%s/32' % ip,
'-m', 'udp', '-p', 'udp', '--dport', '53',
'--on-port', str(dnsport))
if subnets:
for f, swidth, sexclude, snet \
in sorted(subnets, key=lambda s: s[1], reverse=True):
if sexclude:
ipt('-A', mark_chain, '-j', 'RETURN',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'tcp', '-p', 'tcp')
ipt('-A', tproxy_chain, '-j', 'RETURN',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'tcp', '-p', 'tcp')
else:
ipt('-A', mark_chain, '-j', 'MARK',
'--set-mark', '1',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'tcp', '-p', 'tcp')
ipt('-A', tproxy_chain, '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'tcp', '-p', 'tcp',
'--on-port', str(port))
if sexclude and udp:
ipt('-A', mark_chain, '-j', 'RETURN',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'udp', '-p', 'udp')
ipt('-A', tproxy_chain, '-j', 'RETURN',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'udp', '-p', 'udp')
elif udp:
ipt('-A', mark_chain, '-j', 'MARK',
'--set-mark', '1',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'udp', '-p', 'udp')
ipt('-A', tproxy_chain, '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'udp', '-p', 'udp',
'--on-port', str(port))
def ipfw_rule_exists(n): def ipfw_rule_exists(n):
argv = ['ipfw', 'list'] argv = ['ipfw', 'list']
p = ssubprocess.Popen(argv, stdout = ssubprocess.PIPE) p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE)
found = False found = False
for line in p.stdout: for line in p.stdout:
if line.startswith('%05d ' % n): if line.startswith('%05d ' % n):
if not ('ipttl 42' in line if not ('ipttl 42' in line
or ('skipto %d' % (n+1)) in line or ('skipto %d' % (n + 1)) in line
or 'check-state' in line): or 'check-state' in line):
log('non-sshuttle ipfw rule: %r\n' % line.strip()) 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)
@ -132,12 +258,14 @@ def ipfw_rule_exists(n):
_oldctls = {} _oldctls = {}
def _fill_oldctls(prefix): def _fill_oldctls(prefix):
argv = ['sysctl', prefix] argv = ['sysctl', prefix]
p = ssubprocess.Popen(argv, stdout = ssubprocess.PIPE) p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE)
for line in p.stdout: for line in p.stdout:
assert(line[-1] == '\n') assert(line[-1] == '\n')
(k,v) = line[:-1].split(': ', 1) (k, v) = line[:-1].split(': ', 1)
_oldctls[k] = v _oldctls[k] = v
rv = p.wait() rv = p.wait()
if rv: if rv:
@ -146,49 +274,15 @@ def _fill_oldctls(prefix):
raise Fatal('%r returned no data' % (argv,)) raise Fatal('%r returned no data' % (argv,))
KERNEL_FLAGS_PATH = '/Library/Preferences/SystemConfiguration/com.apple.Boot'
KERNEL_FLAGS_NAME = 'Kernel Flags'
def _defaults_read_kernel_flags():
argv = ['defaults', 'read', KERNEL_FLAGS_PATH, KERNEL_FLAGS_NAME]
debug1('>> %s\n' % ' '.join(argv))
p = ssubprocess.Popen(argv, stdout = ssubprocess.PIPE)
flagstr = p.stdout.read().strip()
rv = p.wait()
if rv:
raise Fatal('%r returned %d' % (argv, rv))
flags = flagstr and flagstr.split(' ') or []
return flags
def _defaults_write_kernel_flags(flags):
flagstr = ' '.join(flags)
argv = ['defaults', 'write', KERNEL_FLAGS_PATH, KERNEL_FLAGS_NAME,
flagstr]
_call(argv)
argv = ['plutil', '-convert', 'xml1', KERNEL_FLAGS_PATH + '.plist']
_call(argv)
def defaults_write_kernel_flag(name, val):
flags = _defaults_read_kernel_flags()
found = 0
for i in range(len(flags)):
if flags[i].startswith('%s=' % name):
found += 1
flags[i] = '%s=%s' % (name, val)
if not found:
flags.insert(0, '%s=%s' % (name, val))
_defaults_write_kernel_flags(flags)
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))
return ssubprocess.call(argv, stdout = open('/dev/null', 'w')) return ssubprocess.call(argv, stdout=open('/dev/null', 'w'))
_changedctls = [] _changedctls = []
def sysctl_set(name, val, permanent=False): def sysctl_set(name, val, permanent=False):
PREFIX = 'net.inet.ip' PREFIX = 'net.inet.ip'
assert(name.startswith(PREFIX + '.')) assert(name.startswith(PREFIX + '.'))
@ -197,15 +291,11 @@ def sysctl_set(name, val, permanent=False):
_fill_oldctls(PREFIX) _fill_oldctls(PREFIX)
if not (name in _oldctls): if not (name in _oldctls):
debug1('>> No such sysctl: %r\n' % name) debug1('>> No such sysctl: %r\n' % name)
return NONEXIST return False
oldval = _oldctls[name] oldval = _oldctls[name]
if val == oldval: if val != oldval:
return SAME
rv = _sysctl_set(name, val) rv = _sysctl_set(name, val)
if rv != 0: if rv == 0 and permanent:
return FAILED
if permanent:
debug1('>> ...saving permanently in /etc/sysctl.conf\n') debug1('>> ...saving permanently in /etc/sysctl.conf\n')
f = open('/etc/sysctl.conf', 'a') f = open('/etc/sysctl.conf', 'a')
f.write('\n' f.write('\n'
@ -214,7 +304,7 @@ def sysctl_set(name, val, permanent=False):
f.close() f.close()
else: else:
_changedctls.append(name) _changedctls.append(name)
return SUCCESS return True
def _udp_unpack(p): def _udp_unpack(p):
@ -230,9 +320,11 @@ def _udp_repack(p, src, dst):
_real_dns_server = [None] _real_dns_server = [None]
def _handle_diversion(divertsock, dnsport): def _handle_diversion(divertsock, dnsport):
p,tag = divertsock.recvfrom(4096) p, tag = divertsock.recvfrom(4096)
src,dst = _udp_unpack(p) src, dst = _udp_unpack(p)
debug3('got diverted packet from %r to %r\n' % (src, dst)) debug3('got diverted packet from %r to %r\n' % (src, dst))
if dst[1] == 53: if dst[1] == 53:
# outgoing DNS # outgoing DNS
@ -252,12 +344,23 @@ def _handle_diversion(divertsock, dnsport):
def ipfw(*args): def ipfw(*args):
argv = ['ipfw', '-q'] + list(args) argv = ['ipfw', '-q'] + list(args)
_call(argv) debug1('>> %s\n' % ' '.join(argv))
rv = ssubprocess.call(argv)
if rv:
raise Fatal('%r returned %d' % (argv, rv))
def do_ipfw(port, dnsport, subnets): def do_ipfw(port, dnsport, family, subnets, udp):
# IPv6 not supported
if family not in [socket.AF_INET, ]:
raise Exception(
'Address family "%s" unsupported by ipfw method'
% family_to_string(family))
if udp:
raise Exception("UDP not supported by ipfw method")
sport = str(port) sport = str(port)
xsport = str(port+1) xsport = str(port + 1)
# cleanup any existing rules # cleanup any existing rules
if ipfw_rule_exists(port): if ipfw_rule_exists(port):
@ -270,14 +373,8 @@ def do_ipfw(port, dnsport, subnets):
if subnets or dnsport: if subnets or dnsport:
sysctl_set('net.inet.ip.fw.enable', 1) sysctl_set('net.inet.ip.fw.enable', 1)
changed = sysctl_set('net.inet.ip.scopedroute', 0, permanent=True)
# This seems to be needed on MacOS 10.6 and 10.7. For more if changed:
# information, see:
# http://groups.google.com/group/sshuttle/browse_thread/thread/bc32562e17987b25/6d3aa2bb30a1edab
# and
# http://serverfault.com/questions/138622/transparent-proxying-leaves-sockets-with-syn-rcvd-in-macos-x-10-6-snow-leopard
changeflag = sysctl_set('net.inet.ip.scopedroute', 0, permanent=True)
if changeflag == SUCCESS:
log("\n" log("\n"
" WARNING: ONE-TIME NETWORK DISRUPTION:\n" " WARNING: ONE-TIME NETWORK DISRUPTION:\n"
" =====================================\n" " =====================================\n"
@ -288,36 +385,22 @@ def do_ipfw(port, dnsport, subnets):
"ethernet port) NOW, then restart sshuttle. The fix is\n" "ethernet port) NOW, then restart sshuttle. The fix is\n"
"permanent; you only have to do this once.\n\n") "permanent; you only have to do this once.\n\n")
sys.exit(1) sys.exit(1)
elif changeflag == FAILED:
# On MacOS 10.7, the scopedroute sysctl became read-only, so
# we have to fix it using a kernel boot parameter instead,
# which requires rebooting. For more, see:
# http://groups.google.com/group/sshuttle/browse_thread/thread/a42505ca33e1de80/e5e8f3e5a92d25f7
log('Updating kernel boot flags.\n')
defaults_write_kernel_flag('net.inet.ip.scopedroute', 0)
log("\n"
" YOU MUST REBOOT TO USE SSHUTTLE\n"
" ===============================\n"
"sshuttle has changed a MacOS kernel boot-time setting\n"
"to work around a bug in MacOS 10.7 Lion. You will need\n"
"to reboot before it takes effect. You only have to\n"
"do this once.\n\n")
sys.exit(EXITCODE_NEEDS_REBOOT)
ipfw('add', sport, 'check-state', 'ip', ipfw('add', sport, 'check-state', 'ip',
'from', 'any', 'to', 'any') 'from', 'any', 'to', 'any')
if subnets: if subnets:
# create new subnet entries # create new subnet entries
for swidth,sexclude,snet in sorted(subnets, reverse=True): for f, swidth, sexclude, snet \
in sorted(subnets, key=lambda s: s[1], reverse=True):
if sexclude: if sexclude:
ipfw('add', sport, 'skipto', xsport, ipfw('add', sport, 'skipto', xsport,
'tcp', 'tcp',
'from', 'any', 'to', '%s/%s' % (snet,swidth)) 'from', 'any', 'to', '%s/%s' % (snet, swidth))
else: else:
ipfw('add', sport, 'fwd', '127.0.0.1,%d' % port, ipfw('add', sport, 'fwd', '127.0.0.1,%d' % port,
'tcp', 'tcp',
'from', 'any', 'to', '%s/%s' % (snet,swidth), 'from', 'any', 'to', '%s/%s' % (snet, swidth),
'not', 'ipttl', '42', 'keep-state', 'setup') 'not', 'ipttl', '42', 'keep-state', 'setup')
# This part is much crazier than it is on Linux, because MacOS (at least # This part is much crazier than it is on Linux, because MacOS (at least
@ -355,7 +438,7 @@ def do_ipfw(port, dnsport, subnets):
divertsock.bind(('0.0.0.0', port)) # IP field is ignored divertsock.bind(('0.0.0.0', port)) # IP field is ignored
nslist = resolvconf_nameservers() nslist = resolvconf_nameservers()
for ip in nslist: for f, ip in filter(lambda i: i[0] == family, nslist):
# relabel and then catch outgoing DNS requests # relabel and then catch outgoing DNS requests
ipfw('add', sport, 'divert', sport, ipfw('add', sport, 'divert', sport,
'udp', 'udp',
@ -369,7 +452,7 @@ def do_ipfw(port, dnsport, subnets):
def do_wait(): def do_wait():
while 1: while 1:
r,w,x = select.select([sys.stdin, divertsock], [], []) r, w, x = select.select([sys.stdin, divertsock], [], [])
if divertsock in r: if divertsock in r:
_handle_diversion(divertsock, dnsport) _handle_diversion(divertsock, dnsport)
if sys.stdin in r: if sys.stdin in r:
@ -389,10 +472,12 @@ def program_exists(name):
hostmap = {} hostmap = {}
def rewrite_etc_hosts(port): def rewrite_etc_hosts(port):
HOSTSFILE='/etc/hosts' HOSTSFILE = '/etc/hosts'
BAKFILE='%s.sbak' % HOSTSFILE BAKFILE = '%s.sbak' % HOSTSFILE
APPEND='# sshuttle-firewall-%d AUTOCREATED' % port APPEND = '# sshuttle-firewall-%d AUTOCREATED' % port
old_content = '' old_content = ''
st = None st = None
try: try:
@ -411,8 +496,8 @@ def rewrite_etc_hosts(port):
if line.find(APPEND) >= 0: if line.find(APPEND) >= 0:
continue continue
f.write('%s\n' % line) f.write('%s\n' % line)
for (name,ip) in sorted(hostmap.items()): for (name, ip) in sorted(hostmap.items()):
f.write('%-30s %s\n' % ('%s %s' % (ip,name), APPEND)) f.write('%-30s %s\n' % ('%s %s' % (ip, name), APPEND))
f.close() f.close()
if st: if st:
@ -438,22 +523,36 @@ def restore_etc_hosts(port):
# exit. In case that fails, it's not the end of the world; future runs will # 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 # supercede it in the transproxy list, at least, so the leftover rules
# are hopefully harmless. # are hopefully harmless.
def main(port, dnsport, syslog): def main(port_v6, port_v4, dnsport_v6, dnsport_v4, method, udp, syslog):
assert(port > 0) assert(port_v6 >= 0)
assert(port <= 65535) assert(port_v6 <= 65535)
assert(dnsport >= 0) assert(port_v4 >= 0)
assert(dnsport <= 65535) assert(port_v4 <= 65535)
assert(dnsport_v6 >= 0)
assert(dnsport_v6 <= 65535)
assert(dnsport_v4 >= 0)
assert(dnsport_v4 <= 65535)
if os.getuid() != 0: if os.getuid() != 0:
raise Fatal('you must be root (or enable su/sudo) to set the firewall') raise Fatal('you must be root (or enable su/sudo) to set the firewall')
if method == "auto":
if program_exists('ipfw'): if program_exists('ipfw'):
do_it = do_ipfw method = "ipfw"
elif program_exists('iptables'): elif program_exists('iptables'):
do_it = do_iptables method = "nat"
else: else:
raise Fatal("can't find either ipfw or iptables; check your PATH") raise Fatal("can't find either ipfw or iptables; check your PATH")
if method == "nat":
do_it = do_iptables_nat
elif method == "tproxy":
do_it = do_iptables_tproxy
elif method == "ipfw":
do_it = do_ipfw
else:
raise Exception('Unknown method "%s"' % method)
# because of limitations of the 'su' command, the *real* stdin/stdout # because of limitations of the 'su' command, the *real* stdin/stdout
# are both attached to stdout initially. Clone stdout into stdin so we # are both attached to stdout initially. Clone stdout into stdin so we
# can read from it. # can read from it.
@ -463,8 +562,8 @@ def main(port, dnsport, syslog):
ssyslog.start_syslog() ssyslog.start_syslog()
ssyslog.stderr_to_syslog() ssyslog.stderr_to_syslog()
debug1('firewall manager ready.\n') debug1('firewall manager ready method %s.\n' % method)
sys.stdout.write('READY\n') sys.stdout.write('READY %s\n' % method)
sys.stdout.flush() sys.stdout.flush()
# don't disappear if our controlling terminal or stdout/stderr # don't disappear if our controlling terminal or stdout/stderr
@ -495,15 +594,29 @@ def main(port, dnsport, syslog):
elif line == 'GO\n': elif line == 'GO\n':
break break
try: try:
(width,exclude,ip) = line.strip().split(',', 2) (family, width, exclude, ip) = line.strip().split(',', 3)
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((int(width), bool(int(exclude)), ip)) subnets.append((int(family), int(width), bool(int(exclude)), ip))
try: try:
if line: if line:
debug1('firewall manager: starting transproxy.\n') debug1('firewall manager: starting transproxy.\n')
do_wait = do_it(port, dnsport, subnets)
subnets_v6 = filter(lambda i: i[0] == socket.AF_INET6, subnets)
if port_v6:
do_wait = do_it(
port_v6, dnsport_v6, socket.AF_INET6, subnets_v6, udp)
elif len(subnets_v6) > 0:
debug1("IPv6 subnets defined but IPv6 disabled\n")
subnets_v4 = filter(lambda i: i[0] == socket.AF_INET, subnets)
if port_v4:
do_wait = do_it(
port_v4, dnsport_v4, socket.AF_INET, subnets_v4, udp)
elif len(subnets_v4) > 0:
debug1('IPv4 subnets defined but IPv4 disabled\n')
sys.stdout.write('STARTED\n') sys.stdout.write('STARTED\n')
try: try:
@ -517,12 +630,13 @@ def main(port, dnsport, syslog):
# to stay running so that we don't need a *second* password # to stay running so that we don't need a *second* password
# authentication at shutdown time - that cleanup is important! # authentication at shutdown time - that cleanup is important!
while 1: while 1:
if do_wait: do_wait() if do_wait:
do_wait()
line = sys.stdin.readline(128) line = sys.stdin.readline(128)
if line.startswith('HOST '): if line.startswith('HOST '):
(name,ip) = line[5:].strip().split(',', 1) (name, ip) = line[5:].strip().split(',', 1)
hostmap[name] = ip hostmap[name] = ip
rewrite_etc_hosts(port) rewrite_etc_hosts(port_v6 or port_v4)
elif line: elif line:
raise Fatal('expected EOF, got %r' % line) raise Fatal('expected EOF, got %r' % line)
else: else:
@ -532,5 +646,8 @@ def main(port, dnsport, syslog):
debug1('firewall manager: undoing changes.\n') debug1('firewall manager: undoing changes.\n')
except: except:
pass pass
do_it(port, 0, []) if port_v6:
restore_etc_hosts(port) do_it(port_v6, 0, socket.AF_INET6, [], udp)
if port_v4:
do_it(port_v4, 0, socket.AF_INET, [], udp)
restore_etc_hosts(port_v6 or port_v4)

View File

@ -1,8 +1,11 @@
import sys, os, socket, errno import sys
import socket
import errno
logprefix = '' logprefix = ''
verbose = 0 verbose = 0
def log(s): def log(s):
try: try:
sys.stdout.flush() sys.stdout.flush()
@ -13,14 +16,17 @@ def log(s):
# our tty closes. That sucks, but it's no reason to abort the program. # our tty closes. That sucks, but it's no reason to abort the program.
pass pass
def debug1(s): def debug1(s):
if verbose >= 1: if verbose >= 1:
log(s) log(s)
def debug2(s): def debug2(s):
if verbose >= 2: if verbose >= 2:
log(s) log(s)
def debug3(s): def debug3(s):
if verbose >= 3: if verbose >= 3:
log(s) log(s)
@ -30,11 +36,6 @@ class Fatal(Exception):
pass pass
EXITCODE_NEEDS_REBOOT = 111
class FatalNeedsReboot(Fatal):
pass
def list_contains_any(l, sub): def list_contains_any(l, sub):
for i in sub: for i in sub:
if i in l: if i in l:
@ -47,7 +48,10 @@ def resolvconf_nameservers():
for line in open('/etc/resolv.conf'): for line in open('/etc/resolv.conf'):
words = line.lower().split() words = line.lower().split()
if len(words) >= 2 and words[0] == 'nameserver': if len(words) >= 2 and words[0] == 'nameserver':
l.append(words[1]) if ':' in words[1]:
l.append((socket.AF_INET6, words[1]))
else:
l.append((socket.AF_INET, words[1]))
return l return l
@ -60,11 +64,11 @@ def resolvconf_random_nameserver():
random.shuffle(l) random.shuffle(l)
return l[0] return l[0]
else: else:
return '127.0.0.1' return (socket.AF_INET, '127.0.0.1')
def islocal(ip): def islocal(ip, family):
sock = socket.socket() sock = socket.socket(family)
try: try:
try: try:
sock.bind((ip, 0)) sock.bind((ip, 0))
@ -78,3 +82,10 @@ def islocal(ip):
return True # it's a local IP, or there would have been an error return True # it's a local IP, or there would have been an error
def family_to_string(family):
if family == socket.AF_INET6:
return "AF_INET6"
elif family == socket.AF_INET:
return "AF_INET"
else:
return str(family)

View File

@ -1,12 +1,18 @@
import time, socket, re, select, errno import time
import socket
import re
import select
import errno
import os
import sys
if not globals().get('skip_imports'): if not globals().get('skip_imports'):
import compat.ssubprocess as ssubprocess import compat.ssubprocess as ssubprocess
import helpers import helpers
from helpers import * from helpers import log, debug1, debug2, debug3
POLL_TIME = 60*15 POLL_TIME = 60 * 15
NETSTAT_POLL_TIME = 30 NETSTAT_POLL_TIME = 30
CACHEFILE=os.path.expanduser('~/.sshuttle.hosts') CACHEFILE = os.path.expanduser('~/.sshuttle.hosts')
_nmb_ok = True _nmb_ok = True
@ -28,7 +34,7 @@ def write_host_cache():
tmpname = '%s.%d.tmp' % (CACHEFILE, os.getpid()) tmpname = '%s.%d.tmp' % (CACHEFILE, os.getpid())
try: try:
f = open(tmpname, 'wb') f = open(tmpname, 'wb')
for name,ip in sorted(hostnames.items()): for name, ip in sorted(hostnames.items()):
f.write('%s,%s\n' % (name, ip)) f.write('%s,%s\n' % (name, ip))
f.close() f.close()
os.rename(tmpname, CACHEFILE) os.rename(tmpname, CACHEFILE)
@ -50,7 +56,7 @@ def read_host_cache():
for line in f: for line in f:
words = line.strip().split(',') words = line.strip().split(',')
if len(words) == 2: if len(words) == 2:
(name,ip) = words (name, ip) = words
name = re.sub(r'[^-\w]', '-', name).strip() name = re.sub(r'[^-\w]', '-', name).strip()
ip = re.sub(r'[^0-9.]', '', ip).strip() ip = re.sub(r'[^0-9.]', '', ip).strip()
if name and ip: if name and ip:
@ -94,7 +100,7 @@ def _check_revdns(ip):
debug3('< %s\n' % r[0]) debug3('< %s\n' % r[0])
check_host(r[0]) check_host(r[0])
found_host(r[0], ip) found_host(r[0], ip)
except socket.herror, e: except socket.herror:
pass pass
@ -105,7 +111,7 @@ def _check_dns(hostname):
debug3('< %s\n' % ip) debug3('< %s\n' % ip)
check_host(ip) check_host(ip)
found_host(hostname, ip) found_host(hostname, ip)
except socket.gaierror, e: except socket.gaierror:
pass pass
@ -187,7 +193,7 @@ def _check_nmb(hostname, is_workgroup, is_master):
global _nmb_ok global _nmb_ok
if not _nmb_ok: if not _nmb_ok:
return return
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 = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null) p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null)
@ -228,13 +234,13 @@ def check_workgroup(hostname):
def _enqueue(op, *args): def _enqueue(op, *args):
t = (op,args) t = (op, args)
if queue.get(t) == None: if queue.get(t) is None:
queue[t] = 0 queue[t] = 0
def _stdin_still_ok(timeout): def _stdin_still_ok(timeout):
r,w,x = select.select([sys.stdin.fileno()], [], [], timeout) r, w, x = select.select([sys.stdin.fileno()], [], [], timeout)
if r: if r:
b = os.read(sys.stdin.fileno(), 4096) b = os.read(sys.stdin.fileno(), 4096)
if not b: if not b:
@ -261,8 +267,8 @@ def hw_main(seed_hosts):
while 1: while 1:
now = time.time() now = time.time()
for t,last_polled in queue.items(): for t, last_polled in queue.items():
(op,args) = t (op, args) = t
if not _stdin_still_ok(0): if not _stdin_still_ok(0):
break break
maxtime = POLL_TIME maxtime = POLL_TIME

230
src/main.py Normal file
View File

@ -0,0 +1,230 @@
import sys
import re
import socket
import helpers
import options
import client
import server
import firewall
import hostwatch
from helpers import log, Fatal
# 1.2.3.4/5 or just 1.2.3.4
def parse_subnet4(s):
m = re.match(r'(\d+)(?:\.(\d+)\.(\d+)\.(\d+))?(?:/(\d+))?$', s)
if not m:
raise Fatal('%r is not a valid IP subnet format' % s)
(a, b, c, d, width) = m.groups()
(a, b, c, d) = (int(a or 0), int(b or 0), int(c or 0), int(d or 0))
if width is None:
width = 32
else:
width = int(width)
if a > 255 or b > 255 or c > 255 or d > 255:
raise Fatal('%d.%d.%d.%d has numbers > 255' % (a, b, c, d))
if width > 32:
raise Fatal('*/%d is greater than the maximum of 32' % width)
return(socket.AF_INET, '%d.%d.%d.%d' % (a, b, c, d), width)
# 1:2::3/64 or just 1:2::3
def parse_subnet6(s):
m = re.match(r'(?:([a-fA-F\d:]+))?(?:/(\d+))?$', s)
if not m:
raise Fatal('%r is not a valid IP subnet format' % s)
(net, width) = m.groups()
if width is None:
width = 128
else:
width = int(width)
if width > 128:
raise Fatal('*/%d is greater than the maximum of 128' % width)
return(socket.AF_INET6, net, width)
# Subnet file, supporting empty lines and hash-started comment lines
def parse_subnet_file(s):
try:
handle = open(s, 'r')
except OSError:
raise Fatal('Unable to open subnet file: %s' % s)
raw_config_lines = handle.readlines()
config_lines = []
for line_no, line in enumerate(raw_config_lines):
line = line.strip()
if len(line) == 0:
continue
if line[0] == '#':
continue
config_lines.append(line)
return config_lines
# list of:
# 1.2.3.4/5 or just 1.2.3.4
# 1:2::3/64 or just 1:2::3
def parse_subnets(subnets_str):
subnets = []
for s in subnets_str:
if ':' in s:
subnet = parse_subnet6(s)
else:
subnet = parse_subnet4(s)
subnets.append(subnet)
return subnets
# 1.2.3.4:567 or just 1.2.3.4 or just 567
def parse_ipport4(s):
s = str(s)
m = re.match(r'(?:(\d+)\.(\d+)\.(\d+)\.(\d+))?(?::)?(?:(\d+))?$', s)
if not m:
raise Fatal('%r is not a valid IP:port format' % s)
(a, b, c, d, port) = m.groups()
(a, b, c, d, port) = (int(a or 0), int(b or 0), int(c or 0), int(d or 0),
int(port or 0))
if a > 255 or b > 255 or c > 255 or d > 255:
raise Fatal('%d.%d.%d.%d has numbers > 255' % (a, b, c, d))
if port > 65535:
raise Fatal('*:%d is greater than the maximum of 65535' % port)
if a is None:
a = b = c = d = 0
return ('%d.%d.%d.%d' % (a, b, c, d), port)
# [1:2::3]:456 or [1:2::3] or 456
def parse_ipport6(s):
s = str(s)
m = re.match(r'(?:\[([^]]*)])?(?::)?(?:(\d+))?$', s)
if not m:
raise Fatal('%s is not a valid IP:port format' % s)
(ip, port) = m.groups()
(ip, port) = (ip or '::', int(port or 0))
return (ip, port)
optspec = """
sshuttle [-l [ip:]port] [-r [username@]sshserver[:port]] <subnets...>
sshuttle --server
sshuttle --firewall <port> <subnets...>
sshuttle --hostwatch
--
l,listen= transproxy to this ip address and port number
H,auto-hosts scan for remote hostnames and update local /etc/hosts
N,auto-nets automatically determine subnets to route
dns capture local DNS requests and forward to the remote DNS server
method= auto, nat, tproxy, or ipfw
python= path to python interpreter on the remote server
r,remote= ssh hostname (and optional username) of remote sshuttle server
x,exclude= exclude this subnet (can be used more than once)
X,exclude-from= exclude the subnets in a file (whitespace separated)
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)
no-latency-control sacrifice latency to improve bandwidth benchmarks
wrap= restart counting channel numbers after this number (for testing)
D,daemon run in the background as a daemon
s,subnets= file where the subnets are stored, instead of on the command line
syslog send log messages to syslog (default if you use --daemon)
pidfile= pidfile name (only if using --daemon) [./sshuttle.pid]
server (internal use only)
firewall (internal use only)
hostwatch (internal use only)
"""
o = options.Options(optspec)
(opt, flags, extra) = o.parse(sys.argv[2:])
if opt.daemon:
opt.syslog = 1
if opt.wrap:
import ssnet
ssnet.MAX_CHANNEL = int(opt.wrap)
helpers.verbose = opt.verbose
try:
if opt.server:
if len(extra) != 0:
o.fatal('no arguments expected')
server.latency_control = opt.latency_control
sys.exit(server.main())
elif opt.firewall:
if len(extra) != 6:
o.fatal('exactly six arguments expected')
sys.exit(firewall.main(int(extra[0]), int(extra[1]),
int(extra[2]), int(extra[3]),
extra[4], int(extra[5]), opt.syslog))
elif opt.hostwatch:
sys.exit(hostwatch.hw_main(extra))
else:
if len(extra) < 1 and not opt.auto_nets and not opt.subnets:
o.fatal('at least one subnet, subnet file, or -N expected')
includes = extra
excludes = ['127.0.0.0/8']
for k, v in flags:
if k in ('-x', '--exclude'):
excludes.append(v)
if k in ('-X', '--exclude-from'):
excludes += open(v).read().split()
remotename = opt.remote
if remotename == '' or remotename == '-':
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
if opt.subnets:
includes = parse_subnet_file(opt.subnets)
if not opt.method:
method = "auto"
elif opt.method in ["auto", "nat", "tproxy", "ipfw"]:
method = opt.method
else:
o.fatal("method %s not supported" % opt.method)
if not opt.listen:
if opt.method == "tproxy":
ipport_v6 = parse_ipport6('[::1]:0')
else:
ipport_v6 = None
ipport_v4 = parse_ipport4('127.0.0.1:0')
else:
ipport_v6 = None
ipport_v4 = None
list = opt.listen.split(",")
for ip in list:
if '[' in ip and ']' in ip and opt.method == "tproxy":
ipport_v6 = parse_ipport6(ip)
else:
ipport_v4 = parse_ipport4(ip)
return_code = client.main(ipport_v6, ipport_v4,
opt.ssh_cmd,
remotename,
opt.python,
opt.latency_control,
opt.dns,
method,
sh,
opt.auto_nets,
parse_subnets(includes),
parse_subnets(excludes),
opt.syslog, opt.daemon, opt.pidfile)
if return_code == 0:
log('Normal exit code, exiting...')
else:
log('Abnormal exit code detected, failing...' % return_code)
sys.exit(return_code)
except Fatal, e:
log('fatal: %s\n' % e)
sys.exit(99)
except KeyboardInterrupt:
log('\n')
log('Keyboard interrupt: exiting.\n')
sys.exit(1)

View File

@ -1,9 +1,16 @@
"""Command-line options parser. """Command-line options parser.
With the help of an options spec string, easily parse command-line options. With the help of an options spec string, easily parse command-line options.
""" """
import sys, os, textwrap, getopt, re, struct import sys
import os
import textwrap
import getopt
import re
import struct
class OptDict: class OptDict:
def __init__(self): def __init__(self):
self._opts = {} self._opts = {}
@ -46,7 +53,8 @@ def _atoi(v):
def _remove_negative_kv(k, v): def _remove_negative_kv(k, v):
if k.startswith('no-') or k.startswith('no_'): if k.startswith('no-') or k.startswith('no_'):
return k[3:], not v return k[3:], not v
return k,v return k, v
def _remove_negative_k(k): def _remove_negative_k(k):
return _remove_negative_kv(k, None)[0] return _remove_negative_kv(k, None)[0]
@ -55,15 +63,17 @@ def _remove_negative_k(k):
def _tty_width(): def _tty_width():
s = struct.pack("HHHH", 0, 0, 0, 0) s = struct.pack("HHHH", 0, 0, 0, 0)
try: try:
import fcntl, termios import fcntl
import termios
s = fcntl.ioctl(sys.stderr.fileno(), termios.TIOCGWINSZ, s) s = fcntl.ioctl(sys.stderr.fileno(), termios.TIOCGWINSZ, s)
except (IOError, ImportError): except (IOError, ImportError):
return _atoi(os.environ.get('WIDTH')) or 70 return _atoi(os.environ.get('WIDTH')) or 70
(ysize,xsize,ypix,xpix) = struct.unpack('HHHH', s) (ysize, xsize, ypix, xpix) = struct.unpack('HHHH', s)
return xsize or 70 return xsize or 70
class Options: class Options:
"""Option parser. """Option parser.
When constructed, two strings are mandatory. The first one is the command 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 name showed before error messages. The second one is a string called an
@ -76,6 +86,7 @@ class Options:
By default, the parser function is getopt.gnu_getopt, and the abort By default, the parser function is getopt.gnu_getopt, and the abort
behaviour is to exit the program. behaviour is to exit the program.
""" """
def __init__(self, optspec, optfunc=getopt.gnu_getopt, def __init__(self, optspec, optfunc=getopt.gnu_getopt,
onabort=_default_onabort): onabort=_default_onabort):
self.optspec = optspec self.optspec = optspec
@ -95,7 +106,8 @@ class Options:
first_syn = True first_syn = True
while lines: while lines:
l = lines.pop() l = lines.pop()
if l == '--': break if l == '--':
break
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')
@ -122,7 +134,7 @@ class Options:
flagl = flags.split(',') flagl = flags.split(',')
flagl_nice = [] flagl_nice = []
for _f in flagl: for _f in flagl:
f,dvi = _remove_negative_kv(_f, _intify(defval)) f, dvi = _remove_negative_kv(_f, _intify(defval))
self._aliases[f] = _remove_negative_k(flagl[0]) self._aliases[f] = _remove_negative_k(flagl[0])
self._hasparms[f] = has_parm self._hasparms[f] = has_parm
self._defaults[f] = dvi self._defaults[f] = dvi
@ -141,7 +153,7 @@ class Options:
prefix = ' %-20s ' % flags_nice prefix = ' %-20s ' % flags_nice
argtext = '\n'.join(textwrap.wrap(extra, width=_tty_width(), 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 last_was_option = True
else: else:
@ -170,17 +182,18 @@ class Options:
and "extra" is a list of positional arguments. 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 k,v in self._defaults.iteritems(): for k, v in self._defaults.iteritems():
k = self._aliases[k] k = self._aliases[k]
opt[k] = v opt[k] = v
for (k,v) in flags: for (k, v) in flags:
k = k.lstrip('-') k = k.lstrip('-')
if k in ('h', '?', 'help'): if k in ('h', '?', 'help'):
self.usage() self.usage()
@ -195,6 +208,6 @@ class Options:
else: else:
v = _intify(v) v = _intify(v)
opt[k] = v opt[k] = v
for (f1,f2) in self._aliases.iteritems(): for (f1, f2) in self._aliases.iteritems():
opt[f1] = opt._opts.get(f2) opt[f1] = opt._opts.get(f2)
return (opt,flags,extra) return (opt, flags, extra)

View File

@ -1,9 +1,22 @@
import re, struct, socket, select, traceback, time import re
import struct
import socket
import traceback
import time
import sys
import os
if not globals().get('skip_imports'): if not globals().get('skip_imports'):
import ssnet, helpers, hostwatch import ssnet
import helpers
import hostwatch
import compat.ssubprocess as ssubprocess import compat.ssubprocess as ssubprocess
from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper from ssnet import Handler, Proxy, Mux, MuxWrapper
from helpers import * from helpers import log, debug1, debug2, debug3, Fatal, \
resolvconf_random_nameserver
if not globals().get('latency_control'):
latency_control = None
def _ipmatch(ipstr): def _ipmatch(ipstr):
@ -14,13 +27,13 @@ def _ipmatch(ipstr):
g = m.groups() g = m.groups()
ips = g[0] ips = g[0]
width = int(g[4] or 32) width = int(g[4] or 32)
if g[1] == None: if g[1] is None:
ips += '.0.0.0' ips += '.0.0.0'
width = min(width, 8) width = min(width, 8)
elif g[2] == None: elif g[2] is None:
ips += '.0.0' ips += '.0.0'
width = min(width, 16) width = min(width, 16)
elif g[3] == None: elif g[3] is None:
ips += '.0' ips += '.0'
width = min(width, 24) width = min(width, 24)
return (struct.unpack('!I', socket.inet_aton(ips))[0], width) return (struct.unpack('!I', socket.inet_aton(ips))[0], width)
@ -38,17 +51,12 @@ def _maskbits(netmask):
return 32 return 32
for i in range(32): for i in range(32):
if netmask[0] & _shl(1, i): if netmask[0] & _shl(1, i):
return 32-i return 32 - i
return 0 return 0
def _shl(n, bits): def _shl(n, bits):
# we use our own implementation of left-shift because return n * int(2 ** bits)
# results may be different between older and newer versions
# of python for numbers like 1<<32. We use long() because
# int(2**32) doesn't work in older python, which has limited
# int sizes.
return n * long(2**bits)
def _list_routes(): def _list_routes():
@ -63,8 +71,9 @@ 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] & _shl(_shl(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.AF_INET, socket.inet_ntoa(struct.pack('!I', ip)), width))
rv = p.wait() rv = p.wait()
if rv != 0: if rv != 0:
log('WARNING: %r returned %d\n' % (argv, rv)) log('WARNING: %r returned %d\n' % (argv, rv))
@ -73,11 +82,9 @@ def _list_routes():
def list_routes(): def list_routes():
l = [] for (family, ip, width) in _list_routes():
for (ip,width) in _list_routes():
if not ip.startswith('0.') and not ip.startswith('127.'): if not ip.startswith('0.') and not ip.startswith('127.'):
l.append((ip,width)) yield (family, ip, width)
return l
def _exc_dump(): def _exc_dump():
@ -86,7 +93,7 @@ def _exc_dump():
def start_hostwatch(seed_hosts): def start_hostwatch(seed_hosts):
s1,s2 = socket.socketpair() s1, s2 = socket.socketpair()
pid = os.fork() pid = os.fork()
if not pid: if not pid:
# child # child
@ -98,32 +105,36 @@ def start_hostwatch(seed_hosts):
os.dup2(s1.fileno(), 0) os.dup2(s1.fileno(), 0)
s1.close() s1.close()
rv = hostwatch.hw_main(seed_hosts) or 0 rv = hostwatch.hw_main(seed_hosts) or 0
except Exception, e: except Exception:
log('%s\n' % _exc_dump()) log('%s\n' % _exc_dump())
rv = 98 rv = 98
finally: finally:
os._exit(rv) os._exit(rv)
s1.close() s1.close()
return pid,s2 return pid, s2
class Hostwatch: class Hostwatch:
def __init__(self): def __init__(self):
self.pid = 0 self.pid = 0
self.sock = None self.sock = None
class DnsProxy(Handler): class DnsProxy(Handler):
def __init__(self, mux, chan, request): def __init__(self, mux, chan, request):
# FIXME! IPv4 specific
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
Handler.__init__(self, [sock]) Handler.__init__(self, [sock])
self.timeout = time.time()+30 self.timeout = time.time() + 30
self.mux = mux self.mux = mux
self.chan = chan self.chan = chan
self.tries = 0 self.tries = 0
self.peer = None self.peer = None
self.request = request self.request = request
self.sock = sock self.sock = sock
# FIXME! IPv4 specific
self.sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42) self.sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
self.try_send() self.try_send()
@ -131,7 +142,8 @@ class DnsProxy(Handler):
if self.tries >= 3: if self.tries >= 3:
return return
self.tries += 1 self.tries += 1
self.peer = resolvconf_random_nameserver() # FIXME! Support IPv6 nameservers
self.peer = resolvconf_random_nameserver()[1]
self.sock.connect((self.peer, 53)) self.sock.connect((self.peer, 53))
debug2('DNS: sending to %r\n' % self.peer) debug2('DNS: sending to %r\n' % self.peer)
try: try:
@ -167,17 +179,49 @@ class DnsProxy(Handler):
self.ok = False self.ok = False
class UdpProxy(Handler):
def __init__(self, mux, chan, family):
sock = socket.socket(family, socket.SOCK_DGRAM)
Handler.__init__(self, [sock])
self.timeout = time.time() + 30
self.mux = mux
self.chan = chan
self.sock = sock
if family == socket.AF_INET:
self.sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
def send(self, dstip, data):
debug2('UDP: sending to %r port %d\n' % dstip)
try:
self.sock.sendto(data, dstip)
except socket.error, e:
log('UDP send to %r port %d: %s\n' % (dstip[0], dstip[1], e))
return
def callback(self):
try:
data, peer = self.sock.recvfrom(4096)
except socket.error, e:
log('UDP recv from %r port %d: %s\n' % (peer[0], peer[1], e))
return
debug2('UDP response: %d bytes\n' % len(data))
hdr = "%s,%r," % (peer[0], peer[1])
self.mux.send(self.chan, ssnet.CMD_UDP_DATA, hdr + data)
def main(): def main():
if helpers.verbose >= 1: if helpers.verbose >= 1:
helpers.logprefix = ' s: ' helpers.logprefix = ' s: '
else: else:
helpers.logprefix = 'server: ' helpers.logprefix = 'server: '
assert latency_control is not None
debug1('latency control setting = %r\n' % latency_control) debug1('latency control setting = %r\n' % latency_control)
routes = list(list_routes()) routes = list(list_routes())
debug1('available routes:\n') debug1('available routes:\n')
for r in routes: for r in routes:
debug1(' %s/%d\n' % r) debug1(' %d/%s/%d\n' % r)
# synchronization header # synchronization header
sys.stdout.write('\0\0SSHUTTLE0001') sys.stdout.write('\0\0SSHUTTLE0001')
@ -191,7 +235,7 @@ def main():
handlers.append(mux) handlers.append(mux)
routepkt = '' routepkt = ''
for r in routes: for r in routes:
routepkt += '%s,%d\n' % r routepkt += '%d,%s,%d\n' % r
mux.send(0, ssnet.CMD_ROUTES, routepkt) mux.send(0, ssnet.CMD_ROUTES, routepkt)
hw = Hostwatch() hw = Hostwatch()
@ -214,32 +258,63 @@ def main():
def got_host_req(data): def got_host_req(data):
if not hw.pid: if not hw.pid:
(hw.pid,hw.sock) = start_hostwatch(data.strip().split()) (hw.pid, hw.sock) = start_hostwatch(data.strip().split())
handlers.append(Handler(socks = [hw.sock], handlers.append(Handler(socks=[hw.sock],
callback = hostwatch_ready)) callback=hostwatch_ready))
mux.got_host_req = got_host_req mux.got_host_req = got_host_req
def new_channel(channel, data): def new_channel(channel, data):
(dstip,dstport) = data.split(',', 1) (family, dstip, dstport) = data.split(',', 2)
family = int(family)
dstport = int(dstport) dstport = int(dstport)
outwrap = ssnet.connect_dst(dstip,dstport) outwrap = ssnet.connect_dst(family, 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
dnshandlers = {} dnshandlers = {}
def dns_req(channel, data): def dns_req(channel, data):
debug2('Incoming DNS request.\n') debug2('Incoming DNS request channel=%d.\n' % channel)
h = DnsProxy(mux, channel, data) h = DnsProxy(mux, channel, data)
handlers.append(h) handlers.append(h)
dnshandlers[channel] = h dnshandlers[channel] = h
mux.got_dns_req = dns_req mux.got_dns_req = dns_req
udphandlers = {}
def udp_req(channel, cmd, data):
debug2('Incoming UDP request channel=%d, cmd=%d\n' % (channel, cmd))
if cmd == ssnet.CMD_UDP_DATA:
(dstip, dstport, data) = data.split(",", 2)
dstport = int(dstport)
debug2('is incoming UDP data. %r %d.\n' % (dstip, dstport))
h = udphandlers[channel]
h.send((dstip, dstport), data)
elif cmd == ssnet.CMD_UDP_CLOSE:
debug2('is incoming UDP close\n')
h = udphandlers[channel]
h.ok = False
del mux.channels[channel]
def udp_open(channel, data):
debug2('Incoming UDP open.\n')
family = int(data)
mux.channels[channel] = lambda cmd, data: udp_req(channel, cmd, data)
if channel in udphandlers:
raise Fatal('UDP connection channel %d already open' % channel)
else:
h = UdpProxy(mux, channel, family)
handlers.append(h)
udphandlers[channel] = h
mux.got_udp_open = udp_open
while mux.ok: while mux.ok:
if hw.pid: if hw.pid:
assert(hw.pid > 0) assert(hw.pid > 0)
(rpid, rv) = os.waitpid(hw.pid, os.WNOHANG) (rpid, rv) = os.waitpid(hw.pid, os.WNOHANG)
if rpid: if rpid:
raise Fatal('hostwatch exited unexpectedly: code 0x%04x\n' % rv) raise Fatal(
'hostwatch exited unexpectedly: code 0x%04x\n' % rv)
ssnet.runonce(handlers, mux) ssnet.runonce(handlers, mux)
if latency_control: if latency_control:
@ -248,7 +323,13 @@ def main():
if dnshandlers: if dnshandlers:
now = time.time() now = time.time()
for channel,h in dnshandlers.items(): for channel, h in dnshandlers.items():
if h.timeout < now or not h.ok: if h.timeout < now or not h.ok:
debug3('expiring dnsreqs channel=%d\n' % channel)
del dnshandlers[channel] del dnshandlers[channel]
h.ok = False h.ok = False
for channel, h in udphandlers.items():
if not h.ok:
debug3('expiring UDP channel=%d\n' % channel)
del udphandlers[channel]
h.ok = False

View File

@ -1,7 +1,11 @@
import sys, os, re, socket, zlib import sys
import os
import re
import socket
import zlib
import compat.ssubprocess as ssubprocess import compat.ssubprocess as ssubprocess
import helpers import helpers
from helpers import * from helpers import debug2
def readfile(name): def readfile(name):
@ -15,7 +19,7 @@ def readfile(name):
def empackage(z, filename, data=None): def empackage(z, filename, data=None):
(path,basename) = os.path.split(filename) (path, basename) = os.path.split(filename)
if not data: if not data:
data = readfile(filename) data = readfile(filename)
content = z.compress(data) content = z.compress(data)
@ -24,12 +28,9 @@ def empackage(z, filename, data=None):
def connect(ssh_cmd, rhostport, python, stderr, options): def connect(ssh_cmd, rhostport, python, stderr, options):
main_exe = sys.argv[0]
portl = [] portl = []
rhostIsIPv6 = False
if (rhostport or '').count(':') > 1: if (rhostport or '').count(':') > 1:
rhostIsIPv6 = True
if rhostport.count(']') or rhostport.count('['): if rhostport.count(']') or rhostport.count('['):
result = rhostport.split(']') result = rhostport.split(']')
rhost = result[0].strip('[') rhost = result[0].strip('[')
@ -37,7 +38,9 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
result[1] = result[1].strip(':') result[1] = result[1].strip(':')
if result[1] is not '': if result[1] is not '':
portl = ['-p', str(int(result[1]))] portl = ['-p', str(int(result[1]))]
else: # can't disambiguate IPv6 colons and a port number. pass the hostname through. # can't disambiguate IPv6 colons and a port number. pass the hostname
# through.
else:
rhost = rhostport rhost = rhostport
else: # IPv4 else: # IPv4
l = (rhostport or '').split(':', 1) l = (rhostport or '').split(':', 1)
@ -48,13 +51,9 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
if rhost == '-': if rhost == '-':
rhost = None rhost = None
ipv6flag = []
if rhostIsIPv6:
ipv6flag = ['-6']
z = zlib.compressobj(1) z = zlib.compressobj(1)
content = readfile('assembler.py') content = readfile('assembler.py')
optdata = ''.join("%s=%r\n" % (k,v) for (k,v) in options.items()) optdata = ''.join("%s=%r\n" % (k, v) for (k, v) in options.items())
content2 = (empackage(z, 'cmdline_options.py', optdata) + content2 = (empackage(z, 'cmdline_options.py', optdata) +
empackage(z, 'helpers.py') + empackage(z, 'helpers.py') +
empackage(z, 'compat/ssubprocess.py') + empackage(z, 'compat/ssubprocess.py') +
@ -71,7 +70,6 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
""" % (helpers.verbose or 0, len(content)) """ % (helpers.verbose or 0, len(content))
pyscript = re.sub(r'\s+', ' ', pyscript.strip()) pyscript = re.sub(r'\s+', ' ', pyscript.strip())
if not rhost: if not rhost:
# ignore the --python argument when running locally; we already know # ignore the --python argument when running locally; we already know
# which python version works. # which python version works.
@ -88,13 +86,13 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
"exec \"$P\" -c '%s'") % pyscript "exec \"$P\" -c '%s'") % pyscript
argv = (sshl + argv = (sshl +
portl + portl +
ipv6flag +
[rhost, '--', pycmd]) [rhost, '--', pycmd])
(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()
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 = ssubprocess.Popen(argv, stdin=s1a, stdout=s1b, preexec_fn=setup, p = ssubprocess.Popen(argv, stdin=s1a, stdout=s1b, preexec_fn=setup,

View File

@ -1,6 +1,6 @@
% sshuttle(8) Sshuttle %VERSION% % sshuttle(8) Sshuttle 0.46
% Avery Pennarun <apenwarr@gmail.com> % Avery Pennarun <apenwarr@gmail.com>
% %DATE% % 2011-01-25
# NAME # NAME
@ -94,7 +94,7 @@ entire subnet to the VPN.
`0/0 -x 1.2.3.0/24` to forward everything except the `0/0 -x 1.2.3.0/24` to forward everything except the
local subnet over the VPN, for example. local subnet over the VPN, for example.
--exclude-from=*file* -X, --exclude-from=*file*
: exclude the subnets specified in a file, one subnet per : exclude the subnets specified in a file, one subnet per
line. Useful when you have lots of subnets to exclude. line. Useful when you have lots of subnets to exclude.
@ -271,8 +271,14 @@ just switch your wireless off and on. Sshuttle makes the
kernel setting it changes permanent, so this won't happen kernel setting it changes permanent, so this won't happen
again, even after a reboot. again, even after a reboot.
On MacOS, sshuttle will set the kernel boot flag
net.inet.ip.scopedroute to 0, which interferes with OS X
Internet Sharing and some VPN clients. To reset this flag,
you can remove any reference to net.inet.ip.scopedroute from
/Library/Preferences/SystemConfiguration/com.apple.Boot.plist
and reboot.
# SEE ALSO # SEE ALSO
`ssh`(1), `python`(1) `ssh`(1), `python`(1)

View File

@ -1,6 +1,10 @@
import struct, socket, errno, select import struct
import socket
import errno
import select
import os
if not globals().get('skip_imports'): if not globals().get('skip_imports'):
from helpers import * from helpers import log, debug1, debug2, debug3, Fatal
MAX_CHANNEL = 65535 MAX_CHANNEL = 65535
@ -16,29 +20,35 @@ HDR_LEN = 8
CMD_EXIT = 0x4200 CMD_EXIT = 0x4200
CMD_PING = 0x4201 CMD_PING = 0x4201
CMD_PONG = 0x4202 CMD_PONG = 0x4202
CMD_CONNECT = 0x4203 CMD_TCP_CONNECT = 0x4203
CMD_STOP_SENDING = 0x4204 CMD_TCP_STOP_SENDING = 0x4204
CMD_EOF = 0x4205 CMD_TCP_EOF = 0x4205
CMD_DATA = 0x4206 CMD_TCP_DATA = 0x4206
CMD_ROUTES = 0x4207 CMD_ROUTES = 0x4207
CMD_HOST_REQ = 0x4208 CMD_HOST_REQ = 0x4208
CMD_HOST_LIST = 0x4209 CMD_HOST_LIST = 0x4209
CMD_DNS_REQ = 0x420a CMD_DNS_REQ = 0x420a
CMD_DNS_RESPONSE = 0x420b CMD_DNS_RESPONSE = 0x420b
CMD_UDP_OPEN = 0x420c
CMD_UDP_DATA = 0x420d
CMD_UDP_CLOSE = 0x420e
cmd_to_name = { cmd_to_name = {
CMD_EXIT: 'EXIT', CMD_EXIT: 'EXIT',
CMD_PING: 'PING', CMD_PING: 'PING',
CMD_PONG: 'PONG', CMD_PONG: 'PONG',
CMD_CONNECT: 'CONNECT', CMD_TCP_CONNECT: 'TCP_CONNECT',
CMD_STOP_SENDING: 'STOP_SENDING', CMD_TCP_STOP_SENDING: 'TCP_STOP_SENDING',
CMD_EOF: 'EOF', CMD_TCP_EOF: 'TCP_EOF',
CMD_DATA: 'DATA', CMD_TCP_DATA: 'TCP_DATA',
CMD_ROUTES: 'ROUTES', CMD_ROUTES: 'ROUTES',
CMD_HOST_REQ: 'HOST_REQ', CMD_HOST_REQ: 'HOST_REQ',
CMD_HOST_LIST: 'HOST_LIST', CMD_HOST_LIST: 'HOST_LIST',
CMD_DNS_REQ: 'DNS_REQ', CMD_DNS_REQ: 'DNS_REQ',
CMD_DNS_RESPONSE: 'DNS_RESPONSE', CMD_DNS_RESPONSE: 'DNS_RESPONSE',
CMD_UDP_OPEN: 'UDP_OPEN',
CMD_UDP_DATA: 'UDP_DATA',
CMD_UDP_CLOSE: 'UDP_CLOSE',
} }
@ -86,7 +96,10 @@ def _try_peername(sock):
_swcount = 0 _swcount = 0
class SockWrapper: class SockWrapper:
def __init__(self, rsock, wsock, connect_to=None, peername=None): def __init__(self, rsock, wsock, connect_to=None, peername=None):
global _swcount global _swcount
_swcount += 1 _swcount += 1
@ -128,12 +141,6 @@ class SockWrapper:
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)) debug3('%r: trying connect to %r\n' % (self, self.connect_to))
if socket.inet_aton(self.connect_to[0])[0] == '\0':
self.seterr(Exception("Can't connect to %r: "
"IP address starts with zero\n"
% (self.connect_to,)))
self.connect_to = None
return
try: try:
self.rsock.connect(self.connect_to) self.rsock.connect(self.connect_to)
# connected successfully (Linux) # connected successfully (Linux)
@ -177,7 +184,7 @@ 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(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:
@ -243,7 +250,8 @@ 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 = socks or [] self.socks = socks or []
if callback: if callback:
@ -255,7 +263,7 @@ class Handler:
def callback(self): def callback(self):
log('--no callback defined-- %r\n' % self) log('--no callback defined-- %r\n' % self)
(r,w,x) = select.select(self.socks, [], [], 0) (r, w, x) = select.select(self.socks, [], [], 0)
for s in r: for s in r:
v = s.recv(4096) v = s.recv(4096)
if not v: if not v:
@ -265,6 +273,7 @@ class Handler:
class Proxy(Handler): class Proxy(Handler):
def __init__(self, wrap1, wrap2): def __init__(self, wrap1, wrap2):
Handler.__init__(self, [wrap1.rsock, wrap1.wsock, Handler.__init__(self, [wrap1.rsock, wrap1.wsock,
wrap2.rsock, wrap2.wsock]) wrap2.rsock, wrap2.wsock])
@ -272,8 +281,10 @@ class Proxy(Handler):
self.wrap2 = wrap2 self.wrap2 = wrap2
def pre_select(self, r, w, x): def pre_select(self, r, w, x):
if self.wrap1.shut_write: self.wrap2.noread() if self.wrap1.shut_write:
if self.wrap2.shut_write: self.wrap1.noread() self.wrap2.noread()
if self.wrap2.shut_write:
self.wrap1.noread()
if self.wrap1.connect_to: if self.wrap1.connect_to:
_add(w, self.wrap1.rsock) _add(w, self.wrap1.rsock)
@ -312,11 +323,13 @@ class Proxy(Handler):
class Mux(Handler): class Mux(Handler):
def __init__(self, rsock, wsock): def __init__(self, rsock, wsock):
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 = self.got_dns_req = self.got_routes = None self.new_channel = self.got_dns_req = self.got_routes = None
self.got_udp_open = self.got_udp_data = self.got_udp_close = None
self.got_host_req = self.got_host_list = None self.got_host_req = self.got_host_list = None
self.channels = {} self.channels = {}
self.chani = 0 self.chani = 0
@ -348,7 +361,7 @@ class Mux(Handler):
self.send(0, CMD_PING, 'rttest') self.send(0, CMD_PING, 'rttest')
self.too_full = True self.too_full = True
#ob = [] #ob = []
#for b in self.outbuf: # for b in self.outbuf:
# (s1,s2,c) = struct.unpack('!ccH', b[:4]) # (s1,s2,c) = struct.unpack('!ccH', b[:4])
# ob.append(c) # ob.append(c)
#log('outbuf: %d %r\n' % (self.amount_queued(), ob)) #log('outbuf: %d %r\n' % (self.amount_queued(), ob))
@ -359,13 +372,13 @@ class Mux(Handler):
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 (fullness=%d)\n' debug2(' > channel=%d cmd=%s len=%d (fullness=%d)\n'
% (channel, cmd_to_name.get(cmd,hex(cmd)), % (channel, cmd_to_name.get(cmd, hex(cmd)),
len(data), self.fullness)) len(data), self.fullness))
self.fullness += len(data) 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.get(cmd,hex(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:
@ -374,7 +387,7 @@ class Mux(Handler):
self.fullness = 0 self.fullness = 0
elif cmd == CMD_EXIT: elif cmd == CMD_EXIT:
self.ok = False self.ok = False
elif cmd == CMD_CONNECT: elif cmd == CMD_TCP_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)
@ -382,6 +395,10 @@ class Mux(Handler):
assert(not self.channels.get(channel)) assert(not self.channels.get(channel))
if self.got_dns_req: if self.got_dns_req:
self.got_dns_req(channel, data) self.got_dns_req(channel, data)
elif cmd == CMD_UDP_OPEN:
assert(not self.channels.get(channel))
if self.got_udp_open:
self.got_udp_open(channel, data)
elif cmd == CMD_ROUTES: elif cmd == CMD_ROUTES:
if self.got_routes: if self.got_routes:
self.got_routes(data) self.got_routes(data)
@ -401,7 +418,7 @@ class Mux(Handler):
callback = self.channels.get(channel) callback = self.channels.get(channel)
if not callback: if not callback:
log('warning: closed channel %d got cmd=%s len=%d\n' log('warning: closed channel %d got cmd=%s len=%d\n'
% (channel, cmd_to_name.get(cmd,hex(cmd)), len(data))) % (channel, cmd_to_name.get(cmd, hex(cmd)), len(data)))
else: else:
callback(cmd, data) callback(cmd, data)
@ -429,11 +446,11 @@ class Mux(Handler):
def handle(self): def handle(self):
self.fill() self.fill()
#log('inbuf is: (%d,%d) %r\n' # log('inbuf is: (%d,%d) %r\n'
# % (self.want, len(self.inbuf), self.inbuf)) # % (self.want, len(self.inbuf), self.inbuf))
while 1: while 1:
if len(self.inbuf) >= (self.want or HDR_LEN): if len(self.inbuf) >= (self.want or HDR_LEN):
(s1,s2,channel,cmd,datalen) = \ (s1, s2, channel, cmd, datalen) = \
struct.unpack('!ccHHH', self.inbuf[:HDR_LEN]) struct.unpack('!ccHHH', self.inbuf[:HDR_LEN])
assert(s1 == 'S') assert(s1 == 'S')
assert(s2 == 'S') assert(s2 == 'S')
@ -452,7 +469,7 @@ class Mux(Handler):
_add(w, 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)
if self.rsock in r: if self.rsock in r:
self.handle() self.handle()
if self.outbuf and self.wsock in w: if self.outbuf and self.wsock in w:
@ -460,6 +477,7 @@ class Mux(Handler):
class MuxWrapper(SockWrapper): class MuxWrapper(SockWrapper):
def __init__(self, mux, channel): def __init__(self, mux, channel):
SockWrapper.__init__(self, mux.rsock, mux.wsock) SockWrapper.__init__(self, mux.rsock, mux.wsock)
self.mux = mux self.mux = mux
@ -473,18 +491,18 @@ class MuxWrapper(SockWrapper):
SockWrapper.__del__(self) SockWrapper.__del__(self)
def __repr__(self): def __repr__(self):
return 'SW%r:Mux#%d' % (self.peername,self.channel) return 'SW%r:Mux#%d' % (self.peername, self.channel)
def noread(self): def noread(self):
if not self.shut_read: if not self.shut_read:
self.shut_read = True self.shut_read = True
self.mux.send(self.channel, CMD_STOP_SENDING, '') self.mux.send(self.channel, CMD_TCP_STOP_SENDING, '')
self.maybe_close() self.maybe_close()
def nowrite(self): def nowrite(self):
if not self.shut_write: if not self.shut_write:
self.shut_write = True self.shut_write = True
self.mux.send(self.channel, CMD_EOF, '') self.mux.send(self.channel, CMD_TCP_EOF, '')
self.maybe_close() self.maybe_close()
def maybe_close(self): def maybe_close(self):
@ -501,7 +519,7 @@ class MuxWrapper(SockWrapper):
return 0 # too much already enqueued return 0 # too much already enqueued
if len(buf) > 2048: if len(buf) > 2048:
buf = buf[:2048] buf = buf[:2048]
self.mux.send(self.channel, CMD_DATA, buf) self.mux.send(self.channel, CMD_TCP_DATA, buf)
return len(buf) return len(buf)
def uread(self): def uread(self):
@ -511,24 +529,24 @@ class MuxWrapper(SockWrapper):
return None # no data available right now return None # no data available right now
def got_packet(self, cmd, data): def got_packet(self, cmd, data):
if cmd == CMD_EOF: if cmd == CMD_TCP_EOF:
self.noread() self.noread()
elif cmd == CMD_STOP_SENDING: elif cmd == CMD_TCP_STOP_SENDING:
self.nowrite() self.nowrite()
elif cmd == CMD_DATA: elif cmd == CMD_TCP_DATA:
self.buf.append(data) self.buf.append(data)
else: else:
raise Exception('unknown command %d (%d bytes)' raise Exception('unknown command %d (%d bytes)'
% (cmd, len(data))) % (cmd, len(data)))
def connect_dst(ip, port): def connect_dst(family, ip, port):
debug2('Connecting to %s:%d\n' % (ip, port)) debug2('Connecting to %s:%d\n' % (ip, port))
outsock = socket.socket() outsock = socket.socket(family)
outsock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42) 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))
def runonce(handlers, mux): def runonce(handlers, mux):
@ -540,14 +558,14 @@ def runonce(handlers, mux):
handlers.remove(h) handlers.remove(h)
for s in handlers: for s in handlers:
s.pre_select(r,w,x) s.pre_select(r, w, x)
debug2('Waiting: %d r=%r w=%r x=%r (fullness=%d/%d)\n' debug2('Waiting: %d r=%r w=%r x=%r (fullness=%d/%d)\n'
% (len(handlers), _fds(r), _fds(w), _fds(x), % (len(handlers), _fds(r), _fds(w), _fds(x),
mux.fullness, mux.too_full)) mux.fullness, mux.too_full))
(r,w,x) = select.select(r,w,x) (r, w, x) = select.select(r, w, x)
debug2(' Ready: %d r=%r w=%r x=%r\n' debug2(' Ready: %d r=%r w=%r x=%r\n'
% (len(handlers), _fds(r), _fds(w), _fds(x))) % (len(handlers), _fds(r), _fds(w), _fds(x)))
ready = r+w+x ready = r + w + x
did = {} did = {}
for h in handlers: for h in handlers:
for s in h.socks: for s in h.socks:

View File

@ -1,8 +1,11 @@
import sys, os import sys
import os
from compat import ssubprocess from compat import ssubprocess
_p = None _p = None
def start_syslog(): def start_syslog():
global _p global _p
_p = ssubprocess.Popen(['logger', _p = ssubprocess.Popen(['logger',

View File

@ -1,5 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
import sys, os, socket, select, struct, time import socket
import select
import struct
import time
listener = socket.socket() listener = socket.socket()
listener.bind(('127.0.0.1', 0)) listener.bind(('127.0.0.1', 0))
@ -23,7 +26,7 @@ while 1:
if count >= 16384: if count >= 16384:
count = 1 count = 1
print 'cli CREATING %d' % count print 'cli CREATING %d' % count
b = struct.pack('I', count) + 'x'*count b = struct.pack('I', count) + 'x' * count
remain[c] = count remain[c] = count
print 'cli >> %r' % len(b) print 'cli >> %r' % len(b)
c.send(b) c.send(b)
@ -32,13 +35,13 @@ while 1:
r = [listener] r = [listener]
time.sleep(0.1) time.sleep(0.1)
else: else:
r = [listener]+servers+clients r = [listener] + servers + clients
print 'select(%d)' % len(r) print 'select(%d)' % len(r)
r,w,x = select.select(r, [], [], 5) r, w, x = select.select(r, [], [], 5)
assert(r) assert(r)
for i in r: for i in r:
if i == listener: if i == listener:
s,addr = listener.accept() s, addr = listener.accept()
servers.append(s) servers.append(s)
elif i in servers: elif i in servers:
b = i.recv(4096) b = i.recv(4096)
@ -47,7 +50,7 @@ while 1:
assert(len(b) >= 4) assert(len(b) >= 4)
want = struct.unpack('I', b[:4])[0] want = struct.unpack('I', b[:4])[0]
b = b[4:] b = b[4:]
#i.send('y'*want) # i.send('y'*want)
else: else:
want = remain[i] want = remain[i]
if want < len(b): if want < len(b):
@ -64,7 +67,7 @@ while 1:
del remain[i] del remain[i]
else: else:
print 'srv >> %r' % len(b) print 'srv >> %r' % len(b)
i.send('y'*len(b)) i.send('y' * len(b))
if not want: if not want:
i.shutdown(socket.SHUT_WR) i.shutdown(socket.SHUT_WR)
elif i in clients: elif i in clients:

View File

@ -1,4 +1,6 @@
import sys, os, re, subprocess import re
import subprocess
def askpass(prompt): def askpass(prompt):
prompt = prompt.replace('"', "'") prompt = prompt.replace('"', "'")
@ -6,7 +8,7 @@ def askpass(prompt):
if 'yes/no' in prompt: if 'yes/no' in prompt:
return "yes" return "yes"
script=""" script = """
tell application "Finder" tell application "Finder"
activate activate
display dialog "%s" \ display dialog "%s" \

View File

@ -6,7 +6,7 @@
* NSApplicationMain() looks for Info.plist using the path in argv[0], which * NSApplicationMain() looks for Info.plist using the path in argv[0], which
* goes wrong if your interpreter is /usr/bin/python. * goes wrong if your interpreter is /usr/bin/python.
*/ */
#include <Python.h> #include <Python/Python.h>
#include <string.h> #include <string.h>
#include <unistd.h> #include <unistd.h>

View File

@ -11,7 +11,10 @@ if [ -d /usr/libexec/gcc/darwin ]; then
done done
fi fi
printf "\n" printf "\n"
PYTHON_LDFLAGS=$(python-config --ldflags)
PYTHON_INCLUDES=$(python-config --includes)
gcc $ARCHES \ gcc $ARCHES \
-Wall -o $3 runpython.c \ -Wall -o $3 runpython.c \
-I/usr/include/python2.5 \ $PYTHON_INCLUDES \
-lpython2.5 $PYTHON_LDFLAGS \
-framework Python

View File

Before

Width:  |  Height:  |  Size: 821 B

After

Width:  |  Height:  |  Size: 821 B

View File

Before

Width:  |  Height:  |  Size: 789 B

After

Width:  |  Height:  |  Size: 789 B

View File

Before

Width:  |  Height:  |  Size: 810 B

After

Width:  |  Height:  |  Size: 810 B

View File

@ -1,6 +1,11 @@
import sys, os, pty import sys
import os
import pty
from AppKit import * from AppKit import *
import my, models, askpass import my
import models
import askpass
def sshuttle_args(host, auto_nets, auto_hosts, dns, nets, debug, def sshuttle_args(host, auto_nets, auto_hosts, dns, nets, debug,
no_latency_control): no_latency_control):
@ -21,21 +26,25 @@ def sshuttle_args(host, auto_nets, auto_hosts, dns, nets, debug,
class _Callback(NSObject): class _Callback(NSObject):
def initWithFunc_(self, func): def initWithFunc_(self, func):
self = super(_Callback, self).init() self = super(_Callback, self).init()
self.func = func self.func = func
return self return self
def func_(self, obj): def func_(self, obj):
return self.func(obj) return self.func(obj)
class Callback: class Callback:
def __init__(self, func): def __init__(self, func):
self.obj = _Callback.alloc().initWithFunc_(func) self.obj = _Callback.alloc().initWithFunc_(func)
self.sel = self.obj.func_ self.sel = self.obj.func_
class Runner: class Runner:
def __init__(self, argv, logfunc, promptfunc, serverobj): def __init__(self, argv, logfunc, promptfunc, serverobj):
print 'in __init__' print 'in __init__'
self.id = argv self.id = argv
@ -49,7 +58,7 @@ class Runner:
self.logfunc('\nConnecting to %s.\n' % self.serverobj.host()) self.logfunc('\nConnecting to %s.\n' % self.serverobj.host())
print 'will run: %r' % argv print 'will run: %r' % argv
self.serverobj.setConnected_(False) self.serverobj.setConnected_(False)
pid,fd = pty.fork() pid, fd = pty.fork()
if pid == 0: if pid == 0:
# child # child
try: try:
@ -65,7 +74,8 @@ class Runner:
.initWithFileDescriptor_closeOnDealloc_(fd, True) .initWithFileDescriptor_closeOnDealloc_(fd, True)
self.cb = Callback(self.gotdata) self.cb = Callback(self.gotdata)
NSNotificationCenter.defaultCenter()\ NSNotificationCenter.defaultCenter()\
.addObserver_selector_name_object_(self.cb.obj, self.cb.sel, .addObserver_selector_name_object_(
self.cb.obj, self.cb.sel,
NSFileHandleDataAvailableNotification, self.file) NSFileHandleDataAvailableNotification, self.file)
self.file.waitForDataInBackgroundAndNotify() self.file.waitForDataInBackgroundAndNotify()
@ -73,16 +83,11 @@ class Runner:
self.wait() self.wait()
def _try_wait(self, options): def _try_wait(self, options):
if self.rv == None and self.pid > 0: if self.rv is None and self.pid > 0:
pid,code = os.waitpid(self.pid, options) pid, code = os.waitpid(self.pid, options)
if pid == self.pid: if pid == self.pid:
if os.WIFEXITED(code): if os.WIFEXITED(code):
self.rv = os.WEXITSTATUS(code) self.rv = os.WEXITSTATUS(code)
if self.rv == 111:
NSRunAlertPanel('Sshuttle',
'Please restart your computer to finish '
'installing Sshuttle.',
'Restart Later', None, None)
else: else:
self.rv = -os.WSTOPSIG(code) self.rv = -os.WSTOPSIG(code)
self.serverobj.setConnected_(False) self.serverobj.setConnected_(False)
@ -103,7 +108,7 @@ class Runner:
def kill(self): def kill(self):
assert(self.pid > 0) assert(self.pid > 0)
print 'killing: pid=%r rv=%r' % (self.pid, self.rv) print 'killing: pid=%r rv=%r' % (self.pid, self.rv)
if self.rv == None: if self.rv is None:
self.logfunc('Disconnecting from %s.\n' % self.serverobj.host()) self.logfunc('Disconnecting from %s.\n' % self.serverobj.host())
os.kill(self.pid, 15) os.kill(self.pid, 15)
self.wait() self.wait()
@ -126,10 +131,11 @@ class Runner:
self.file.writeData_(my.Data(resp + '\n')) self.file.writeData_(my.Data(resp + '\n'))
self.file.waitForDataInBackgroundAndNotify() self.file.waitForDataInBackgroundAndNotify()
self.poll() self.poll()
#print 'gotdata done!' # print 'gotdata done!'
class SshuttleApp(NSObject): class SshuttleApp(NSObject):
def initialize(self): def initialize(self):
d = my.PList('UserDefaults') d = my.PList('UserDefaults')
my.Defaults().registerDefaults_(d) my.Defaults().registerDefaults_(d)
@ -153,12 +159,14 @@ class SshuttleController(NSObject):
host = server.host() host = server.host()
print 'connecting %r' % host print 'connecting %r' % host
self.fill_menu() self.fill_menu()
def logfunc(msg): def logfunc(msg):
print 'log! (%d bytes)' % len(msg) print 'log! (%d bytes)' % len(msg)
self.logField.textStorage()\ self.logField.textStorage()\
.appendAttributedString_(NSAttributedString.alloc()\ .appendAttributedString_(NSAttributedString.alloc()
.initWithString_(msg)) .initWithString_(msg))
self.logField.didChangeText() self.logField.didChangeText()
def promptfunc(prompt): def promptfunc(prompt):
print 'prompt! %r' % prompt print 'prompt! %r' % prompt
return askpass.askpass(prompt) return askpass.askpass(prompt)
@ -172,12 +180,12 @@ class SshuttleController(NSObject):
manual_nets = [] manual_nets = []
noLatencyControl = (server.latencyControl() != models.LAT_INTERACTIVE) noLatencyControl = (server.latencyControl() != models.LAT_INTERACTIVE)
conn = Runner(sshuttle_args(host, conn = Runner(sshuttle_args(host,
auto_nets = nets_mode == models.NET_AUTO, auto_nets=nets_mode == models.NET_AUTO,
auto_hosts = server.autoHosts(), auto_hosts=server.autoHosts(),
dns = server.useDns(), dns=server.useDns(),
nets = manual_nets, nets=manual_nets,
debug = self.debugField.state(), debug=self.debugField.state(),
no_latency_control = noLatencyControl), no_latency_control=noLatencyControl),
logfunc=logfunc, promptfunc=promptfunc, logfunc=logfunc, promptfunc=promptfunc,
serverobj=server) serverobj=server)
self.conns[host] = conn self.conns[host] = conn
@ -221,6 +229,7 @@ class SshuttleController(NSObject):
it.setRepresentedObject_(obj) it.setRepresentedObject_(obj)
it.setTarget_(self) it.setTarget_(self)
it.setAction_(func) it.setAction_(func)
def addnote(name): def addnote(name):
additem(name, None, None) additem(name, None, None)
@ -279,7 +288,8 @@ class SshuttleController(NSObject):
sl = [] sl = []
for s in l: for s in l:
host = s.get('host', None) host = s.get('host', None)
if not host: continue if not host:
continue
nets = s.get('nets', []) nets = s.get('nets', [])
nl = [] nl = []
@ -310,11 +320,13 @@ class SshuttleController(NSObject):
l = [] l = []
for s in self.servers: for s in self.servers:
host = s.host() host = s.host()
if not host: continue if not host:
continue
nets = [] nets = []
for n in s.nets(): for n in s.nets():
subnet = n.subnet() subnet = n.subnet()
if not subnet: continue if not subnet:
continue
nets.append((subnet, n.width())) nets.append((subnet, n.width()))
d = dict(host=s.host(), d = dict(host=s.host(),
nets=nets, nets=nets,

View File

@ -35,24 +35,29 @@ def _validate_width(v):
class SshuttleNet(NSObject): class SshuttleNet(NSObject):
def subnet(self): def subnet(self):
return getattr(self, '_k_subnet', None) return getattr(self, '_k_subnet', None)
def setSubnet_(self, v): def setSubnet_(self, v):
self._k_subnet = v self._k_subnet = v
config_changed() config_changed()
@objc_validator @objc_validator
def validateSubnet_error_(self, value, error): def validateSubnet_error_(self, value, error):
#print 'validateSubnet!' # print 'validateSubnet!'
return True, _validate_ip(value), error return True, _validate_ip(value), error
def width(self): def width(self):
return getattr(self, '_k_width', 24) return getattr(self, '_k_width', 24)
def setWidth_(self, v): def setWidth_(self, v):
self._k_width = v self._k_width = v
config_changed() config_changed()
@objc_validator @objc_validator
def validateWidth_error_(self, value, error): def validateWidth_error_(self, value, error):
#print 'validateWidth!' # print 'validateWidth!'
return True, _validate_width(value), error return True, _validate_width(value), error
NET_ALL = 0 NET_ALL = 0
@ -62,7 +67,9 @@ NET_MANUAL = 2
LAT_BANDWIDTH = 0 LAT_BANDWIDTH = 0
LAT_INTERACTIVE = 1 LAT_INTERACTIVE = 1
class SshuttleServer(NSObject): class SshuttleServer(NSObject):
def init(self): def init(self):
self = super(SshuttleServer, self).init() self = super(SshuttleServer, self).init()
config_changed() config_changed()
@ -70,22 +77,27 @@ class SshuttleServer(NSObject):
def wantConnect(self): def wantConnect(self):
return getattr(self, '_k_wantconnect', False) return getattr(self, '_k_wantconnect', False)
def setWantConnect_(self, v): def setWantConnect_(self, v):
self._k_wantconnect = v self._k_wantconnect = v
self.setError_(None) self.setError_(None)
config_changed() config_changed()
if setconnect_callback: setconnect_callback(self) if setconnect_callback:
setconnect_callback(self)
def connected(self): def connected(self):
return getattr(self, '_k_connected', False) return getattr(self, '_k_connected', False)
def setConnected_(self, v): def setConnected_(self, v):
print 'setConnected of %r to %r' % (self, v) print 'setConnected of %r to %r' % (self, v)
self._k_connected = v self._k_connected = v
if v: self.setError_(None) # connected ok, so no error if v:
self.setError_(None) # connected ok, so no error
config_changed() config_changed()
def error(self): def error(self):
return getattr(self, '_k_error', None) return getattr(self, '_k_error', None)
def setError_(self, v): def setError_(self, v):
self._k_error = v self._k_error = v
config_changed() config_changed()
@ -107,40 +119,47 @@ class SshuttleServer(NSObject):
suffix = " (all traffic)" suffix = " (all traffic)"
elif an == NET_MANUAL: elif an == NET_MANUAL:
n = self.nets() n = self.nets()
suffix = ' (%d subnet%s)' % (len(n), len(n)!=1 and 's' or '') suffix = ' (%d subnet%s)' % (len(n), len(n) != 1 and 's' or '')
return self.host() + suffix return self.host() + suffix
def setTitle_(self, v): def setTitle_(self, v):
# title is always auto-generated # title is always auto-generated
config_changed() config_changed()
def host(self): def host(self):
return getattr(self, '_k_host', None) return getattr(self, '_k_host', None)
def setHost_(self, v): def setHost_(self, v):
self._k_host = v self._k_host = v
self.setTitle_(None) self.setTitle_(None)
config_changed() config_changed()
@objc_validator @objc_validator
def validateHost_error_(self, value, error): def validateHost_error_(self, value, error):
#print 'validatehost! %r %r %r' % (self, value, error) # print 'validatehost! %r %r %r' % (self, value, error)
while value.startswith('-'): while value.startswith('-'):
value = value[1:] value = value[1:]
return True, value, error return True, value, error
def nets(self): def nets(self):
return getattr(self, '_k_nets', []) return getattr(self, '_k_nets', [])
def setNets_(self, v): def setNets_(self, v):
self._k_nets = v self._k_nets = v
self.setTitle_(None) self.setTitle_(None)
config_changed() config_changed()
def netsHidden(self): def netsHidden(self):
#print 'checking netsHidden' # print 'checking netsHidden'
return self.autoNets() != NET_MANUAL return self.autoNets() != NET_MANUAL
def setNetsHidden_(self, v): def setNetsHidden_(self, v):
config_changed() config_changed()
#print 'setting netsHidden to %r' % v # print 'setting netsHidden to %r' % v
def autoNets(self): def autoNets(self):
return getattr(self, '_k_autoNets', NET_AUTO) return getattr(self, '_k_autoNets', NET_AUTO)
def setAutoNets_(self, v): def setAutoNets_(self, v):
self._k_autoNets = v self._k_autoNets = v
self.setNetsHidden_(-1) self.setNetsHidden_(-1)
@ -150,18 +169,21 @@ class SshuttleServer(NSObject):
def autoHosts(self): def autoHosts(self):
return getattr(self, '_k_autoHosts', True) return getattr(self, '_k_autoHosts', True)
def setAutoHosts_(self, v): def setAutoHosts_(self, v):
self._k_autoHosts = v self._k_autoHosts = v
config_changed() config_changed()
def useDns(self): def useDns(self):
return getattr(self, '_k_useDns', False) return getattr(self, '_k_useDns', False)
def setUseDns_(self, v): def setUseDns_(self, v):
self._k_useDns = v self._k_useDns = v
config_changed() config_changed()
def latencyControl(self): def latencyControl(self):
return getattr(self, '_k_latencyControl', LAT_INTERACTIVE) return getattr(self, '_k_latencyControl', LAT_INTERACTIVE)
def setLatencyControl_(self, v): def setLatencyControl_(self, v):
self._k_latencyControl = v self._k_latencyControl = v
config_changed() config_changed()

View File

@ -1,4 +1,4 @@
import sys, os import os
from AppKit import * from AppKit import *
import PyObjCTools.AppHelper import PyObjCTools.AppHelper
@ -44,11 +44,13 @@ def Defaults():
# #
def DelayedCallback(func, *args, **kwargs): def DelayedCallback(func, *args, **kwargs):
flag = [0] flag = [0]
def _go(): def _go():
if flag[0]: if flag[0]:
print 'running %r (flag=%r)' % (func, flag) print 'running %r (flag=%r)' % (func, flag)
flag[0] = 0 flag[0] = 0
func(*args, **kwargs) func(*args, **kwargs)
def call(): def call():
flag[0] += 1 flag[0] += 1
PyObjCTools.AppHelper.callAfter(_go) PyObjCTools.AppHelper.callAfter(_go)

View File

@ -1 +0,0 @@
gitvars.pre export-subst

3
version/.gitignore vendored
View File

@ -1,3 +0,0 @@
/vars
/gitvars
/_version.py

View File

@ -1 +0,0 @@
from _version import COMMIT, TAG, DATE

View File

@ -1,3 +0,0 @@
redo-ifchange vars
cat vars

View File

@ -1,2 +0,0 @@
redo-ifchange vars _version.py

View File

@ -1,3 +0,0 @@
rm -f *~ .*~ *.pyc _version.py vars gitvars

View File

@ -1,28 +0,0 @@
redo-ifchange gitvars.pre prodname
read PROD <prodname
exec >$3
# Fix each line from gitvars.pre where git may or may not have already
# substituted the variables. If someone generated a tarball with 'git archive',
# then the data will have been substituted already. If we're in a checkout of
# the git repo, then it won't, but we can just ask git to do the substitutions
# right now.
while read line; do
# Lines *may* be of the form: $Format: ... $
x=${line#\$Format:} # remove prefix
if [ "$x" != "$line" ]; then
# git didn't substitute it
redo-always # git this from the git repo
x=${x%\$} # remove trailing $
if [ "$x" = "%d" ]; then
tag=$(git describe --match="$PROD-*")
x="(tag: $tag)"
else
x=$(git log -1 --pretty=format:"$x")
fi
fi
echo "$x"
done <gitvars.pre
redo-stamp <$3

View File

@ -1,3 +0,0 @@
$Format:%H$
$Format:%d$
$Format:%ci$

View File

@ -1 +0,0 @@
sshuttle

View File

@ -1,40 +0,0 @@
redo-ifchange gitvars prodname
read PROD <prodname
exec <gitvars
read COMMIT
read NAMES
read DATE
# the list of names is of the form:
# (x,y,tag: $PROD-####,tag: $PROD-####,a,b)
# The entries we want are the ones starting with "tag: $PROD-" since those
# refer to the right actual git tags.
names_to_tag()
{
x=${1#\(}
x=${x%\)}
cur=
while [ "$cur" != "$x" ]; do
x=${x# }
x=${x#tag: }
cur=${x%%,*}
tagpost=${cur#$PROD-}
if [ "$cur" != "$tagpost" ]; then
echo "$tagpost"
return 0
fi
x=${x#*,}
done
commitpost=${COMMIT#???????}
commitpre=${COMMIT%$commitpost}
echo "unknown-$commitpre"
}
sTAG=$(names_to_tag "$NAMES")
echo "COMMIT='$COMMIT'"
echo "TAG='$sTAG'"
echo "DATE='${DATE%% *}'"