summaryrefslogtreecommitdiffstats
path: root/Sshuttle VPN.app/Contents/Resources/sshuttle/client.py
diff options
context:
space:
mode:
Diffstat (limited to 'Sshuttle VPN.app/Contents/Resources/sshuttle/client.py')
-rw-r--r--Sshuttle VPN.app/Contents/Resources/sshuttle/client.py356
1 files changed, 356 insertions, 0 deletions
diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/client.py b/Sshuttle VPN.app/Contents/Resources/sshuttle/client.py
new file mode 100644
index 0000000..dbd11de
--- /dev/null
+++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/client.py
@@ -0,0 +1,356 @@
+import struct, socket, select, errno, re, signal
+import compat.ssubprocess as ssubprocess
+import helpers, ssnet, ssh, ssyslog
+from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
+from helpers import *
+
+_extra_fd = os.open('/dev/null', os.O_RDONLY)
+
+def _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)
+
+
+_pidname = None
+def check_daemon(pidfile):
+ global _pidname
+ _pidname = os.path.abspath(pidfile)
+ try:
+ oldpid = open(_pidname).read(1024)
+ except IOError, e:
+ if e.errno == errno.ENOENT:
+ return # no pidfile, ok
+ else:
+ raise Fatal("can't read %s: %s" % (_pidname, e))
+ if not oldpid:
+ os.unlink(_pidname)
+ return # invalid pidfile, ok
+ oldpid = int(oldpid.strip() or 0)
+ if oldpid <= 0:
+ os.unlink(_pidname)
+ return # invalid pidfile, ok
+ try:
+ os.kill(oldpid, 0)
+ except OSError, e:
+ if e.errno == errno.ESRCH:
+ os.unlink(_pidname)
+ return # outdated pidfile, ok
+ elif e.errno == errno.EPERM:
+ pass
+ else:
+ raise
+ raise Fatal("%s: sshuttle is already running (pid=%d)"
+ % (_pidname, oldpid))
+
+
+def daemonize():
+ if os.fork():
+ os._exit(0)
+ os.setsid()
+ if os.fork():
+ os._exit(0)
+
+ outfd = os.open(_pidname, os.O_WRONLY|os.O_CREAT|os.O_EXCL, 0666)
+ try:
+ os.write(outfd, '%d\n' % os.getpid())
+ finally:
+ os.close(outfd)
+ os.chdir("/")
+
+ # Normal exit when killed, or try/finally won't work and the pidfile won't
+ # be deleted.
+ signal.signal(signal.SIGTERM, got_signal)
+
+ si = open('/dev/null', 'r+')
+ os.dup2(si.fileno(), 0)
+ os.dup2(si.fileno(), 1)
+ si.close()
+
+ ssyslog.stderr_to_syslog()
+
+
+def daemon_cleanup():
+ try:
+ os.unlink(_pidname)
+ except OSError, e:
+ if e.errno == errno.ENOENT:
+ pass
+ else:
+ raise
+
+
+def original_dst(sock):
+ try:
+ SO_ORIGINAL_DST = 80
+ SOCKADDR_MIN = 16
+ sockaddr_in = sock.getsockopt(socket.SOL_IP,
+ SO_ORIGINAL_DST, SOCKADDR_MIN)
+ (proto, port, a,b,c,d) = struct.unpack('!HHBBBB', sockaddr_in[:8])
+ assert(socket.htons(proto) == socket.AF_INET)
+ ip = '%d.%d.%d.%d' % (a,b,c,d)
+ return (ip,port)
+ except socket.error, e:
+ if e.args[0] == errno.ENOPROTOOPT:
+ return sock.getsockname()
+ raise
+
+
+class FirewallClient:
+ def __init__(self, port, subnets_include, subnets_exclude):
+ self.port = port
+ self.auto_nets = []
+ self.subnets_include = subnets_include
+ self.subnets_exclude = subnets_exclude
+ argvbase = ([sys.argv[0]] +
+ ['-v'] * (helpers.verbose or 0) +
+ ['--firewall', str(port)])
+ if ssyslog._p:
+ argvbase += ['--syslog']
+ argv_tries = [
+ ['sudo', '-p', '[local sudo] Password: '] + argvbase,
+ ['su', '-c', ' '.join(argvbase)],
+ argvbase
+ ]
+
+ # we can't use stdin/stdout=subprocess.PIPE here, as we normally would,
+ # because stupid Linux 'su' requires that stdin be attached to a tty.
+ # Instead, attach a *bidirectional* socket to its stdout, and use
+ # that for talking in both directions.
+ (s1,s2) = socket.socketpair()
+ def setup():
+ # run in the child process
+ s2.close()
+ e = None
+ if os.getuid() == 0:
+ argv_tries = argv_tries[-1:] # last entry only
+ for argv in argv_tries:
+ try:
+ if argv[0] == 'su':
+ sys.stderr.write('[local su] ')
+ self.p = ssubprocess.Popen(argv, stdout=s1, preexec_fn=setup)
+ e = None
+ break
+ except OSError, e:
+ pass
+ self.argv = argv
+ s1.close()
+ self.pfile = s2.makefile('wb+')
+ if e:
+ log('Spawning firewall manager: %r\n' % self.argv)
+ raise Fatal(e)
+ line = self.pfile.readline()
+ self.check()
+ if line != 'READY\n':
+ raise Fatal('%r expected READY, got %r' % (self.argv, line))
+
+ def check(self):
+ rv = self.p.poll()
+ if rv:
+ raise Fatal('%r returned %d' % (self.argv, rv))
+
+ def start(self):
+ self.pfile.write('ROUTES\n')
+ for (ip,width) in self.subnets_include+self.auto_nets:
+ self.pfile.write('%d,0,%s\n' % (width, ip))
+ for (ip,width) in self.subnets_exclude:
+ self.pfile.write('%d,1,%s\n' % (width, ip))
+ self.pfile.write('GO\n')
+ self.pfile.flush()
+ line = self.pfile.readline()
+ self.check()
+ if line != 'STARTED\n':
+ raise Fatal('%r expected STARTED, got %r' % (self.argv, line))
+
+ def sethostip(self, hostname, ip):
+ assert(not re.search(r'[^-\w]', hostname))
+ assert(not re.search(r'[^0-9.]', ip))
+ self.pfile.write('HOST %s,%s\n' % (hostname, ip))
+ self.pfile.flush()
+
+ def done(self):
+ self.pfile.close()
+ rv = self.p.wait()
+ if rv:
+ raise Fatal('cleanup: %r returned %d' % (self.argv, rv))
+
+
+def _main(listener, fw, ssh_cmd, remotename, python, seed_hosts, auto_nets,
+ syslog, daemon):
+ handlers = []
+ if helpers.verbose >= 1:
+ helpers.logprefix = 'c : '
+ else:
+ helpers.logprefix = 'client: '
+ debug1('connecting to server...\n')
+
+ try:
+ (serverproc, serversock) = ssh.connect(ssh_cmd, remotename, python,
+ stderr=ssyslog._p and ssyslog._p.stdin)
+ except socket.error, e:
+ if e.args[0] == errno.EPIPE:
+ raise Fatal("failed to establish ssh session (1)")
+ else:
+ raise
+ mux = Mux(serversock, serversock)
+ handlers.append(mux)
+
+ expected = 'SSHUTTLE0001'
+ try:
+ initstring = serversock.recv(len(expected))
+ except socket.error, e:
+ if e.args[0] == errno.ECONNRESET:
+ raise Fatal("failed to establish ssh session (2)")
+ else:
+ raise
+
+ rv = serverproc.poll()
+ if rv:
+ raise Fatal('server died with error code %d' % rv)
+
+ if initstring != expected:
+ raise Fatal('expected server init string %r; got %r'
+ % (expected, initstring))
+ debug1('connected.\n')
+ print 'Connected.'
+ sys.stdout.flush()
+ if daemon:
+ daemonize()
+ log('daemonizing (%s).\n' % _pidname)
+ elif syslog:
+ debug1('switching to syslog.\n')
+ ssyslog.stderr_to_syslog()
+
+ def onroutes(routestr):
+ if auto_nets:
+ for line in routestr.strip().split('\n'):
+ (ip,width) = line.split(',', 1)
+ fw.auto_nets.append((ip,int(width)))
+
+ # we definitely want to do this *after* starting ssh, or we might end
+ # up intercepting the ssh connection!
+ #
+ # Moreover, now that we have the --auto-nets option, we have to wait
+ # for the server to send us that message anyway. Even if we haven't
+ # set --auto-nets, we might as well wait for the message first, then
+ # ignore its contents.
+ mux.got_routes = None
+ fw.start()
+ mux.got_routes = onroutes
+
+ def onhostlist(hostlist):
+ debug2('got host list: %r\n' % hostlist)
+ for line in hostlist.strip().split():
+ if line:
+ name,ip = line.split(',', 1)
+ fw.sethostip(name, ip)
+ mux.got_host_list = onhostlist
+
+ 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()
+ 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))
+
+ if seed_hosts != None:
+ debug1('seed_hosts: %r\n' % seed_hosts)
+ mux.send(0, ssnet.CMD_HOST_REQ, '\n'.join(seed_hosts))
+
+ while 1:
+ rv = serverproc.poll()
+ if rv:
+ raise Fatal('server died with error code %d' % rv)
+
+ ssnet.runonce(handlers, mux)
+ mux.callback()
+ mux.check_fullness()
+
+
+def main(listenip, ssh_cmd, remotename, python, seed_hosts, auto_nets,
+ subnets_include, subnets_exclude, syslog, daemon, pidfile):
+ if syslog:
+ ssyslog.start_syslog()
+ if daemon:
+ try:
+ check_daemon(pidfile)
+ except Fatal, e:
+ log("%s\n" % e)
+ return 5
+ debug1('Starting sshuttle proxy.\n')
+ listener = socket.socket()
+ listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ if listenip[1]:
+ ports = [listenip[1]]
+ else:
+ ports = xrange(12300,9000,-1)
+ last_e = None
+ bound = False
+ debug2('Binding:')
+ for port in ports:
+ debug2(' %d' % port)
+ try:
+ listener.bind((listenip[0], port))
+ bound = True
+ break
+ except socket.error, e:
+ last_e = e
+ debug2('\n')
+ if not bound:
+ assert(last_e)
+ raise last_e
+ listener.listen(10)
+ listenip = listener.getsockname()
+ debug1('Listening on %r.\n' % (listenip,))
+
+ fw = FirewallClient(listenip[1], subnets_include, subnets_exclude)
+
+ try:
+ return _main(listener, fw, ssh_cmd, remotename,
+ python, seed_hosts, auto_nets, syslog, daemon)
+ finally:
+ try:
+ if daemon:
+ # it's not our child anymore; can't waitpid
+ fw.p.returncode = 0
+ fw.done()
+ finally:
+ if daemon:
+ daemon_cleanup()