From 76359bc71ce4d850cb5778026dd359a0ee3cb6f8 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Tue, 1 Feb 2011 03:57:29 -0800 Subject: MacOS precompiled app package for sshuttle-0.50a-2-ga238f76 --- .../Contents/Resources/English.lproj/MainMenu.nib | Bin 27923 -> 29570 bytes Sshuttle VPN.app/Contents/Resources/askpass.pyc | Bin 0 -> 1146 bytes Sshuttle VPN.app/Contents/Resources/main.py | 33 +++-- Sshuttle VPN.app/Contents/Resources/models.py | 26 ++++ Sshuttle VPN.app/Contents/Resources/models.pyc | Bin 0 -> 8985 bytes Sshuttle VPN.app/Contents/Resources/my.pyc | Bin 0 -> 2809 bytes .../Contents/Resources/sshuttle/client.py | 81 +++++++---- .../Contents/Resources/sshuttle/client.pyc | Bin 0 -> 13665 bytes .../Resources/sshuttle/compat/__init__.pyc | Bin 0 -> 188 bytes .../Resources/sshuttle/compat/ssubprocess.pyc | Bin 0 -> 36231 bytes .../Contents/Resources/sshuttle/firewall.py | 161 +++++++++++++++++++-- .../Contents/Resources/sshuttle/firewall.pyc | Bin 0 -> 12663 bytes .../Contents/Resources/sshuttle/helpers.py | 40 ++++- .../Contents/Resources/sshuttle/helpers.pyc | Bin 0 -> 3118 bytes .../Contents/Resources/sshuttle/hostwatch.pyc | Bin 0 -> 9157 bytes .../Contents/Resources/sshuttle/main.py | 13 +- .../Contents/Resources/sshuttle/options.py | 9 +- .../Contents/Resources/sshuttle/options.pyc | Bin 0 -> 8735 bytes .../Contents/Resources/sshuttle/server.py | 40 ++++- .../Contents/Resources/sshuttle/server.pyc | Bin 0 -> 9224 bytes .../Contents/Resources/sshuttle/ssh.py | 14 +- .../Contents/Resources/sshuttle/ssh.pyc | Bin 0 -> 3963 bytes .../Contents/Resources/sshuttle/sshuttle | 13 +- .../Contents/Resources/sshuttle/ssnet.py | 10 +- .../Contents/Resources/sshuttle/ssnet.pyc | Bin 0 -> 21065 bytes .../Contents/Resources/sshuttle/ssyslog.pyc | Bin 0 -> 965 bytes Sshuttle VPN.app/Contents/Resources/stupid.py | 14 -- 27 files changed, 362 insertions(+), 92 deletions(-) create mode 100644 Sshuttle VPN.app/Contents/Resources/askpass.pyc create mode 100644 Sshuttle VPN.app/Contents/Resources/models.pyc create mode 100644 Sshuttle VPN.app/Contents/Resources/my.pyc create mode 100644 Sshuttle VPN.app/Contents/Resources/sshuttle/client.pyc create mode 100644 Sshuttle VPN.app/Contents/Resources/sshuttle/compat/__init__.pyc create mode 100644 Sshuttle VPN.app/Contents/Resources/sshuttle/compat/ssubprocess.pyc create mode 100644 Sshuttle VPN.app/Contents/Resources/sshuttle/firewall.pyc create mode 100644 Sshuttle VPN.app/Contents/Resources/sshuttle/helpers.pyc create mode 100644 Sshuttle VPN.app/Contents/Resources/sshuttle/hostwatch.pyc create mode 100644 Sshuttle VPN.app/Contents/Resources/sshuttle/options.pyc create mode 100644 Sshuttle VPN.app/Contents/Resources/sshuttle/server.pyc create mode 100644 Sshuttle VPN.app/Contents/Resources/sshuttle/ssh.pyc create mode 100644 Sshuttle VPN.app/Contents/Resources/sshuttle/ssnet.pyc create mode 100644 Sshuttle VPN.app/Contents/Resources/sshuttle/ssyslog.pyc delete mode 100644 Sshuttle VPN.app/Contents/Resources/stupid.py diff --git a/Sshuttle VPN.app/Contents/Resources/English.lproj/MainMenu.nib b/Sshuttle VPN.app/Contents/Resources/English.lproj/MainMenu.nib index 302f9fb..e0e3d32 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 new file mode 100644 index 0000000..180ae1a Binary files /dev/null and b/Sshuttle VPN.app/Contents/Resources/askpass.pyc differ diff --git a/Sshuttle VPN.app/Contents/Resources/main.py b/Sshuttle VPN.app/Contents/Resources/main.py index baa290d..cadf0d9 100644 --- a/Sshuttle VPN.app/Contents/Resources/main.py +++ b/Sshuttle VPN.app/Contents/Resources/main.py @@ -2,7 +2,8 @@ import sys, os, pty from AppKit import * import my, models, askpass -def sshuttle_args(host, auto_nets, auto_hosts, nets, debug): +def sshuttle_args(host, auto_nets, auto_hosts, dns, nets, debug, + no_latency_control): argv = [my.bundle_path('sshuttle/sshuttle', ''), '-r', host] assert(argv[0]) if debug: @@ -11,6 +12,10 @@ def sshuttle_args(host, auto_nets, auto_hosts, nets, debug): argv.append('--auto-nets') if auto_hosts: argv.append('--auto-hosts') + if dns: + argv.append('--dns') + if no_latency_control: + argv.append('--no-latency-control') argv += nets return argv @@ -131,6 +136,7 @@ class SshuttleController(NSObject): prefsWindow = objc.IBOutlet() serversController = objc.IBOutlet() logField = objc.IBOutlet() + noLatencyControlField = objc.IBOutlet() servers = [] conns = {} @@ -159,8 +165,11 @@ class SshuttleController(NSObject): conn = Runner(sshuttle_args(host, auto_nets = nets_mode == models.NET_AUTO, auto_hosts = server.autoHosts(), + dns = server.useDns(), nets = manual_nets, - debug = self.debugField.state()), + debug = self.debugField.state(), + no_latency_control + = self.noLatencyControlField.state()), logfunc=logfunc, promptfunc=promptfunc, serverobj=server) self.conns[host] = conn @@ -213,6 +222,7 @@ class SshuttleController(NSObject): if len(self.servers): for i in self.servers: host = i.host() + title = i.title() want = i.wantConnect() connected = i.connected() numnets = len(list(i.nets())) @@ -222,9 +232,9 @@ class SshuttleController(NSObject): additem('Connect %s (no routes)' % host, None, i) elif want: any_conn = i - additem('Disconnect %s' % host, self.cmd_disconnect, i) + additem('Disconnect %s' % title, self.cmd_disconnect, i) else: - additem('Connect %s' % host, self.cmd_connect, i) + additem('Connect %s' % title, self.cmd_connect, i) if not want: msg = 'Off' elif i.error(): @@ -236,12 +246,6 @@ class SshuttleController(NSObject): msg = 'Connecting...' any_inprogress = i addnote(' State: %s' % msg) - if i.autoNets() == 0: - addnote(' Routes: All') - elif i.autoNets() == 2: - addnote(' Routes: Auto') - else: - addnote(' Routes: Custom') else: addnote('No servers defined yet') @@ -279,13 +283,15 @@ class SshuttleController(NSObject): net.setWidth_(width) nl.append(net) - autoNets = s.get('autoNets', 1) - autoHosts = s.get('autoHosts', 1) + autoNets = s.get('autoNets', models.NET_AUTO) + autoHosts = s.get('autoHosts', True) + useDns = s.get('useDns', autoNets == models.NET_ALL) srv = models.SshuttleServer.alloc().init() srv.setHost_(host) srv.setAutoNets_(autoNets) srv.setAutoHosts_(autoHosts) srv.setNets_(nl) + srv.setUseDns_(useDns) sl.append(srv) self.serversController.addObjects_(sl) self.serversController.setSelectionIndex_(0) @@ -303,7 +309,8 @@ class SshuttleController(NSObject): d = dict(host=s.host(), nets=nets, autoNets=s.autoNets(), - autoHosts=s.autoHosts()) + autoHosts=s.autoHosts(), + useDns=s.useDns()) l.append(d) my.Defaults().setObject_forKey_(l, 'servers') self.fill_menu() diff --git a/Sshuttle VPN.app/Contents/Resources/models.py b/Sshuttle VPN.app/Contents/Resources/models.py index 858975e..ad8e538 100644 --- a/Sshuttle VPN.app/Contents/Resources/models.py +++ b/Sshuttle VPN.app/Contents/Resources/models.py @@ -92,11 +92,28 @@ class SshuttleServer(NSObject): if self.autoNets() == NET_MANUAL and not len(list(self.nets())): return False return True + + def title(self): + host = self.host() + if not host: + return host + an = self.autoNets() + suffix = "" + if an == NET_ALL: + suffix = " (all traffic)" + elif an == NET_MANUAL: + n = self.nets() + suffix = ' (%d subnet%s)' % (len(n), len(n)!=1 and 's' or '') + return self.host() + suffix + def setTitle_(self, v): + # title is always auto-generated + config_changed() def host(self): return getattr(self, '_k_host', None) def setHost_(self, v): self._k_host = v + self.setTitle_(None) config_changed() @objc.accessor def validateHost_error_(self, value, error): @@ -109,6 +126,7 @@ class SshuttleServer(NSObject): return getattr(self, '_k_nets', []) def setNets_(self, v): self._k_nets = v + self.setTitle_(None) config_changed() def netsHidden(self): #print 'checking netsHidden' @@ -122,6 +140,8 @@ class SshuttleServer(NSObject): def setAutoNets_(self, v): self._k_autoNets = v self.setNetsHidden_(-1) + self.setUseDns_(v == NET_ALL) + self.setTitle_(None) config_changed() def autoHosts(self): @@ -129,3 +149,9 @@ class SshuttleServer(NSObject): def setAutoHosts_(self, v): self._k_autoHosts = v config_changed() + + def useDns(self): + return getattr(self, '_k_useDns', False) + def setUseDns_(self, v): + self._k_useDns = v + config_changed() diff --git a/Sshuttle VPN.app/Contents/Resources/models.pyc b/Sshuttle VPN.app/Contents/Resources/models.pyc new file mode 100644 index 0000000..bc9c70a Binary files /dev/null and b/Sshuttle VPN.app/Contents/Resources/models.pyc differ diff --git a/Sshuttle VPN.app/Contents/Resources/my.pyc b/Sshuttle VPN.app/Contents/Resources/my.pyc new file mode 100644 index 0000000..112d1e6 Binary files /dev/null and b/Sshuttle VPN.app/Contents/Resources/my.pyc differ diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/client.py b/Sshuttle VPN.app/Contents/Resources/sshuttle/client.py index dbd11de..1ade5d9 100644 --- a/Sshuttle VPN.app/Contents/Resources/sshuttle/client.py +++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/client.py @@ -1,4 +1,4 @@ -import struct, socket, select, errno, re, signal +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 @@ -6,21 +6,6 @@ from helpers import * _extra_fd = os.open('/dev/null', os.O_RDONLY) -def _islocal(ip): - sock = socket.socket() - try: - try: - sock.bind((ip, 0)) - except socket.error, e: - if e.args[0] == errno.EADDRNOTAVAIL: - return False # not a local IP - else: - raise - finally: - sock.close() - return True # it's a local IP, or there would have been an error - - def got_signal(signum, frame): log('exiting on signal %d\n' % signum) sys.exit(1) @@ -111,14 +96,15 @@ def original_dst(sock): class FirewallClient: - def __init__(self, port, subnets_include, subnets_exclude): + 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[0]] + ['-v'] * (helpers.verbose or 0) + - ['--firewall', str(port)]) + ['--firewall', str(port), str(dnsport)]) if ssyslog._p: argvbase += ['--syslog'] argv_tries = [ @@ -189,7 +175,8 @@ class FirewallClient: raise Fatal('cleanup: %r returned %d' % (self.argv, rv)) -def _main(listener, fw, ssh_cmd, remotename, python, seed_hosts, auto_nets, +def _main(listener, fw, ssh_cmd, remotename, python, latency_control, + dnslistener, seed_hosts, auto_nets, syslog, daemon): handlers = [] if helpers.verbose >= 1: @@ -200,7 +187,8 @@ def _main(listener, fw, ssh_cmd, remotename, python, seed_hosts, auto_nets, try: (serverproc, serversock) = ssh.connect(ssh_cmd, remotename, python, - stderr=ssyslog._p and ssyslog._p.stdin) + 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)") @@ -280,7 +268,7 @@ def _main(listener, fw, ssh_cmd, remotename, python, seed_hosts, auto_nets, 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]): + if dstip[1] == listener.getsockname()[1] and islocal(dstip[0]): debug1("-- ignored: that's my address!\n") sock.close() return @@ -290,6 +278,30 @@ def _main(listener, fw, ssh_cmd, remotename, python, seed_hosts, auto_nets, 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)) + if dnslistener: + handlers.append(Handler([dnslistener], ondns)) + if seed_hosts != None: debug1('seed_hosts: %r\n' % seed_hosts) mux.send(0, ssnet.CMD_HOST_REQ, '\n'.join(seed_hosts)) @@ -300,11 +312,13 @@ def _main(listener, fw, ssh_cmd, remotename, python, seed_hosts, auto_nets, raise Fatal('server died with error code %d' % rv) ssnet.runonce(handlers, mux) + if latency_control: + mux.check_fullness() mux.callback() - mux.check_fullness() -def main(listenip, ssh_cmd, remotename, python, seed_hosts, auto_nets, +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() @@ -315,8 +329,7 @@ def main(listenip, ssh_cmd, remotename, python, seed_hosts, auto_nets, log("%s\n" % e) return 5 debug1('Starting sshuttle proxy.\n') - listener = socket.socket() - listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + if listenip[1]: ports = [listenip[1]] else: @@ -326,8 +339,13 @@ def main(listenip, ssh_cmd, remotename, python, seed_hosts, auto_nets, 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: @@ -340,11 +358,20 @@ def main(listenip, ssh_cmd, remotename, python, seed_hosts, auto_nets, listenip = listener.getsockname() debug1('Listening on %r.\n' % (listenip,)) - fw = FirewallClient(listenip[1], subnets_include, subnets_exclude) + 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, seed_hosts, auto_nets, syslog, daemon) + python, latency_control, dnslistener, + seed_hosts, auto_nets, syslog, daemon) finally: try: if daemon: diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/client.pyc b/Sshuttle VPN.app/Contents/Resources/sshuttle/client.pyc new file mode 100644 index 0000000..64b7efb Binary files /dev/null and b/Sshuttle VPN.app/Contents/Resources/sshuttle/client.pyc differ diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/compat/__init__.pyc b/Sshuttle VPN.app/Contents/Resources/sshuttle/compat/__init__.pyc new file mode 100644 index 0000000..736e5ec Binary files /dev/null and b/Sshuttle VPN.app/Contents/Resources/sshuttle/compat/__init__.pyc differ diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/compat/ssubprocess.pyc b/Sshuttle VPN.app/Contents/Resources/sshuttle/compat/ssubprocess.pyc new file mode 100644 index 0000000..dcb10bd Binary files /dev/null and b/Sshuttle VPN.app/Contents/Resources/sshuttle/compat/ssubprocess.pyc differ diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/firewall.py b/Sshuttle VPN.app/Contents/Resources/sshuttle/firewall.py index 044ac52..c7557ed 100644 --- a/Sshuttle VPN.app/Contents/Resources/sshuttle/firewall.py +++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/firewall.py @@ -1,8 +1,11 @@ -import re, errno +import re, errno, socket, select, struct import compat.ssubprocess as ssubprocess import helpers, ssyslog from helpers import * +# python doesn't have a definition for this +IPPROTO_DIVERT = 254 + def ipt_chain_exists(name): argv = ['iptables', '-t', 'nat', '-nL'] @@ -23,12 +26,33 @@ def ipt(*args): raise Fatal('%r returned %d' % (argv, rv)) +_no_ttl_module = False +def ipt_ttl(*args): + global _no_ttl_module + if not _no_ttl_module: + # we avoid infinite loops by generating server-side connections + # with ttl 42. This makes the client side not recapture those + # connections, in case client == server. + try: + argsplus = list(args) + ['-m', 'ttl', '!', '--ttl', '42'] + ipt(*argsplus) + except Fatal: + ipt(*args) + # we only get here if the non-ttl attempt succeeds + log('sshuttle: warning: your iptables is missing ' + 'the ttl module.\n') + _no_ttl_module = True + else: + ipt(*args) + + + # We name the chain based on the transproxy port number so that it's possible # to run multiple copies of sshuttle at the same time. Of course, the # multiple copies shouldn't have overlapping subnets, or only the most- # recently-started one will win (because we use "-I OUTPUT 1" instead of # "-A OUTPUT"). -def do_iptables(port, subnets): +def do_iptables(port, dnsport, subnets): chain = 'sshuttle-%s' % port # basic cleanup/setup of chains @@ -38,12 +62,13 @@ def do_iptables(port, subnets): ipt('-F', chain) ipt('-X', chain) - if subnets: + if subnets or dnsport: ipt('-N', chain) ipt('-F', chain) ipt('-I', 'OUTPUT', '1', '-j', chain) ipt('-I', 'PREROUTING', '1', '-j', chain) + if subnets: # create new subnet entries. Note that we're sorting in a very # particular order: we need to go from most-specific (largest swidth) # to least-specific, and at any given level of specificity, we want @@ -55,12 +80,19 @@ def do_iptables(port, subnets): '--dest', '%s/%s' % (snet,swidth), '-p', 'tcp') else: - ipt('-A', chain, '-j', 'REDIRECT', - '--dest', '%s/%s' % (snet,swidth), - '-p', 'tcp', - '--to-ports', str(port), - '-m', 'ttl', '!', '--ttl', '42' # to prevent infinite loops - ) + ipt_ttl('-A', chain, '-j', 'REDIRECT', + '--dest', '%s/%s' % (snet,swidth), + '-p', 'tcp', + '--to-ports', str(port)) + + if dnsport: + nslist = resolvconf_nameservers() + for ip in nslist: + ipt_ttl('-A', chain, '-j', 'REDIRECT', + '--dest', '%s/32' % ip, + '-p', 'udp', + '--dport', '53', + '--to-ports', str(dnsport)) def ipfw_rule_exists(n): @@ -69,7 +101,7 @@ def ipfw_rule_exists(n): found = False for line in p.stdout: if line.startswith('%05d ' % n): - if not ('ipttl 42 setup keep-state' in line + if not ('ipttl 42' in line or ('skipto %d' % (n+1)) in line or 'check-state' in line): log('non-sshuttle ipfw rule: %r\n' % line.strip()) @@ -116,6 +148,39 @@ def sysctl_set(name, val): if val != oldval: _changedctls.append(name) return _sysctl_set(name, val) + + +def _udp_unpack(p): + src = (socket.inet_ntoa(p[12:16]), struct.unpack('!H', p[20:22])[0]) + dst = (socket.inet_ntoa(p[16:20]), struct.unpack('!H', p[22:24])[0]) + return src, dst + + +def _udp_repack(p, src, dst): + addrs = socket.inet_aton(src[0]) + socket.inet_aton(dst[0]) + ports = struct.pack('!HH', src[1], dst[1]) + return p[:12] + addrs + ports + p[24:] + + +_real_dns_server = [None] +def _handle_diversion(divertsock, dnsport): + p,tag = divertsock.recvfrom(4096) + src,dst = _udp_unpack(p) + debug3('got diverted packet from %r to %r\n' % (src, dst)) + if dst[1] == 53: + # outgoing DNS + debug3('...packet is a DNS request.\n') + _real_dns_server[0] = dst + dst = ('127.0.0.1', dnsport) + elif src[1] == dnsport: + if islocal(src[0]): + debug3('...packet is a DNS response.\n') + src = _real_dns_server[0] + else: + log('weird?! unexpected divert from %r to %r\n' % (src, dst)) + assert(0) + newp = _udp_repack(p, src, dst) + divertsock.sendto(newp, tag) def ipfw(*args): @@ -126,7 +191,7 @@ def ipfw(*args): raise Fatal('%r returned %d' % (argv, rv)) -def do_ipfw(port, subnets): +def do_ipfw(port, dnsport, subnets): sport = str(port) xsport = str(port+1) @@ -139,13 +204,14 @@ def do_ipfw(port, subnets): oldval = _oldctls[name] _sysctl_set(name, oldval) - if subnets: + if subnets or dnsport: sysctl_set('net.inet.ip.fw.enable', 1) sysctl_set('net.inet.ip.scopedroute', 0) ipfw('add', sport, 'check-state', 'ip', 'from', 'any', 'to', 'any') - + + if subnets: # create new subnet entries for swidth,sexclude,snet in sorted(subnets, reverse=True): if sexclude: @@ -158,6 +224,65 @@ def do_ipfw(port, subnets): 'from', 'any', 'to', '%s/%s' % (snet,swidth), 'not', 'ipttl', '42', 'keep-state', 'setup') + # This part is much crazier than it is on Linux, because MacOS (at least + # 10.6, and probably other versions, and maybe FreeBSD too) doesn't + # correctly fixup the dstip/dstport for UDP packets when it puts them + # through a 'fwd' rule. It also doesn't fixup the srcip/srcport in the + # response packet. In Linux iptables, all that happens magically for us, + # so we just redirect the packets and relax. + # + # On MacOS, we have to fix the ports ourselves. For that, we use a + # 'divert' socket, which receives raw packets and lets us mangle them. + # + # Here's how it works. Let's say the local DNS server is 1.1.1.1:53, + # and the remote DNS server is 2.2.2.2:53, and the local transproxy port + # is 10.0.0.1:12300, and a client machine is making a request from + # 10.0.0.5:9999. We see a packet like this: + # 10.0.0.5:9999 -> 1.1.1.1:53 + # Since the destip:port matches one of our local nameservers, it will + # match a 'fwd' rule, thus grabbing it on the local machine. However, + # the local kernel will then see a packet addressed to *:53 and + # not know what to do with it; there's nobody listening on port 53. Thus, + # we divert it, rewriting it into this: + # 10.0.0.5:9999 -> 10.0.0.1:12300 + # This gets proxied out to the server, which sends it to 2.2.2.2:53, + # and the answer comes back, and the proxy sends it back out like this: + # 10.0.0.1:12300 -> 10.0.0.5:9999 + # But that's wrong! The original machine expected an answer from + # 1.1.1.1:53, so we have to divert the *answer* and rewrite it: + # 1.1.1.1:53 -> 10.0.0.5:9999 + # + # See? Easy stuff. + if dnsport: + divertsock = socket.socket(socket.AF_INET, socket.SOCK_RAW, + IPPROTO_DIVERT) + divertsock.bind(('0.0.0.0', port)) # IP field is ignored + + nslist = resolvconf_nameservers() + for ip in nslist: + # relabel and then catch outgoing DNS requests + ipfw('add', sport, 'divert', sport, + 'log', 'udp', + 'from', 'any', 'to', '%s/32' % ip, '53', + 'not', 'ipttl', '42') + # relabel DNS responses + ipfw('add', sport, 'divert', sport, + 'log', 'udp', + 'from', 'any', str(dnsport), 'to', 'any', + 'not', 'ipttl', '42') + + def do_wait(): + while 1: + r,w,x = select.select([sys.stdin, divertsock], [], []) + if divertsock in r: + _handle_diversion(divertsock, dnsport) + if sys.stdin in r: + return + else: + do_wait = None + + return do_wait + def program_exists(name): paths = (os.getenv('PATH') or os.defpath).split(os.pathsep) @@ -166,6 +291,7 @@ def program_exists(name): if os.path.exists(fn): return not os.path.isdir(fn) and os.access(fn, os.X_OK) + hostmap = {} def rewrite_etc_hosts(port): HOSTSFILE='/etc/hosts' @@ -216,9 +342,11 @@ def restore_etc_hosts(port): # 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 # are hopefully harmless. -def main(port, syslog): +def main(port, dnsport, syslog): assert(port > 0) assert(port <= 65535) + assert(dnsport >= 0) + assert(dnsport <= 65535) if os.getuid() != 0: raise Fatal('you must be root (or enable su/sudo) to set the firewall') @@ -272,7 +400,7 @@ def main(port, syslog): try: if line: debug1('firewall manager: starting transproxy.\n') - do_it(port, subnets) + do_wait = do_it(port, dnsport, subnets) sys.stdout.write('STARTED\n') try: @@ -286,6 +414,7 @@ def main(port, syslog): # to stay running so that we don't need a *second* password # authentication at shutdown time - that cleanup is important! while 1: + if do_wait: do_wait() line = sys.stdin.readline(128) if line.startswith('HOST '): (name,ip) = line[5:].strip().split(',', 1) @@ -300,5 +429,5 @@ def main(port, syslog): debug1('firewall manager: undoing changes.\n') except: pass - do_it(port, []) + do_it(port, 0, []) restore_etc_hosts(port) diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/firewall.pyc b/Sshuttle VPN.app/Contents/Resources/sshuttle/firewall.pyc new file mode 100644 index 0000000..7539b6f Binary files /dev/null and b/Sshuttle VPN.app/Contents/Resources/sshuttle/firewall.pyc differ diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/helpers.py b/Sshuttle VPN.app/Contents/Resources/sshuttle/helpers.py index 18871a2..c169d0c 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 +import sys, os, socket logprefix = '' verbose = 0 @@ -35,3 +35,41 @@ def list_contains_any(l, sub): if i in l: return True return False + + +def resolvconf_nameservers(): + l = [] + for line in open('/etc/resolv.conf'): + words = line.lower().split() + if len(words) >= 2 and words[0] == 'nameserver': + l.append(words[1]) + return l + + +def resolvconf_random_nameserver(): + l = resolvconf_nameservers() + if l: + if len(l) > 1: + # don't import this unless we really need it + import random + random.shuffle(l) + return l[0] + else: + return '127.0.0.1' + + +def islocal(ip): + sock = socket.socket() + try: + try: + sock.bind((ip, 0)) + except socket.error, e: + if e.args[0] == errno.EADDRNOTAVAIL: + return False # not a local IP + else: + raise + finally: + sock.close() + return True # it's a local IP, or there would have been an error + + diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/helpers.pyc b/Sshuttle VPN.app/Contents/Resources/sshuttle/helpers.pyc new file mode 100644 index 0000000..ced3de3 Binary files /dev/null and b/Sshuttle VPN.app/Contents/Resources/sshuttle/helpers.pyc differ diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/hostwatch.pyc b/Sshuttle VPN.app/Contents/Resources/sshuttle/hostwatch.pyc new file mode 100644 index 0000000..5dbb1cc Binary files /dev/null and b/Sshuttle VPN.app/Contents/Resources/sshuttle/hostwatch.pyc differ diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/main.py b/Sshuttle VPN.app/Contents/Resources/sshuttle/main.py index 66954f8..e76e596 100755 --- a/Sshuttle VPN.app/Contents/Resources/sshuttle/main.py +++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/main.py @@ -54,12 +54,14 @@ 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 [python] r,remote= ssh hostname (and optional username) of remote sshuttle server x,exclude= exclude this subnet (can be used more than once) v,verbose increase debug message verbosity e,ssh-cmd= the command to use to connect to the remote [ssh] seed-hosts= with -H, use these hostnames for initial scan (comma-separated) +no-latency-control sacrifice latency to improve bandwidth benchmarks D,daemon run in the background as a daemon syslog send log messages to syslog (default if you use --daemon) pidfile= pidfile name (only if using --daemon) [./sshuttle.pid] @@ -67,7 +69,7 @@ server (internal use only) firewall (internal use only) hostwatch (internal use only) """ -o = options.Options('sshuttle', optspec) +o = options.Options(optspec) (opt, flags, extra) = o.parse(sys.argv[1:]) if opt.daemon: @@ -78,11 +80,12 @@ 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) != 1: - o.fatal('exactly one argument expected') - sys.exit(firewall.main(int(extra[0]), opt.syslog)) + 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: @@ -108,6 +111,8 @@ try: opt.ssh_cmd, remotename, opt.python, + opt.latency_control, + opt.dns, sh, opt.auto_nets, parse_subnets(includes), diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/options.py b/Sshuttle VPN.app/Contents/Resources/sshuttle/options.py index 25322fb..2f3fff6 100644 --- a/Sshuttle VPN.app/Contents/Resources/sshuttle/options.py +++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/options.py @@ -76,9 +76,8 @@ class Options: By default, the parser function is getopt.gnu_getopt, and the abort behaviour is to exit the program. """ - def __init__(self, exe, optspec, optfunc=getopt.gnu_getopt, + def __init__(self, optspec, optfunc=getopt.gnu_getopt, onabort=_default_onabort): - self.exe = exe self.optspec = optspec self._onabort = onabort self.optfunc = optfunc @@ -122,8 +121,8 @@ class Options: defval = None flagl = flags.split(',') flagl_nice = [] - for f in flagl: - f,dvi = _remove_negative_kv(f, _intify(defval)) + for _f in flagl: + f,dvi = _remove_negative_kv(_f, _intify(defval)) self._aliases[f] = _remove_negative_k(flagl[0]) self._hasparms[f] = has_parm self._defaults[f] = dvi @@ -135,7 +134,7 @@ class Options: self._aliases[f_nice] = _remove_negative_k(flagl[0]) self._longopts.append(f + (has_parm and '=' or '')) self._longopts.append('no-' + f) - flagl_nice.append('--' + f) + flagl_nice.append('--' + _f) flags_nice = ', '.join(flagl_nice) if has_parm: flags_nice += ' ...' diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/options.pyc b/Sshuttle VPN.app/Contents/Resources/sshuttle/options.pyc new file mode 100644 index 0000000..99ebce5 Binary files /dev/null and b/Sshuttle VPN.app/Contents/Resources/sshuttle/options.pyc differ diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/server.py b/Sshuttle VPN.app/Contents/Resources/sshuttle/server.py index 24dd462..45bc2fc 100644 --- a/Sshuttle VPN.app/Contents/Resources/sshuttle/server.py +++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/server.py @@ -1,4 +1,4 @@ -import re, struct, socket, select, traceback +import re, struct, socket, select, traceback, time if not globals().get('skip_imports'): import ssnet, helpers, hostwatch import compat.ssubprocess as ssubprocess @@ -106,11 +106,31 @@ class Hostwatch: self.sock = None +class DnsProxy(Handler): + def __init__(self, mux, chan, request): + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + Handler.__init__(self, [sock]) + self.sock = sock + self.timeout = time.time()+30 + self.mux = mux + self.chan = chan + self.sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42) + self.sock.connect((resolvconf_random_nameserver(), 53)) + self.sock.send(request) + + def callback(self): + data = self.sock.recv(4096) + debug2('DNS response: %d bytes\n' % len(data)) + self.mux.send(self.chan, ssnet.CMD_DNS_RESPONSE, data) + self.ok = False + + def main(): if helpers.verbose >= 1: helpers.logprefix = ' s: ' else: helpers.logprefix = 'server: ' + debug1('latency control setting = %r\n' % latency_control) routes = list(list_routes()) debug1('available routes:\n') @@ -164,6 +184,14 @@ def main(): handlers.append(Proxy(MuxWrapper(mux, channel), outwrap)) mux.new_channel = new_channel + dnshandlers = {} + def dns_req(channel, data): + debug2('Incoming DNS request.\n') + h = DnsProxy(mux, channel, data) + handlers.append(h) + dnshandlers[channel] = h + mux.got_dns_req = dns_req + while mux.ok: if hw.pid: assert(hw.pid > 0) @@ -172,5 +200,13 @@ def main(): raise Fatal('hostwatch exited unexpectedly: code 0x%04x\n' % rv) ssnet.runonce(handlers, mux) - mux.check_fullness() + if latency_control: + mux.check_fullness() mux.callback() + + if dnshandlers: + now = time.time() + for channel,h in dnshandlers.items(): + if h.timeout < now or not h.ok: + del dnshandlers[channel] + h.ok = False diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/server.pyc b/Sshuttle VPN.app/Contents/Resources/sshuttle/server.pyc new file mode 100644 index 0000000..6e4a14b Binary files /dev/null and b/Sshuttle VPN.app/Contents/Resources/sshuttle/server.pyc differ diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/ssh.py b/Sshuttle VPN.app/Contents/Resources/sshuttle/ssh.py index ac7f411..9a6270a 100644 --- a/Sshuttle VPN.app/Contents/Resources/sshuttle/ssh.py +++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/ssh.py @@ -14,14 +14,16 @@ def readfile(name): raise Exception("can't find file %r in any of %r" % (name, path)) -def empackage(z, filename): +def empackage(z, filename, data=None): (path,basename) = os.path.split(filename) - content = z.compress(readfile(filename)) + if not data: + data = readfile(filename) + content = z.compress(data) content += z.flush(zlib.Z_SYNC_FLUSH) - return '%s\n%d\n%s' % (basename,len(content), content) + return '%s\n%d\n%s' % (basename, len(content), content) -def connect(ssh_cmd, rhostport, python, stderr): +def connect(ssh_cmd, rhostport, python, stderr, options): main_exe = sys.argv[0] portl = [] @@ -52,7 +54,9 @@ def connect(ssh_cmd, rhostport, python, stderr): z = zlib.compressobj(1) content = readfile('assembler.py') - content2 = (empackage(z, 'helpers.py') + + optdata = ''.join("%s=%r\n" % (k,v) for (k,v) in options.items()) + content2 = (empackage(z, 'cmdline_options.py', optdata) + + empackage(z, 'helpers.py') + empackage(z, 'compat/ssubprocess.py') + empackage(z, 'ssnet.py') + empackage(z, 'hostwatch.py') + diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/ssh.pyc b/Sshuttle VPN.app/Contents/Resources/sshuttle/ssh.pyc new file mode 100644 index 0000000..6118b39 Binary files /dev/null and b/Sshuttle VPN.app/Contents/Resources/sshuttle/ssh.pyc differ diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/sshuttle b/Sshuttle VPN.app/Contents/Resources/sshuttle/sshuttle index 66954f8..e76e596 100755 --- a/Sshuttle VPN.app/Contents/Resources/sshuttle/sshuttle +++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/sshuttle @@ -54,12 +54,14 @@ 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 [python] r,remote= ssh hostname (and optional username) of remote sshuttle server x,exclude= exclude this subnet (can be used more than once) v,verbose increase debug message verbosity e,ssh-cmd= the command to use to connect to the remote [ssh] seed-hosts= with -H, use these hostnames for initial scan (comma-separated) +no-latency-control sacrifice latency to improve bandwidth benchmarks D,daemon run in the background as a daemon syslog send log messages to syslog (default if you use --daemon) pidfile= pidfile name (only if using --daemon) [./sshuttle.pid] @@ -67,7 +69,7 @@ server (internal use only) firewall (internal use only) hostwatch (internal use only) """ -o = options.Options('sshuttle', optspec) +o = options.Options(optspec) (opt, flags, extra) = o.parse(sys.argv[1:]) if opt.daemon: @@ -78,11 +80,12 @@ 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) != 1: - o.fatal('exactly one argument expected') - sys.exit(firewall.main(int(extra[0]), opt.syslog)) + 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: @@ -108,6 +111,8 @@ try: opt.ssh_cmd, remotename, opt.python, + opt.latency_control, + opt.dns, sh, opt.auto_nets, parse_subnets(includes), diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/ssnet.py b/Sshuttle VPN.app/Contents/Resources/sshuttle/ssnet.py index 62fa378..554d870 100644 --- a/Sshuttle VPN.app/Contents/Resources/sshuttle/ssnet.py +++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/ssnet.py @@ -21,6 +21,8 @@ CMD_DATA = 0x4206 CMD_ROUTES = 0x4207 CMD_HOST_REQ = 0x4208 CMD_HOST_LIST = 0x4209 +CMD_DNS_REQ = 0x420a +CMD_DNS_RESPONSE = 0x420b cmd_to_name = { CMD_EXIT: 'EXIT', @@ -33,6 +35,8 @@ cmd_to_name = { CMD_ROUTES: 'ROUTES', CMD_HOST_REQ: 'HOST_REQ', CMD_HOST_LIST: 'HOST_LIST', + CMD_DNS_REQ: 'DNS_REQ', + CMD_DNS_RESPONSE: 'DNS_RESPONSE', } @@ -281,7 +285,7 @@ class Mux(Handler): Handler.__init__(self, [rsock, wsock]) self.rsock = rsock self.wsock = wsock - self.new_channel = self.got_routes = None + self.new_channel = self.got_dns_req = self.got_routes = None self.got_host_req = self.got_host_list = None self.channels = {} self.chani = 0 @@ -343,6 +347,10 @@ class Mux(Handler): assert(not self.channels.get(channel)) if self.new_channel: self.new_channel(channel, data) + elif cmd == CMD_DNS_REQ: + assert(not self.channels.get(channel)) + if self.got_dns_req: + self.got_dns_req(channel, data) elif cmd == CMD_ROUTES: if self.got_routes: self.got_routes(data) diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/ssnet.pyc b/Sshuttle VPN.app/Contents/Resources/sshuttle/ssnet.pyc new file mode 100644 index 0000000..a810bc3 Binary files /dev/null and b/Sshuttle VPN.app/Contents/Resources/sshuttle/ssnet.pyc differ diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/ssyslog.pyc b/Sshuttle VPN.app/Contents/Resources/sshuttle/ssyslog.pyc new file mode 100644 index 0000000..b5ac023 Binary files /dev/null and b/Sshuttle VPN.app/Contents/Resources/sshuttle/ssyslog.pyc differ diff --git a/Sshuttle VPN.app/Contents/Resources/stupid.py b/Sshuttle VPN.app/Contents/Resources/stupid.py deleted file mode 100644 index fdb1e0b..0000000 --- a/Sshuttle VPN.app/Contents/Resources/stupid.py +++ /dev/null @@ -1,14 +0,0 @@ -import os - -pid = os.fork() -if pid == 0: - # child - try: - os.setsid() - #os.execvp('sudo', ['sudo', 'SSH_ASKPASS=%s' % os.path.abspath('askpass.py'), 'ssh', 'afterlife', 'ls']) - os.execvp('ssh', ['ssh', 'afterlife', 'ls']) - finally: - os._exit(44) -else: - # parent - os.wait() -- cgit v1.2.3