From 2268e76771389ef5137a78340ea239e3885aad03 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Mon, 2 Jan 2012 18:19:08 -0500 Subject: [PATCH 01/21] ipfw: don't use 'log' parameter. I guess we were causing the kernel to syslog on every single packet on MacOS. Oops. --- firewall.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/firewall.py b/firewall.py index 4fd8c79..1eda23d 100644 --- a/firewall.py +++ b/firewall.py @@ -243,11 +243,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 +289,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') From 89e914e9d182a4464b8cd7166517b397cad66737 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Mon, 2 Jan 2012 18:46:30 -0500 Subject: [PATCH 02/21] 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. --- ui-macos/main.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ui-macos/main.py b/ui-macos/main.py index 3e6c2a1..7915e91 100644 --- a/ui-macos/main.py +++ b/ui-macos/main.py @@ -87,7 +87,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) From e2c682084c798f8f252b0e5167d2cc412061344f Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Thu, 5 Jan 2012 21:16:24 -0500 Subject: [PATCH 03/21] 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. --- firewall.py | 7 ++++++- main.py | 0 2 files changed, 6 insertions(+), 1 deletion(-) mode change 100644 => 100755 main.py diff --git a/firewall.py b/firewall.py index 1eda23d..98f0fe0 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 * @@ -398,6 +398,11 @@ 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) + # ctrl-c shouldn't be passed along to me. When the main sshuttle dies, # I'll die automatically. os.setsid() diff --git a/main.py b/main.py old mode 100644 new mode 100755 From 6698992f4f33e5e49dff5ceddb24a52992a06ae7 Mon Sep 17 00:00:00 2001 From: Jimmy Tang Date: Wed, 4 Jan 2012 20:37:50 +0000 Subject: [PATCH 04/21] Use the new arguments from redo v0.10. (apenwarr: also updates to the matching, latest minimal/do) --- default.8.do | 2 +- do | 2 +- ui-macos/default.app.do | 10 +++++----- ui-macos/default.app.tar.gz.do | 4 ++-- ui-macos/default.app.zip.do | 4 ++-- ui-macos/default.nib.do | 4 ++-- 6 files changed, 13 insertions(+), 13 deletions(-) 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/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 From cbc32ff8d8e35c648b09114db0bad7af1c078f14 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Fri, 6 Jan 2012 13:35:12 -0500 Subject: [PATCH 05/21] Import the non-pandoc manpage generator from redo. This makes it easier (possible?) to generate sshuttle.8 from sshuttle.md on MacOS. We also import the git-enhanced version numbering magic so the generated manpage can have a real version number. --- Documentation/.gitignore | 3 + Documentation/all.do | 5 + Documentation/clean.do | 1 + Documentation/default.8.do | 2 + Documentation/default.md.tmp.do | 3 + Documentation/md-to-man.do | 8 + Documentation/md2man.py | 278 +++++++++++++++++++++++ sshuttle.md => Documentation/sshuttle.md | 4 +- all.do | 4 +- clean.do | 2 +- version/.gitattributes | 1 + version/.gitignore | 3 + version/__init__.py | 1 + version/_version.py.do | 3 + version/all.do | 2 + version/clean.do | 3 + version/gitvars.do | 28 +++ version/gitvars.pre | 3 + version/prodname | 1 + version/vars.do | 40 ++++ 20 files changed, 390 insertions(+), 5 deletions(-) create mode 100644 Documentation/.gitignore create mode 100644 Documentation/all.do create mode 100644 Documentation/clean.do create mode 100644 Documentation/default.8.do create mode 100644 Documentation/default.md.tmp.do create mode 100644 Documentation/md-to-man.do create mode 100755 Documentation/md2man.py rename sshuttle.md => Documentation/sshuttle.md (99%) create mode 100644 version/.gitattributes create mode 100644 version/.gitignore create mode 100644 version/__init__.py create mode 100644 version/_version.py.do create mode 100644 version/all.do create mode 100644 version/clean.do create mode 100644 version/gitvars.do create mode 100644 version/gitvars.pre create mode 100644 version/prodname create mode 100644 version/vars.do 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 99% rename from sshuttle.md rename to Documentation/sshuttle.md index 16bedda..f6e202c 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 diff --git a/all.do b/all.do index 7ee8426..9fe5099 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 $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..12c1109 100644 --- a/clean.do +++ b/clean.do @@ -1,2 +1,2 @@ -redo ui-macos/clean +redo ui-macos/clean Documentation/clean rm -f *~ */*~ .*~ */.*~ *.8 *.tmp */*.tmp *.pyc */*.pyc 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 Date: Fri, 6 Jan 2012 13:45:43 -0500 Subject: [PATCH 06/21] Add a --version (-V) option. Now that we imported the feature from redo, might as well use it. --- all.do | 2 +- main.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/all.do b/all.do index 9fe5099..7282cb5 100644 --- a/all.do +++ b/all.do @@ -1,7 +1,7 @@ exec >&2 UI= [ "$(uname)" = "Darwin" ] && UI=ui-macos/all -redo-ifchange Documentation/all $UI +redo-ifchange Documentation/all version/all $UI echo echo "What now?" diff --git a/main.py b/main.py index 1cf00af..a88ca9d 100755 --- a/main.py +++ b/main.py @@ -63,6 +63,7 @@ def parse_ipport(s): 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 +73,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: From 4c1a505e37f6a0c59acfd347c290ab9d3258f6a7 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sun, 8 Jan 2012 18:18:42 -0500 Subject: [PATCH 07/21] firewall.py: workaround MacOS 10.7 Lion bug. On top of the bug that already existed in 10.6, Lion also makes the sysctl needed to fix the problem into a read-only variable, so we have to actually change it at kernel boot time and force people to reboot. Nice job, Apple. --- firewall.py | 94 ++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 79 insertions(+), 15 deletions(-) diff --git a/firewall.py b/firewall.py index 98f0fe0..83ff6dc 100644 --- a/firewall.py +++ b/firewall.py @@ -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: @@ -135,6 +141,48 @@ 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] + debug1('>> %s\n' % ' '.join(argv)) + rv = ssubprocess.call(argv) + if rv: + raise Fatal('%r returned %d' (argv, rv)) + argv = ['plutil', '-convert', 'xml1', KERNEL_FLAGS_PATH + '.plist'] + debug1('>> %s\n' % ' '.join(argv)) + rv = ssubprocess.call(argv) + if rv: + raise Fatal('%r returned %d' (argv, rv)) + + + +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 +198,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): @@ -222,8 +274,8 @@ 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: + changeflag = sysctl_set('net.inet.ip.scopedroute', 0, permanent=True) + if changeflag == SUCCESS: log("\n" " WARNING: ONE-TIME NETWORK DISRUPTION:\n" " =====================================\n" @@ -234,6 +286,18 @@ 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: + 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(1) + ipfw('add', sport, 'check-state', 'ip', 'from', 'any', 'to', 'any') From bd20841782881684ed38b6538085714f757f2bc0 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sun, 8 Jan 2012 18:42:38 -0500 Subject: [PATCH 08/21] firewall.py: clean up repeated calls to ssubprocess.call(). And make sshuttle exit with a well-defined exit code (111) if it needs to reboot. --- client.py | 4 +++- firewall.py | 31 +++++++++++++------------------ helpers.py | 5 +++++ main.py | 3 +++ 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/client.py b/client.py index 7310c25..06fc573 100644 --- a/client.py +++ b/client.py @@ -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/firewall.py b/firewall.py index 83ff6dc..de85bd0 100644 --- a/firewall.py +++ b/firewall.py @@ -20,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) @@ -33,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 @@ -159,15 +164,9 @@ def _defaults_write_kernel_flags(flags): flagstr = ' '.join(flags) argv = ['defaults', 'write', KERNEL_FLAGS_PATH, KERNEL_FLAGS_NAME, flagstr] - debug1('>> %s\n' % ' '.join(argv)) - rv = ssubprocess.call(argv) - if rv: - raise Fatal('%r returned %d' (argv, rv)) + _call(argv) argv = ['plutil', '-convert', 'xml1', KERNEL_FLAGS_PATH + '.plist'] - debug1('>> %s\n' % ' '.join(argv)) - rv = ssubprocess.call(argv) - if rv: - raise Fatal('%r returned %d' (argv, rv)) + _call(argv) @@ -253,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): @@ -296,8 +292,7 @@ def do_ipfw(port, dnsport, subnets): "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(1) - + sys.exit(EXITCODE_NEEDS_REBOOT) ipfw('add', sport, 'check-state', 'ip', 'from', 'any', 'to', 'any') diff --git a/helpers.py b/helpers.py index af49788..45a028b 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: diff --git a/main.py b/main.py index a88ca9d..daf3b87 100755 --- a/main.py +++ b/main.py @@ -126,6 +126,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) From d9f761a8a31c211687b2860b4a223b13fa44b398 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sun, 8 Jan 2012 18:57:56 -0500 Subject: [PATCH 09/21] ui-macos: tell the user that we need to reboot on MacOS Lion. --- ui-macos/main.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ui-macos/main.py b/ui-macos/main.py index 7915e91..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) From e737f4b944c590581f67953cb5f051cebbbbdb8e Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sun, 8 Jan 2012 19:13:39 -0500 Subject: [PATCH 10/21] firewall.py: add comments about sysctl problems. --- firewall.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/firewall.py b/firewall.py index de85bd0..452715e 100644 --- a/firewall.py +++ b/firewall.py @@ -270,6 +270,12 @@ def do_ipfw(port, dnsport, subnets): if subnets or dnsport: sysctl_set('net.inet.ip.fw.enable', 1) + + # 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" @@ -283,6 +289,10 @@ def do_ipfw(port, dnsport, subnets): "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" From 12f6a52ec61203302a8f57a6f1b2b60ac2c083ad Mon Sep 17 00:00:00 2001 From: David Held Date: Thu, 2 Feb 2012 11:23:50 -0800 Subject: [PATCH 11/21] Fix runpython.do for systems with unxpected configurations. If the expected arch directory doesn't exist, give up and don't specify arch at all. Currently it expands to '*' which fails. [slightly modified by apenwarr] --- ui-macos/bits/runpython.do | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) 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 \ From 274ee854d4f05fd55a7c1c94e0ffb29bed8008eb Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Tue, 7 Feb 2012 12:17:56 -0500 Subject: [PATCH 12/21] clean.do: don't forget to do version/clean. --- clean.do | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clean.do b/clean.do index 12c1109..1bbb0fc 100644 --- a/clean.do +++ b/clean.do @@ -1,2 +1,2 @@ -redo ui-macos/clean Documentation/clean +redo ui-macos/clean Documentation/clean version/clean rm -f *~ */*~ .*~ */.*~ *.8 *.tmp */*.tmp *.pyc */*.pyc From 42bc6d62db3a81834f429da6db12422e0f736b9e Mon Sep 17 00:00:00 2001 From: Saul Date: Thu, 19 Apr 2012 22:48:09 -0700 Subject: [PATCH 13/21] Two small changes to server.py that allow it to run on python2.2 --- server.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server.py b/server.py index e1b327d..6b64b20 100644 --- a/server.py +++ b/server.py @@ -1,3 +1,4 @@ +from __future__ import generators # add yield for python2.2 import re, struct, socket, select, traceback, time if not globals().get('skip_imports'): import ssnet, helpers, hostwatch @@ -43,7 +44,7 @@ def _maskbits(netmask): def _shl(n, bits): - return n * int(2**bits) + return n * long(2**bits) def _list_routes(): From 5743f29ed6cf8ff61affc79c047832185b5e30d0 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sun, 1 Jul 2012 15:32:34 -0400 Subject: [PATCH 14/21] server.py: slightly rearrange previous commit. Add some documentation about the int() vs long() and the reason behind _shl(). Instead of "from __future__ import generators", just don't use generators. --- server.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/server.py b/server.py index 6b64b20..5f2e5e4 100644 --- a/server.py +++ b/server.py @@ -1,4 +1,3 @@ -from __future__ import generators # add yield for python2.2 import re, struct, socket, select, traceback, time if not globals().get('skip_imports'): import ssnet, helpers, hostwatch @@ -44,6 +43,11 @@ def _maskbits(netmask): 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) @@ -69,9 +73,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(): From cce6a9d96db197dda76a194cd7a6420e6fcb08d2 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Fri, 6 Jul 2012 14:51:46 -0400 Subject: [PATCH 15/21] 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.) --- firewall.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/firewall.py b/firewall.py index 452715e..53d866e 100644 --- a/firewall.py +++ b/firewall.py @@ -471,6 +471,8 @@ def main(port, dnsport, syslog): # 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. From bff16100509ed94e93c0331819f1a0730fc0431a Mon Sep 17 00:00:00 2001 From: Miguel Landaeta Date: Wed, 6 Jul 2011 19:48:05 -0430 Subject: [PATCH 16/21] Document missing --dns option in sshuttle manpage --- Documentation/sshuttle.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Documentation/sshuttle.md b/Documentation/sshuttle.md index f6e202c..59901ec 100644 --- a/Documentation/sshuttle.md +++ b/Documentation/sshuttle.md @@ -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 From 29d2e06bf5cd3d575015e23c638ca9e5a10ee29c Mon Sep 17 00:00:00 2001 From: Tianyi Cui Date: Tue, 19 Apr 2011 20:19:06 +0800 Subject: [PATCH 17/21] Added --exclude-from feature. (Slightly modified by apenwarr) --- Documentation/sshuttle.md | 4 ++++ main.py | 3 +++ 2 files changed, 7 insertions(+) diff --git a/Documentation/sshuttle.md b/Documentation/sshuttle.md index 59901ec..4caf6cc 100644 --- a/Documentation/sshuttle.md +++ b/Documentation/sshuttle.md @@ -94,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/main.py b/main.py index daf3b87..34d9fb1 100755 --- a/main.py +++ b/main.py @@ -57,6 +57,7 @@ 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) @@ -104,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 From 432e98cad5eac07bb150ae6c5fb785d27c26d963 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Fri, 6 Jul 2012 16:07:05 -0400 Subject: [PATCH 18/21] auto-hosts: don't add hosts that aren't being routed by sshuttle. I've been meaning to add this patch for a long time, but it's especially important once we add FQDN support to --auto-hosts. Basically, auto-hosts will still discover all the hostnames it can, but we'll only add them to /etc/hosts if their IP address is in one of the routed subnet ranges. That prevents polluting the /etc/hosts file with cruft. --- firewall.py | 18 ++++++++++++++++-- helpers.py | 7 +++++++ server.py | 13 ++----------- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/firewall.py b/firewall.py index 53d866e..cbc3312 100644 --- a/firewall.py +++ b/firewall.py @@ -430,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. @@ -521,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 45a028b..d8de08d 100644 --- a/helpers.py +++ b/helpers.py @@ -78,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/server.py b/server.py index 5f2e5e4..1032d4c 100644 --- a/server.py +++ b/server.py @@ -37,20 +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): - # 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) - - def _list_routes(): argv = ['netstat', '-rn'] p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE) @@ -63,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: From 6450c37043c37b1d5df355efeb360c02f95d49eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?George=20Guimara=CC=83es?= Date: Fri, 20 May 2011 14:47:11 -0300 Subject: [PATCH 19/21] hostwatch: handle fully qualified domain names (slightly modified by apenwarr) --- client.py | 2 +- hostwatch.py | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/client.py b/client.py index 06fc573..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() 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 From 9ce2fa00f94c2f2e5c310abeb1a2907ae7e7a7b0 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Fri, 10 Aug 2012 22:30:15 -0400 Subject: [PATCH 20/21] README: add a suggestion to try the MacOS GUI app. --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 7b1eece..81ca6e0 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,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 From 582f63fd69e39a731132ea15c19cfacca044fafa Mon Sep 17 00:00:00 2001 From: TedSinger Date: Sun, 4 Nov 2012 19:55:10 -0500 Subject: [PATCH 21/21] Update server.py I made _ipmatch understandable. Also, it will be easier in the future for it to accommodate IPv6 with this structure. --- server.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/server.py b/server.py index 1032d4c..c1aad6e 100644 --- a/server.py +++ b/server.py @@ -9,20 +9,20 @@ def _ipmatch(ipstr): if ipstr == 'default': ipstr = '0.0.0.0/0' - m = re.match(r'^(\d+(\.\d+(\.\d+(\.\d+)?)?)?)(?:/(\d+))?$', ipstr) + # Regex to check that ipstr is formatted like an IPv4 address + m = re.match('^(\d+)' + '(\.\d+)?' * 3 + '(?:/(\d+))?$', ipstr) if m: - g = m.groups() - ips = g[0] - width = int(g[4] or 32) - if g[1] == None: - ips += '.0.0.0' - width = min(width, 8) - elif g[2] == None: - ips += '.0.0' - width = min(width, 16) - elif g[3] == None: - ips += '.0' - width = min(width, 24) + # If the network prefix width is specified, use it. Otherwise, assume 32 + if '/' in ipstr: + ips, width = ipstr.split('/') + width = int(width) + else: + ips, width = ipstr, 32 + # If ips doesn't have four octets, then assume that the network + # prefix width is constrained. Pad ips with 0s + octets = ips.count('.') + 1 + width = min(width, 8 * octets) + ips += '.0' * (4 - octets) return (struct.unpack('!I', socket.inet_aton(ips))[0], width)