mirror of
https://github.com/sshuttle/sshuttle.git
synced 2025-07-04 00:30:35 +02:00
Compare commits
54 Commits
sshuttle-0
...
sshuttle-0
Author | SHA1 | Date | |
---|---|---|---|
4fde980f46 | |||
621997b279 | |||
ca7d38dc1a | |||
a81972b2b5 | |||
a238f7636c | |||
62e1ac4b46 | |||
f2297066e7 | |||
0bf0351d9b | |||
9731680d2e | |||
88937e148e | |||
7f3c522c56 | |||
ebfc3703ec | |||
760740e9aa | |||
b570778894 | |||
4c5185dc55 | |||
a2fcb08a2d | |||
e7a19890aa | |||
d9b1bb52e5 | |||
a30c4d7ccb | |||
9877a8d6fb | |||
8fde1155da | |||
fdb7c9b995 | |||
675f19f57e | |||
049a0c40ac | |||
668441adb6 | |||
d4ccd30c24 | |||
522bc23854 | |||
191d97a897 | |||
da7a490cd7 | |||
342ce12840 | |||
701f59a5b8 | |||
9f7b2bb4f6 | |||
d301184184 | |||
659a57beb2 | |||
c70b9937df | |||
415be935d4 | |||
d4c9d31068 | |||
38bb7f3c21 | |||
b7f1530aef | |||
973d5a95a1 | |||
95ab6e7119 | |||
e6d7c44e27 | |||
5bf6e40682 | |||
8a5ae1a40a | |||
651b607361 | |||
dc9a5e63c7 | |||
33bc55be27 | |||
c3204d2728 | |||
b1edb226a5 | |||
7fa1c3c4e4 | |||
cca69eb496 | |||
91f65132be | |||
2ef3a301fb | |||
41fd0348eb |
203
client.py
203
client.py
@ -1,9 +1,83 @@
|
||||
import struct, socket, select, errno, re
|
||||
import struct, socket, select, errno, re, signal, time
|
||||
import compat.ssubprocess as ssubprocess
|
||||
import helpers, ssnet, ssh
|
||||
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 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:
|
||||
@ -22,14 +96,17 @@ 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 = [
|
||||
['sudo', '-p', '[local sudo] Password: '] + argvbase,
|
||||
['su', '-c', ' '.join(argvbase)],
|
||||
@ -98,25 +175,36 @@ 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:
|
||||
helpers.logprefix = 'c : '
|
||||
else:
|
||||
helpers.logprefix = 'client: '
|
||||
debug1('connecting to server...\n')
|
||||
|
||||
try:
|
||||
(serverproc, serversock) = ssh.connect(ssh_cmd, remotename, python)
|
||||
(serverproc, serversock) = ssh.connect(ssh_cmd, remotename, python,
|
||||
stderr=ssyslog._p and ssyslog._p.stdin,
|
||||
options=dict(latency_control=latency_control))
|
||||
except socket.error, e:
|
||||
if e.errno == errno.EPIPE:
|
||||
raise Fatal("failed to establish ssh session")
|
||||
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:
|
||||
@ -126,6 +214,14 @@ def _main(listener, fw, ssh_cmd, remotename, python, seed_hosts, auto_nets):
|
||||
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:
|
||||
@ -153,20 +249,63 @@ def _main(listener, fw, ssh_cmd, remotename, python, seed_hosts, auto_nets):
|
||||
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: %r:%r -> %r:%r.\n' % (srcip[0],srcip[1],
|
||||
debug1('Accept: %s:%r -> %s:%r.\n' % (srcip[0],srcip[1],
|
||||
dstip[0],dstip[1]))
|
||||
if dstip == listener.getsockname():
|
||||
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))
|
||||
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))
|
||||
@ -177,15 +316,24 @@ 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)
|
||||
mux.callback()
|
||||
if latency_control:
|
||||
mux.check_fullness()
|
||||
mux.callback()
|
||||
|
||||
|
||||
def main(listenip, ssh_cmd, remotename, python, seed_hosts, auto_nets,
|
||||
subnets_include, subnets_exclude):
|
||||
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()
|
||||
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:
|
||||
@ -195,8 +343,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:
|
||||
@ -209,10 +362,26 @@ 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)
|
||||
python, latency_control, dnslistener,
|
||||
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()
|
||||
|
177
firewall.py
177
firewall.py
@ -1,8 +1,11 @@
|
||||
import re, errno
|
||||
import re, errno, socket, select, struct
|
||||
import compat.ssubprocess as ssubprocess
|
||||
import helpers
|
||||
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',
|
||||
ipt_ttl('-A', chain, '-j', 'REDIRECT',
|
||||
'--dest', '%s/%s' % (snet,swidth),
|
||||
'-p', 'tcp',
|
||||
'--to-ports', str(port),
|
||||
'-m', 'ttl', '!', '--ttl', '42' # to prevent infinite loops
|
||||
)
|
||||
'--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())
|
||||
@ -99,11 +131,11 @@ def _fill_oldctls(prefix):
|
||||
def _sysctl_set(name, val):
|
||||
argv = ['sysctl', '-w', '%s=%s' % (name, val)]
|
||||
debug1('>> %s\n' % ' '.join(argv))
|
||||
rv = ssubprocess.call(argv, stdout = open('/dev/null', 'w'))
|
||||
return ssubprocess.call(argv, stdout = open('/dev/null', 'w'))
|
||||
|
||||
|
||||
_changedctls = []
|
||||
def sysctl_set(name, val):
|
||||
def sysctl_set(name, val, permanent=False):
|
||||
PREFIX = 'net.inet.ip'
|
||||
assert(name.startswith(PREFIX + '.'))
|
||||
val = str(val)
|
||||
@ -114,8 +146,49 @@ def sysctl_set(name, val):
|
||||
return
|
||||
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 _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 +199,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 +212,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)
|
||||
sysctl_set('net.inet.ip.scopedroute', 0, permanent=True)
|
||||
|
||||
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 +232,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 +299,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 +350,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):
|
||||
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')
|
||||
@ -235,6 +371,10 @@ def main(port):
|
||||
# can read from it.
|
||||
os.dup2(1, 0)
|
||||
|
||||
if syslog:
|
||||
ssyslog.start_syslog()
|
||||
ssyslog.stderr_to_syslog()
|
||||
|
||||
debug1('firewall manager ready.\n')
|
||||
sys.stdout.write('READY\n')
|
||||
sys.stdout.flush()
|
||||
@ -268,7 +408,7 @@ def main(port):
|
||||
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:
|
||||
@ -282,6 +422,7 @@ def main(port):
|
||||
# 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)
|
||||
@ -296,5 +437,5 @@ def main(port):
|
||||
debug1('firewall manager: undoing changes.\n')
|
||||
except:
|
||||
pass
|
||||
do_it(port, [])
|
||||
do_it(port, 0, [])
|
||||
restore_etc_hosts(port)
|
||||
|
40
helpers.py
40
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
|
||||
|
||||
|
||||
|
33
main.py
33
main.py
@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
import sys, os, re
|
||||
import helpers, options, client, server, firewall, hostwatch
|
||||
import compat.ssubprocess as ssubprocess
|
||||
from helpers import *
|
||||
|
||||
|
||||
@ -46,36 +47,49 @@ def parse_ipport(s):
|
||||
|
||||
optspec = """
|
||||
sshuttle [-l [ip:]port] [-r [username@]sshserver[:port]] <subnets...>
|
||||
sshuttle --firewall <port> <subnets...>
|
||||
sshuttle --server
|
||||
sshuttle --firewall <port> <subnets...>
|
||||
sshuttle --hostwatch
|
||||
--
|
||||
l,listen= transproxy to this ip address and port number [0.0.0.0:0]
|
||||
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
|
||||
python= specify the name/path of the python interpreter on the remote server [python]
|
||||
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
|
||||
wrap= restart counting channel numbers after this number (for testing)
|
||||
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]
|
||||
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:
|
||||
opt.syslog = 1
|
||||
if opt.wrap:
|
||||
import ssnet
|
||||
ssnet.MAX_CHANNEL = int(opt.wrap)
|
||||
helpers.verbose = opt.verbose
|
||||
|
||||
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])))
|
||||
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:
|
||||
@ -101,10 +115,13 @@ try:
|
||||
opt.ssh_cmd,
|
||||
remotename,
|
||||
opt.python,
|
||||
opt.latency_control,
|
||||
opt.dns,
|
||||
sh,
|
||||
opt.auto_nets,
|
||||
parse_subnets(includes),
|
||||
parse_subnets(excludes)))
|
||||
parse_subnets(excludes),
|
||||
opt.syslog, opt.daemon, opt.pidfile))
|
||||
except Fatal, e:
|
||||
log('fatal: %s\n' % e)
|
||||
sys.exit(99)
|
||||
|
11
options.py
11
options.py
@ -60,7 +60,7 @@ def _tty_width():
|
||||
except (IOError, ImportError):
|
||||
return _atoi(os.environ.get('WIDTH')) or 70
|
||||
(ysize,xsize,ypix,xpix) = struct.unpack('HHHH', s)
|
||||
return xsize
|
||||
return xsize or 70
|
||||
|
||||
|
||||
class Options:
|
||||
@ -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 += ' ...'
|
||||
|
39
server.py
39
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,12 +184,29 @@ 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)
|
||||
(rpid, rv) = os.waitpid(hw.pid, os.WNOHANG)
|
||||
if rpid:
|
||||
raise Fatal('hostwatch exited unexpectedly: code 0x%04x\n' % rv)
|
||||
|
||||
ssnet.runonce(handlers, mux)
|
||||
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
|
||||
|
16
ssh.py
16
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):
|
||||
def connect(ssh_cmd, rhostport, python, stderr, options):
|
||||
main_exe = sys.argv[0]
|
||||
portl = []
|
||||
|
||||
@ -52,7 +54,9 @@ def connect(ssh_cmd, rhostport, python):
|
||||
|
||||
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') +
|
||||
@ -87,7 +91,7 @@ def connect(ssh_cmd, rhostport, python):
|
||||
s1.close()
|
||||
debug2('executing: %r\n' % argv)
|
||||
p = ssubprocess.Popen(argv, stdin=s1a, stdout=s1b, preexec_fn=setup,
|
||||
close_fds=True)
|
||||
close_fds=True, stderr=stderr)
|
||||
os.close(s1a)
|
||||
os.close(s1b)
|
||||
s2.sendall(content)
|
||||
|
48
sshuttle.md
48
sshuttle.md
@ -1,6 +1,6 @@
|
||||
% sshuttle(8) Sshuttle 0.42
|
||||
% sshuttle(8) Sshuttle 0.46
|
||||
% Avery Pennarun <apenwarr@gmail.com>
|
||||
% 2010-11-09
|
||||
% 2011-01-25
|
||||
|
||||
# NAME
|
||||
|
||||
@ -41,7 +41,13 @@ entire subnet to the VPN.
|
||||
-l, --listen=*[ip:]port*
|
||||
: use this ip address and port number as the transparent
|
||||
proxy port. By default sshuttle finds an available
|
||||
port automatically, so you don't need to override it.
|
||||
port automatically and listens on IP 127.0.0.1
|
||||
(localhost), so you don't need to override it, and
|
||||
connections are only proxied from the local machine,
|
||||
not from outside machines. If you want to accept
|
||||
connections from other machines on your network (ie. to
|
||||
run sshuttle on a router) try enabling IP Forwarding in
|
||||
your kernel, then using `--listen 0.0.0.0:0`.
|
||||
|
||||
-H, --auto-hosts
|
||||
: scan for remote hostnames and update the local /etc/hosts
|
||||
@ -103,6 +109,36 @@ entire subnet to the VPN.
|
||||
if you use this option to give it a few names to start
|
||||
from.
|
||||
|
||||
--no-latency-control
|
||||
: sacrifice latency to improve bandwidth benchmarks. ssh
|
||||
uses really big socket buffers, which can overload the
|
||||
connection if you start doing large file transfers,
|
||||
thus making all your other sessions inside the same
|
||||
tunnel go slowly. Normally, sshuttle tries to avoid
|
||||
this problem using a "fullness check" that allows only
|
||||
a certain amount of outstanding data to be buffered at
|
||||
a time. But on high-bandwidth links, this can leave a
|
||||
lot of your bandwidth underutilized. It also makes
|
||||
sshuttle seem slow in bandwidth benchmarks (benchmarks
|
||||
rarely test ping latency, which is what sshuttle is
|
||||
trying to control). This option disables the latency
|
||||
control feature, maximizing bandwidth usage. Use at
|
||||
your own risk.
|
||||
|
||||
-D, --daemon
|
||||
: automatically fork into the background after connecting
|
||||
to the remote server. Implies `--syslog`.
|
||||
|
||||
--syslog
|
||||
: after connecting, send all log messages to the
|
||||
`syslog`(3) service instead of stderr. This is
|
||||
implicit if you use `--daemon`.
|
||||
|
||||
--pidfile=*pidfilename*
|
||||
: when using `--daemon`, save sshuttle's pid to
|
||||
*pidfilename*. The default is `sshuttle.pid` in the
|
||||
current directory.
|
||||
|
||||
--server
|
||||
: (internal use only) run the sshuttle server on
|
||||
stdin/stdout. This is what the client runs on
|
||||
@ -139,8 +175,8 @@ Test locally by proxying all local connections, without using ssh:
|
||||
s: 192.168.42.0/24
|
||||
c : connected.
|
||||
firewall manager: starting transproxy.
|
||||
c : Accept: '192.168.42.106':50035 -> '192.168.42.121':139.
|
||||
c : Accept: '192.168.42.121':47523 -> '77.141.99.22':443.
|
||||
c : Accept: 192.168.42.106:50035 -> 192.168.42.121:139.
|
||||
c : Accept: 192.168.42.121:47523 -> 77.141.99.22:443.
|
||||
...etc...
|
||||
^C
|
||||
firewall manager: undoing changes.
|
||||
@ -166,7 +202,7 @@ and subnet guessing:
|
||||
hostwatch: Found: testbox1: 1.2.3.4
|
||||
hostwatch: Found: mytest2: 5.6.7.8
|
||||
hostwatch: Found: domaincontroller: 99.1.2.3
|
||||
c : Accept: '192.168.42.121':60554 -> '77.141.99.22':22.
|
||||
c : Accept: 192.168.42.121:60554 -> 77.141.99.22:22.
|
||||
^C
|
||||
firewall manager: undoing changes.
|
||||
c : Keyboard interrupt: exiting.
|
||||
|
44
ssnet.py
44
ssnet.py
@ -2,6 +2,8 @@ import struct, socket, errno, select
|
||||
if not globals().get('skip_imports'):
|
||||
from helpers import *
|
||||
|
||||
MAX_CHANNEL = 65535
|
||||
|
||||
# these don't exist in the socket module in python 2.3!
|
||||
SHUT_RD = 0
|
||||
SHUT_WR = 1
|
||||
@ -15,23 +17,28 @@ CMD_EXIT = 0x4200
|
||||
CMD_PING = 0x4201
|
||||
CMD_PONG = 0x4202
|
||||
CMD_CONNECT = 0x4203
|
||||
# CMD_CLOSE = 0x4204 # never used - removed
|
||||
CMD_STOP_SENDING = 0x4204
|
||||
CMD_EOF = 0x4205
|
||||
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',
|
||||
CMD_PING: 'PING',
|
||||
CMD_PONG: 'PONG',
|
||||
CMD_CONNECT: 'CONNECT',
|
||||
CMD_STOP_SENDING: 'STOP_SENDING',
|
||||
CMD_EOF: 'EOF',
|
||||
CMD_DATA: 'DATA',
|
||||
CMD_ROUTES: 'ROUTES',
|
||||
CMD_HOST_REQ: 'HOST_REQ',
|
||||
CMD_HOST_LIST: 'HOST_LIST',
|
||||
CMD_DNS_REQ: 'DNS_REQ',
|
||||
CMD_DNS_RESPONSE: 'DNS_RESPONSE',
|
||||
}
|
||||
|
||||
|
||||
@ -106,6 +113,8 @@ class SockWrapper:
|
||||
def seterr(self, e):
|
||||
if not self.exc:
|
||||
self.exc = e
|
||||
self.nowrite()
|
||||
self.noread()
|
||||
|
||||
def try_connect(self):
|
||||
if self.connect_to and self.shut_write:
|
||||
@ -148,7 +157,7 @@ class SockWrapper:
|
||||
try:
|
||||
self.wsock.shutdown(SHUT_WR)
|
||||
except socket.error, e:
|
||||
self.seterr(e)
|
||||
self.seterr('nowrite: %s' % e)
|
||||
|
||||
def too_full(self):
|
||||
return False # fullness is determined by the socket's select() state
|
||||
@ -160,10 +169,13 @@ class SockWrapper:
|
||||
try:
|
||||
return _nb_clean(os.write, self.wsock.fileno(), buf)
|
||||
except OSError, e:
|
||||
# unexpected error... stream is dead
|
||||
self.seterr(e)
|
||||
if e.errno == errno.EPIPE:
|
||||
debug1('%r: uwrite: got EPIPE\n' % self)
|
||||
self.nowrite()
|
||||
self.noread()
|
||||
return 0
|
||||
else:
|
||||
# unexpected error... stream is dead
|
||||
self.seterr('uwrite: %s' % e)
|
||||
return 0
|
||||
|
||||
def write(self, buf):
|
||||
@ -179,7 +191,7 @@ class SockWrapper:
|
||||
try:
|
||||
return _nb_clean(os.read, self.rsock.fileno(), 65536)
|
||||
except OSError, e:
|
||||
self.seterr(e)
|
||||
self.seterr('uread: %s' % e)
|
||||
return '' # unexpected error... we'll call it EOF
|
||||
|
||||
def fill(self):
|
||||
@ -231,6 +243,9 @@ class Proxy(Handler):
|
||||
self.wrap2 = wrap2
|
||||
|
||||
def pre_select(self, r, w, x):
|
||||
if self.wrap1.shut_write: self.wrap2.noread()
|
||||
if self.wrap2.shut_write: self.wrap1.noread()
|
||||
|
||||
if self.wrap1.connect_to:
|
||||
_add(w, self.wrap1.rsock)
|
||||
elif self.wrap1.buf:
|
||||
@ -272,7 +287,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
|
||||
@ -287,7 +302,7 @@ class Mux(Handler):
|
||||
# channel 0 is special, so we never allocate it
|
||||
for timeout in xrange(1024):
|
||||
self.chani += 1
|
||||
if self.chani > 65535:
|
||||
if self.chani > MAX_CHANNEL:
|
||||
self.chani = 1
|
||||
if not self.channels.get(self.chani):
|
||||
return self.chani
|
||||
@ -334,6 +349,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)
|
||||
@ -350,7 +369,11 @@ class Mux(Handler):
|
||||
else:
|
||||
raise Exception('got CMD_HOST_LIST without got_host_list?')
|
||||
else:
|
||||
callback = self.channels[channel]
|
||||
callback = self.channels.get(channel)
|
||||
if not callback:
|
||||
log('warning: closed channel %d got cmd=%s len=%d\n'
|
||||
% (channel, cmd_to_name.get(cmd,hex(cmd)), len(data)))
|
||||
else:
|
||||
callback(cmd, data)
|
||||
|
||||
def flush(self):
|
||||
@ -426,6 +449,7 @@ class MuxWrapper(SockWrapper):
|
||||
def noread(self):
|
||||
if not self.shut_read:
|
||||
self.shut_read = True
|
||||
self.mux.send(self.channel, CMD_STOP_SENDING, '')
|
||||
self.maybe_close()
|
||||
|
||||
def nowrite(self):
|
||||
@ -460,6 +484,8 @@ class MuxWrapper(SockWrapper):
|
||||
def got_packet(self, cmd, data):
|
||||
if cmd == CMD_EOF:
|
||||
self.noread()
|
||||
elif cmd == CMD_STOP_SENDING:
|
||||
self.nowrite()
|
||||
elif cmd == CMD_DATA:
|
||||
self.buf.append(data)
|
||||
else:
|
||||
|
16
ssyslog.py
Normal file
16
ssyslog.py
Normal file
@ -0,0 +1,16 @@
|
||||
import sys, os
|
||||
from compat import ssubprocess
|
||||
|
||||
|
||||
_p = None
|
||||
def start_syslog():
|
||||
global _p
|
||||
_p = ssubprocess.Popen(['logger',
|
||||
'-p', 'daemon.notice',
|
||||
'-t', 'sshuttle'], stdin=ssubprocess.PIPE)
|
||||
|
||||
|
||||
def stderr_to_syslog():
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
os.dup2(_p.stdin.fileno(), 2)
|
86
stresstest.py
Executable file
86
stresstest.py
Executable file
@ -0,0 +1,86 @@
|
||||
#!/usr/bin/python
|
||||
import sys, os, socket, select, struct, time
|
||||
|
||||
listener = socket.socket()
|
||||
listener.bind(('127.0.0.1', 0))
|
||||
listener.listen(500)
|
||||
|
||||
servers = []
|
||||
clients = []
|
||||
remain = {}
|
||||
|
||||
NUMCLIENTS = 50
|
||||
count = 0
|
||||
|
||||
|
||||
while 1:
|
||||
if len(clients) < NUMCLIENTS:
|
||||
c = socket.socket()
|
||||
c.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
c.bind(('0.0.0.0', 0))
|
||||
c.connect(listener.getsockname())
|
||||
count += 1
|
||||
if count >= 16384:
|
||||
count = 1
|
||||
print 'cli CREATING %d' % count
|
||||
b = struct.pack('I', count) + 'x'*count
|
||||
remain[c] = count
|
||||
print 'cli >> %r' % len(b)
|
||||
c.send(b)
|
||||
c.shutdown(socket.SHUT_WR)
|
||||
clients.append(c)
|
||||
r = [listener]
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
r = [listener]+servers+clients
|
||||
print 'select(%d)' % len(r)
|
||||
r,w,x = select.select(r, [], [], 5)
|
||||
assert(r)
|
||||
for i in r:
|
||||
if i == listener:
|
||||
s,addr = listener.accept()
|
||||
servers.append(s)
|
||||
elif i in servers:
|
||||
b = i.recv(4096)
|
||||
print 'srv << %r' % len(b)
|
||||
if not i in remain:
|
||||
assert(len(b) >= 4)
|
||||
want = struct.unpack('I', b[:4])[0]
|
||||
b = b[4:]
|
||||
#i.send('y'*want)
|
||||
else:
|
||||
want = remain[i]
|
||||
if want < len(b):
|
||||
print 'weird wanted %d bytes, got %d: %r' % (want, len(b), b)
|
||||
assert(want >= len(b))
|
||||
want -= len(b)
|
||||
remain[i] = want
|
||||
if not b: # EOF
|
||||
if want:
|
||||
print 'weird: eof but wanted %d more' % want
|
||||
assert(want == 0)
|
||||
i.close()
|
||||
servers.remove(i)
|
||||
del remain[i]
|
||||
else:
|
||||
print 'srv >> %r' % len(b)
|
||||
i.send('y'*len(b))
|
||||
if not want:
|
||||
i.shutdown(socket.SHUT_WR)
|
||||
elif i in clients:
|
||||
b = i.recv(4096)
|
||||
print 'cli << %r' % len(b)
|
||||
want = remain[i]
|
||||
if want < len(b):
|
||||
print 'weird wanted %d bytes, got %d: %r' % (want, len(b), b)
|
||||
assert(want >= len(b))
|
||||
want -= len(b)
|
||||
remain[i] = want
|
||||
if not b: # EOF
|
||||
if want:
|
||||
print 'weird: eof but wanted %d more' % want
|
||||
assert(want == 0)
|
||||
i.close()
|
||||
clients.remove(i)
|
||||
del remain[i]
|
||||
listener.accept()
|
8
ui-macos/.gitignore
vendored
Normal file
8
ui-macos/.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
*.pyc
|
||||
*~
|
||||
/*.nib
|
||||
/debug.app
|
||||
/sources.list
|
||||
/Sshuttle VPN.app
|
||||
/*.tar.gz
|
||||
/*.zip
|
40
ui-macos/Info.plist
Normal file
40
ui-macos/Info.plist
Normal file
@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>English</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Sshuttle VPN</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>Sshuttle</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>app.icns</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>ca.apenwarr.Sshuttle</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Sshuttle VPN</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.0.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>0.0.0</string>
|
||||
<key>LSUIElement</key>
|
||||
<string>1</string>
|
||||
<key>LSHasLocalizedDisplayName</key>
|
||||
<false/>
|
||||
<key>NSAppleScriptEnabled</key>
|
||||
<false/>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>GNU LGPL Version 2</string>
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
</dict>
|
||||
</plist>
|
2609
ui-macos/MainMenu.xib
Normal file
2609
ui-macos/MainMenu.xib
Normal file
File diff suppressed because it is too large
Load Diff
10
ui-macos/UserDefaults.plist
Normal file
10
ui-macos/UserDefaults.plist
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>startAtLogin</key>
|
||||
<false/>
|
||||
<key>autoReconnect</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
1
ui-macos/all.do
Normal file
1
ui-macos/all.do
Normal file
@ -0,0 +1 @@
|
||||
redo-ifchange debug.app dist
|
BIN
ui-macos/app.icns
Normal file
BIN
ui-macos/app.icns
Normal file
Binary file not shown.
28
ui-macos/askpass.py
Normal file
28
ui-macos/askpass.py
Normal file
@ -0,0 +1,28 @@
|
||||
import sys, os, re, subprocess
|
||||
|
||||
def askpass(prompt):
|
||||
prompt = prompt.replace('"', "'")
|
||||
|
||||
if 'yes/no' in prompt:
|
||||
return "yes"
|
||||
|
||||
script="""
|
||||
tell application "Finder"
|
||||
activate
|
||||
display dialog "%s" \
|
||||
with title "Sshuttle SSH Connection" \
|
||||
default answer "" \
|
||||
with icon caution \
|
||||
with hidden answer
|
||||
end tell
|
||||
""" % prompt
|
||||
|
||||
p = subprocess.Popen(['osascript', '-e', script], stdout=subprocess.PIPE)
|
||||
out = p.stdout.read()
|
||||
rv = p.wait()
|
||||
if rv:
|
||||
return None
|
||||
g = re.match("text returned:(.*), button returned:.*", out)
|
||||
if not g:
|
||||
return None
|
||||
return g.group(1)
|
1
ui-macos/bits/.gitignore
vendored
Normal file
1
ui-macos/bits/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/runpython
|
1
ui-macos/bits/PkgInfo
Normal file
1
ui-macos/bits/PkgInfo
Normal file
@ -0,0 +1 @@
|
||||
APPL????
|
23
ui-macos/bits/runpython.c
Normal file
23
ui-macos/bits/runpython.c
Normal file
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* This rather pointless program acts like the python interpreter, except
|
||||
* it's intended to sit inside a MacOS .app package, so that its argv[0]
|
||||
* will point inside the package.
|
||||
*
|
||||
* NSApplicationMain() looks for Info.plist using the path in argv[0], which
|
||||
* goes wrong if your interpreter is /usr/bin/python.
|
||||
*/
|
||||
#include <Python.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
char *path = strdup(argv[0]), *cptr;
|
||||
char *args[] = {argv[0], "../Resources/main.py", NULL};
|
||||
cptr = strrchr(path, '/');
|
||||
if (cptr)
|
||||
*cptr = 0;
|
||||
chdir(path);
|
||||
free(path);
|
||||
return Py_Main(2, args);
|
||||
}
|
5
ui-macos/bits/runpython.do
Normal file
5
ui-macos/bits/runpython.do
Normal file
@ -0,0 +1,5 @@
|
||||
exec >&2
|
||||
redo-ifchange runpython.c
|
||||
gcc -Wall -o $3 runpython.c \
|
||||
-I/usr/include/python2.5 \
|
||||
-lpython2.5
|
BIN
ui-macos/chicken-tiny-bw.png
Normal file
BIN
ui-macos/chicken-tiny-bw.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 821 B |
BIN
ui-macos/chicken-tiny-err.png
Normal file
BIN
ui-macos/chicken-tiny-err.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 789 B |
BIN
ui-macos/chicken-tiny.png
Normal file
BIN
ui-macos/chicken-tiny.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 810 B |
4
ui-macos/clean.do
Normal file
4
ui-macos/clean.do
Normal file
@ -0,0 +1,4 @@
|
||||
exec >&2
|
||||
find -name '*~' | xargs rm -f
|
||||
rm -rf *.app *.zip *.tar.gz
|
||||
rm -f bits/runpython *.nib sources.list
|
15
ui-macos/debug.app.do
Normal file
15
ui-macos/debug.app.do
Normal file
@ -0,0 +1,15 @@
|
||||
redo-ifchange bits/runpython MainMenu.nib
|
||||
rm -rf debug.app
|
||||
mkdir debug.app debug.app/Contents
|
||||
cd debug.app/Contents
|
||||
ln -s ../.. Resources
|
||||
ln -s ../.. English.lproj
|
||||
ln -s ../../Info.plist .
|
||||
ln -s ../../app.icns .
|
||||
|
||||
mkdir MacOS
|
||||
cd MacOS
|
||||
ln -s ../../../bits/runpython Sshuttle
|
||||
|
||||
cd ../../..
|
||||
redo-ifchange $(find debug.app -type f)
|
28
ui-macos/default.app.do
Normal file
28
ui-macos/default.app.do
Normal file
@ -0,0 +1,28 @@
|
||||
TOP=$PWD
|
||||
redo-ifchange sources.list
|
||||
redo-ifchange Info.plist bits/runpython \
|
||||
$(while read name newname; do echo "$name"; done <sources.list)
|
||||
|
||||
rm -rf "$1.app"
|
||||
mkdir "$1.app" "$1.app/Contents"
|
||||
cd "$1.app/Contents"
|
||||
|
||||
cp "$TOP/Info.plist" .
|
||||
|
||||
mkdir MacOS
|
||||
cp "$TOP/bits/runpython" MacOS/Sshuttle
|
||||
|
||||
mkdir Resources
|
||||
|
||||
cd "$TOP"
|
||||
while read name newname; do
|
||||
[ -z "$name" ] && continue
|
||||
: "${newname:=$name}"
|
||||
outname=$1.app/Contents/Resources/$newname
|
||||
outdir=$(dirname "$outname")
|
||||
[ -d "$outdir" ] || mkdir "$outdir"
|
||||
cp "${name-$newname}" "$outname"
|
||||
done <sources.list
|
||||
|
||||
cd "$1.app"
|
||||
redo-ifchange $(find . -type f)
|
5
ui-macos/default.app.tar.gz.do
Normal file
5
ui-macos/default.app.tar.gz.do
Normal file
@ -0,0 +1,5 @@
|
||||
exec >&2
|
||||
IFS="
|
||||
"
|
||||
redo-ifchange $1.app
|
||||
tar -czf $3 $1.app/
|
5
ui-macos/default.app.zip.do
Normal file
5
ui-macos/default.app.zip.do
Normal file
@ -0,0 +1,5 @@
|
||||
exec >&2
|
||||
IFS="
|
||||
"
|
||||
redo-ifchange $1.app
|
||||
zip -q -r $3 $1.app/
|
2
ui-macos/default.nib.do
Normal file
2
ui-macos/default.nib.do
Normal file
@ -0,0 +1,2 @@
|
||||
redo-ifchange $1.xib
|
||||
ibtool --compile $3 $1.xib
|
1
ui-macos/dist.do
Normal file
1
ui-macos/dist.do
Normal file
@ -0,0 +1 @@
|
||||
redo-ifchange "Sshuttle VPN.app.zip" "Sshuttle VPN.app.tar.gz"
|
19
ui-macos/git-export.do
Normal file
19
ui-macos/git-export.do
Normal file
@ -0,0 +1,19 @@
|
||||
# update a local branch with pregenerated output files, so people can download
|
||||
# the completed tarballs from github. Since we don't have any real binaries,
|
||||
# our final distribution package contains mostly blobs from the source code,
|
||||
# so this doesn't cost us much extra space in the repo.
|
||||
BRANCH=dist/macos
|
||||
redo-ifchange 'Sshuttle VPN.app'
|
||||
git update-ref refs/heads/$BRANCH origin/$BRANCH '' 2>/dev/null || true
|
||||
|
||||
export GIT_INDEX_FILE=$PWD/gitindex.tmp
|
||||
rm -f "$GIT_INDEX_FILE"
|
||||
git add -f 'Sshuttle VPN.app'
|
||||
|
||||
MSG="MacOS precompiled app package for $(git describe)"
|
||||
TREE=$(git write-tree --prefix=ui-macos)
|
||||
git show-ref refs/heads/$BRANCH >/dev/null && PARENT="-p refs/heads/$BRANCH"
|
||||
COMMITID=$(echo "$MSG" | git commit-tree $TREE $PARENT)
|
||||
|
||||
git update-ref refs/heads/$BRANCH $COMMITID
|
||||
rm -f "$GIT_INDEX_FILE"
|
367
ui-macos/main.py
Normal file
367
ui-macos/main.py
Normal file
@ -0,0 +1,367 @@
|
||||
import sys, os, pty
|
||||
from AppKit import *
|
||||
import my, models, askpass
|
||||
|
||||
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:
|
||||
argv.append('-v')
|
||||
if auto_nets:
|
||||
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
|
||||
|
||||
|
||||
class _Callback(NSObject):
|
||||
def initWithFunc_(self, func):
|
||||
self = super(_Callback, self).init()
|
||||
self.func = func
|
||||
return self
|
||||
def func_(self, obj):
|
||||
return self.func(obj)
|
||||
|
||||
|
||||
class Callback:
|
||||
def __init__(self, func):
|
||||
self.obj = _Callback.alloc().initWithFunc_(func)
|
||||
self.sel = self.obj.func_
|
||||
|
||||
|
||||
class Runner:
|
||||
def __init__(self, argv, logfunc, promptfunc, serverobj):
|
||||
print 'in __init__'
|
||||
self.id = argv
|
||||
self.rv = None
|
||||
self.pid = None
|
||||
self.fd = None
|
||||
self.logfunc = logfunc
|
||||
self.promptfunc = promptfunc
|
||||
self.serverobj = serverobj
|
||||
self.buf = ''
|
||||
self.logfunc('\nConnecting to %s.\n' % self.serverobj.host())
|
||||
print 'will run: %r' % argv
|
||||
self.serverobj.setConnected_(False)
|
||||
pid,fd = pty.fork()
|
||||
if pid == 0:
|
||||
# child
|
||||
try:
|
||||
os.execvp(argv[0], argv)
|
||||
except Exception, e:
|
||||
sys.stderr.write('failed to start: %r\n' % e)
|
||||
raise
|
||||
finally:
|
||||
os._exit(42)
|
||||
# parent
|
||||
self.pid = pid
|
||||
self.file = NSFileHandle.alloc()\
|
||||
.initWithFileDescriptor_closeOnDealloc_(fd, True)
|
||||
self.cb = Callback(self.gotdata)
|
||||
NSNotificationCenter.defaultCenter()\
|
||||
.addObserver_selector_name_object_(self.cb.obj, self.cb.sel,
|
||||
NSFileHandleDataAvailableNotification, self.file)
|
||||
self.file.waitForDataInBackgroundAndNotify()
|
||||
|
||||
def __del__(self):
|
||||
self.wait()
|
||||
|
||||
def _try_wait(self, options):
|
||||
if self.rv == None and self.pid > 0:
|
||||
pid,code = os.waitpid(self.pid, options)
|
||||
if pid == self.pid:
|
||||
if os.WIFEXITED(code):
|
||||
self.rv = os.WEXITSTATUS(code)
|
||||
else:
|
||||
self.rv = -os.WSTOPSIG(code)
|
||||
self.serverobj.setConnected_(False)
|
||||
self.serverobj.setError_('VPN process died')
|
||||
self.logfunc('Disconnected.\n')
|
||||
print 'wait_result: %r' % self.rv
|
||||
return self.rv
|
||||
|
||||
def wait(self):
|
||||
return self._try_wait(0)
|
||||
|
||||
def poll(self):
|
||||
return self._try_wait(os.WNOHANG)
|
||||
|
||||
def kill(self):
|
||||
assert(self.pid > 0)
|
||||
print 'killing: pid=%r rv=%r' % (self.pid, self.rv)
|
||||
if self.rv == None:
|
||||
self.logfunc('Disconnecting from %s.\n' % self.serverobj.host())
|
||||
os.kill(self.pid, 15)
|
||||
self.wait()
|
||||
|
||||
def gotdata(self, notification):
|
||||
print 'gotdata!'
|
||||
d = str(self.file.availableData())
|
||||
if d:
|
||||
self.logfunc(d)
|
||||
self.buf = self.buf + d
|
||||
if 'Connected.\r\n' in self.buf:
|
||||
self.serverobj.setConnected_(True)
|
||||
self.buf = self.buf[-4096:]
|
||||
if self.buf.strip().endswith(':'):
|
||||
lastline = self.buf.rstrip().split('\n')[-1]
|
||||
resp = self.promptfunc(lastline)
|
||||
add = ' (response)\n'
|
||||
self.buf += add
|
||||
self.logfunc(add)
|
||||
self.file.writeData_(my.Data(resp + '\n'))
|
||||
self.file.waitForDataInBackgroundAndNotify()
|
||||
self.poll()
|
||||
#print 'gotdata done!'
|
||||
|
||||
|
||||
class SshuttleApp(NSObject):
|
||||
def initialize(self):
|
||||
d = my.PList('UserDefaults')
|
||||
my.Defaults().registerDefaults_(d)
|
||||
|
||||
|
||||
class SshuttleController(NSObject):
|
||||
# Interface builder outlets
|
||||
startAtLoginField = objc.IBOutlet()
|
||||
autoReconnectField = objc.IBOutlet()
|
||||
debugField = objc.IBOutlet()
|
||||
routingField = objc.IBOutlet()
|
||||
prefsWindow = objc.IBOutlet()
|
||||
serversController = objc.IBOutlet()
|
||||
logField = objc.IBOutlet()
|
||||
latencyControlField = objc.IBOutlet()
|
||||
|
||||
servers = []
|
||||
conns = {}
|
||||
|
||||
def _connect(self, server):
|
||||
host = server.host()
|
||||
print 'connecting %r' % host
|
||||
self.fill_menu()
|
||||
def logfunc(msg):
|
||||
print 'log! (%d bytes)' % len(msg)
|
||||
self.logField.textStorage()\
|
||||
.appendAttributedString_(NSAttributedString.alloc()\
|
||||
.initWithString_(msg))
|
||||
self.logField.didChangeText()
|
||||
def promptfunc(prompt):
|
||||
print 'prompt! %r' % prompt
|
||||
return askpass.askpass(prompt)
|
||||
nets_mode = server.autoNets()
|
||||
if nets_mode == models.NET_MANUAL:
|
||||
manual_nets = ["%s/%d" % (i.subnet(), i.width())
|
||||
for i in server.nets()]
|
||||
elif nets_mode == models.NET_ALL:
|
||||
manual_nets = ['0/0']
|
||||
else:
|
||||
manual_nets = []
|
||||
noLatencyControl = (server.latencyControl() != models.LAT_INTERACTIVE)
|
||||
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(),
|
||||
no_latency_control = noLatencyControl),
|
||||
logfunc=logfunc, promptfunc=promptfunc,
|
||||
serverobj=server)
|
||||
self.conns[host] = conn
|
||||
|
||||
def _disconnect(self, server):
|
||||
host = server.host()
|
||||
print 'disconnecting %r' % host
|
||||
conn = self.conns.get(host)
|
||||
if conn:
|
||||
conn.kill()
|
||||
self.fill_menu()
|
||||
self.logField.textStorage().setAttributedString_(
|
||||
NSAttributedString.alloc().initWithString_(''))
|
||||
|
||||
@objc.IBAction
|
||||
def cmd_connect(self, sender):
|
||||
server = sender.representedObject()
|
||||
server.setWantConnect_(True)
|
||||
|
||||
@objc.IBAction
|
||||
def cmd_disconnect(self, sender):
|
||||
server = sender.representedObject()
|
||||
server.setWantConnect_(False)
|
||||
|
||||
@objc.IBAction
|
||||
def cmd_show(self, sender):
|
||||
self.prefsWindow.makeKeyAndOrderFront_(self)
|
||||
NSApp.activateIgnoringOtherApps_(True)
|
||||
|
||||
@objc.IBAction
|
||||
def cmd_quit(self, sender):
|
||||
NSApp.performSelector_withObject_afterDelay_(NSApp.terminate_,
|
||||
None, 0.0)
|
||||
|
||||
def fill_menu(self):
|
||||
menu = self.menu
|
||||
menu.removeAllItems()
|
||||
|
||||
def additem(name, func, obj):
|
||||
it = menu.addItemWithTitle_action_keyEquivalent_(name, None, "")
|
||||
it.setRepresentedObject_(obj)
|
||||
it.setTarget_(self)
|
||||
it.setAction_(func)
|
||||
def addnote(name):
|
||||
additem(name, None, None)
|
||||
|
||||
any_inprogress = None
|
||||
any_conn = None
|
||||
any_err = None
|
||||
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()))
|
||||
if not host:
|
||||
additem('Connect Untitled', None, i)
|
||||
elif i.autoNets() == models.NET_MANUAL and not numnets:
|
||||
additem('Connect %s (no routes)' % host, None, i)
|
||||
elif want:
|
||||
any_conn = i
|
||||
additem('Disconnect %s' % title, self.cmd_disconnect, i)
|
||||
else:
|
||||
additem('Connect %s' % title, self.cmd_connect, i)
|
||||
if not want:
|
||||
msg = 'Off'
|
||||
elif i.error():
|
||||
msg = 'ERROR - try reconnecting'
|
||||
any_err = i
|
||||
elif connected:
|
||||
msg = 'Connected'
|
||||
else:
|
||||
msg = 'Connecting...'
|
||||
any_inprogress = i
|
||||
addnote(' State: %s' % msg)
|
||||
else:
|
||||
addnote('No servers defined yet')
|
||||
|
||||
menu.addItem_(NSMenuItem.separatorItem())
|
||||
additem('Preferences...', self.cmd_show, None)
|
||||
additem('Quit Sshuttle VPN', self.cmd_quit, None)
|
||||
|
||||
if any_err:
|
||||
self.statusitem.setImage_(self.img_err)
|
||||
self.statusitem.setTitle_('Error!')
|
||||
elif any_conn:
|
||||
self.statusitem.setImage_(self.img_running)
|
||||
if any_inprogress:
|
||||
self.statusitem.setTitle_('Connecting...')
|
||||
else:
|
||||
self.statusitem.setTitle_('')
|
||||
else:
|
||||
self.statusitem.setImage_(self.img_idle)
|
||||
self.statusitem.setTitle_('')
|
||||
|
||||
def load_servers(self):
|
||||
l = my.Defaults().arrayForKey_('servers') or []
|
||||
sl = []
|
||||
for s in l:
|
||||
host = s.get('host', None)
|
||||
if not host: continue
|
||||
|
||||
nets = s.get('nets', [])
|
||||
nl = []
|
||||
for n in nets:
|
||||
subnet = n[0]
|
||||
width = n[1]
|
||||
net = models.SshuttleNet.alloc().init()
|
||||
net.setSubnet_(subnet)
|
||||
net.setWidth_(width)
|
||||
nl.append(net)
|
||||
|
||||
autoNets = s.get('autoNets', models.NET_AUTO)
|
||||
autoHosts = s.get('autoHosts', True)
|
||||
useDns = s.get('useDns', autoNets == models.NET_ALL)
|
||||
latencyControl = s.get('latencyControl', models.LAT_INTERACTIVE)
|
||||
srv = models.SshuttleServer.alloc().init()
|
||||
srv.setHost_(host)
|
||||
srv.setAutoNets_(autoNets)
|
||||
srv.setAutoHosts_(autoHosts)
|
||||
srv.setNets_(nl)
|
||||
srv.setUseDns_(useDns)
|
||||
srv.setLatencyControl_(latencyControl)
|
||||
sl.append(srv)
|
||||
self.serversController.addObjects_(sl)
|
||||
self.serversController.setSelectionIndex_(0)
|
||||
|
||||
def save_servers(self):
|
||||
l = []
|
||||
for s in self.servers:
|
||||
host = s.host()
|
||||
if not host: continue
|
||||
nets = []
|
||||
for n in s.nets():
|
||||
subnet = n.subnet()
|
||||
if not subnet: continue
|
||||
nets.append((subnet, n.width()))
|
||||
d = dict(host=s.host(),
|
||||
nets=nets,
|
||||
autoNets=s.autoNets(),
|
||||
autoHosts=s.autoHosts(),
|
||||
useDns=s.useDns(),
|
||||
latencyControl=s.latencyControl())
|
||||
l.append(d)
|
||||
my.Defaults().setObject_forKey_(l, 'servers')
|
||||
self.fill_menu()
|
||||
|
||||
def awakeFromNib(self):
|
||||
self.routingField.removeAllItems()
|
||||
tf = self.routingField.addItemWithTitle_
|
||||
tf('Send all traffic through this server')
|
||||
tf('Determine automatically')
|
||||
tf('Custom...')
|
||||
|
||||
self.latencyControlField.removeAllItems()
|
||||
tf = self.latencyControlField.addItemWithTitle_
|
||||
tf('Fast transfer')
|
||||
tf('Low latency')
|
||||
|
||||
# Hmm, even when I mark this as !enabled in the .nib, it still comes
|
||||
# through as enabled. So let's just disable it here (since we don't
|
||||
# support this feature yet).
|
||||
self.startAtLoginField.setEnabled_(False)
|
||||
self.startAtLoginField.setState_(False)
|
||||
self.autoReconnectField.setEnabled_(False)
|
||||
self.autoReconnectField.setState_(False)
|
||||
|
||||
self.load_servers()
|
||||
|
||||
# Initialize our menu item
|
||||
self.menu = NSMenu.alloc().initWithTitle_('Sshuttle')
|
||||
bar = NSStatusBar.systemStatusBar()
|
||||
statusitem = bar.statusItemWithLength_(NSVariableStatusItemLength)
|
||||
self.statusitem = statusitem
|
||||
self.img_idle = my.Image('chicken-tiny-bw', 'png')
|
||||
self.img_running = my.Image('chicken-tiny', 'png')
|
||||
self.img_err = my.Image('chicken-tiny-err', 'png')
|
||||
statusitem.setImage_(self.img_idle)
|
||||
statusitem.setHighlightMode_(True)
|
||||
statusitem.setMenu_(self.menu)
|
||||
self.fill_menu()
|
||||
|
||||
models.configchange_callback = my.DelayedCallback(self.save_servers)
|
||||
|
||||
def sc(server):
|
||||
if server.wantConnect():
|
||||
self._connect(server)
|
||||
else:
|
||||
self._disconnect(server)
|
||||
models.setconnect_callback = sc
|
||||
|
||||
|
||||
# Note: NSApplicationMain calls sys.exit(), so this never returns.
|
||||
NSApplicationMain(sys.argv)
|
166
ui-macos/models.py
Normal file
166
ui-macos/models.py
Normal file
@ -0,0 +1,166 @@
|
||||
from AppKit import *
|
||||
import my
|
||||
|
||||
|
||||
configchange_callback = setconnect_callback = None
|
||||
|
||||
|
||||
def config_changed():
|
||||
if configchange_callback:
|
||||
configchange_callback()
|
||||
|
||||
|
||||
def _validate_ip(v):
|
||||
parts = v.split('.')[:4]
|
||||
if len(parts) < 4:
|
||||
parts += ['0'] * (4 - len(parts))
|
||||
for i in range(4):
|
||||
n = my.atoi(parts[i])
|
||||
if n < 0:
|
||||
n = 0
|
||||
elif n > 255:
|
||||
n = 255
|
||||
parts[i] = str(n)
|
||||
return '.'.join(parts)
|
||||
|
||||
|
||||
def _validate_width(v):
|
||||
n = my.atoi(v)
|
||||
if n < 0:
|
||||
n = 0
|
||||
elif n > 32:
|
||||
n = 32
|
||||
return n
|
||||
|
||||
|
||||
class SshuttleNet(NSObject):
|
||||
def subnet(self):
|
||||
return getattr(self, '_k_subnet', None)
|
||||
def setSubnet_(self, v):
|
||||
self._k_subnet = v
|
||||
config_changed()
|
||||
@objc.accessor
|
||||
def validateSubnet_error_(self, value, error):
|
||||
#print 'validateSubnet!'
|
||||
return True, _validate_ip(value), error
|
||||
|
||||
def width(self):
|
||||
return getattr(self, '_k_width', 24)
|
||||
def setWidth_(self, v):
|
||||
self._k_width = v
|
||||
config_changed()
|
||||
@objc.accessor
|
||||
def validateWidth_error_(self, value, error):
|
||||
#print 'validateWidth!'
|
||||
return True, _validate_width(value), error
|
||||
|
||||
NET_ALL = 0
|
||||
NET_AUTO = 1
|
||||
NET_MANUAL = 2
|
||||
|
||||
LAT_BANDWIDTH = 0
|
||||
LAT_INTERACTIVE = 1
|
||||
|
||||
class SshuttleServer(NSObject):
|
||||
def init(self):
|
||||
self = super(SshuttleServer, self).init()
|
||||
config_changed()
|
||||
return self
|
||||
|
||||
def wantConnect(self):
|
||||
return getattr(self, '_k_wantconnect', False)
|
||||
def setWantConnect_(self, v):
|
||||
self._k_wantconnect = v
|
||||
self.setError_(None)
|
||||
config_changed()
|
||||
if setconnect_callback: setconnect_callback(self)
|
||||
|
||||
def connected(self):
|
||||
return getattr(self, '_k_connected', False)
|
||||
def setConnected_(self, v):
|
||||
print 'setConnected of %r to %r' % (self, v)
|
||||
self._k_connected = v
|
||||
if v: self.setError_(None) # connected ok, so no error
|
||||
config_changed()
|
||||
|
||||
def error(self):
|
||||
return getattr(self, '_k_error', None)
|
||||
def setError_(self, v):
|
||||
self._k_error = v
|
||||
config_changed()
|
||||
|
||||
def isValid(self):
|
||||
if not self.host():
|
||||
return False
|
||||
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):
|
||||
#print 'validatehost! %r %r %r' % (self, value, error)
|
||||
while value.startswith('-'):
|
||||
value = value[1:]
|
||||
return True, value, error
|
||||
|
||||
def nets(self):
|
||||
return getattr(self, '_k_nets', [])
|
||||
def setNets_(self, v):
|
||||
self._k_nets = v
|
||||
self.setTitle_(None)
|
||||
config_changed()
|
||||
def netsHidden(self):
|
||||
#print 'checking netsHidden'
|
||||
return self.autoNets() != NET_MANUAL
|
||||
def setNetsHidden_(self, v):
|
||||
config_changed()
|
||||
#print 'setting netsHidden to %r' % v
|
||||
|
||||
def autoNets(self):
|
||||
return getattr(self, '_k_autoNets', NET_AUTO)
|
||||
def setAutoNets_(self, v):
|
||||
self._k_autoNets = v
|
||||
self.setNetsHidden_(-1)
|
||||
self.setUseDns_(v == NET_ALL)
|
||||
self.setTitle_(None)
|
||||
config_changed()
|
||||
|
||||
def autoHosts(self):
|
||||
return getattr(self, '_k_autoHosts', True)
|
||||
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()
|
||||
|
||||
def latencyControl(self):
|
||||
return getattr(self, '_k_latencyControl', LAT_INTERACTIVE)
|
||||
def setLatencyControl_(self, v):
|
||||
self._k_latencyControl = v
|
||||
config_changed()
|
62
ui-macos/my.py
Normal file
62
ui-macos/my.py
Normal file
@ -0,0 +1,62 @@
|
||||
import sys, os
|
||||
from AppKit import *
|
||||
import PyObjCTools.AppHelper
|
||||
|
||||
|
||||
def bundle_path(name, typ):
|
||||
if typ:
|
||||
return NSBundle.mainBundle().pathForResource_ofType_(name, typ)
|
||||
else:
|
||||
return os.path.join(NSBundle.mainBundle().resourcePath(), name)
|
||||
|
||||
|
||||
# Load an NSData using a python string
|
||||
def Data(s):
|
||||
return NSData.alloc().initWithBytes_length_(s, len(s))
|
||||
|
||||
|
||||
# Load a property list from a file in the application bundle.
|
||||
def PList(name):
|
||||
path = bundle_path(name, 'plist')
|
||||
return NSDictionary.dictionaryWithContentsOfFile_(path)
|
||||
|
||||
|
||||
# Load an NSImage from a file in the application bundle.
|
||||
def Image(name, ext):
|
||||
bytes = open(bundle_path(name, ext)).read()
|
||||
img = NSImage.alloc().initWithData_(Data(bytes))
|
||||
return img
|
||||
|
||||
|
||||
# Return the NSUserDefaults shared object.
|
||||
def Defaults():
|
||||
return NSUserDefaults.standardUserDefaults()
|
||||
|
||||
|
||||
# Usage:
|
||||
# f = DelayedCallback(func, args...)
|
||||
# later:
|
||||
# f()
|
||||
#
|
||||
# When you call f(), it will schedule a call to func() next time the
|
||||
# ObjC event loop iterates. Multiple calls to f() in a single iteration
|
||||
# will only result in one call to func().
|
||||
#
|
||||
def DelayedCallback(func, *args, **kwargs):
|
||||
flag = [0]
|
||||
def _go():
|
||||
if flag[0]:
|
||||
print 'running %r (flag=%r)' % (func, flag)
|
||||
flag[0] = 0
|
||||
func(*args, **kwargs)
|
||||
def call():
|
||||
flag[0] += 1
|
||||
PyObjCTools.AppHelper.callAfter(_go)
|
||||
return call
|
||||
|
||||
|
||||
def atoi(s):
|
||||
try:
|
||||
return int(s)
|
||||
except ValueError:
|
||||
return 0
|
4
ui-macos/run.do
Normal file
4
ui-macos/run.do
Normal file
@ -0,0 +1,4 @@
|
||||
redo-ifchange debug.app
|
||||
exec >&2
|
||||
./debug.app/Contents/MacOS/Sshuttle
|
||||
|
14
ui-macos/sources.list.do
Normal file
14
ui-macos/sources.list.do
Normal file
@ -0,0 +1,14 @@
|
||||
redo-always
|
||||
exec >$3
|
||||
cat <<-EOF
|
||||
app.icns
|
||||
MainMenu.nib English.lproj/MainMenu.nib
|
||||
UserDefaults.plist
|
||||
chicken-tiny.png
|
||||
chicken-tiny-bw.png
|
||||
chicken-tiny-err.png
|
||||
EOF
|
||||
for d in *.py sshuttle/*.py sshuttle/sshuttle sshuttle/compat/*.py; do
|
||||
echo $d
|
||||
done
|
||||
redo-stamp <$3
|
1
ui-macos/sshuttle
Symbolic link
1
ui-macos/sshuttle
Symbolic link
@ -0,0 +1 @@
|
||||
..
|
Reference in New Issue
Block a user