From d5c3aa61b8b77d3b12647bb15a83ba51a5f4d312 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sun, 8 Jan 2012 19:16:05 -0500 Subject: MacOS precompiled app package for sshuttle-0.60 --- Sshuttle VPN.app/Contents/MacOS/Sshuttle | Bin 25248 -> 37568 bytes .../Contents/Resources/English.lproj/MainMenu.nib | Bin 28750 -> 27144 bytes Sshuttle VPN.app/Contents/Resources/askpass.pyc | Bin 1146 -> 0 bytes Sshuttle VPN.app/Contents/Resources/main.py | 10 +- Sshuttle VPN.app/Contents/Resources/models.py | 7 +- Sshuttle VPN.app/Contents/Resources/models.pyc | Bin 9502 -> 0 bytes Sshuttle VPN.app/Contents/Resources/my.pyc | Bin 2809 -> 0 bytes .../Contents/Resources/sshuttle/client.py | 122 ++++++++++--------- .../Contents/Resources/sshuttle/firewall.py | 130 ++++++++++++++++----- .../Contents/Resources/sshuttle/helpers.py | 7 +- .../Contents/Resources/sshuttle/main.py | 8 ++ .../Contents/Resources/sshuttle/ssnet.py | 11 ++ 12 files changed, 205 insertions(+), 90 deletions(-) delete mode 100644 Sshuttle VPN.app/Contents/Resources/askpass.pyc delete mode 100644 Sshuttle VPN.app/Contents/Resources/models.pyc delete mode 100644 Sshuttle VPN.app/Contents/Resources/my.pyc mode change 100644 => 100755 Sshuttle VPN.app/Contents/Resources/sshuttle/main.py diff --git a/Sshuttle VPN.app/Contents/MacOS/Sshuttle b/Sshuttle VPN.app/Contents/MacOS/Sshuttle index f45f4e2..334693b 100755 Binary files a/Sshuttle VPN.app/Contents/MacOS/Sshuttle and b/Sshuttle VPN.app/Contents/MacOS/Sshuttle differ diff --git a/Sshuttle VPN.app/Contents/Resources/English.lproj/MainMenu.nib b/Sshuttle VPN.app/Contents/Resources/English.lproj/MainMenu.nib index af813ea..53b254c 100644 Binary files a/Sshuttle VPN.app/Contents/Resources/English.lproj/MainMenu.nib and b/Sshuttle VPN.app/Contents/Resources/English.lproj/MainMenu.nib differ diff --git a/Sshuttle VPN.app/Contents/Resources/askpass.pyc b/Sshuttle VPN.app/Contents/Resources/askpass.pyc deleted file mode 100644 index 3be75fc..0000000 Binary files a/Sshuttle VPN.app/Contents/Resources/askpass.pyc and /dev/null differ diff --git a/Sshuttle VPN.app/Contents/Resources/main.py b/Sshuttle VPN.app/Contents/Resources/main.py index 3e6c2a1..fc67a34 100644 --- a/Sshuttle VPN.app/Contents/Resources/main.py +++ b/Sshuttle VPN.app/Contents/Resources/main.py @@ -78,6 +78,11 @@ class Runner: 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 @@ class Runner: 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/Sshuttle VPN.app/Contents/Resources/models.py b/Sshuttle VPN.app/Contents/Resources/models.py index c4cbe8b..e71fce5 100644 --- a/Sshuttle VPN.app/Contents/Resources/models.py +++ b/Sshuttle VPN.app/Contents/Resources/models.py @@ -3,6 +3,7 @@ import my configchange_callback = setconnect_callback = None +objc_validator = objc.signature('@@:N^@o^@') def config_changed(): @@ -39,7 +40,7 @@ class SshuttleNet(NSObject): def setSubnet_(self, v): self._k_subnet = v config_changed() - @objc.accessor + @objc_validator def validateSubnet_error_(self, value, error): #print 'validateSubnet!' return True, _validate_ip(value), error @@ -49,7 +50,7 @@ class SshuttleNet(NSObject): def setWidth_(self, v): self._k_width = v config_changed() - @objc.accessor + @objc_validator def validateWidth_error_(self, value, error): #print 'validateWidth!' return True, _validate_width(value), error @@ -118,7 +119,7 @@ class SshuttleServer(NSObject): self._k_host = v self.setTitle_(None) config_changed() - @objc.accessor + @objc_validator def validateHost_error_(self, value, error): #print 'validatehost! %r %r %r' % (self, value, error) while value.startswith('-'): diff --git a/Sshuttle VPN.app/Contents/Resources/models.pyc b/Sshuttle VPN.app/Contents/Resources/models.pyc deleted file mode 100644 index 6163bd5..0000000 Binary files a/Sshuttle VPN.app/Contents/Resources/models.pyc and /dev/null differ diff --git a/Sshuttle VPN.app/Contents/Resources/my.pyc b/Sshuttle VPN.app/Contents/Resources/my.pyc deleted file mode 100644 index 188ff60..0000000 Binary files a/Sshuttle VPN.app/Contents/Resources/my.pyc and /dev/null differ diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/client.py b/Sshuttle VPN.app/Contents/Resources/sshuttle/client.py index 449a75a..06fc573 100644 --- a/Sshuttle VPN.app/Contents/Resources/sshuttle/client.py +++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/client.py @@ -171,10 +171,71 @@ class FirewallClient: 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)) +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): @@ -255,63 +316,10 @@ def _main(listener, fw, ssh_cmd, remotename, python, latency_control, fw.sethostip(name, ip) mux.got_host_list = onhostlist - def onaccept(): - 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)) - handlers.append(Handler([listener], onaccept)) - - dnsreqs = {} - def dns_done(chan, data): - peer,timeout = dnsreqs.get(chan) or (None,None) - debug3('dns_done: channel=%r peer=%r\n' % (chan, peer)) - if peer: - del dnsreqs[chan] - debug3('doing sendto %r\n' % (peer,)) - dnslistener.sendto(data, peer) - def ondns(): - pkt,peer = dnslistener.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,now+30 - mux.send(chan, ssnet.CMD_DNS_REQ, pkt) - mux.channels[chan] = lambda cmd,data: dns_done(chan,data) - for chan,(peer,timeout) in dnsreqs.items(): - if timeout < now: - del dnsreqs[chan] - debug3('Remaining DNS requests: %d\n' % len(dnsreqs)) + handlers.append(Handler([listener], lambda: onaccept(listener, mux, handlers))) + if dnslistener: - handlers.append(Handler([dnslistener], ondns)) + handlers.append(Handler([dnslistener], lambda: ondns(dnslistener, mux, handlers))) if seed_hosts != None: debug1('seed_hosts: %r\n' % seed_hosts) diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/firewall.py b/Sshuttle VPN.app/Contents/Resources/sshuttle/firewall.py index 4fd8c79..452715e 100644 --- a/Sshuttle VPN.app/Contents/Resources/sshuttle/firewall.py +++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/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 @@ from helpers import * # 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') @@ -398,6 +467,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/Sshuttle VPN.app/Contents/Resources/sshuttle/helpers.py b/Sshuttle VPN.app/Contents/Resources/sshuttle/helpers.py index c169d0c..45a028b 100644 --- a/Sshuttle VPN.app/Contents/Resources/sshuttle/helpers.py +++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/helpers.py @@ -1,4 +1,4 @@ -import sys, os, socket +import sys, os, socket, errno logprefix = '' verbose = 0 @@ -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/Sshuttle VPN.app/Contents/Resources/sshuttle/main.py b/Sshuttle VPN.app/Contents/Resources/sshuttle/main.py old mode 100644 new mode 100755 index 1cf00af..daf3b87 --- a/Sshuttle VPN.app/Contents/Resources/sshuttle/main.py +++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/main.py @@ -63,6 +63,7 @@ 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 +73,10 @@ 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: @@ -121,6 +126,9 @@ try: 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/Sshuttle VPN.app/Contents/Resources/sshuttle/ssnet.py b/Sshuttle VPN.app/Contents/Resources/sshuttle/ssnet.py index b6d73c2..a636ce1 100644 --- a/Sshuttle VPN.app/Contents/Resources/sshuttle/ssnet.py +++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/ssnet.py @@ -152,6 +152,17 @@ class SockWrapper: debug3('%r: fixed connect result: %s\n' % (self, e)) if e.args[0] in [errno.EINPROGRESS, errno.EALREADY]: pass # not connected yet + elif e.args[0] == 0: + # connected successfully (weird Linux bug?) + # Sometimes Linux seems to return EINVAL when it isn't + # invalid. This *may* be caused by a race condition + # between connect() and getsockopt(SO_ERROR) (ie. it + # finishes connecting in between the two, so there is no + # longer an error). However, I'm not sure of that. + # + # I did get at least one report that the problem went away + # when we added this, however. + self.connect_to = None elif e.args[0] == errno.EISCONN: # connected successfully (BSD) self.connect_to = None -- cgit v1.2.3