diff --git a/Documentation/.gitignore b/Documentation/.gitignore
new file mode 100644
index 0000000..3001d69
--- /dev/null
+++ b/Documentation/.gitignore
@@ -0,0 +1,3 @@
+*.8
+/md-to-man
+/*.md.tmp
diff --git a/Documentation/all.do b/Documentation/all.do
new file mode 100644
index 0000000..1eb177e
--- /dev/null
+++ b/Documentation/all.do
@@ -0,0 +1,5 @@
+/bin/ls *.md |
+sed 's/\.md/.8/' |
+xargs redo-ifchange
+
+redo-always
diff --git a/Documentation/clean.do b/Documentation/clean.do
new file mode 100644
index 0000000..2dc34b4
--- /dev/null
+++ b/Documentation/clean.do
@@ -0,0 +1 @@
+rm -f *~ .*~ *.8 t/*.8 md-to-man *.tmp t/*.tmp
diff --git a/Documentation/default.8.do b/Documentation/default.8.do
new file mode 100644
index 0000000..b2c9875
--- /dev/null
+++ b/Documentation/default.8.do
@@ -0,0 +1,2 @@
+redo-ifchange md-to-man $2.md.tmp
+. ./md-to-man $1 $2 $3
diff --git a/Documentation/default.md.tmp.do b/Documentation/default.md.tmp.do
new file mode 100644
index 0000000..4c4bbca
--- /dev/null
+++ b/Documentation/default.md.tmp.do
@@ -0,0 +1,3 @@
+redo-ifchange ../version/vars $2.md
+. ../version/vars
+sed -e "s/%VERSION%/$TAG/" -e "s/%DATE%/$DATE/" $2.md
diff --git a/Documentation/md-to-man.do b/Documentation/md-to-man.do
new file mode 100644
index 0000000..2b08acf
--- /dev/null
+++ b/Documentation/md-to-man.do
@@ -0,0 +1,8 @@
+redo-ifchange md2man.py
+if ./md2man.py /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
diff --git a/Documentation/md2man.py b/Documentation/md2man.py
new file mode 100755
index 0000000..54c1918
--- /dev/null
+++ b/Documentation/md2man.py
@@ -0,0 +1,278 @@
+#!/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 foochickenwickendicken 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
is allowed inside : 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)
diff --git a/sshuttle.md b/Documentation/sshuttle.md
similarity index 97%
rename from sshuttle.md
rename to Documentation/sshuttle.md
index 16bedda..4caf6cc 100644
--- a/sshuttle.md
+++ b/Documentation/sshuttle.md
@@ -1,6 +1,6 @@
-% sshuttle(8) Sshuttle 0.46
+% sshuttle(8) Sshuttle %VERSION%
% Avery Pennarun
-% 2011-01-25
+% %DATE%
# NAME
@@ -71,6 +71,10 @@ entire subnet to the VPN.
are taken automatically from the server's routing
table.
+--dns
+: capture local DNS requests and forward to the remote DNS
+ server.
+
--python
: specify the name/path of the remote python interpreter.
The default is just `python`, which means to use the
@@ -90,6 +94,10 @@ entire subnet to the VPN.
`0/0 -x 1.2.3.0/24` to forward everything except the
local subnet over the VPN, for example.
+--exclude-from=*file*
+: exclude the subnets specified in a file, one subnet per
+ line. Useful when you have lots of subnets to exclude.
+
-v, --verbose
: print more information about the session. This option
can be used more than once for increased verbosity. By
diff --git a/README.md b/README.md
index 7b1eece..2db6db6 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,3 @@
-
WARNING:
On MacOS 10.6 (at least up to 10.6.6), your network will
stop responding about 10 minutes after the first time you
@@ -59,7 +58,7 @@ Prerequisites
This is how you use it:
-----------------------
- - git clone git://github.com/apenwarr/sshuttle
+ - git clone --depth=1 http://github.com/apenwarr/sshuttle
on your client machine. You'll need root or sudo
access, and python needs to be installed.
@@ -77,6 +76,10 @@ This is how you use it:
The above is probably what you want to use to prevent
local network attacks such as Firesheep and friends.
+ - OR if you have MacOS and want to try the GUI version:
+ make
+ open ui-macos/Sshuttle*.app
+
(You may be prompted for one or more passwords; first, the
local password to become root using either sudo or su, and
then the remote ssh password. Or you might have sudo and ssh set
diff --git a/all.do b/all.do
index 7ee8426..7282cb5 100644
--- a/all.do
+++ b/all.do
@@ -1,11 +1,11 @@
exec >&2
UI=
[ "$(uname)" = "Darwin" ] && UI=ui-macos/all
-redo-ifchange sshuttle.8 $UI
+redo-ifchange Documentation/all version/all $UI
echo
echo "What now?"
[ -z "$UI" ] || echo "- Try the MacOS GUI: open ui-macos/Sshuttle*.app"
echo "- Run sshuttle: ./sshuttle --dns -r HOSTNAME 0/0"
echo "- Read the README: less README.md"
-echo "- Read the man page: less sshuttle.md"
+echo "- Read the man page: less Documentation/sshuttle.md"
diff --git a/clean.do b/clean.do
index 2baeb36..1bbb0fc 100644
--- a/clean.do
+++ b/clean.do
@@ -1,2 +1,2 @@
-redo ui-macos/clean
+redo ui-macos/clean Documentation/clean version/clean
rm -f *~ */*~ .*~ */.*~ *.8 *.tmp */*.tmp *.pyc */*.pyc
diff --git a/client.py b/client.py
index 7310c25..a03ac3a 100644
--- a/client.py
+++ b/client.py
@@ -163,7 +163,7 @@ def start(self):
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'[^-\w\.]', hostname))
assert(not re.search(r'[^0-9.]', ip))
self.pfile.write('HOST %s,%s\n' % (hostname, ip))
self.pfile.flush()
@@ -171,7 +171,9 @@ def sethostip(self, hostname, ip):
def done(self):
self.pfile.close()
rv = self.p.wait()
- if rv:
+ if rv == EXITCODE_NEEDS_REBOOT:
+ raise FatalNeedsReboot()
+ elif rv:
raise Fatal('cleanup: %r returned %d' % (self.argv, rv))
diff --git a/default.8.do b/default.8.do
index 6d8dc8c..467bfbe 100644
--- a/default.8.do
+++ b/default.8.do
@@ -1,6 +1,6 @@
exec >&2
if pandoc /dev/null; then
- pandoc -s -r markdown -w man -o $3 $1.md
+ pandoc -s -r markdown -w man -o $3 $2.md
else
echo "Warning: pandoc not installed; can't generate manpages."
redo-always
diff --git a/do b/do
index f08e002..d84c442 100755
--- a/do
+++ b/do
@@ -121,7 +121,7 @@ _do()
fi
[ ! -e "$DO_BUILT" ] || [ ! -d "$(dirname "$target")" ] ||
: >>"$target.did"
- ( _run_dofile "$base" "$ext" "$tmp.tmp" )
+ ( _run_dofile "$target" "$base" "$tmp.tmp" )
rv=$?
if [ $rv != 0 ]; then
printf "do: %s%s\n" "$DO_DEPTH" \
diff --git a/firewall.py b/firewall.py
index 4fd8c79..cbc3312 100644
--- a/firewall.py
+++ b/firewall.py
@@ -1,4 +1,4 @@
-import re, errno, socket, select, struct
+import re, errno, socket, select, signal, struct
import compat.ssubprocess as ssubprocess
import helpers, ssyslog
from helpers import *
@@ -6,6 +6,12 @@
# python doesn't have a definition for this
IPPROTO_DIVERT = 254
+# return values from sysctl_set
+SUCCESS = 0
+SAME = 1
+FAILED = -1
+NONEXIST = -2
+
def nonfatal(func, *args):
try:
@@ -14,6 +20,14 @@ def nonfatal(func, *args):
log('error: %s\n' % e)
+def _call(argv):
+ debug1('>> %s\n' % ' '.join(argv))
+ rv = ssubprocess.call(argv)
+ if rv:
+ raise Fatal('%r returned %d' % (argv, rv))
+ return rv
+
+
def ipt_chain_exists(name):
argv = ['iptables', '-t', 'nat', '-nL']
p = ssubprocess.Popen(argv, stdout = ssubprocess.PIPE)
@@ -27,10 +41,7 @@ def ipt_chain_exists(name):
def ipt(*args):
argv = ['iptables', '-t', 'nat'] + list(args)
- debug1('>> %s\n' % ' '.join(argv))
- rv = ssubprocess.call(argv)
- if rv:
- raise Fatal('%r returned %d' % (argv, rv))
+ _call(argv)
_no_ttl_module = False
@@ -135,6 +146,42 @@ def _fill_oldctls(prefix):
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):
argv = ['sysctl', '-w', '%s=%s' % (name, val)]
debug1('>> %s\n' % ' '.join(argv))
@@ -150,20 +197,24 @@ def sysctl_set(name, val, permanent=False):
_fill_oldctls(PREFIX)
if not (name in _oldctls):
debug1('>> No such sysctl: %r\n' % name)
- return False
+ return NONEXIST
oldval = _oldctls[name]
- if val != oldval:
- rv = _sysctl_set(name, val)
- if rv==0 and permanent:
- debug1('>> ...saving permanently in /etc/sysctl.conf\n')
- f = open('/etc/sysctl.conf', 'a')
- f.write('\n'
- '# Added by sshuttle\n'
- '%s=%s\n' % (name, val))
- f.close()
- else:
- _changedctls.append(name)
- return True
+ if val == oldval:
+ return SAME
+
+ rv = _sysctl_set(name, val)
+ if rv != 0:
+ return FAILED
+ if permanent:
+ debug1('>> ...saving permanently in /etc/sysctl.conf\n')
+ f = open('/etc/sysctl.conf', 'a')
+ f.write('\n'
+ '# Added by sshuttle\n'
+ '%s=%s\n' % (name, val))
+ f.close()
+ else:
+ _changedctls.append(name)
+ return SUCCESS
def _udp_unpack(p):
@@ -201,10 +252,7 @@ def _handle_diversion(divertsock, dnsport):
def ipfw(*args):
argv = ['ipfw', '-q'] + list(args)
- debug1('>> %s\n' % ' '.join(argv))
- rv = ssubprocess.call(argv)
- if rv:
- raise Fatal('%r returned %d' % (argv, rv))
+ _call(argv)
def do_ipfw(port, dnsport, subnets):
@@ -222,8 +270,14 @@ def do_ipfw(port, dnsport, subnets):
if subnets or dnsport:
sysctl_set('net.inet.ip.fw.enable', 1)
- changed = sysctl_set('net.inet.ip.scopedroute', 0, permanent=True)
- if changed:
+
+ # This seems to be needed on MacOS 10.6 and 10.7. For more
+ # 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"
" WARNING: ONE-TIME NETWORK DISRUPTION:\n"
" =====================================\n"
@@ -234,6 +288,21 @@ def do_ipfw(port, dnsport, subnets):
"ethernet port) NOW, then restart sshuttle. The fix is\n"
"permanent; you only have to do this once.\n\n")
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',
'from', 'any', 'to', 'any')
@@ -243,11 +312,11 @@ def do_ipfw(port, dnsport, subnets):
for swidth,sexclude,snet in sorted(subnets, reverse=True):
if sexclude:
ipfw('add', sport, 'skipto', xsport,
- 'log', 'tcp',
+ 'tcp',
'from', 'any', 'to', '%s/%s' % (snet,swidth))
else:
ipfw('add', sport, 'fwd', '127.0.0.1,%d' % port,
- 'log', 'tcp',
+ 'tcp',
'from', 'any', 'to', '%s/%s' % (snet,swidth),
'not', 'ipttl', '42', 'keep-state', 'setup')
@@ -289,12 +358,12 @@ def do_ipfw(port, dnsport, subnets):
for ip in nslist:
# relabel and then catch outgoing DNS requests
ipfw('add', sport, 'divert', sport,
- 'log', 'udp',
+ 'udp',
'from', 'any', 'to', '%s/32' % ip, '53',
'not', 'ipttl', '42')
# relabel DNS responses
ipfw('add', sport, 'divert', sport,
- 'log', 'udp',
+ 'udp',
'from', 'any', str(dnsport), 'to', 'any',
'not', 'ipttl', '42')
@@ -361,6 +430,19 @@ def restore_etc_hosts(port):
rewrite_etc_hosts(port)
+def _mask(ip, width):
+ nip = struct.unpack('!I', socket.inet_aton(ip))[0]
+ masked = nip & shl(shl(1, width) - 1, 32-width)
+ return socket.inet_ntoa(struct.pack('!I', masked))
+
+
+def ip_in_subnets(ip, subnets):
+ for swidth,sexclude,snet in sorted(subnets, reverse=True):
+ if _mask(snet, swidth) == _mask(ip, swidth):
+ return not sexclude
+ return False
+
+
# This is some voodoo for setting up the kernel's transparent
# proxying stuff. If subnets is empty, we just delete our sshuttle rules;
# otherwise we delete it, then make them from scratch.
@@ -398,6 +480,13 @@ def main(port, dnsport, syslog):
sys.stdout.write('READY\n')
sys.stdout.flush()
+ # don't disappear if our controlling terminal or stdout/stderr
+ # disappears; we still have to clean up.
+ signal.signal(signal.SIGHUP, signal.SIG_IGN)
+ signal.signal(signal.SIGPIPE, signal.SIG_IGN)
+ signal.signal(signal.SIGTERM, signal.SIG_IGN)
+ signal.signal(signal.SIGINT, signal.SIG_IGN)
+
# ctrl-c shouldn't be passed along to me. When the main sshuttle dies,
# I'll die automatically.
os.setsid()
@@ -445,8 +534,9 @@ def main(port, dnsport, syslog):
line = sys.stdin.readline(128)
if line.startswith('HOST '):
(name,ip) = line[5:].strip().split(',', 1)
- hostmap[name] = ip
- rewrite_etc_hosts(port)
+ if ip_in_subnets(ip, subnets):
+ hostmap[name] = ip
+ rewrite_etc_hosts(port)
elif line:
raise Fatal('expected EOF, got %r' % line)
else:
diff --git a/helpers.py b/helpers.py
index af49788..d8de08d 100644
--- a/helpers.py
+++ b/helpers.py
@@ -30,6 +30,11 @@ class Fatal(Exception):
pass
+EXITCODE_NEEDS_REBOOT = 111
+class FatalNeedsReboot(Fatal):
+ pass
+
+
def list_contains_any(l, sub):
for i in sub:
if i in l:
@@ -73,3 +78,10 @@ def islocal(ip):
return True # it's a local IP, or there would have been an error
+def shl(n, bits):
+ # we use our own implementation of left-shift because
+ # 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)
diff --git a/hostwatch.py b/hostwatch.py
index 66e7461..e2bdb2b 100644
--- a/hostwatch.py
+++ b/hostwatch.py
@@ -51,15 +51,20 @@ def read_host_cache():
words = line.strip().split(',')
if len(words) == 2:
(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()
if name and ip:
found_host(name, ip)
-
-def found_host(hostname, ip):
- hostname = re.sub(r'\..*', '', hostname)
- hostname = re.sub(r'[^-\w]', '_', hostname)
+
+def found_host(full_hostname, ip):
+ full_hostname = re.sub(r'[^-\w\.]', '_', full_hostname)
+ hostname = re.sub(r'\..*', '', full_hostname)
+ _insert_host(full_hostname, ip)
+ _insert_host(hostname, ip)
+
+
+def _insert_host(hostname, ip):
if (ip.startswith('127.') or ip.startswith('255.')
or hostname == 'localhost'):
return
diff --git a/main.py b/main.py
old mode 100644
new mode 100755
index 1cf00af..34d9fb1
--- a/main.py
+++ b/main.py
@@ -57,12 +57,14 @@ def parse_ipport(s):
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)
@@ -72,6 +74,10 @@ def parse_ipport(s):
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:
@@ -99,6 +105,8 @@ def parse_ipport(s):
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
@@ -121,6 +129,9 @@ def parse_ipport(s):
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)
diff --git a/server.py b/server.py
index e1b327d..1032d4c 100644
--- a/server.py
+++ b/server.py
@@ -37,15 +37,11 @@ def _maskbits(netmask):
if not netmask:
return 32
for i in range(32):
- if netmask[0] & _shl(1, i):
+ if netmask[0] & shl(1, i):
return 32-i
return 0
-def _shl(n, bits):
- return n * int(2**bits)
-
-
def _list_routes():
argv = ['netstat', '-rn']
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE)
@@ -58,7 +54,7 @@ def _list_routes():
maskw = _ipmatch(cols[2]) # linux only
mask = _maskbits(maskw) # returns 32 if maskw is null
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))
rv = p.wait()
if rv != 0:
@@ -68,9 +64,11 @@ def _list_routes():
def list_routes():
+ l = []
for (ip,width) in _list_routes():
if not ip.startswith('0.') and not ip.startswith('127.'):
- yield (ip,width)
+ l.append((ip,width))
+ return l
def _exc_dump():
diff --git a/ui-macos/bits/runpython.do b/ui-macos/bits/runpython.do
index a53247f..9791a87 100644
--- a/ui-macos/bits/runpython.do
+++ b/ui-macos/bits/runpython.do
@@ -2,12 +2,14 @@ exec >&2
redo-ifchange runpython.c
ARCHES=""
printf "Platforms: "
-for d in /usr/libexec/gcc/darwin/*; do
- PLAT=$(basename "$d")
- [ "$PLAT" != "ppc64" ] || continue # fails for some reason on my Mac
- ARCHES="$ARCHES -arch $PLAT"
- printf "$PLAT "
-done
+if [ -d /usr/libexec/gcc/darwin ]; then
+ for d in /usr/libexec/gcc/darwin/*; do
+ PLAT=$(basename "$d")
+ [ "$PLAT" != "ppc64" ] || continue # fails for some reason on my Mac
+ ARCHES="$ARCHES -arch $PLAT"
+ printf "$PLAT "
+ done
+fi
printf "\n"
gcc $ARCHES \
-Wall -o $3 runpython.c \
diff --git a/ui-macos/default.app.do b/ui-macos/default.app.do
index 64e3a52..5c88273 100644
--- a/ui-macos/default.app.do
+++ b/ui-macos/default.app.do
@@ -3,9 +3,9 @@ redo-ifchange sources.list
redo-ifchange Info.plist bits/runpython \
$(while read name newname; do echo "$name"; done &2
IFS="
"
-redo-ifchange $1.app
-tar -czf $3 $1.app/
+redo-ifchange $2.app
+tar -czf $3 $2.app/
diff --git a/ui-macos/default.app.zip.do b/ui-macos/default.app.zip.do
index c12e2d2..64f3a10 100644
--- a/ui-macos/default.app.zip.do
+++ b/ui-macos/default.app.zip.do
@@ -1,5 +1,5 @@
exec >&2
IFS="
"
-redo-ifchange $1.app
-zip -q -r $3 $1.app/
+redo-ifchange $2.app
+zip -q -r $3 $2.app/
diff --git a/ui-macos/default.nib.do b/ui-macos/default.nib.do
index afa91f4..02ddec6 100644
--- a/ui-macos/default.nib.do
+++ b/ui-macos/default.nib.do
@@ -1,2 +1,2 @@
-redo-ifchange $1.xib
-ibtool --compile $3 $1.xib
+redo-ifchange $2.xib
+ibtool --compile $3 $2.xib
diff --git a/ui-macos/main.py b/ui-macos/main.py
index 3e6c2a1..fc67a34 100644
--- a/ui-macos/main.py
+++ b/ui-macos/main.py
@@ -78,6 +78,11 @@ def _try_wait(self, options):
if pid == self.pid:
if os.WIFEXITED(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:
self.rv = -os.WSTOPSIG(code)
self.serverobj.setConnected_(False)
@@ -87,7 +92,10 @@ def _try_wait(self, options):
return self.rv
def wait(self):
- return self._try_wait(0)
+ rv = None
+ while rv is None:
+ self.gotdata(None)
+ rv = self._try_wait(os.WNOHANG)
def poll(self):
return self._try_wait(os.WNOHANG)
diff --git a/version/.gitattributes b/version/.gitattributes
new file mode 100644
index 0000000..1dbc5f8
--- /dev/null
+++ b/version/.gitattributes
@@ -0,0 +1 @@
+gitvars.pre export-subst
diff --git a/version/.gitignore b/version/.gitignore
new file mode 100644
index 0000000..dccdd45
--- /dev/null
+++ b/version/.gitignore
@@ -0,0 +1,3 @@
+/vars
+/gitvars
+/_version.py
diff --git a/version/__init__.py b/version/__init__.py
new file mode 100644
index 0000000..2863a1d
--- /dev/null
+++ b/version/__init__.py
@@ -0,0 +1 @@
+from _version import COMMIT, TAG, DATE
diff --git a/version/_version.py.do b/version/_version.py.do
new file mode 100644
index 0000000..cc22aad
--- /dev/null
+++ b/version/_version.py.do
@@ -0,0 +1,3 @@
+redo-ifchange vars
+cat vars
+
diff --git a/version/all.do b/version/all.do
new file mode 100644
index 0000000..db6567c
--- /dev/null
+++ b/version/all.do
@@ -0,0 +1,2 @@
+redo-ifchange vars _version.py
+
diff --git a/version/clean.do b/version/clean.do
new file mode 100644
index 0000000..16f3859
--- /dev/null
+++ b/version/clean.do
@@ -0,0 +1,3 @@
+rm -f *~ .*~ *.pyc _version.py vars gitvars
+
+
diff --git a/version/gitvars.do b/version/gitvars.do
new file mode 100644
index 0000000..483e825
--- /dev/null
+++ b/version/gitvars.do
@@ -0,0 +1,28 @@
+redo-ifchange gitvars.pre prodname
+
+read PROD $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