mirror of
https://github.com/sshuttle/sshuttle.git
synced 2025-07-04 08:40:30 +02:00
Compare commits
102 Commits
sshuttle-0
...
sshuttle-0
Author | SHA1 | Date | |
---|---|---|---|
2cfc39fac8 | |||
29819ea0af | |||
e43a40565b | |||
57d1cb1e11 | |||
6e32d1445a | |||
bdad253ef5 | |||
49c55f6825 | |||
1874aaceb4 | |||
4c31bc02a4 | |||
84047089a9 | |||
8be9270fdb | |||
10dc229125 | |||
cd77ad5e7b | |||
c13cb9b8ca | |||
0fe48a4682 | |||
6121a6dca3 | |||
c576682caf | |||
343905784b | |||
91d705c24f | |||
e5251969b0 | |||
b8e150fc4d | |||
36378efe5e | |||
cba8b261c6 | |||
39425a03c5 | |||
5a39341d50 | |||
3eef3635ac | |||
f1c79c7e92 | |||
5529a04cc9 | |||
035c5ad7a6 | |||
c013386ecb | |||
a33f6199c4 | |||
0f2c249e4d | |||
192e5b36e8 | |||
4036b7dfcf | |||
8ec6daf02a | |||
e2507f86d5 | |||
e4fe62de3c | |||
734f32d112 | |||
a34e106b55 | |||
e6e80f1f04 | |||
32865bd2dd | |||
2f11f50bc2 | |||
a95491765d | |||
d8754dc3a0 | |||
3956a5df94 | |||
7442eb61e9 | |||
6107abf10f | |||
5e8ad544ee | |||
20254bab57 | |||
f41c6b62e5 | |||
9a7412c08f | |||
c6200eecdc | |||
55f86a8b3f | |||
e7caae8126 | |||
4db9b372c2 | |||
061e6a0933 | |||
50849b86b0 | |||
6b7cf770f5 | |||
b26e1ee652 | |||
6500067905 | |||
50c2b86f15 | |||
97dca42291 | |||
6e53b07002 | |||
08bd1dca46 | |||
94566b5efc | |||
a8b71f6387 | |||
4bfcd7091d | |||
bd489b3319 | |||
8ab5ef283d | |||
e67208a294 | |||
7859be13c2 | |||
f313d50690 | |||
15e26d2e0e | |||
e2ec475de3 | |||
57e744dadd | |||
c13be0b817 | |||
da2c6273f6 | |||
7712c60c36 | |||
65b0390fe9 | |||
c5834a9773 | |||
e2474543fc | |||
8636378870 | |||
f5eed4c809 | |||
783d33cada | |||
94241b938b | |||
9031de1527 | |||
cfb2592346 | |||
2e8381ecda | |||
7d35690e41 | |||
141d9760b9 | |||
0658c85ffe | |||
90a55a33a2 | |||
c3399595d2 | |||
6ef9ae1796 | |||
1ca8aa5b89 | |||
a62975e0ce | |||
4fde980f46 | |||
621997b279 | |||
ca7d38dc1a | |||
a81972b2b5 | |||
a238f7636c | |||
62e1ac4b46 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,3 +1,6 @@
|
||||
*.pyc
|
||||
*~
|
||||
*.8
|
||||
/.do_built
|
||||
/.do_built.dir
|
||||
/.redo
|
||||
|
19
Makefile
19
Makefile
@ -1,19 +0,0 @@
|
||||
PANDOC:=$(shell \
|
||||
if pandoc </dev/null 2>/dev/null; then \
|
||||
echo pandoc; \
|
||||
else \
|
||||
echo "Warning: pandoc not installed; can't generate manpages." >&2; \
|
||||
echo '@echo Skipping: pandoc'; \
|
||||
fi)
|
||||
|
||||
default: all
|
||||
|
||||
all: sshuttle.8
|
||||
|
||||
sshuttle.8: sshuttle.md
|
||||
|
||||
%.8: %.md
|
||||
$(PANDOC) -s -r markdown -w man -o $@ $<
|
||||
|
||||
clean:
|
||||
rm -f *~ */*~ .*~ */.*~ *.8 *.tmp */*.tmp *.pyc */*.pyc
|
61
README.md
61
README.md
@ -1,3 +1,22 @@
|
||||
|
||||
WARNING:
|
||||
On MacOS 10.6 (at least up to 10.6.6), your network will
|
||||
stop responding about 10 minutes after the first time you
|
||||
start sshuttle, because of a MacOS kernel bug relating to
|
||||
arp and the net.inet.ip.scopedroute sysctl. To fix it,
|
||||
just switch your wireless off and on. Sshuttle makes the
|
||||
kernel setting it changes permanent, so this won't happen
|
||||
again, even after a reboot.
|
||||
|
||||
Required Software
|
||||
=================
|
||||
|
||||
- You need PyXAPI, available here:
|
||||
http://www.pps.univ-paris-diderot.fr/~ylg/PyXAPI/
|
||||
- You also need autossh, available in various package management systems
|
||||
- Python 2.x, both locally and the remote system
|
||||
|
||||
|
||||
sshuttle: where transparent proxy meets VPN meets ssh
|
||||
=====================================================
|
||||
|
||||
@ -40,19 +59,46 @@ Prerequisites
|
||||
Linux.)
|
||||
|
||||
- If you use MacOS or BSD on your client machine:
|
||||
Your kernel needs to be compiled with IPFIREWALL_FORWARD
|
||||
Your kernel needs to be compiled with `IPFIREWALL_FORWARD`
|
||||
(MacOS has this by default) and you need to have ipfw
|
||||
available. (The server doesn't need to be MacOS or BSD.)
|
||||
|
||||
|
||||
This is how you use it:
|
||||
Obtaining sshuttle
|
||||
------------------
|
||||
|
||||
- First, go get PyXAPI from the link above
|
||||
|
||||
- Clone: `git clone https://github.com/sshuttle/sshuttle.git`
|
||||
|
||||
|
||||
Usage on (Ubuntu) Linux
|
||||
-----------------------
|
||||
|
||||
- <tt>git clone git://github.com/apenwarr/sshuttle</tt>
|
||||
on your client machine. You'll need root or sudo
|
||||
access, and python needs to be installed.
|
||||
- `cd packaging; ./make_deb`
|
||||
|
||||
- <tt>./sshuttle -r username@sshserver 0.0.0.0/0 -vv</tt>
|
||||
- `sudo dpkg -i ./sshuttle-VERSION.deb`
|
||||
|
||||
- Check out the files in `/etc/sshuttle`; configure them so your tunnel works
|
||||
|
||||
- `sudo service sshuttle start`
|
||||
|
||||
|
||||
Usage on other Linuxes and OSes
|
||||
-------------------------------
|
||||
|
||||
<tt>./sshuttle -r username@sshserver 0.0.0.0/0 -vv</tt>
|
||||
|
||||
- There is a shortcut for 0.0.0.0/0 for those that value
|
||||
their wrists
|
||||
<tt>./sshuttle -r username@sshserver 0/0 -vv</tt>
|
||||
|
||||
- If you would also like your DNS queries to be proxied
|
||||
through the DNS server of the server you are connect to:
|
||||
<tt>./sshuttle --dns -vvr username@sshserver 0/0</tt>
|
||||
|
||||
The above is probably what you want to use to prevent
|
||||
local network attacks such as Firesheep and friends.
|
||||
|
||||
(You may be prompted for one or more passwords; first, the
|
||||
local password to become root using either sudo or su, and
|
||||
@ -60,6 +106,9 @@ then the remote ssh password. Or you might have sudo and ssh set
|
||||
up to not require passwords, in which case you won't be
|
||||
prompted at all.)
|
||||
|
||||
Usage Notes
|
||||
-----------
|
||||
|
||||
That's it! Now your local machine can access the remote network as if you
|
||||
were right there. And if your "client" machine is a router, everyone on
|
||||
your local network can make connections to your remote network.
|
||||
|
383
client.py
383
client.py
@ -1,383 +0,0 @@
|
||||
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
|
||||
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:
|
||||
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, 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), str(dnsport)])
|
||||
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, 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,
|
||||
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)")
|
||||
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))
|
||||
|
||||
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))
|
||||
|
||||
while 1:
|
||||
rv = serverproc.poll()
|
||||
if rv:
|
||||
raise Fatal('server died with error code %d' % rv)
|
||||
|
||||
ssnet.runonce(handlers, mux)
|
||||
if latency_control:
|
||||
mux.check_fullness()
|
||||
mux.callback()
|
||||
|
||||
|
||||
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')
|
||||
|
||||
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)
|
||||
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:
|
||||
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,))
|
||||
|
||||
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, 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()
|
433
firewall.py
433
firewall.py
@ -1,433 +0,0 @@
|
||||
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']
|
||||
p = ssubprocess.Popen(argv, stdout = ssubprocess.PIPE)
|
||||
for line in p.stdout:
|
||||
if line.startswith('Chain %s ' % name):
|
||||
return True
|
||||
rv = p.wait()
|
||||
if rv:
|
||||
raise Fatal('%r returned %d' % (argv, rv))
|
||||
|
||||
|
||||
def ipt(*args):
|
||||
argv = ['iptables', '-t', 'nat'] + list(args)
|
||||
debug1('>> %s\n' % ' '.join(argv))
|
||||
rv = ssubprocess.call(argv)
|
||||
if rv:
|
||||
raise Fatal('%r returned %d' % (argv, rv))
|
||||
|
||||
|
||||
_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, dnsport, subnets):
|
||||
chain = 'sshuttle-%s' % port
|
||||
|
||||
# basic cleanup/setup of chains
|
||||
if ipt_chain_exists(chain):
|
||||
ipt('-D', 'OUTPUT', '-j', chain)
|
||||
ipt('-D', 'PREROUTING', '-j', chain)
|
||||
ipt('-F', chain)
|
||||
ipt('-X', chain)
|
||||
|
||||
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
|
||||
# excludes to come first. That's why the columns are in such a non-
|
||||
# intuitive order.
|
||||
for swidth,sexclude,snet in sorted(subnets, reverse=True):
|
||||
if sexclude:
|
||||
ipt('-A', chain, '-j', 'RETURN',
|
||||
'--dest', '%s/%s' % (snet,swidth),
|
||||
'-p', 'tcp')
|
||||
else:
|
||||
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):
|
||||
argv = ['ipfw', 'list']
|
||||
p = ssubprocess.Popen(argv, stdout = ssubprocess.PIPE)
|
||||
found = False
|
||||
for line in p.stdout:
|
||||
if line.startswith('%05d ' % n):
|
||||
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())
|
||||
raise Fatal('non-sshuttle ipfw rule #%d already exists!' % n)
|
||||
found = True
|
||||
rv = p.wait()
|
||||
if rv:
|
||||
raise Fatal('%r returned %d' % (argv, rv))
|
||||
return found
|
||||
|
||||
|
||||
_oldctls = {}
|
||||
def _fill_oldctls(prefix):
|
||||
argv = ['sysctl', prefix]
|
||||
p = ssubprocess.Popen(argv, stdout = ssubprocess.PIPE)
|
||||
for line in p.stdout:
|
||||
assert(line[-1] == '\n')
|
||||
(k,v) = line[:-1].split(': ', 1)
|
||||
_oldctls[k] = v
|
||||
rv = p.wait()
|
||||
if rv:
|
||||
raise Fatal('%r returned %d' % (argv, rv))
|
||||
if not line:
|
||||
raise Fatal('%r returned no data' % (argv,))
|
||||
|
||||
|
||||
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'))
|
||||
|
||||
|
||||
_changedctls = []
|
||||
def sysctl_set(name, val):
|
||||
PREFIX = 'net.inet.ip'
|
||||
assert(name.startswith(PREFIX + '.'))
|
||||
val = str(val)
|
||||
if not _oldctls:
|
||||
_fill_oldctls(PREFIX)
|
||||
if not (name in _oldctls):
|
||||
debug1('>> No such sysctl: %r\n' % name)
|
||||
return
|
||||
oldval = _oldctls[name]
|
||||
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):
|
||||
argv = ['ipfw', '-q'] + list(args)
|
||||
debug1('>> %s\n' % ' '.join(argv))
|
||||
rv = ssubprocess.call(argv)
|
||||
if rv:
|
||||
raise Fatal('%r returned %d' % (argv, rv))
|
||||
|
||||
|
||||
def do_ipfw(port, dnsport, subnets):
|
||||
sport = str(port)
|
||||
xsport = str(port+1)
|
||||
|
||||
# cleanup any existing rules
|
||||
if ipfw_rule_exists(port):
|
||||
ipfw('delete', sport)
|
||||
|
||||
while _changedctls:
|
||||
name = _changedctls.pop()
|
||||
oldval = _oldctls[name]
|
||||
_sysctl_set(name, oldval)
|
||||
|
||||
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:
|
||||
ipfw('add', sport, 'skipto', xsport,
|
||||
'log', 'tcp',
|
||||
'from', 'any', 'to', '%s/%s' % (snet,swidth))
|
||||
else:
|
||||
ipfw('add', sport, 'fwd', '127.0.0.1,%d' % port,
|
||||
'log', 'tcp',
|
||||
'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)
|
||||
for p in paths:
|
||||
fn = '%s/%s' % (p, 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'
|
||||
BAKFILE='%s.sbak' % HOSTSFILE
|
||||
APPEND='# sshuttle-firewall-%d AUTOCREATED' % port
|
||||
old_content = ''
|
||||
st = None
|
||||
try:
|
||||
old_content = open(HOSTSFILE).read()
|
||||
st = os.stat(HOSTSFILE)
|
||||
except IOError, e:
|
||||
if e.errno == errno.ENOENT:
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
if old_content.strip() and not os.path.exists(BAKFILE):
|
||||
os.link(HOSTSFILE, BAKFILE)
|
||||
tmpname = "%s.%d.tmp" % (HOSTSFILE, port)
|
||||
f = open(tmpname, 'w')
|
||||
for line in old_content.rstrip().split('\n'):
|
||||
if line.find(APPEND) >= 0:
|
||||
continue
|
||||
f.write('%s\n' % line)
|
||||
for (name,ip) in sorted(hostmap.items()):
|
||||
f.write('%-30s %s\n' % ('%s %s' % (ip,name), APPEND))
|
||||
f.close()
|
||||
|
||||
if st:
|
||||
os.chown(tmpname, st.st_uid, st.st_gid)
|
||||
os.chmod(tmpname, st.st_mode)
|
||||
else:
|
||||
os.chown(tmpname, 0, 0)
|
||||
os.chmod(tmpname, 0644)
|
||||
os.rename(tmpname, HOSTSFILE)
|
||||
|
||||
|
||||
def restore_etc_hosts(port):
|
||||
global hostmap
|
||||
hostmap = {}
|
||||
rewrite_etc_hosts(port)
|
||||
|
||||
|
||||
# This is some voodoo for setting up the kernel's transparent
|
||||
# proxying stuff. If subnets is empty, we just delete our sshuttle rules;
|
||||
# otherwise we delete it, then make them from scratch.
|
||||
#
|
||||
# This code is supposed to clean up after itself by deleting its rules on
|
||||
# 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, 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')
|
||||
|
||||
if program_exists('ipfw'):
|
||||
do_it = do_ipfw
|
||||
elif program_exists('iptables'):
|
||||
do_it = do_iptables
|
||||
else:
|
||||
raise Fatal("can't find either ipfw or iptables; check your PATH")
|
||||
|
||||
# because of limitations of the 'su' command, the *real* stdin/stdout
|
||||
# are both attached to stdout initially. Clone stdout into stdin so we
|
||||
# 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()
|
||||
|
||||
# ctrl-c shouldn't be passed along to me. When the main sshuttle dies,
|
||||
# I'll die automatically.
|
||||
os.setsid()
|
||||
|
||||
# we wait until we get some input before creating the rules. That way,
|
||||
# sshuttle can launch us as early as possible (and get sudo password
|
||||
# authentication as early in the startup process as possible).
|
||||
line = sys.stdin.readline(128)
|
||||
if not line:
|
||||
return # parent died; nothing to do
|
||||
|
||||
subnets = []
|
||||
if line != 'ROUTES\n':
|
||||
raise Fatal('firewall: expected ROUTES but got %r' % line)
|
||||
while 1:
|
||||
line = sys.stdin.readline(128)
|
||||
if not line:
|
||||
raise Fatal('firewall: expected route but got %r' % line)
|
||||
elif line == 'GO\n':
|
||||
break
|
||||
try:
|
||||
(width,exclude,ip) = line.strip().split(',', 2)
|
||||
except:
|
||||
raise Fatal('firewall: expected route or GO but got %r' % line)
|
||||
subnets.append((int(width), bool(int(exclude)), ip))
|
||||
|
||||
try:
|
||||
if line:
|
||||
debug1('firewall manager: starting transproxy.\n')
|
||||
do_wait = do_it(port, dnsport, subnets)
|
||||
sys.stdout.write('STARTED\n')
|
||||
|
||||
try:
|
||||
sys.stdout.flush()
|
||||
except IOError:
|
||||
# the parent process died for some reason; he's surely been loud
|
||||
# enough, so no reason to report another error
|
||||
return
|
||||
|
||||
# Now we wait until EOF or any other kind of exception. We need
|
||||
# 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)
|
||||
hostmap[name] = ip
|
||||
rewrite_etc_hosts(port)
|
||||
elif line:
|
||||
raise Fatal('expected EOF, got %r' % line)
|
||||
else:
|
||||
break
|
||||
finally:
|
||||
try:
|
||||
debug1('firewall manager: undoing changes.\n')
|
||||
except:
|
||||
pass
|
||||
do_it(port, 0, [])
|
||||
restore_etc_hosts(port)
|
127
main.py
127
main.py
@ -1,127 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
import sys, os, re
|
||||
import helpers, options, client, server, firewall, hostwatch
|
||||
import compat.ssubprocess as ssubprocess
|
||||
from helpers import *
|
||||
|
||||
|
||||
# list of:
|
||||
# 1.2.3.4/5 or just 1.2.3.4
|
||||
def parse_subnets(subnets_str):
|
||||
subnets = []
|
||||
for s in subnets_str:
|
||||
m = re.match(r'(\d+)(?:\.(\d+)\.(\d+)\.(\d+))?(?:/(\d+))?$', s)
|
||||
if not m:
|
||||
raise Fatal('%r is not a valid IP subnet format' % s)
|
||||
(a,b,c,d,width) = m.groups()
|
||||
(a,b,c,d) = (int(a or 0), int(b or 0), int(c or 0), int(d or 0))
|
||||
if width == None:
|
||||
width = 32
|
||||
else:
|
||||
width = int(width)
|
||||
if a > 255 or b > 255 or c > 255 or d > 255:
|
||||
raise Fatal('%d.%d.%d.%d has numbers > 255' % (a,b,c,d))
|
||||
if width > 32:
|
||||
raise Fatal('*/%d is greater than the maximum of 32' % width)
|
||||
subnets.append(('%d.%d.%d.%d' % (a,b,c,d), width))
|
||||
return subnets
|
||||
|
||||
|
||||
# 1.2.3.4:567 or just 1.2.3.4 or just 567
|
||||
def parse_ipport(s):
|
||||
s = str(s)
|
||||
m = re.match(r'(?:(\d+)\.(\d+)\.(\d+)\.(\d+))?(?::)?(?:(\d+))?$', s)
|
||||
if not m:
|
||||
raise Fatal('%r is not a valid IP:port format' % s)
|
||||
(a,b,c,d,port) = m.groups()
|
||||
(a,b,c,d,port) = (int(a or 0), int(b or 0), int(c or 0), int(d or 0),
|
||||
int(port or 0))
|
||||
if a > 255 or b > 255 or c > 255 or d > 255:
|
||||
raise Fatal('%d.%d.%d.%d has numbers > 255' % (a,b,c,d))
|
||||
if port > 65535:
|
||||
raise Fatal('*:%d is greater than the maximum of 65535' % port)
|
||||
if a == None:
|
||||
a = b = c = d = 0
|
||||
return ('%d.%d.%d.%d' % (a,b,c,d), port)
|
||||
|
||||
|
||||
optspec = """
|
||||
sshuttle [-l [ip:]port] [-r [username@]sshserver[:port]] <subnets...>
|
||||
sshuttle --server
|
||||
sshuttle --firewall <port> <subnets...>
|
||||
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]
|
||||
server (internal use only)
|
||||
firewall (internal use only)
|
||||
hostwatch (internal use only)
|
||||
"""
|
||||
o = options.Options(optspec)
|
||||
(opt, flags, extra) = o.parse(sys.argv[1:])
|
||||
|
||||
if opt.daemon:
|
||||
opt.syslog = 1
|
||||
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) != 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:
|
||||
if len(extra) < 1 and not opt.auto_nets:
|
||||
o.fatal('at least one subnet (or -N) expected')
|
||||
includes = extra
|
||||
excludes = ['127.0.0.0/8']
|
||||
for k,v in flags:
|
||||
if k in ('-x','--exclude'):
|
||||
excludes.append(v)
|
||||
remotename = opt.remote
|
||||
if remotename == '' or remotename == '-':
|
||||
remotename = None
|
||||
if opt.seed_hosts and not opt.auto_hosts:
|
||||
o.fatal('--seed-hosts only works if you also use -H')
|
||||
if opt.seed_hosts:
|
||||
sh = re.split(r'[\s,]+', (opt.seed_hosts or "").strip())
|
||||
elif opt.auto_hosts:
|
||||
sh = []
|
||||
else:
|
||||
sh = None
|
||||
sys.exit(client.main(parse_ipport(opt.listen or '0.0.0.0:0'),
|
||||
opt.ssh_cmd,
|
||||
remotename,
|
||||
opt.python,
|
||||
opt.latency_control,
|
||||
opt.dns,
|
||||
sh,
|
||||
opt.auto_nets,
|
||||
parse_subnets(includes),
|
||||
parse_subnets(excludes),
|
||||
opt.syslog, opt.daemon, opt.pidfile))
|
||||
except Fatal, e:
|
||||
log('fatal: %s\n' % e)
|
||||
sys.exit(99)
|
||||
except KeyboardInterrupt:
|
||||
log('\n')
|
||||
log('Keyboard interrupt: exiting.\n')
|
||||
sys.exit(1)
|
26
packaging/control
Normal file
26
packaging/control
Normal file
@ -0,0 +1,26 @@
|
||||
Package: sshuttle
|
||||
Version: 0.2
|
||||
Architecture: i386
|
||||
Maintainer: Jim Wyllie <jwyllie83@gmail.com>
|
||||
Depends: autossh, upstart, python (>=2.6)
|
||||
Section: utils
|
||||
Priority: optional
|
||||
Homepage: http://github.com/jwyllie83/sshuttle.udp
|
||||
Description: "Full-featured" VPN over an SSH tunnel, allowing full remote
|
||||
access somewhere where all you have is an SSH connection. It works well if
|
||||
you generally find yourself in the following situation:
|
||||
.
|
||||
- Your client machine (or router) is Linux, FreeBSD, or MacOS.
|
||||
- You have access to a remote network via ssh.
|
||||
- You don't necessarily have admin access on the remote network.
|
||||
- You do not wish to, or can't, use other VPN software
|
||||
- You don't want to create an ssh port forward for every
|
||||
single host/port on the remote network.
|
||||
- You hate openssh's port forwarding because it's randomly
|
||||
slow and/or stupid.
|
||||
- You can't use openssh's PermitTunnel feature because
|
||||
it's disabled by default on openssh servers; plus it does
|
||||
TCP-over-TCP, which has suboptimal performance
|
||||
.
|
||||
It also has hooks for more complicated setups (VPN-in-a-SSH-VPN, etc) to allow
|
||||
you to set it up as you like.
|
41
packaging/make_deb
Executable file
41
packaging/make_deb
Executable file
@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# This script puts together a .deb package suitable for installing on an Ubuntu
|
||||
# system
|
||||
|
||||
B="/tmp/sshuttle/build"
|
||||
|
||||
if [ ! -x /usr/bin/dpkg ]; then
|
||||
echo 'Unable to build: dpkg not found on system'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create the new directory structure
|
||||
mkdir -p ${B}/etc/sshuttle/pre-start.d
|
||||
mkdir -p ${B}/etc/sshuttle/post-stop.d
|
||||
mkdir -p ${B}/usr/share/sshuttle
|
||||
mkdir -p ${B}/usr/bin
|
||||
mkdir -p ${B}/etc/init
|
||||
mkdir -p ${B}/DEBIAN
|
||||
|
||||
# Copy over all of the files
|
||||
cp -r ../src/* ${B}/usr/share/sshuttle
|
||||
cp ../src/sshuttle ${B}/usr/bin
|
||||
cp -r sshuttle.conf ${B}/etc/init
|
||||
cp prefixes.conf ${B}/etc/sshuttle
|
||||
cp tunnel.conf ${B}/etc/sshuttle
|
||||
|
||||
# Copy the control file over, as well
|
||||
cp control ${B}/DEBIAN
|
||||
|
||||
# Create the md5sum manifest
|
||||
if [ -x /usr/bin/md5sum ]; then
|
||||
cd ${B}
|
||||
find . -type f | egrep -v DEBIAN | sed -re 's/^..//' | xargs md5sum > ${B}/DEBIAN/md5sums
|
||||
cd ${OLDPWD}
|
||||
fi
|
||||
|
||||
# Build the debian package
|
||||
VERSION=$(egrep -e '^Version' control | sed -re 's/^[^:]*: //')
|
||||
dpkg --build ${B} ./sshuttle-${VERSION}.deb
|
||||
rm -rf ${B}
|
5
packaging/prefixes.conf
Normal file
5
packaging/prefixes.conf
Normal file
@ -0,0 +1,5 @@
|
||||
# Output prefixes here, one per line. Prefix is in:
|
||||
# prefix/netmask format
|
||||
# Like this:
|
||||
# 192.168.0.0/16
|
||||
# 192.0.43.10/32
|
90
packaging/sshuttle.conf
Normal file
90
packaging/sshuttle.conf
Normal file
@ -0,0 +1,90 @@
|
||||
description "Create a transparent proxy over SSH"
|
||||
author "Jim Wyllie <jwyllie83@gmail.com>"
|
||||
|
||||
manual
|
||||
nice -5
|
||||
|
||||
# Edit this file with network prefixes that should be loaded through the SSH
|
||||
# tunnel.
|
||||
env PREFIX_LOCATION=/etc/sshuttle/prefixes.conf
|
||||
|
||||
# Routing table; defaults to 100
|
||||
env ROUTE_TABLE=100
|
||||
|
||||
# fwmark; defaults to 1
|
||||
env FWMARK=1
|
||||
|
||||
# SSH tunnel configuration file
|
||||
env SSHUTTLE_TUNNEL_FILE=/etc/sshuttle/tunnel.conf
|
||||
|
||||
# File containing the tunnel proxy name / host / whatever
|
||||
env TUNNEL_PROXY="/etc/sshuttle/tunnel.conf"
|
||||
|
||||
# Any other commands needed to run before or after loading the SSH tunnel.
|
||||
# This is where you can put any of your hacks to set up tunnels-in-tunnels,
|
||||
# etc. Scripts in this directory are executed in order.
|
||||
env MISC_START_DIR=/etc/sshuttle/pre-start.d
|
||||
env MISC_STOP_DIR=/etc/sshuttle/post-stop.d
|
||||
|
||||
start on (local-filesystems and net-device-up IFACE!=lo)
|
||||
stop on stopping network-services
|
||||
|
||||
#respawn
|
||||
|
||||
pre-start script
|
||||
# Make sure we have created the routes
|
||||
sudo ip rule add fwmark ${FWMARK} lookup ${ROUTE_TABLE}
|
||||
logger "Starting sshuttle..."
|
||||
|
||||
if [ -f "${PREFIX_LOCATION}" ]; then
|
||||
cat "${PREFIX_LOCATION}" | while read ROUTE; do
|
||||
|
||||
# Skip comments
|
||||
if [ -n "$(echo ${ROUTE} | egrep "^[ ]*#")" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Skip empty lines
|
||||
if [ -z "${ROUTE}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
logger "Adding route: ${ROUTE}"
|
||||
ip route add local ${ROUTE} dev lo table ${ROUTE_TABLE}
|
||||
done
|
||||
fi
|
||||
|
||||
for RUNFILE in ${MISC_START_DIR}/*; do
|
||||
logger "Executing ${RUNFILE}"
|
||||
/bin/sh -c "${RUNFILE}"
|
||||
done
|
||||
end script
|
||||
|
||||
post-stop script
|
||||
if [ -f "${PREFIX_LOCATION}" ]; then
|
||||
cat "${PREFIX_LOCATION}" | while read ROUTE; do
|
||||
|
||||
# Skip comments
|
||||
if [ -n "$(echo ${ROUTE} | egrep "^[ ]*#")" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Skip empty lines
|
||||
if [ -z "${ROUTE}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
logger "Deleting route: ${ROUTE}"
|
||||
ip route del local ${ROUTE} dev lo table ${ROUTE_TABLE}
|
||||
done
|
||||
fi
|
||||
|
||||
ip rule del fwmark ${FWMARK}
|
||||
|
||||
for RUNFILE in "${MISC_STOP_DIR}/*"; do
|
||||
logger "Executing ${RUNFILE}"
|
||||
/bin/sh -c "${RUNFILE}"
|
||||
done
|
||||
end script
|
||||
|
||||
exec /usr/bin/sshuttle --dns --method=tproxy --listen 0.0.0.0 --remote sshuttle_tunnel -s /etc/sshuttle/prefixes.conf -e "ssh -F ${TUNNEL_PROXY}"
|
19
packaging/tunnel.conf
Normal file
19
packaging/tunnel.conf
Normal file
@ -0,0 +1,19 @@
|
||||
# Here is where you can specify any SSH tunnel options See ssh_config(5) for
|
||||
# details. You need to leave the Host line intact, but everything else can
|
||||
# specify whatever you want
|
||||
Host sshuttle_tunnel
|
||||
|
||||
# REQUIRED: Set this to be the host to which you would like to connect your
|
||||
# tunnel
|
||||
#Hostname localhost
|
||||
|
||||
# REQUIRED: Set this to be the target SSH user on the remote system
|
||||
#User foo
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The rest are all optional; see ssh_config(5) for the full list of what can
|
||||
# be specified. Some very commonly needed ones are below.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# SSH key used for connecting
|
||||
#IdentityFile /path/to/key
|
212
server.py
212
server.py
@ -1,212 +0,0 @@
|
||||
import re, struct, socket, select, traceback, time
|
||||
if not globals().get('skip_imports'):
|
||||
import ssnet, helpers, hostwatch
|
||||
import compat.ssubprocess as ssubprocess
|
||||
from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
|
||||
from helpers import *
|
||||
|
||||
|
||||
def _ipmatch(ipstr):
|
||||
if ipstr == 'default':
|
||||
ipstr = '0.0.0.0/0'
|
||||
m = re.match(r'^(\d+(\.\d+(\.\d+(\.\d+)?)?)?)(?:/(\d+))?$', ipstr)
|
||||
if m:
|
||||
g = m.groups()
|
||||
ips = g[0]
|
||||
width = int(g[4] or 32)
|
||||
if g[1] == None:
|
||||
ips += '.0.0.0'
|
||||
width = min(width, 8)
|
||||
elif g[2] == None:
|
||||
ips += '.0.0'
|
||||
width = min(width, 16)
|
||||
elif g[3] == None:
|
||||
ips += '.0'
|
||||
width = min(width, 24)
|
||||
return (struct.unpack('!I', socket.inet_aton(ips))[0], width)
|
||||
|
||||
|
||||
def _ipstr(ip, width):
|
||||
if width >= 32:
|
||||
return ip
|
||||
else:
|
||||
return "%s/%d" % (ip, width)
|
||||
|
||||
|
||||
def _maskbits(netmask):
|
||||
if not netmask:
|
||||
return 32
|
||||
for i in range(32):
|
||||
if netmask[0] & _shl(1, i):
|
||||
return 32-i
|
||||
return 0
|
||||
|
||||
|
||||
def _shl(n, bits):
|
||||
return n * int(2**bits)
|
||||
|
||||
|
||||
def _list_routes():
|
||||
argv = ['netstat', '-rn']
|
||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE)
|
||||
routes = []
|
||||
for line in p.stdout:
|
||||
cols = re.split(r'\s+', line)
|
||||
ipw = _ipmatch(cols[0])
|
||||
if not ipw:
|
||||
continue # some lines won't be parseable; never mind
|
||||
maskw = _ipmatch(cols[2]) # linux only
|
||||
mask = _maskbits(maskw) # returns 32 if maskw is null
|
||||
width = min(ipw[1], mask)
|
||||
ip = ipw[0] & _shl(_shl(1, width) - 1, 32-width)
|
||||
routes.append((socket.inet_ntoa(struct.pack('!I', ip)), width))
|
||||
rv = p.wait()
|
||||
if rv != 0:
|
||||
log('WARNING: %r returned %d\n' % (argv, rv))
|
||||
log('WARNING: That prevents --auto-nets from working.\n')
|
||||
return routes
|
||||
|
||||
|
||||
def list_routes():
|
||||
for (ip,width) in _list_routes():
|
||||
if not ip.startswith('0.') and not ip.startswith('127.'):
|
||||
yield (ip,width)
|
||||
|
||||
|
||||
def _exc_dump():
|
||||
exc_info = sys.exc_info()
|
||||
return ''.join(traceback.format_exception(*exc_info))
|
||||
|
||||
|
||||
def start_hostwatch(seed_hosts):
|
||||
s1,s2 = socket.socketpair()
|
||||
pid = os.fork()
|
||||
if not pid:
|
||||
# child
|
||||
rv = 99
|
||||
try:
|
||||
try:
|
||||
s2.close()
|
||||
os.dup2(s1.fileno(), 1)
|
||||
os.dup2(s1.fileno(), 0)
|
||||
s1.close()
|
||||
rv = hostwatch.hw_main(seed_hosts) or 0
|
||||
except Exception, e:
|
||||
log('%s\n' % _exc_dump())
|
||||
rv = 98
|
||||
finally:
|
||||
os._exit(rv)
|
||||
s1.close()
|
||||
return pid,s2
|
||||
|
||||
|
||||
class Hostwatch:
|
||||
def __init__(self):
|
||||
self.pid = 0
|
||||
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')
|
||||
for r in routes:
|
||||
debug1(' %s/%d\n' % r)
|
||||
|
||||
# synchronization header
|
||||
sys.stdout.write('SSHUTTLE0001')
|
||||
sys.stdout.flush()
|
||||
|
||||
handlers = []
|
||||
mux = Mux(socket.fromfd(sys.stdin.fileno(),
|
||||
socket.AF_INET, socket.SOCK_STREAM),
|
||||
socket.fromfd(sys.stdout.fileno(),
|
||||
socket.AF_INET, socket.SOCK_STREAM))
|
||||
handlers.append(mux)
|
||||
routepkt = ''
|
||||
for r in routes:
|
||||
routepkt += '%s,%d\n' % r
|
||||
mux.send(0, ssnet.CMD_ROUTES, routepkt)
|
||||
|
||||
hw = Hostwatch()
|
||||
hw.leftover = ''
|
||||
|
||||
def hostwatch_ready():
|
||||
assert(hw.pid)
|
||||
content = hw.sock.recv(4096)
|
||||
if content:
|
||||
lines = (hw.leftover + content).split('\n')
|
||||
if lines[-1]:
|
||||
# no terminating newline: entry isn't complete yet!
|
||||
hw.leftover = lines.pop()
|
||||
lines.append('')
|
||||
else:
|
||||
hw.leftover = ''
|
||||
mux.send(0, ssnet.CMD_HOST_LIST, '\n'.join(lines))
|
||||
else:
|
||||
raise Fatal('hostwatch process died')
|
||||
|
||||
def got_host_req(data):
|
||||
if not hw.pid:
|
||||
(hw.pid,hw.sock) = start_hostwatch(data.strip().split())
|
||||
handlers.append(Handler(socks = [hw.sock],
|
||||
callback = hostwatch_ready))
|
||||
mux.got_host_req = got_host_req
|
||||
|
||||
def new_channel(channel, data):
|
||||
(dstip,dstport) = data.split(',', 1)
|
||||
dstport = int(dstport)
|
||||
outwrap = ssnet.connect_dst(dstip,dstport)
|
||||
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
|
10
src/Makefile
Normal file
10
src/Makefile
Normal file
@ -0,0 +1,10 @@
|
||||
all:
|
||||
|
||||
Makefile:
|
||||
@
|
||||
|
||||
%: FORCE
|
||||
+./do $@
|
||||
|
||||
.PHONY: FORCE
|
||||
|
11
src/all.do
Normal file
11
src/all.do
Normal file
@ -0,0 +1,11 @@
|
||||
exec >&2
|
||||
UI=
|
||||
[ "$(uname)" = "Darwin" ] && UI=ui-macos/all
|
||||
redo-ifchange sshuttle.8 $UI
|
||||
|
||||
echo
|
||||
echo "What now?"
|
||||
[ -z "$UI" ] || echo "- Try the MacOS GUI: open ui-macos/Sshuttle*.app"
|
||||
echo "- Run sshuttle: ./sshuttle --dns -r HOSTNAME 0/0"
|
||||
echo "- Read the README: less README.md"
|
||||
echo "- Read the man page: less sshuttle.md"
|
@ -1,4 +1,5 @@
|
||||
import sys, zlib
|
||||
import sys
|
||||
import zlib
|
||||
|
||||
z = zlib.decompressobj()
|
||||
mainmod = sys.modules[__name__]
|
2
src/clean.do
Normal file
2
src/clean.do
Normal file
@ -0,0 +1,2 @@
|
||||
redo ui-macos/clean
|
||||
rm -f *~ */*~ .*~ */.*~ *.8 *.tmp */*.tmp *.pyc */*.pyc
|
776
src/client.py
Normal file
776
src/client.py
Normal file
@ -0,0 +1,776 @@
|
||||
import struct
|
||||
import errno
|
||||
import re
|
||||
import signal
|
||||
import time
|
||||
import compat.ssubprocess as ssubprocess
|
||||
import helpers
|
||||
import os
|
||||
import ssnet
|
||||
import ssh
|
||||
import ssyslog
|
||||
import sys
|
||||
from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
|
||||
from helpers import log, debug1, debug2, debug3, Fatal, islocal
|
||||
|
||||
recvmsg = None
|
||||
try:
|
||||
# try getting recvmsg from python
|
||||
import socket as pythonsocket
|
||||
getattr(pythonsocket.socket, "recvmsg")
|
||||
socket = pythonsocket
|
||||
recvmsg = "python"
|
||||
except AttributeError:
|
||||
# try getting recvmsg from socket_ext library
|
||||
try:
|
||||
import socket_ext
|
||||
getattr(socket_ext.socket, "recvmsg")
|
||||
socket = socket_ext
|
||||
recvmsg = "socket_ext"
|
||||
except ImportError:
|
||||
import socket
|
||||
|
||||
_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
|
||||
IP_TRANSPARENT = 19
|
||||
IP_ORIGDSTADDR = 20
|
||||
IP_RECVORIGDSTADDR = IP_ORIGDSTADDR
|
||||
SOL_IPV6 = 41
|
||||
IPV6_ORIGDSTADDR = 74
|
||||
IPV6_RECVORIGDSTADDR = IPV6_ORIGDSTADDR
|
||||
|
||||
|
||||
if recvmsg == "python":
|
||||
def recv_udp(listener, bufsize):
|
||||
debug3('Accept UDP python using recvmsg.\n')
|
||||
data, ancdata, msg_flags, srcip = listener.recvmsg(
|
||||
4096, socket.CMSG_SPACE(24))
|
||||
dstip = None
|
||||
family = None
|
||||
for cmsg_level, cmsg_type, cmsg_data in ancdata:
|
||||
if cmsg_level == socket.SOL_IP and cmsg_type == IP_ORIGDSTADDR:
|
||||
family, port = struct.unpack('=HH', cmsg_data[0:4])
|
||||
port = socket.htons(port)
|
||||
if family == socket.AF_INET:
|
||||
start = 4
|
||||
length = 4
|
||||
else:
|
||||
raise Fatal("Unsupported socket type '%s'" % family)
|
||||
ip = socket.inet_ntop(family, cmsg_data[start:start + length])
|
||||
dstip = (ip, port)
|
||||
break
|
||||
elif cmsg_level == SOL_IPV6 and cmsg_type == IPV6_ORIGDSTADDR:
|
||||
family, port = struct.unpack('=HH', cmsg_data[0:4])
|
||||
port = socket.htons(port)
|
||||
if family == socket.AF_INET6:
|
||||
start = 8
|
||||
length = 16
|
||||
else:
|
||||
raise Fatal("Unsupported socket type '%s'" % family)
|
||||
ip = socket.inet_ntop(family, cmsg_data[start:start + length])
|
||||
dstip = (ip, port)
|
||||
break
|
||||
return (srcip, dstip, data)
|
||||
elif recvmsg == "socket_ext":
|
||||
def recv_udp(listener, bufsize):
|
||||
debug3('Accept UDP using socket_ext recvmsg.\n')
|
||||
srcip, data, adata, flags = listener.recvmsg(
|
||||
(bufsize,), socket.CMSG_SPACE(24))
|
||||
dstip = None
|
||||
family = None
|
||||
for a in adata:
|
||||
if a.cmsg_level == socket.SOL_IP and a.cmsg_type == IP_ORIGDSTADDR:
|
||||
family, port = struct.unpack('=HH', a.cmsg_data[0:4])
|
||||
port = socket.htons(port)
|
||||
if family == socket.AF_INET:
|
||||
start = 4
|
||||
length = 4
|
||||
else:
|
||||
raise Fatal("Unsupported socket type '%s'" % family)
|
||||
ip = socket.inet_ntop(
|
||||
family, a.cmsg_data[start:start + length])
|
||||
dstip = (ip, port)
|
||||
break
|
||||
elif a.cmsg_level == SOL_IPV6 and a.cmsg_type == IPV6_ORIGDSTADDR:
|
||||
family, port = struct.unpack('=HH', a.cmsg_data[0:4])
|
||||
port = socket.htons(port)
|
||||
if family == socket.AF_INET6:
|
||||
start = 8
|
||||
length = 16
|
||||
else:
|
||||
raise Fatal("Unsupported socket type '%s'" % family)
|
||||
ip = socket.inet_ntop(
|
||||
family, a.cmsg_data[start:start + length])
|
||||
dstip = (ip, port)
|
||||
break
|
||||
return (srcip, dstip, data[0])
|
||||
else:
|
||||
def recv_udp(listener, bufsize):
|
||||
debug3('Accept UDP using recvfrom.\n')
|
||||
data, srcip = listener.recvfrom(bufsize)
|
||||
return (srcip, None, data)
|
||||
|
||||
|
||||
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
|
||||
|
||||
pf_command_file = None
|
||||
|
||||
def pf_dst(sock):
|
||||
peer = sock.getpeername()
|
||||
proxy = sock.getsockname()
|
||||
|
||||
argv = (sock.family, socket.IPPROTO_TCP, peer[0], peer[1], proxy[0], proxy[1])
|
||||
pf_command_file.write("QUERY_PF_NAT %r,%r,%s,%r,%s,%r\n" % argv)
|
||||
pf_command_file.flush()
|
||||
line = pf_command_file.readline()
|
||||
debug2("QUERY_PF_NAT %r,%r,%s,%r,%s,%r" % argv + ' > ' + line)
|
||||
if line.startswith('QUERY_PF_NAT_SUCCESS '):
|
||||
(ip, port) = line[21:].split(',')
|
||||
return (ip, int(port))
|
||||
|
||||
return sock.getsockname()
|
||||
|
||||
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 MultiListener:
|
||||
|
||||
def __init__(self, type=socket.SOCK_STREAM, proto=0):
|
||||
self.v6 = socket.socket(socket.AF_INET6, type, proto)
|
||||
self.v4 = socket.socket(socket.AF_INET, type, proto)
|
||||
|
||||
def setsockopt(self, level, optname, value):
|
||||
if self.v6:
|
||||
self.v6.setsockopt(level, optname, value)
|
||||
if self.v4:
|
||||
self.v4.setsockopt(level, optname, value)
|
||||
|
||||
def add_handler(self, handlers, callback, method, mux):
|
||||
if self.v6:
|
||||
handlers.append(
|
||||
Handler(
|
||||
[self.v6],
|
||||
lambda: callback(self.v6, method, mux, handlers)))
|
||||
if self.v4:
|
||||
handlers.append(
|
||||
Handler(
|
||||
[self.v4],
|
||||
lambda: callback(self.v4, method, mux, handlers)))
|
||||
|
||||
def listen(self, backlog):
|
||||
if self.v6:
|
||||
self.v6.listen(backlog)
|
||||
if self.v4:
|
||||
try:
|
||||
self.v4.listen(backlog)
|
||||
except socket.error, e:
|
||||
# on some systems v4 bind will fail if the v6 suceeded,
|
||||
# in this case the v6 socket will receive v4 too.
|
||||
if e.errno == errno.EADDRINUSE and self.v6:
|
||||
self.v4 = None
|
||||
else:
|
||||
raise e
|
||||
|
||||
def bind(self, address_v6, address_v4):
|
||||
if address_v6 and self.v6:
|
||||
self.v6.bind(address_v6)
|
||||
else:
|
||||
self.v6 = None
|
||||
if address_v4 and self.v4:
|
||||
self.v4.bind(address_v4)
|
||||
else:
|
||||
self.v4 = None
|
||||
|
||||
def print_listening(self, what):
|
||||
if self.v6:
|
||||
listenip = self.v6.getsockname()
|
||||
debug1('%s listening on %r.\n' % (what, listenip))
|
||||
if self.v4:
|
||||
listenip = self.v4.getsockname()
|
||||
debug1('%s listening on %r.\n' % (what, listenip))
|
||||
|
||||
|
||||
class FirewallClient:
|
||||
|
||||
def __init__(self, port_v6, port_v4, subnets_include, subnets_exclude,
|
||||
dnsport_v6, dnsport_v4, method, udp):
|
||||
self.auto_nets = []
|
||||
self.subnets_include = subnets_include
|
||||
self.subnets_exclude = subnets_exclude
|
||||
argvbase = ([sys.argv[1], sys.argv[0], sys.argv[1]] +
|
||||
['-v'] * (helpers.verbose or 0) +
|
||||
['--firewall', str(port_v6), str(port_v4),
|
||||
str(dnsport_v6), str(dnsport_v4),
|
||||
method, str(int(udp))])
|
||||
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[0:5] != 'READY':
|
||||
raise Fatal('%r expected READY, got %r' % (self.argv, line))
|
||||
self.method = line[6:-1]
|
||||
|
||||
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 (family, ip, width) in self.subnets_include + self.auto_nets:
|
||||
self.pfile.write('%d,%d,0,%s\n' % (family, width, ip))
|
||||
for (family, ip, width) in self.subnets_exclude:
|
||||
self.pfile.write('%d,%d,1,%s\n' % (family, 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))
|
||||
|
||||
|
||||
dnsreqs = {}
|
||||
udp_by_src = {}
|
||||
|
||||
|
||||
def expire_connections(now, mux):
|
||||
for chan, timeout in dnsreqs.items():
|
||||
if timeout < now:
|
||||
debug3('expiring dnsreqs channel=%d\n' % chan)
|
||||
del mux.channels[chan]
|
||||
del dnsreqs[chan]
|
||||
debug3('Remaining DNS requests: %d\n' % len(dnsreqs))
|
||||
for peer, (chan, timeout) in udp_by_src.items():
|
||||
if timeout < now:
|
||||
debug3('expiring UDP channel channel=%d peer=%r\n' % (chan, peer))
|
||||
mux.send(chan, ssnet.CMD_UDP_CLOSE, '')
|
||||
del mux.channels[chan]
|
||||
del udp_by_src[peer]
|
||||
debug3('Remaining UDP channels: %d\n' % len(udp_by_src))
|
||||
|
||||
|
||||
def onaccept_tcp(listener, method, mux, handlers):
|
||||
global _extra_fd
|
||||
try:
|
||||
sock, srcip = listener.accept()
|
||||
except socket.error, e:
|
||||
if e.args[0] in [errno.EMFILE, errno.ENFILE]:
|
||||
debug1('Rejected incoming connection: too many open files!\n')
|
||||
# free up an fd so we can eat the connection
|
||||
os.close(_extra_fd)
|
||||
try:
|
||||
sock, srcip = listener.accept()
|
||||
sock.close()
|
||||
finally:
|
||||
_extra_fd = os.open('/dev/null', os.O_RDONLY)
|
||||
return
|
||||
else:
|
||||
raise
|
||||
if method == "tproxy":
|
||||
dstip = sock.getsockname()
|
||||
elif method == "pf":
|
||||
dstip = pf_dst(sock)
|
||||
else:
|
||||
dstip = original_dst(sock)
|
||||
debug1('Accept TCP: %s:%r -> %s:%r.\n' % (srcip[0], srcip[1],
|
||||
dstip[0], dstip[1]))
|
||||
if dstip[1] == sock.getsockname()[1] and islocal(dstip[0], sock.family):
|
||||
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_TCP_CONNECT, '%d,%s,%s' %
|
||||
(sock.family, dstip[0], dstip[1]))
|
||||
outwrap = MuxWrapper(mux, chan)
|
||||
handlers.append(Proxy(SockWrapper(sock, sock), outwrap))
|
||||
expire_connections(time.time(), mux)
|
||||
|
||||
|
||||
def udp_done(chan, data, method, family, dstip):
|
||||
(src, srcport, data) = data.split(",", 2)
|
||||
srcip = (src, int(srcport))
|
||||
debug3('doing send from %r to %r\n' % (srcip, dstip,))
|
||||
|
||||
try:
|
||||
sender = socket.socket(family, socket.SOCK_DGRAM)
|
||||
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sender.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
|
||||
sender.bind(srcip)
|
||||
sender.sendto(data, dstip)
|
||||
sender.close()
|
||||
except socket.error, e:
|
||||
debug1('-- ignored socket error sending UDP data: %r\n' % e)
|
||||
|
||||
|
||||
def onaccept_udp(listener, method, mux, handlers):
|
||||
now = time.time()
|
||||
srcip, dstip, data = recv_udp(listener, 4096)
|
||||
if not dstip:
|
||||
debug1(
|
||||
"-- ignored UDP from %r: "
|
||||
"couldn't determine destination IP address\n" % (srcip,))
|
||||
return
|
||||
debug1('Accept UDP: %r -> %r.\n' % (srcip, dstip,))
|
||||
if srcip in udp_by_src:
|
||||
chan, timeout = udp_by_src[srcip]
|
||||
else:
|
||||
chan = mux.next_channel()
|
||||
mux.channels[chan] = lambda cmd, data: udp_done(
|
||||
chan, data, method, listener.family, dstip=srcip)
|
||||
mux.send(chan, ssnet.CMD_UDP_OPEN, listener.family)
|
||||
udp_by_src[srcip] = chan, now + 30
|
||||
|
||||
hdr = "%s,%r," % (dstip[0], dstip[1])
|
||||
mux.send(chan, ssnet.CMD_UDP_DATA, hdr + data)
|
||||
|
||||
expire_connections(now, mux)
|
||||
|
||||
|
||||
def dns_done(chan, data, method, sock, srcip, dstip, mux):
|
||||
debug3('dns_done: channel=%d src=%r dst=%r\n' % (chan, srcip, dstip))
|
||||
del mux.channels[chan]
|
||||
del dnsreqs[chan]
|
||||
if method == "tproxy":
|
||||
debug3('doing send from %r to %r\n' % (srcip, dstip,))
|
||||
sender = socket.socket(sock.family, socket.SOCK_DGRAM)
|
||||
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sender.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
|
||||
sender.bind(srcip)
|
||||
sender.sendto(data, dstip)
|
||||
sender.close()
|
||||
else:
|
||||
debug3('doing sendto %r\n' % (dstip,))
|
||||
sock.sendto(data, dstip)
|
||||
|
||||
|
||||
def ondns(listener, method, mux, handlers):
|
||||
now = time.time()
|
||||
srcip, dstip, data = recv_udp(listener, 4096)
|
||||
if method == "tproxy" and not dstip:
|
||||
debug1(
|
||||
"-- ignored UDP from %r: "
|
||||
"couldn't determine destination IP address\n" % (srcip,))
|
||||
return
|
||||
debug1('DNS request from %r to %r: %d bytes\n' % (srcip, dstip, len(data)))
|
||||
chan = mux.next_channel()
|
||||
dnsreqs[chan] = now + 30
|
||||
mux.send(chan, ssnet.CMD_DNS_REQ, data)
|
||||
mux.channels[chan] = lambda cmd, data: dns_done(
|
||||
chan, data, method, listener, srcip=dstip, dstip=srcip, mux=mux)
|
||||
expire_connections(now, mux)
|
||||
|
||||
|
||||
def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
||||
python, latency_control,
|
||||
dns_listener, method, 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,
|
||||
options=dict(latency_control=latency_control, method=method))
|
||||
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:
|
||||
v = 'x'
|
||||
while v and v != '\0':
|
||||
v = serversock.recv(1)
|
||||
v = 'x'
|
||||
while v and v != '\0':
|
||||
v = serversock.recv(1)
|
||||
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'):
|
||||
(family, ip, width) = line.split(',', 2)
|
||||
fw.auto_nets.append((family, 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
|
||||
|
||||
tcp_listener.add_handler(handlers, onaccept_tcp, method, mux)
|
||||
|
||||
if udp_listener:
|
||||
udp_listener.add_handler(handlers, onaccept_udp, method, mux)
|
||||
|
||||
if dns_listener:
|
||||
dns_listener.add_handler(handlers, ondns, method, mux)
|
||||
|
||||
if seed_hosts is not 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)
|
||||
if latency_control:
|
||||
mux.check_fullness()
|
||||
mux.callback()
|
||||
|
||||
|
||||
def main(listenip_v6, listenip_v4,
|
||||
ssh_cmd, remotename, python, latency_control, dns,
|
||||
method, 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')
|
||||
|
||||
if recvmsg is not None:
|
||||
debug1("recvmsg %s support enabled.\n" % recvmsg)
|
||||
|
||||
if method == "tproxy":
|
||||
if recvmsg is not None:
|
||||
debug1("tproxy UDP support enabled.\n")
|
||||
udp = True
|
||||
else:
|
||||
debug1("tproxy UDP support requires recvmsg function.\n")
|
||||
udp = False
|
||||
if dns and recvmsg is None:
|
||||
debug1("tproxy DNS support requires recvmsg function.\n")
|
||||
dns = False
|
||||
else:
|
||||
debug1("UDP support requires tproxy; disabling UDP.\n")
|
||||
udp = False
|
||||
|
||||
if listenip_v6 and listenip_v6[1] and listenip_v4 and listenip_v4[1]:
|
||||
# if both ports given, no need to search for a spare port
|
||||
ports = [0, ]
|
||||
else:
|
||||
# if at least one port missing, we have to search
|
||||
ports = xrange(12300, 9000, -1)
|
||||
|
||||
# search for free ports and try to bind
|
||||
last_e = None
|
||||
redirectport_v6 = 0
|
||||
redirectport_v4 = 0
|
||||
bound = False
|
||||
debug2('Binding redirector:')
|
||||
for port in ports:
|
||||
debug2(' %d' % port)
|
||||
tcp_listener = MultiListener()
|
||||
tcp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
|
||||
if udp:
|
||||
udp_listener = MultiListener(socket.SOCK_DGRAM)
|
||||
udp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
else:
|
||||
udp_listener = None
|
||||
|
||||
if listenip_v6 and listenip_v6[1]:
|
||||
lv6 = listenip_v6
|
||||
redirectport_v6 = lv6[1]
|
||||
elif listenip_v6:
|
||||
lv6 = (listenip_v6[0], port)
|
||||
redirectport_v6 = port
|
||||
else:
|
||||
lv6 = None
|
||||
redirectport_v6 = 0
|
||||
|
||||
if listenip_v4 and listenip_v4[1]:
|
||||
lv4 = listenip_v4
|
||||
redirectport_v4 = lv4[1]
|
||||
elif listenip_v4:
|
||||
lv4 = (listenip_v4[0], port)
|
||||
redirectport_v4 = port
|
||||
else:
|
||||
lv4 = None
|
||||
redirectport_v4 = 0
|
||||
|
||||
try:
|
||||
tcp_listener.bind(lv6, lv4)
|
||||
if udp_listener:
|
||||
udp_listener.bind(lv6, lv4)
|
||||
bound = True
|
||||
break
|
||||
except socket.error, e:
|
||||
if e.errno == errno.EADDRINUSE:
|
||||
last_e = e
|
||||
else:
|
||||
raise e
|
||||
debug2('\n')
|
||||
if not bound:
|
||||
assert(last_e)
|
||||
raise last_e
|
||||
tcp_listener.listen(10)
|
||||
tcp_listener.print_listening("TCP redirector")
|
||||
if udp_listener:
|
||||
udp_listener.print_listening("UDP redirector")
|
||||
|
||||
bound = False
|
||||
if dns:
|
||||
# search for spare port for DNS
|
||||
debug2('Binding DNS:')
|
||||
ports = xrange(12300, 9000, -1)
|
||||
for port in ports:
|
||||
debug2(' %d' % port)
|
||||
dns_listener = MultiListener(socket.SOCK_DGRAM)
|
||||
|
||||
if listenip_v6:
|
||||
lv6 = (listenip_v6[0], port)
|
||||
dnsport_v6 = port
|
||||
else:
|
||||
lv6 = None
|
||||
dnsport_v6 = 0
|
||||
|
||||
if listenip_v4:
|
||||
lv4 = (listenip_v4[0], port)
|
||||
dnsport_v4 = port
|
||||
else:
|
||||
lv4 = None
|
||||
dnsport_v4 = 0
|
||||
|
||||
try:
|
||||
dns_listener.bind(lv6, lv4)
|
||||
bound = True
|
||||
break
|
||||
except socket.error, e:
|
||||
if e.errno == errno.EADDRINUSE:
|
||||
last_e = e
|
||||
else:
|
||||
raise e
|
||||
debug2('\n')
|
||||
dns_listener.print_listening("DNS")
|
||||
if not bound:
|
||||
assert(last_e)
|
||||
raise last_e
|
||||
else:
|
||||
dnsport_v6 = 0
|
||||
dnsport_v4 = 0
|
||||
dns_listener = None
|
||||
|
||||
fw = FirewallClient(redirectport_v6, redirectport_v4, subnets_include,
|
||||
subnets_exclude, dnsport_v6, dnsport_v4, method, udp)
|
||||
|
||||
if fw.method == "tproxy":
|
||||
tcp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
|
||||
if udp_listener:
|
||||
udp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
|
||||
if udp_listener.v4 is not None:
|
||||
udp_listener.v4.setsockopt(
|
||||
socket.SOL_IP, IP_RECVORIGDSTADDR, 1)
|
||||
if udp_listener.v6 is not None:
|
||||
udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1)
|
||||
if dns_listener:
|
||||
dns_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
|
||||
if dns_listener.v4 is not None:
|
||||
dns_listener.v4.setsockopt(
|
||||
socket.SOL_IP, IP_RECVORIGDSTADDR, 1)
|
||||
if dns_listener.v6 is not None:
|
||||
dns_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1)
|
||||
|
||||
if fw.method == "pf":
|
||||
global pf_command_file
|
||||
pf_command_file = fw.pfile
|
||||
|
||||
try:
|
||||
return _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
||||
python, latency_control, dns_listener,
|
||||
fw.method, 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()
|
7
src/default.8.do
Normal file
7
src/default.8.do
Normal file
@ -0,0 +1,7 @@
|
||||
exec >&2
|
||||
if pandoc </dev/null 2>/dev/null; then
|
||||
pandoc -s -r markdown -w man -o $3 $2.md
|
||||
else
|
||||
echo "Warning: pandoc not installed; can't generate manpages."
|
||||
redo-always
|
||||
fi
|
175
src/do
Executable file
175
src/do
Executable file
@ -0,0 +1,175 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# A minimal alternative to djb redo that doesn't support incremental builds.
|
||||
# For the full version, visit http://github.com/apenwarr/redo
|
||||
#
|
||||
# The author disclaims copyright to this source file and hereby places it in
|
||||
# the public domain. (2010 12 14)
|
||||
#
|
||||
|
||||
# By default, no output coloring.
|
||||
green=""
|
||||
bold=""
|
||||
plain=""
|
||||
|
||||
if [ -n "$TERM" -a "$TERM" != "dumb" ] && tty <&2 >/dev/null 2>&1; then
|
||||
green="$(printf '\033[32m')"
|
||||
bold="$(printf '\033[1m')"
|
||||
plain="$(printf '\033[m')"
|
||||
fi
|
||||
|
||||
_dirsplit()
|
||||
{
|
||||
base=${1##*/}
|
||||
dir=${1%$base}
|
||||
}
|
||||
|
||||
dirname()
|
||||
(
|
||||
_dirsplit "$1"
|
||||
dir=${dir%/}
|
||||
echo "${dir:-.}"
|
||||
)
|
||||
|
||||
_dirsplit "$0"
|
||||
export REDO=$(cd "${dir:-.}" && echo "$PWD/$base")
|
||||
|
||||
DO_TOP=
|
||||
if [ -z "$DO_BUILT" ]; then
|
||||
DO_TOP=1
|
||||
[ -n "$*" ] || set all # only toplevel redo has a default target
|
||||
export DO_BUILT=$PWD/.do_built
|
||||
: >>"$DO_BUILT"
|
||||
echo "Removing previously built files..." >&2
|
||||
sort -u "$DO_BUILT" | tee "$DO_BUILT.new" |
|
||||
while read f; do printf "%s\0%s.did\0" "$f" "$f"; done |
|
||||
xargs -0 rm -f 2>/dev/null
|
||||
mv "$DO_BUILT.new" "$DO_BUILT"
|
||||
DO_PATH=$DO_BUILT.dir
|
||||
export PATH=$DO_PATH:$PATH
|
||||
rm -rf "$DO_PATH"
|
||||
mkdir "$DO_PATH"
|
||||
for d in redo redo-ifchange; do
|
||||
ln -s "$REDO" "$DO_PATH/$d";
|
||||
done
|
||||
[ -e /bin/true ] && TRUE=/bin/true || TRUE=/usr/bin/true
|
||||
for d in redo-ifcreate redo-stamp redo-always; do
|
||||
ln -s $TRUE "$DO_PATH/$d";
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
_find_dofile_pwd()
|
||||
{
|
||||
dofile=default.$1.do
|
||||
while :; do
|
||||
dofile=default.${dofile#default.*.}
|
||||
[ -e "$dofile" -o "$dofile" = default.do ] && break
|
||||
done
|
||||
ext=${dofile#default}
|
||||
ext=${ext%.do}
|
||||
base=${1%$ext}
|
||||
}
|
||||
|
||||
|
||||
_find_dofile()
|
||||
{
|
||||
local prefix=
|
||||
while :; do
|
||||
_find_dofile_pwd "$1"
|
||||
[ -e "$dofile" ] && break
|
||||
[ "$PWD" = "/" ] && break
|
||||
target=${PWD##*/}/$target
|
||||
tmp=${PWD##*/}/$tmp
|
||||
prefix=${PWD##*/}/$prefix
|
||||
cd ..
|
||||
done
|
||||
base=$prefix$base
|
||||
}
|
||||
|
||||
|
||||
_run_dofile()
|
||||
{
|
||||
export DO_DEPTH="$DO_DEPTH "
|
||||
export REDO_TARGET=$PWD/$target
|
||||
local line1
|
||||
set -e
|
||||
read line1 <"$PWD/$dofile"
|
||||
cmd=${line1#"#!/"}
|
||||
if [ "$cmd" != "$line1" ]; then
|
||||
/$cmd "$PWD/$dofile" "$@" >"$tmp.tmp2"
|
||||
else
|
||||
:; . "$PWD/$dofile" >"$tmp.tmp2"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
_do()
|
||||
{
|
||||
local dir=$1 target=$2 tmp=$3
|
||||
if [ ! -e "$target" ] || [ -d "$target" -a ! -e "$target.did" ]; then
|
||||
printf '%sdo %s%s%s%s\n' \
|
||||
"$green" "$DO_DEPTH" "$bold" "$dir$target" "$plain" >&2
|
||||
echo "$PWD/$target" >>"$DO_BUILT"
|
||||
dofile=$target.do
|
||||
base=$target
|
||||
ext=
|
||||
[ -e "$target.do" ] || _find_dofile "$target"
|
||||
if [ ! -e "$dofile" ]; then
|
||||
echo "do: $target: no .do file" >&2
|
||||
return 1
|
||||
fi
|
||||
[ ! -e "$DO_BUILT" ] || [ ! -d "$(dirname "$target")" ] ||
|
||||
: >>"$target.did"
|
||||
( _run_dofile "$target" "$base" "$tmp.tmp" )
|
||||
rv=$?
|
||||
if [ $rv != 0 ]; then
|
||||
printf "do: %s%s\n" "$DO_DEPTH" \
|
||||
"$dir$target: got exit code $rv" >&2
|
||||
rm -f "$tmp.tmp" "$tmp.tmp2"
|
||||
return $rv
|
||||
fi
|
||||
mv "$tmp.tmp" "$target" 2>/dev/null ||
|
||||
! test -s "$tmp.tmp2" ||
|
||||
mv "$tmp.tmp2" "$target" 2>/dev/null
|
||||
rm -f "$tmp.tmp2"
|
||||
else
|
||||
echo "do $DO_DEPTH$target exists." >&2
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
# Make corrections for directories that don't actually exist yet.
|
||||
_dir_shovel()
|
||||
{
|
||||
local dir base
|
||||
xdir=$1 xbase=$2 xbasetmp=$2
|
||||
while [ ! -d "$xdir" -a -n "$xdir" ]; do
|
||||
_dirsplit "${xdir%/}"
|
||||
xbasetmp=${base}__$xbase
|
||||
xdir=$dir xbase=$base/$xbase
|
||||
echo "xbasetmp='$xbasetmp'" >&2
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
redo()
|
||||
{
|
||||
for i in "$@"; do
|
||||
_dirsplit "$i"
|
||||
_dir_shovel "$dir" "$base"
|
||||
dir=$xdir base=$xbase basetmp=$xbasetmp
|
||||
( cd "$dir" && _do "$dir" "$base" "$basetmp" ) || return 1
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
set -e
|
||||
redo "$@"
|
||||
|
||||
if [ -n "$DO_TOP" ]; then
|
||||
echo "Removing stamp files..." >&2
|
||||
[ ! -e "$DO_BUILT" ] ||
|
||||
while read f; do printf "%s.did\0" "$f"; done <"$DO_BUILT" |
|
||||
xargs -0 rm -f 2>/dev/null
|
||||
fi
|
832
src/firewall.py
Normal file
832
src/firewall.py
Normal file
@ -0,0 +1,832 @@
|
||||
import errno
|
||||
import socket
|
||||
import select
|
||||
import signal
|
||||
import struct
|
||||
import compat.ssubprocess as ssubprocess
|
||||
import ssyslog
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
from helpers import log, debug1, debug3, islocal, Fatal, family_to_string, \
|
||||
resolvconf_nameservers
|
||||
from fcntl import ioctl
|
||||
from ctypes import c_char, c_uint8, c_uint16, c_uint32, Union, Structure, \
|
||||
sizeof, addressof, memmove
|
||||
|
||||
|
||||
# python doesn't have a definition for this
|
||||
IPPROTO_DIVERT = 254
|
||||
|
||||
|
||||
def nonfatal(func, *args):
|
||||
try:
|
||||
func(*args)
|
||||
except Fatal, e:
|
||||
log('error: %s\n' % e)
|
||||
|
||||
|
||||
def ipt_chain_exists(family, table, name):
|
||||
if family == socket.AF_INET6:
|
||||
cmd = 'ip6tables'
|
||||
elif family == socket.AF_INET:
|
||||
cmd = 'iptables'
|
||||
else:
|
||||
raise Exception('Unsupported family "%s"' % family_to_string(family))
|
||||
argv = [cmd, '-t', table, '-nL']
|
||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE)
|
||||
for line in p.stdout:
|
||||
if line.startswith('Chain %s ' % name):
|
||||
return True
|
||||
rv = p.wait()
|
||||
if rv:
|
||||
raise Fatal('%r returned %d' % (argv, rv))
|
||||
|
||||
|
||||
def _ipt(family, table, *args):
|
||||
if family == socket.AF_INET6:
|
||||
argv = ['ip6tables', '-t', table] + list(args)
|
||||
elif family == socket.AF_INET:
|
||||
argv = ['iptables', '-t', table] + list(args)
|
||||
else:
|
||||
raise Exception('Unsupported family "%s"' % family_to_string(family))
|
||||
debug1('>> %s\n' % ' '.join(argv))
|
||||
rv = ssubprocess.call(argv)
|
||||
if rv:
|
||||
raise Fatal('%r returned %d' % (argv, rv))
|
||||
|
||||
|
||||
_no_ttl_module = False
|
||||
|
||||
|
||||
def _ipt_ttl(family, *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(family, *argsplus)
|
||||
except Fatal:
|
||||
_ipt(family, *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(family, *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_nat(port, dnsport, family, subnets, udp):
|
||||
# only ipv4 supported with NAT
|
||||
if family != socket.AF_INET:
|
||||
raise Exception(
|
||||
'Address family "%s" unsupported by nat method'
|
||||
% family_to_string(family))
|
||||
if udp:
|
||||
raise Exception("UDP not supported by nat method")
|
||||
|
||||
table = "nat"
|
||||
|
||||
def ipt(*args):
|
||||
return _ipt(family, table, *args)
|
||||
|
||||
def ipt_ttl(*args):
|
||||
return _ipt_ttl(family, table, *args)
|
||||
|
||||
chain = 'sshuttle-%s' % port
|
||||
|
||||
# basic cleanup/setup of chains
|
||||
if ipt_chain_exists(family, table, chain):
|
||||
nonfatal(ipt, '-D', 'OUTPUT', '-j', chain)
|
||||
nonfatal(ipt, '-D', 'PREROUTING', '-j', chain)
|
||||
nonfatal(ipt, '-F', chain)
|
||||
ipt('-X', chain)
|
||||
|
||||
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
|
||||
# excludes to come first. That's why the columns are in such a non-
|
||||
# intuitive order.
|
||||
for f, swidth, sexclude, snet \
|
||||
in sorted(subnets, key=lambda s: s[1], reverse=True):
|
||||
if sexclude:
|
||||
ipt('-A', chain, '-j', 'RETURN',
|
||||
'--dest', '%s/%s' % (snet, swidth),
|
||||
'-p', 'tcp')
|
||||
else:
|
||||
ipt_ttl('-A', chain, '-j', 'REDIRECT',
|
||||
'--dest', '%s/%s' % (snet, swidth),
|
||||
'-p', 'tcp',
|
||||
'--to-ports', str(port))
|
||||
|
||||
if dnsport:
|
||||
nslist = resolvconf_nameservers()
|
||||
for f, ip in filter(lambda i: i[0] == family, nslist):
|
||||
ipt_ttl('-A', chain, '-j', 'REDIRECT',
|
||||
'--dest', '%s/32' % ip,
|
||||
'-p', 'udp',
|
||||
'--dport', '53',
|
||||
'--to-ports', str(dnsport))
|
||||
|
||||
|
||||
def do_iptables_tproxy(port, dnsport, family, subnets, udp):
|
||||
if family not in [socket.AF_INET, socket.AF_INET6]:
|
||||
raise Exception(
|
||||
'Address family "%s" unsupported by tproxy method'
|
||||
% family_to_string(family))
|
||||
|
||||
table = "mangle"
|
||||
|
||||
def ipt(*args):
|
||||
return _ipt(family, table, *args)
|
||||
|
||||
def ipt_ttl(*args):
|
||||
return _ipt_ttl(family, table, *args)
|
||||
|
||||
mark_chain = 'sshuttle-m-%s' % port
|
||||
tproxy_chain = 'sshuttle-t-%s' % port
|
||||
divert_chain = 'sshuttle-d-%s' % port
|
||||
|
||||
# basic cleanup/setup of chains
|
||||
if ipt_chain_exists(family, table, mark_chain):
|
||||
ipt('-D', 'OUTPUT', '-j', mark_chain)
|
||||
ipt('-F', mark_chain)
|
||||
ipt('-X', mark_chain)
|
||||
|
||||
if ipt_chain_exists(family, table, tproxy_chain):
|
||||
ipt('-D', 'PREROUTING', '-j', tproxy_chain)
|
||||
ipt('-F', tproxy_chain)
|
||||
ipt('-X', tproxy_chain)
|
||||
|
||||
if ipt_chain_exists(family, table, divert_chain):
|
||||
ipt('-F', divert_chain)
|
||||
ipt('-X', divert_chain)
|
||||
|
||||
if subnets or dnsport:
|
||||
ipt('-N', mark_chain)
|
||||
ipt('-F', mark_chain)
|
||||
ipt('-N', divert_chain)
|
||||
ipt('-F', divert_chain)
|
||||
ipt('-N', tproxy_chain)
|
||||
ipt('-F', tproxy_chain)
|
||||
ipt('-I', 'OUTPUT', '1', '-j', mark_chain)
|
||||
ipt('-I', 'PREROUTING', '1', '-j', tproxy_chain)
|
||||
ipt('-A', divert_chain, '-j', 'MARK', '--set-mark', '1')
|
||||
ipt('-A', divert_chain, '-j', 'ACCEPT')
|
||||
ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain,
|
||||
'-m', 'tcp', '-p', 'tcp')
|
||||
if subnets and udp:
|
||||
ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain,
|
||||
'-m', 'udp', '-p', 'udp')
|
||||
|
||||
if dnsport:
|
||||
nslist = resolvconf_nameservers()
|
||||
for f, ip in filter(lambda i: i[0] == family, nslist):
|
||||
ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
|
||||
'--dest', '%s/32' % ip,
|
||||
'-m', 'udp', '-p', 'udp', '--dport', '53')
|
||||
ipt('-A', tproxy_chain, '-j', 'TPROXY', '--tproxy-mark', '0x1/0x1',
|
||||
'--dest', '%s/32' % ip,
|
||||
'-m', 'udp', '-p', 'udp', '--dport', '53',
|
||||
'--on-port', str(dnsport))
|
||||
|
||||
if subnets:
|
||||
for f, swidth, sexclude, snet \
|
||||
in sorted(subnets, key=lambda s: s[1], reverse=True):
|
||||
if sexclude:
|
||||
ipt('-A', mark_chain, '-j', 'RETURN',
|
||||
'--dest', '%s/%s' % (snet, swidth),
|
||||
'-m', 'tcp', '-p', 'tcp')
|
||||
ipt('-A', tproxy_chain, '-j', 'RETURN',
|
||||
'--dest', '%s/%s' % (snet, swidth),
|
||||
'-m', 'tcp', '-p', 'tcp')
|
||||
else:
|
||||
ipt('-A', mark_chain, '-j', 'MARK',
|
||||
'--set-mark', '1',
|
||||
'--dest', '%s/%s' % (snet, swidth),
|
||||
'-m', 'tcp', '-p', 'tcp')
|
||||
ipt('-A', tproxy_chain, '-j', 'TPROXY',
|
||||
'--tproxy-mark', '0x1/0x1',
|
||||
'--dest', '%s/%s' % (snet, swidth),
|
||||
'-m', 'tcp', '-p', 'tcp',
|
||||
'--on-port', str(port))
|
||||
|
||||
if sexclude and udp:
|
||||
ipt('-A', mark_chain, '-j', 'RETURN',
|
||||
'--dest', '%s/%s' % (snet, swidth),
|
||||
'-m', 'udp', '-p', 'udp')
|
||||
ipt('-A', tproxy_chain, '-j', 'RETURN',
|
||||
'--dest', '%s/%s' % (snet, swidth),
|
||||
'-m', 'udp', '-p', 'udp')
|
||||
elif udp:
|
||||
ipt('-A', mark_chain, '-j', 'MARK',
|
||||
'--set-mark', '1',
|
||||
'--dest', '%s/%s' % (snet, swidth),
|
||||
'-m', 'udp', '-p', 'udp')
|
||||
ipt('-A', tproxy_chain, '-j', 'TPROXY',
|
||||
'--tproxy-mark', '0x1/0x1',
|
||||
'--dest', '%s/%s' % (snet, swidth),
|
||||
'-m', 'udp', '-p', 'udp',
|
||||
'--on-port', str(port))
|
||||
|
||||
|
||||
def ipfw_rule_exists(n):
|
||||
argv = ['ipfw', 'list']
|
||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE)
|
||||
found = False
|
||||
for line in p.stdout:
|
||||
if line.startswith('%05d ' % n):
|
||||
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())
|
||||
raise Fatal('non-sshuttle ipfw rule #%d already exists!' % n)
|
||||
found = True
|
||||
rv = p.wait()
|
||||
if rv:
|
||||
raise Fatal('%r returned %d' % (argv, rv))
|
||||
return found
|
||||
|
||||
|
||||
_oldctls = {}
|
||||
|
||||
|
||||
def _fill_oldctls(prefix):
|
||||
argv = ['sysctl', prefix]
|
||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE)
|
||||
for line in p.stdout:
|
||||
assert(line[-1] == '\n')
|
||||
(k, v) = line[:-1].split(': ', 1)
|
||||
_oldctls[k] = v
|
||||
rv = p.wait()
|
||||
if rv:
|
||||
raise Fatal('%r returned %d' % (argv, rv))
|
||||
if not line:
|
||||
raise Fatal('%r returned no data' % (argv,))
|
||||
|
||||
|
||||
def _sysctl_set(name, val):
|
||||
argv = ['sysctl', '-w', '%s=%s' % (name, val)]
|
||||
debug1('>> %s\n' % ' '.join(argv))
|
||||
return ssubprocess.call(argv, stdout=open('/dev/null', 'w'))
|
||||
|
||||
|
||||
_changedctls = []
|
||||
|
||||
|
||||
def sysctl_set(name, val, permanent=False):
|
||||
PREFIX = 'net.inet.ip'
|
||||
assert(name.startswith(PREFIX + '.'))
|
||||
val = str(val)
|
||||
if not _oldctls:
|
||||
_fill_oldctls(PREFIX)
|
||||
if not (name in _oldctls):
|
||||
debug1('>> No such sysctl: %r\n' % name)
|
||||
return False
|
||||
oldval = _oldctls[name]
|
||||
if val != oldval:
|
||||
rv = _sysctl_set(name, val)
|
||||
if rv == 0 and permanent:
|
||||
debug1('>> ...saving permanently in /etc/sysctl.conf\n')
|
||||
f = open('/etc/sysctl.conf', 'a')
|
||||
f.write('\n'
|
||||
'# Added by sshuttle\n'
|
||||
'%s=%s\n' % (name, val))
|
||||
f.close()
|
||||
else:
|
||||
_changedctls.append(name)
|
||||
return True
|
||||
|
||||
|
||||
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):
|
||||
argv = ['ipfw', '-q'] + list(args)
|
||||
debug1('>> %s\n' % ' '.join(argv))
|
||||
rv = ssubprocess.call(argv)
|
||||
if rv:
|
||||
raise Fatal('%r returned %d' % (argv, rv))
|
||||
|
||||
|
||||
def do_ipfw(port, dnsport, family, subnets, udp):
|
||||
# IPv6 not supported
|
||||
if family not in [socket.AF_INET, ]:
|
||||
raise Exception(
|
||||
'Address family "%s" unsupported by ipfw method'
|
||||
% family_to_string(family))
|
||||
if udp:
|
||||
raise Exception("UDP not supported by ipfw method")
|
||||
|
||||
sport = str(port)
|
||||
xsport = str(port + 1)
|
||||
|
||||
# cleanup any existing rules
|
||||
if ipfw_rule_exists(port):
|
||||
ipfw('delete', sport)
|
||||
|
||||
while _changedctls:
|
||||
name = _changedctls.pop()
|
||||
oldval = _oldctls[name]
|
||||
_sysctl_set(name, oldval)
|
||||
|
||||
if subnets or dnsport:
|
||||
sysctl_set('net.inet.ip.fw.enable', 1)
|
||||
changed = sysctl_set('net.inet.ip.scopedroute', 0, permanent=True)
|
||||
if changed:
|
||||
log("\n"
|
||||
" WARNING: ONE-TIME NETWORK DISRUPTION:\n"
|
||||
" =====================================\n"
|
||||
"sshuttle has changed a MacOS kernel setting to work around\n"
|
||||
"a bug in MacOS 10.6. This will cause your network to drop\n"
|
||||
"within 5-10 minutes unless you restart your network\n"
|
||||
"interface (change wireless networks or unplug/plug the\n"
|
||||
"ethernet port) NOW, then restart sshuttle. The fix is\n"
|
||||
"permanent; you only have to do this once.\n\n")
|
||||
sys.exit(1)
|
||||
|
||||
ipfw('add', sport, 'check-state', 'ip',
|
||||
'from', 'any', 'to', 'any')
|
||||
|
||||
if subnets:
|
||||
# create new subnet entries
|
||||
for f, swidth, sexclude, snet \
|
||||
in sorted(subnets, key=lambda s: s[1], reverse=True):
|
||||
if sexclude:
|
||||
ipfw('add', sport, 'skipto', xsport,
|
||||
'tcp',
|
||||
'from', 'any', 'to', '%s/%s' % (snet, swidth))
|
||||
else:
|
||||
ipfw('add', sport, 'fwd', '127.0.0.1,%d' % port,
|
||||
'tcp',
|
||||
'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 f, ip in filter(lambda i: i[0] == family, nslist):
|
||||
# relabel and then catch outgoing DNS requests
|
||||
ipfw('add', sport, 'divert', sport,
|
||||
'udp',
|
||||
'from', 'any', 'to', '%s/32' % ip, '53',
|
||||
'not', 'ipttl', '42')
|
||||
# relabel DNS responses
|
||||
ipfw('add', sport, 'divert', sport,
|
||||
'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 pfctl(args, stdin = None):
|
||||
argv = ['pfctl'] + list(args.split(" "))
|
||||
debug1('>> %s\n' % ' '.join(argv))
|
||||
|
||||
p = ssubprocess.Popen(argv, stdin = ssubprocess.PIPE,
|
||||
stdout = ssubprocess.PIPE,
|
||||
stderr = ssubprocess.PIPE)
|
||||
o = p.communicate(stdin)
|
||||
if p.returncode:
|
||||
raise Fatal('%r returned %d' % (argv, p.returncode))
|
||||
|
||||
return o
|
||||
|
||||
_pf_context = {'started_by_sshuttle': False, 'Xtoken':''}
|
||||
|
||||
def do_pf(port, dnsport, family, subnets, udp):
|
||||
global _pf_started_by_sshuttle
|
||||
tables = []
|
||||
translating_rules = []
|
||||
filtering_rules = []
|
||||
|
||||
if subnets:
|
||||
include_subnets = filter(lambda s:not s[2], sorted(subnets, reverse=True))
|
||||
if include_subnets:
|
||||
tables.append('table <include_subnets> {%s}' % ','.join(["%s/%s" % (n[3], n[1]) for n in include_subnets]))
|
||||
translating_rules.append('rdr pass on lo0 proto tcp to <include_subnets> -> 127.0.0.1 port %r' % port)
|
||||
filtering_rules.append('pass out route-to lo0 inet proto tcp to <include_subnets> keep state')
|
||||
|
||||
exclude_subnets = filter(lambda s:s[2], sorted(subnets, reverse=True))
|
||||
if exclude_subnets:
|
||||
tables.append('table <exclude_subnets> {%s}' % ','.join(["%s/%s" % (n[3], n[1]) for n in exclude_subnets]))
|
||||
filtering_rules.append('pass out route-to lo0 inet proto tcp to <exclude_subnets> keep state')
|
||||
|
||||
if dnsport:
|
||||
nslist = resolvconf_nameservers()
|
||||
tables.append('table <dns_servers> {%s}' % ','.join([ns[1] for ns in nslist]))
|
||||
translating_rules.append('rdr pass on lo0 proto udp to <dns_servers> port 53 -> 127.0.0.1 port %r' % dnsport)
|
||||
filtering_rules.append('pass out route-to lo0 inet proto udp to <dns_servers> port 53 keep state')
|
||||
|
||||
rules = '\n'.join(tables + translating_rules + filtering_rules) + '\n'
|
||||
|
||||
pf_status = pfctl('-s all')[0]
|
||||
if not '\nrdr-anchor "sshuttle" all\n' in pf_status:
|
||||
pf_add_anchor_rule(PF_RDR, "sshuttle")
|
||||
if not '\nanchor "sshuttle" all\n' in pf_status:
|
||||
pf_add_anchor_rule(PF_PASS, "sshuttle")
|
||||
|
||||
pfctl('-a sshuttle -f /dev/stdin', rules)
|
||||
if sys.platform == "darwin":
|
||||
o = pfctl('-E')
|
||||
_pf_context['Xtoken'] = re.search(r'Token : (.+)', o[1]).group(1)
|
||||
elif 'INFO:\nStatus: Disabled' in pf_status:
|
||||
pfctl('-e')
|
||||
_pf_context['started_by_sshuttle'] = True
|
||||
else:
|
||||
pfctl('-a sshuttle -F all')
|
||||
if sys.platform == "darwin":
|
||||
pfctl('-X %s' % _pf_context['Xtoken'])
|
||||
elif _pf_context['started_by_sshuttle']:
|
||||
pfctl('-d')
|
||||
|
||||
|
||||
def program_exists(name):
|
||||
paths = (os.getenv('PATH') or os.defpath).split(os.pathsep)
|
||||
for p in paths:
|
||||
fn = '%s/%s' % (p, 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'
|
||||
BAKFILE = '%s.sbak' % HOSTSFILE
|
||||
APPEND = '# sshuttle-firewall-%d AUTOCREATED' % port
|
||||
old_content = ''
|
||||
st = None
|
||||
try:
|
||||
old_content = open(HOSTSFILE).read()
|
||||
st = os.stat(HOSTSFILE)
|
||||
except IOError, e:
|
||||
if e.errno == errno.ENOENT:
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
if old_content.strip() and not os.path.exists(BAKFILE):
|
||||
os.link(HOSTSFILE, BAKFILE)
|
||||
tmpname = "%s.%d.tmp" % (HOSTSFILE, port)
|
||||
f = open(tmpname, 'w')
|
||||
for line in old_content.rstrip().split('\n'):
|
||||
if line.find(APPEND) >= 0:
|
||||
continue
|
||||
f.write('%s\n' % line)
|
||||
for (name, ip) in sorted(hostmap.items()):
|
||||
f.write('%-30s %s\n' % ('%s %s' % (ip, name), APPEND))
|
||||
f.close()
|
||||
|
||||
if st:
|
||||
os.chown(tmpname, st.st_uid, st.st_gid)
|
||||
os.chmod(tmpname, st.st_mode)
|
||||
else:
|
||||
os.chown(tmpname, 0, 0)
|
||||
os.chmod(tmpname, 0644)
|
||||
os.rename(tmpname, HOSTSFILE)
|
||||
|
||||
|
||||
def restore_etc_hosts(port):
|
||||
global hostmap
|
||||
hostmap = {}
|
||||
rewrite_etc_hosts(port)
|
||||
|
||||
|
||||
# This are some classes and functions used to support pf in yosemite.
|
||||
class pf_state_xport(Union):
|
||||
_fields_ = [("port", c_uint16),
|
||||
("call_id", c_uint16),
|
||||
("spi", c_uint32)]
|
||||
|
||||
class pf_addr(Structure):
|
||||
class _pfa(Union):
|
||||
_fields_ = [("v4", c_uint32), # struct in_addr
|
||||
("v6", c_uint32 * 4), # struct in6_addr
|
||||
("addr8", c_uint8 * 16),
|
||||
("addr16", c_uint16 * 8),
|
||||
("addr32", c_uint32 * 4)]
|
||||
|
||||
_fields_ = [("pfa", _pfa)]
|
||||
_anonymous_ = ("pfa",)
|
||||
|
||||
class pfioc_natlook(Structure):
|
||||
_fields_ = [("saddr", pf_addr),
|
||||
("daddr", pf_addr),
|
||||
("rsaddr", pf_addr),
|
||||
("rdaddr", pf_addr),
|
||||
("sxport", pf_state_xport),
|
||||
("dxport", pf_state_xport),
|
||||
("rsxport", pf_state_xport),
|
||||
("rdxport", pf_state_xport),
|
||||
("af", c_uint8), # sa_family_t
|
||||
("proto", c_uint8),
|
||||
("proto_variant", c_uint8),
|
||||
("direction", c_uint8)]
|
||||
|
||||
pfioc_rule = c_char * 3104 # sizeof(struct pfioc_rule)
|
||||
|
||||
pfioc_pooladdr = c_char * 1136 # sizeof(struct pfioc_pooladdr)
|
||||
|
||||
MAXPATHLEN = 1024
|
||||
|
||||
DIOCNATLOOK = ((0x40000000L | 0x80000000L) | ((sizeof(pfioc_natlook) & 0x1fff) << 16) | ((ord('D')) << 8) | (23))
|
||||
DIOCCHANGERULE = ((0x40000000L | 0x80000000L) | ((sizeof(pfioc_rule) & 0x1fff) << 16) | ((ord('D')) << 8) | (26))
|
||||
DIOCBEGINADDRS = ((0x40000000L | 0x80000000L) | ((sizeof(pfioc_pooladdr) & 0x1fff) << 16) | ((ord('D')) << 8) | (51))
|
||||
|
||||
PF_CHANGE_ADD_TAIL = 2
|
||||
PF_CHANGE_GET_TICKET = 6
|
||||
|
||||
PF_PASS = 0
|
||||
PF_RDR = 8
|
||||
|
||||
PF_OUT = 2
|
||||
|
||||
_pf_fd = None
|
||||
|
||||
def pf_get_dev():
|
||||
global _pf_fd
|
||||
if _pf_fd == None:
|
||||
_pf_fd = os.open('/dev/pf', os.O_RDWR)
|
||||
|
||||
return _pf_fd
|
||||
|
||||
def pf_query_nat(family, proto, src_ip, src_port, dst_ip, dst_port):
|
||||
[proto, family, src_port, dst_port] = [int(v) for v in [proto, family, src_port, dst_port]]
|
||||
|
||||
length = 4 if family == socket.AF_INET else 16
|
||||
|
||||
pnl = pfioc_natlook()
|
||||
pnl.proto = proto
|
||||
pnl.direction = PF_OUT
|
||||
pnl.af = family
|
||||
memmove(addressof(pnl.saddr), socket.inet_pton(pnl.af, src_ip), length)
|
||||
pnl.sxport.port = socket.htons(src_port)
|
||||
memmove(addressof(pnl.daddr), socket.inet_pton(pnl.af, dst_ip), length)
|
||||
pnl.dxport.port = socket.htons(dst_port)
|
||||
|
||||
ioctl(pf_get_dev(), DIOCNATLOOK, (c_char * sizeof(pnl)).from_address(addressof(pnl)))
|
||||
|
||||
ip = socket.inet_ntop(pnl.af, (c_char * length).from_address(addressof(pnl.rdaddr)))
|
||||
port = socket.ntohs(pnl.rdxport.port)
|
||||
return (ip, port)
|
||||
|
||||
def pf_add_anchor_rule(type, name):
|
||||
ACTION_OFFSET = 0
|
||||
POOL_TICKET_OFFSET = 8
|
||||
ANCHOR_CALL_OFFSET = 1040
|
||||
RULE_ACTION_OFFSET = 3068
|
||||
|
||||
pr = pfioc_rule()
|
||||
ppa = pfioc_pooladdr()
|
||||
|
||||
ioctl(pf_get_dev(), DIOCBEGINADDRS, ppa)
|
||||
|
||||
memmove(addressof(pr) + POOL_TICKET_OFFSET, ppa[4:8], 4) #pool_ticket
|
||||
memmove(addressof(pr) + ANCHOR_CALL_OFFSET, name, min(MAXPATHLEN, len(name))) #anchor_call = name
|
||||
memmove(addressof(pr) + RULE_ACTION_OFFSET, struct.pack('I', type), 4) #rule.action = type
|
||||
|
||||
memmove(addressof(pr) + ACTION_OFFSET, struct.pack('I', PF_CHANGE_GET_TICKET), 4) #action = PF_CHANGE_GET_TICKET
|
||||
ioctl(pf_get_dev(), DIOCCHANGERULE, pr)
|
||||
|
||||
memmove(addressof(pr) + ACTION_OFFSET, struct.pack('I', PF_CHANGE_ADD_TAIL), 4) #action = PF_CHANGE_ADD_TAIL
|
||||
ioctl(pf_get_dev(), DIOCCHANGERULE, pr)
|
||||
|
||||
|
||||
# This is some voodoo for setting up the kernel's transparent
|
||||
# proxying stuff. If subnets is empty, we just delete our sshuttle rules;
|
||||
# otherwise we delete it, then make them from scratch.
|
||||
#
|
||||
# This code is supposed to clean up after itself by deleting its rules on
|
||||
# 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_v6, port_v4, dnsport_v6, dnsport_v4, method, udp, syslog):
|
||||
assert(port_v6 >= 0)
|
||||
assert(port_v6 <= 65535)
|
||||
assert(port_v4 >= 0)
|
||||
assert(port_v4 <= 65535)
|
||||
assert(dnsport_v6 >= 0)
|
||||
assert(dnsport_v6 <= 65535)
|
||||
assert(dnsport_v4 >= 0)
|
||||
assert(dnsport_v4 <= 65535)
|
||||
|
||||
if os.getuid() != 0:
|
||||
raise Fatal('you must be root (or enable su/sudo) to set the firewall')
|
||||
|
||||
if method == "auto":
|
||||
if program_exists('ipfw'):
|
||||
method = "ipfw"
|
||||
elif program_exists('iptables'):
|
||||
method = "nat"
|
||||
elif program_exists('pfctl'):
|
||||
method = "pf"
|
||||
else:
|
||||
raise Fatal("can't find either ipfw, iptables or pfctl; check your PATH")
|
||||
|
||||
if method == "nat":
|
||||
do_it = do_iptables_nat
|
||||
elif method == "tproxy":
|
||||
do_it = do_iptables_tproxy
|
||||
elif method == "ipfw":
|
||||
do_it = do_ipfw
|
||||
elif method == "pf":
|
||||
do_it = do_pf
|
||||
else:
|
||||
raise Exception('Unknown method "%s"' % method)
|
||||
|
||||
# because of limitations of the 'su' command, the *real* stdin/stdout
|
||||
# are both attached to stdout initially. Clone stdout into stdin so we
|
||||
# can read from it.
|
||||
os.dup2(1, 0)
|
||||
|
||||
if syslog:
|
||||
ssyslog.start_syslog()
|
||||
ssyslog.stderr_to_syslog()
|
||||
|
||||
debug1('firewall manager ready method %s.\n' % method)
|
||||
sys.stdout.write('READY %s\n' % method)
|
||||
sys.stdout.flush()
|
||||
|
||||
# don't disappear if our controlling terminal or stdout/stderr
|
||||
# disappears; we still have to clean up.
|
||||
signal.signal(signal.SIGHUP, signal.SIG_IGN)
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_IGN)
|
||||
signal.signal(signal.SIGTERM, signal.SIG_IGN)
|
||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||
|
||||
# ctrl-c shouldn't be passed along to me. When the main sshuttle dies,
|
||||
# I'll die automatically.
|
||||
os.setsid()
|
||||
|
||||
# we wait until we get some input before creating the rules. That way,
|
||||
# sshuttle can launch us as early as possible (and get sudo password
|
||||
# authentication as early in the startup process as possible).
|
||||
line = sys.stdin.readline(128)
|
||||
if not line:
|
||||
return # parent died; nothing to do
|
||||
|
||||
subnets = []
|
||||
if line != 'ROUTES\n':
|
||||
raise Fatal('firewall: expected ROUTES but got %r' % line)
|
||||
while 1:
|
||||
line = sys.stdin.readline(128)
|
||||
if not line:
|
||||
raise Fatal('firewall: expected route but got %r' % line)
|
||||
elif line == 'GO\n':
|
||||
break
|
||||
try:
|
||||
(family, width, exclude, ip) = line.strip().split(',', 3)
|
||||
except:
|
||||
raise Fatal('firewall: expected route or GO but got %r' % line)
|
||||
subnets.append((int(family), int(width), bool(int(exclude)), ip))
|
||||
|
||||
try:
|
||||
if line:
|
||||
debug1('firewall manager: starting transproxy.\n')
|
||||
|
||||
subnets_v6 = filter(lambda i: i[0] == socket.AF_INET6, subnets)
|
||||
if port_v6:
|
||||
do_wait = do_it(
|
||||
port_v6, dnsport_v6, socket.AF_INET6, subnets_v6, udp)
|
||||
elif len(subnets_v6) > 0:
|
||||
debug1("IPv6 subnets defined but IPv6 disabled\n")
|
||||
|
||||
subnets_v4 = filter(lambda i: i[0] == socket.AF_INET, subnets)
|
||||
if port_v4:
|
||||
do_wait = do_it(
|
||||
port_v4, dnsport_v4, socket.AF_INET, subnets_v4, udp)
|
||||
elif len(subnets_v4) > 0:
|
||||
debug1('IPv4 subnets defined but IPv4 disabled\n')
|
||||
|
||||
sys.stdout.write('STARTED\n')
|
||||
|
||||
try:
|
||||
sys.stdout.flush()
|
||||
except IOError:
|
||||
# the parent process died for some reason; he's surely been loud
|
||||
# enough, so no reason to report another error
|
||||
return
|
||||
|
||||
# Now we wait until EOF or any other kind of exception. We need
|
||||
# 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)
|
||||
hostmap[name] = ip
|
||||
rewrite_etc_hosts(port_v6 or port_v4)
|
||||
elif line.startswith('QUERY_PF_NAT '):
|
||||
try:
|
||||
dst = pf_query_nat(*(line[13:].split(',')))
|
||||
sys.stdout.write('QUERY_PF_NAT_SUCCESS %s,%r\n' % dst)
|
||||
except IOError, e:
|
||||
sys.stdout.write('QUERY_PF_NAT_FAILURE %s\n' % e)
|
||||
|
||||
sys.stdout.flush()
|
||||
elif line:
|
||||
raise Fatal('expected EOF, got %r' % line)
|
||||
else:
|
||||
break
|
||||
finally:
|
||||
try:
|
||||
debug1('firewall manager: undoing changes.\n')
|
||||
except:
|
||||
pass
|
||||
if port_v6:
|
||||
do_it(port_v6, 0, socket.AF_INET6, [], udp)
|
||||
if port_v4:
|
||||
do_it(port_v4, 0, socket.AF_INET, [], udp)
|
||||
restore_etc_hosts(port_v6 or port_v4)
|
@ -1,8 +1,11 @@
|
||||
import sys, os, socket
|
||||
import sys
|
||||
import socket
|
||||
import errno
|
||||
|
||||
logprefix = ''
|
||||
verbose = 0
|
||||
|
||||
|
||||
def log(s):
|
||||
try:
|
||||
sys.stdout.flush()
|
||||
@ -13,14 +16,17 @@ def log(s):
|
||||
# our tty closes. That sucks, but it's no reason to abort the program.
|
||||
pass
|
||||
|
||||
|
||||
def debug1(s):
|
||||
if verbose >= 1:
|
||||
log(s)
|
||||
|
||||
|
||||
def debug2(s):
|
||||
if verbose >= 2:
|
||||
log(s)
|
||||
|
||||
|
||||
def debug3(s):
|
||||
if verbose >= 3:
|
||||
log(s)
|
||||
@ -42,7 +48,10 @@ def resolvconf_nameservers():
|
||||
for line in open('/etc/resolv.conf'):
|
||||
words = line.lower().split()
|
||||
if len(words) >= 2 and words[0] == 'nameserver':
|
||||
l.append(words[1])
|
||||
if ':' in words[1]:
|
||||
l.append((socket.AF_INET6, words[1]))
|
||||
else:
|
||||
l.append((socket.AF_INET, words[1]))
|
||||
return l
|
||||
|
||||
|
||||
@ -55,11 +64,11 @@ def resolvconf_random_nameserver():
|
||||
random.shuffle(l)
|
||||
return l[0]
|
||||
else:
|
||||
return '127.0.0.1'
|
||||
return (socket.AF_INET, '127.0.0.1')
|
||||
|
||||
|
||||
def islocal(ip):
|
||||
sock = socket.socket()
|
||||
def islocal(ip, family):
|
||||
sock = socket.socket(family)
|
||||
try:
|
||||
try:
|
||||
sock.bind((ip, 0))
|
||||
@ -73,3 +82,10 @@ def islocal(ip):
|
||||
return True # it's a local IP, or there would have been an error
|
||||
|
||||
|
||||
def family_to_string(family):
|
||||
if family == socket.AF_INET6:
|
||||
return "AF_INET6"
|
||||
elif family == socket.AF_INET:
|
||||
return "AF_INET"
|
||||
else:
|
||||
return str(family)
|
@ -1,8 +1,14 @@
|
||||
import time, socket, re, select, errno
|
||||
import time
|
||||
import socket
|
||||
import re
|
||||
import select
|
||||
import errno
|
||||
import os
|
||||
import sys
|
||||
if not globals().get('skip_imports'):
|
||||
import compat.ssubprocess as ssubprocess
|
||||
import helpers
|
||||
from helpers import *
|
||||
from helpers import log, debug1, debug2, debug3
|
||||
|
||||
POLL_TIME = 60 * 15
|
||||
NETSTAT_POLL_TIME = 30
|
||||
@ -13,7 +19,11 @@ _nmb_ok = True
|
||||
_smb_ok = True
|
||||
hostnames = {}
|
||||
queue = {}
|
||||
null = open('/dev/null', 'rb+')
|
||||
try:
|
||||
null = open('/dev/null', 'wb')
|
||||
except IOError, e:
|
||||
log('warning: %s\n' % e)
|
||||
null = os.popen("sh -c 'while read x; do :; done'", 'wb', 4096)
|
||||
|
||||
|
||||
def _is_ip(s):
|
||||
@ -90,7 +100,7 @@ def _check_revdns(ip):
|
||||
debug3('< %s\n' % r[0])
|
||||
check_host(r[0])
|
||||
found_host(r[0], ip)
|
||||
except socket.herror, e:
|
||||
except socket.herror:
|
||||
pass
|
||||
|
||||
|
||||
@ -101,7 +111,7 @@ def _check_dns(hostname):
|
||||
debug3('< %s\n' % ip)
|
||||
check_host(ip)
|
||||
found_host(hostname, ip)
|
||||
except socket.gaierror, e:
|
||||
except socket.gaierror:
|
||||
pass
|
||||
|
||||
|
||||
@ -225,7 +235,7 @@ def check_workgroup(hostname):
|
||||
|
||||
def _enqueue(op, *args):
|
||||
t = (op, args)
|
||||
if queue.get(t) == None:
|
||||
if queue.get(t) is None:
|
||||
queue[t] = 0
|
||||
|
||||
|
230
src/main.py
Normal file
230
src/main.py
Normal file
@ -0,0 +1,230 @@
|
||||
import sys
|
||||
import re
|
||||
import socket
|
||||
import helpers
|
||||
import options
|
||||
import client
|
||||
import server
|
||||
import firewall
|
||||
import hostwatch
|
||||
from helpers import log, Fatal
|
||||
|
||||
|
||||
# 1.2.3.4/5 or just 1.2.3.4
|
||||
def parse_subnet4(s):
|
||||
m = re.match(r'(\d+)(?:\.(\d+)\.(\d+)\.(\d+))?(?:/(\d+))?$', s)
|
||||
if not m:
|
||||
raise Fatal('%r is not a valid IP subnet format' % s)
|
||||
(a, b, c, d, width) = m.groups()
|
||||
(a, b, c, d) = (int(a or 0), int(b or 0), int(c or 0), int(d or 0))
|
||||
if width is None:
|
||||
width = 32
|
||||
else:
|
||||
width = int(width)
|
||||
if a > 255 or b > 255 or c > 255 or d > 255:
|
||||
raise Fatal('%d.%d.%d.%d has numbers > 255' % (a, b, c, d))
|
||||
if width > 32:
|
||||
raise Fatal('*/%d is greater than the maximum of 32' % width)
|
||||
return(socket.AF_INET, '%d.%d.%d.%d' % (a, b, c, d), width)
|
||||
|
||||
|
||||
# 1:2::3/64 or just 1:2::3
|
||||
def parse_subnet6(s):
|
||||
m = re.match(r'(?:([a-fA-F\d:]+))?(?:/(\d+))?$', s)
|
||||
if not m:
|
||||
raise Fatal('%r is not a valid IP subnet format' % s)
|
||||
(net, width) = m.groups()
|
||||
if width is None:
|
||||
width = 128
|
||||
else:
|
||||
width = int(width)
|
||||
if width > 128:
|
||||
raise Fatal('*/%d is greater than the maximum of 128' % width)
|
||||
return(socket.AF_INET6, net, width)
|
||||
|
||||
|
||||
# Subnet file, supporting empty lines and hash-started comment lines
|
||||
def parse_subnet_file(s):
|
||||
try:
|
||||
handle = open(s, 'r')
|
||||
except OSError:
|
||||
raise Fatal('Unable to open subnet file: %s' % s)
|
||||
|
||||
raw_config_lines = handle.readlines()
|
||||
config_lines = []
|
||||
for line_no, line in enumerate(raw_config_lines):
|
||||
line = line.strip()
|
||||
if len(line) == 0:
|
||||
continue
|
||||
if line[0] == '#':
|
||||
continue
|
||||
config_lines.append(line)
|
||||
|
||||
return config_lines
|
||||
|
||||
|
||||
# list of:
|
||||
# 1.2.3.4/5 or just 1.2.3.4
|
||||
# 1:2::3/64 or just 1:2::3
|
||||
def parse_subnets(subnets_str):
|
||||
subnets = []
|
||||
for s in subnets_str:
|
||||
if ':' in s:
|
||||
subnet = parse_subnet6(s)
|
||||
else:
|
||||
subnet = parse_subnet4(s)
|
||||
subnets.append(subnet)
|
||||
return subnets
|
||||
|
||||
|
||||
# 1.2.3.4:567 or just 1.2.3.4 or just 567
|
||||
def parse_ipport4(s):
|
||||
s = str(s)
|
||||
m = re.match(r'(?:(\d+)\.(\d+)\.(\d+)\.(\d+))?(?::)?(?:(\d+))?$', s)
|
||||
if not m:
|
||||
raise Fatal('%r is not a valid IP:port format' % s)
|
||||
(a, b, c, d, port) = m.groups()
|
||||
(a, b, c, d, port) = (int(a or 0), int(b or 0), int(c or 0), int(d or 0),
|
||||
int(port or 0))
|
||||
if a > 255 or b > 255 or c > 255 or d > 255:
|
||||
raise Fatal('%d.%d.%d.%d has numbers > 255' % (a, b, c, d))
|
||||
if port > 65535:
|
||||
raise Fatal('*:%d is greater than the maximum of 65535' % port)
|
||||
if a is None:
|
||||
a = b = c = d = 0
|
||||
return ('%d.%d.%d.%d' % (a, b, c, d), port)
|
||||
|
||||
|
||||
# [1:2::3]:456 or [1:2::3] or 456
|
||||
def parse_ipport6(s):
|
||||
s = str(s)
|
||||
m = re.match(r'(?:\[([^]]*)])?(?::)?(?:(\d+))?$', s)
|
||||
if not m:
|
||||
raise Fatal('%s is not a valid IP:port format' % s)
|
||||
(ip, port) = m.groups()
|
||||
(ip, port) = (ip or '::', int(port or 0))
|
||||
return (ip, port)
|
||||
|
||||
|
||||
optspec = """
|
||||
sshuttle [-l [ip:]port] [-r [username@]sshserver[:port]] <subnets...>
|
||||
sshuttle --server
|
||||
sshuttle --firewall <port> <subnets...>
|
||||
sshuttle --hostwatch
|
||||
--
|
||||
l,listen= transproxy to this ip address and port number
|
||||
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
|
||||
method= auto, nat, tproxy, pf or ipfw
|
||||
python= path to python interpreter on the remote server
|
||||
r,remote= ssh hostname (and optional username) of remote sshuttle server
|
||||
x,exclude= exclude this subnet (can be used more than once)
|
||||
X,exclude-from= exclude the subnets in a file (whitespace separated)
|
||||
v,verbose increase debug message verbosity
|
||||
e,ssh-cmd= the command to use to connect to the remote [ssh]
|
||||
seed-hosts= with -H, use these hostnames for initial scan (comma-separated)
|
||||
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
|
||||
s,subnets= file where the subnets are stored, instead of on the command line
|
||||
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(optspec)
|
||||
(opt, flags, extra) = o.parse(sys.argv[2:])
|
||||
|
||||
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) != 6:
|
||||
o.fatal('exactly six arguments expected')
|
||||
sys.exit(firewall.main(int(extra[0]), int(extra[1]),
|
||||
int(extra[2]), int(extra[3]),
|
||||
extra[4], int(extra[5]), opt.syslog))
|
||||
elif opt.hostwatch:
|
||||
sys.exit(hostwatch.hw_main(extra))
|
||||
else:
|
||||
if len(extra) < 1 and not opt.auto_nets and not opt.subnets:
|
||||
o.fatal('at least one subnet, subnet file, or -N expected')
|
||||
includes = extra
|
||||
excludes = ['127.0.0.0/8']
|
||||
for k, v in flags:
|
||||
if k in ('-x', '--exclude'):
|
||||
excludes.append(v)
|
||||
if k in ('-X', '--exclude-from'):
|
||||
excludes += open(v).read().split()
|
||||
remotename = opt.remote
|
||||
if remotename == '' or remotename == '-':
|
||||
remotename = None
|
||||
if opt.seed_hosts and not opt.auto_hosts:
|
||||
o.fatal('--seed-hosts only works if you also use -H')
|
||||
if opt.seed_hosts:
|
||||
sh = re.split(r'[\s,]+', (opt.seed_hosts or "").strip())
|
||||
elif opt.auto_hosts:
|
||||
sh = []
|
||||
else:
|
||||
sh = None
|
||||
if opt.subnets:
|
||||
includes = parse_subnet_file(opt.subnets)
|
||||
if not opt.method:
|
||||
method = "auto"
|
||||
elif opt.method in ["auto", "nat", "tproxy", "ipfw", "pf"]:
|
||||
method = opt.method
|
||||
else:
|
||||
o.fatal("method %s not supported" % opt.method)
|
||||
if not opt.listen:
|
||||
if opt.method == "tproxy":
|
||||
ipport_v6 = parse_ipport6('[::1]:0')
|
||||
else:
|
||||
ipport_v6 = None
|
||||
ipport_v4 = parse_ipport4('127.0.0.1:0')
|
||||
else:
|
||||
ipport_v6 = None
|
||||
ipport_v4 = None
|
||||
list = opt.listen.split(",")
|
||||
for ip in list:
|
||||
if '[' in ip and ']' in ip and opt.method == "tproxy":
|
||||
ipport_v6 = parse_ipport6(ip)
|
||||
else:
|
||||
ipport_v4 = parse_ipport4(ip)
|
||||
return_code = client.main(ipport_v6, ipport_v4,
|
||||
opt.ssh_cmd,
|
||||
remotename,
|
||||
opt.python,
|
||||
opt.latency_control,
|
||||
opt.dns,
|
||||
method,
|
||||
sh,
|
||||
opt.auto_nets,
|
||||
parse_subnets(includes),
|
||||
parse_subnets(excludes),
|
||||
opt.syslog, opt.daemon, opt.pidfile)
|
||||
|
||||
if return_code == 0:
|
||||
log('Normal exit code, exiting...')
|
||||
else:
|
||||
log('Abnormal exit code detected, failing...' % return_code)
|
||||
sys.exit(return_code)
|
||||
|
||||
except Fatal, e:
|
||||
log('fatal: %s\n' % e)
|
||||
sys.exit(99)
|
||||
except KeyboardInterrupt:
|
||||
log('\n')
|
||||
log('Keyboard interrupt: exiting.\n')
|
||||
sys.exit(1)
|
@ -1,9 +1,16 @@
|
||||
"""Command-line options parser.
|
||||
With the help of an options spec string, easily parse command-line options.
|
||||
"""
|
||||
import sys, os, textwrap, getopt, re, struct
|
||||
import sys
|
||||
import os
|
||||
import textwrap
|
||||
import getopt
|
||||
import re
|
||||
import struct
|
||||
|
||||
|
||||
class OptDict:
|
||||
|
||||
def __init__(self):
|
||||
self._opts = {}
|
||||
|
||||
@ -48,6 +55,7 @@ def _remove_negative_kv(k, v):
|
||||
return k[3:], not v
|
||||
return k, v
|
||||
|
||||
|
||||
def _remove_negative_k(k):
|
||||
return _remove_negative_kv(k, None)[0]
|
||||
|
||||
@ -55,7 +63,8 @@ def _remove_negative_k(k):
|
||||
def _tty_width():
|
||||
s = struct.pack("HHHH", 0, 0, 0, 0)
|
||||
try:
|
||||
import fcntl, termios
|
||||
import fcntl
|
||||
import termios
|
||||
s = fcntl.ioctl(sys.stderr.fileno(), termios.TIOCGWINSZ, s)
|
||||
except (IOError, ImportError):
|
||||
return _atoi(os.environ.get('WIDTH')) or 70
|
||||
@ -64,6 +73,7 @@ def _tty_width():
|
||||
|
||||
|
||||
class Options:
|
||||
|
||||
"""Option parser.
|
||||
When constructed, two strings are mandatory. The first one is the command
|
||||
name showed before error messages. The second one is a string called an
|
||||
@ -76,6 +86,7 @@ class Options:
|
||||
By default, the parser function is getopt.gnu_getopt, and the abort
|
||||
behaviour is to exit the program.
|
||||
"""
|
||||
|
||||
def __init__(self, optspec, optfunc=getopt.gnu_getopt,
|
||||
onabort=_default_onabort):
|
||||
self.optspec = optspec
|
||||
@ -95,7 +106,8 @@ class Options:
|
||||
first_syn = True
|
||||
while lines:
|
||||
l = lines.pop()
|
||||
if l == '--': break
|
||||
if l == '--':
|
||||
break
|
||||
out.append('%s: %s\n' % (first_syn and 'usage' or ' or', l))
|
||||
first_syn = False
|
||||
out.append('\n')
|
||||
@ -170,7 +182,8 @@ class Options:
|
||||
and "extra" is a list of positional arguments.
|
||||
"""
|
||||
try:
|
||||
(flags,extra) = self.optfunc(args, self._shortopts, self._longopts)
|
||||
(flags, extra) = self.optfunc(
|
||||
args, self._shortopts, self._longopts)
|
||||
except getopt.GetoptError, e:
|
||||
self.fatal(e)
|
||||
|
336
src/server.py
Normal file
336
src/server.py
Normal file
@ -0,0 +1,336 @@
|
||||
import re
|
||||
import struct
|
||||
import socket
|
||||
import traceback
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
if not globals().get('skip_imports'):
|
||||
import ssnet
|
||||
import helpers
|
||||
import hostwatch
|
||||
import compat.ssubprocess as ssubprocess
|
||||
from ssnet import Handler, Proxy, Mux, MuxWrapper
|
||||
from helpers import log, debug1, debug2, debug3, Fatal, \
|
||||
resolvconf_random_nameserver
|
||||
|
||||
|
||||
if not globals().get('latency_control'):
|
||||
latency_control = None
|
||||
|
||||
|
||||
def _ipmatch(ipstr):
|
||||
if ipstr == 'default':
|
||||
ipstr = '0.0.0.0/0'
|
||||
m = re.match(r'^(\d+(\.\d+(\.\d+(\.\d+)?)?)?)(?:/(\d+))?$', ipstr)
|
||||
if m:
|
||||
g = m.groups()
|
||||
ips = g[0]
|
||||
width = int(g[4] or 32)
|
||||
if g[1] is None:
|
||||
ips += '.0.0.0'
|
||||
width = min(width, 8)
|
||||
elif g[2] is None:
|
||||
ips += '.0.0'
|
||||
width = min(width, 16)
|
||||
elif g[3] is None:
|
||||
ips += '.0'
|
||||
width = min(width, 24)
|
||||
return (struct.unpack('!I', socket.inet_aton(ips))[0], width)
|
||||
|
||||
|
||||
def _ipstr(ip, width):
|
||||
if width >= 32:
|
||||
return ip
|
||||
else:
|
||||
return "%s/%d" % (ip, width)
|
||||
|
||||
|
||||
def _maskbits(netmask):
|
||||
if not netmask:
|
||||
return 32
|
||||
for i in range(32):
|
||||
if netmask[0] & _shl(1, i):
|
||||
return 32 - i
|
||||
return 0
|
||||
|
||||
|
||||
def _shl(n, bits):
|
||||
return n * int(2 ** bits)
|
||||
|
||||
|
||||
def _list_routes():
|
||||
argv = ['netstat', '-rn']
|
||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE)
|
||||
routes = []
|
||||
for line in p.stdout:
|
||||
cols = re.split(r'\s+', line)
|
||||
ipw = _ipmatch(cols[0])
|
||||
if not ipw:
|
||||
continue # some lines won't be parseable; never mind
|
||||
maskw = _ipmatch(cols[2]) # linux only
|
||||
mask = _maskbits(maskw) # returns 32 if maskw is null
|
||||
width = min(ipw[1], mask)
|
||||
ip = ipw[0] & _shl(_shl(1, width) - 1, 32 - width)
|
||||
routes.append(
|
||||
(socket.AF_INET, socket.inet_ntoa(struct.pack('!I', ip)), width))
|
||||
rv = p.wait()
|
||||
if rv != 0:
|
||||
log('WARNING: %r returned %d\n' % (argv, rv))
|
||||
log('WARNING: That prevents --auto-nets from working.\n')
|
||||
return routes
|
||||
|
||||
|
||||
def list_routes():
|
||||
for (family, ip, width) in _list_routes():
|
||||
if not ip.startswith('0.') and not ip.startswith('127.'):
|
||||
yield (family, ip, width)
|
||||
|
||||
|
||||
def _exc_dump():
|
||||
exc_info = sys.exc_info()
|
||||
return ''.join(traceback.format_exception(*exc_info))
|
||||
|
||||
|
||||
def start_hostwatch(seed_hosts):
|
||||
s1, s2 = socket.socketpair()
|
||||
pid = os.fork()
|
||||
if not pid:
|
||||
# child
|
||||
rv = 99
|
||||
try:
|
||||
try:
|
||||
s2.close()
|
||||
os.dup2(s1.fileno(), 1)
|
||||
os.dup2(s1.fileno(), 0)
|
||||
s1.close()
|
||||
rv = hostwatch.hw_main(seed_hosts) or 0
|
||||
except Exception:
|
||||
log('%s\n' % _exc_dump())
|
||||
rv = 98
|
||||
finally:
|
||||
os._exit(rv)
|
||||
s1.close()
|
||||
return pid, s2
|
||||
|
||||
|
||||
class Hostwatch:
|
||||
|
||||
def __init__(self):
|
||||
self.pid = 0
|
||||
self.sock = None
|
||||
|
||||
|
||||
class DnsProxy(Handler):
|
||||
|
||||
def __init__(self, mux, chan, request):
|
||||
# FIXME! IPv4 specific
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
Handler.__init__(self, [sock])
|
||||
self.timeout = time.time() + 30
|
||||
self.mux = mux
|
||||
self.chan = chan
|
||||
self.tries = 0
|
||||
self.peer = None
|
||||
self.request = request
|
||||
self.sock = sock
|
||||
# FIXME! IPv4 specific
|
||||
self.sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
|
||||
self.try_send()
|
||||
|
||||
def try_send(self):
|
||||
if self.tries >= 3:
|
||||
return
|
||||
self.tries += 1
|
||||
# FIXME! Support IPv6 nameservers
|
||||
self.peer = resolvconf_random_nameserver()[1]
|
||||
self.sock.connect((self.peer, 53))
|
||||
debug2('DNS: sending to %r\n' % self.peer)
|
||||
try:
|
||||
self.sock.send(self.request)
|
||||
except socket.error, e:
|
||||
if e.args[0] in ssnet.NET_ERRS:
|
||||
# might have been spurious; try again.
|
||||
# Note: these errors sometimes are reported by recv(),
|
||||
# and sometimes by send(). We have to catch both.
|
||||
debug2('DNS send to %r: %s\n' % (self.peer, e))
|
||||
self.try_send()
|
||||
return
|
||||
else:
|
||||
log('DNS send to %r: %s\n' % (self.peer, e))
|
||||
return
|
||||
|
||||
def callback(self):
|
||||
try:
|
||||
data = self.sock.recv(4096)
|
||||
except socket.error, e:
|
||||
if e.args[0] in ssnet.NET_ERRS:
|
||||
# might have been spurious; try again.
|
||||
# Note: these errors sometimes are reported by recv(),
|
||||
# and sometimes by send(). We have to catch both.
|
||||
debug2('DNS recv from %r: %s\n' % (self.peer, e))
|
||||
self.try_send()
|
||||
return
|
||||
else:
|
||||
log('DNS recv from %r: %s\n' % (self.peer, e))
|
||||
return
|
||||
debug2('DNS response: %d bytes\n' % len(data))
|
||||
self.mux.send(self.chan, ssnet.CMD_DNS_RESPONSE, data)
|
||||
self.ok = False
|
||||
|
||||
|
||||
class UdpProxy(Handler):
|
||||
|
||||
def __init__(self, mux, chan, family):
|
||||
sock = socket.socket(family, socket.SOCK_DGRAM)
|
||||
Handler.__init__(self, [sock])
|
||||
self.timeout = time.time() + 30
|
||||
self.mux = mux
|
||||
self.chan = chan
|
||||
self.sock = sock
|
||||
if family == socket.AF_INET:
|
||||
self.sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
|
||||
|
||||
def send(self, dstip, data):
|
||||
debug2('UDP: sending to %r port %d\n' % dstip)
|
||||
try:
|
||||
self.sock.sendto(data, dstip)
|
||||
except socket.error, e:
|
||||
log('UDP send to %r port %d: %s\n' % (dstip[0], dstip[1], e))
|
||||
return
|
||||
|
||||
def callback(self):
|
||||
try:
|
||||
data, peer = self.sock.recvfrom(4096)
|
||||
except socket.error, e:
|
||||
log('UDP recv from %r port %d: %s\n' % (peer[0], peer[1], e))
|
||||
return
|
||||
debug2('UDP response: %d bytes\n' % len(data))
|
||||
hdr = "%s,%r," % (peer[0], peer[1])
|
||||
self.mux.send(self.chan, ssnet.CMD_UDP_DATA, hdr + data)
|
||||
|
||||
|
||||
def main():
|
||||
if helpers.verbose >= 1:
|
||||
helpers.logprefix = ' s: '
|
||||
else:
|
||||
helpers.logprefix = 'server: '
|
||||
assert latency_control is not None
|
||||
debug1('latency control setting = %r\n' % latency_control)
|
||||
|
||||
routes = list(list_routes())
|
||||
debug1('available routes:\n')
|
||||
for r in routes:
|
||||
debug1(' %d/%s/%d\n' % r)
|
||||
|
||||
# synchronization header
|
||||
sys.stdout.write('\0\0SSHUTTLE0001')
|
||||
sys.stdout.flush()
|
||||
|
||||
handlers = []
|
||||
mux = Mux(socket.fromfd(sys.stdin.fileno(),
|
||||
socket.AF_INET, socket.SOCK_STREAM),
|
||||
socket.fromfd(sys.stdout.fileno(),
|
||||
socket.AF_INET, socket.SOCK_STREAM))
|
||||
handlers.append(mux)
|
||||
routepkt = ''
|
||||
for r in routes:
|
||||
routepkt += '%d,%s,%d\n' % r
|
||||
mux.send(0, ssnet.CMD_ROUTES, routepkt)
|
||||
|
||||
hw = Hostwatch()
|
||||
hw.leftover = ''
|
||||
|
||||
def hostwatch_ready():
|
||||
assert(hw.pid)
|
||||
content = hw.sock.recv(4096)
|
||||
if content:
|
||||
lines = (hw.leftover + content).split('\n')
|
||||
if lines[-1]:
|
||||
# no terminating newline: entry isn't complete yet!
|
||||
hw.leftover = lines.pop()
|
||||
lines.append('')
|
||||
else:
|
||||
hw.leftover = ''
|
||||
mux.send(0, ssnet.CMD_HOST_LIST, '\n'.join(lines))
|
||||
else:
|
||||
raise Fatal('hostwatch process died')
|
||||
|
||||
def got_host_req(data):
|
||||
if not hw.pid:
|
||||
(hw.pid, hw.sock) = start_hostwatch(data.strip().split())
|
||||
handlers.append(Handler(socks=[hw.sock],
|
||||
callback=hostwatch_ready))
|
||||
mux.got_host_req = got_host_req
|
||||
|
||||
def new_channel(channel, data):
|
||||
(family, dstip, dstport) = data.split(',', 2)
|
||||
family = int(family)
|
||||
dstport = int(dstport)
|
||||
outwrap = ssnet.connect_dst(family, dstip, dstport)
|
||||
handlers.append(Proxy(MuxWrapper(mux, channel), outwrap))
|
||||
mux.new_channel = new_channel
|
||||
|
||||
dnshandlers = {}
|
||||
|
||||
def dns_req(channel, data):
|
||||
debug2('Incoming DNS request channel=%d.\n' % channel)
|
||||
h = DnsProxy(mux, channel, data)
|
||||
handlers.append(h)
|
||||
dnshandlers[channel] = h
|
||||
mux.got_dns_req = dns_req
|
||||
|
||||
udphandlers = {}
|
||||
|
||||
def udp_req(channel, cmd, data):
|
||||
debug2('Incoming UDP request channel=%d, cmd=%d\n' % (channel, cmd))
|
||||
if cmd == ssnet.CMD_UDP_DATA:
|
||||
(dstip, dstport, data) = data.split(",", 2)
|
||||
dstport = int(dstport)
|
||||
debug2('is incoming UDP data. %r %d.\n' % (dstip, dstport))
|
||||
h = udphandlers[channel]
|
||||
h.send((dstip, dstport), data)
|
||||
elif cmd == ssnet.CMD_UDP_CLOSE:
|
||||
debug2('is incoming UDP close\n')
|
||||
h = udphandlers[channel]
|
||||
h.ok = False
|
||||
del mux.channels[channel]
|
||||
|
||||
def udp_open(channel, data):
|
||||
debug2('Incoming UDP open.\n')
|
||||
family = int(data)
|
||||
mux.channels[channel] = lambda cmd, data: udp_req(channel, cmd, data)
|
||||
if channel in udphandlers:
|
||||
raise Fatal('UDP connection channel %d already open' % channel)
|
||||
else:
|
||||
h = UdpProxy(mux, channel, family)
|
||||
handlers.append(h)
|
||||
udphandlers[channel] = h
|
||||
mux.got_udp_open = udp_open
|
||||
|
||||
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:
|
||||
debug3('expiring dnsreqs channel=%d\n' % channel)
|
||||
del dnshandlers[channel]
|
||||
h.ok = False
|
||||
if udphandlers:
|
||||
for channel, h in udphandlers.items():
|
||||
if not h.ok:
|
||||
debug3('expiring UDP channel=%d\n' % channel)
|
||||
del udphandlers[channel]
|
||||
h.ok = False
|
@ -1,7 +1,11 @@
|
||||
import sys, os, re, socket, zlib
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import zlib
|
||||
import compat.ssubprocess as ssubprocess
|
||||
import helpers
|
||||
from helpers import *
|
||||
from helpers import debug2
|
||||
|
||||
|
||||
def readfile(name):
|
||||
@ -24,12 +28,9 @@ def empackage(z, filename, data=None):
|
||||
|
||||
|
||||
def connect(ssh_cmd, rhostport, python, stderr, options):
|
||||
main_exe = sys.argv[0]
|
||||
portl = []
|
||||
|
||||
rhostIsIPv6 = False
|
||||
if (rhostport or '').count(':') > 1:
|
||||
rhostIsIPv6 = True
|
||||
if rhostport.count(']') or rhostport.count('['):
|
||||
result = rhostport.split(']')
|
||||
rhost = result[0].strip('[')
|
||||
@ -37,7 +38,9 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
|
||||
result[1] = result[1].strip(':')
|
||||
if result[1] is not '':
|
||||
portl = ['-p', str(int(result[1]))]
|
||||
else: # can't disambiguate IPv6 colons and a port number. pass the hostname through.
|
||||
# can't disambiguate IPv6 colons and a port number. pass the hostname
|
||||
# through.
|
||||
else:
|
||||
rhost = rhostport
|
||||
else: # IPv4
|
||||
l = (rhostport or '').split(':', 1)
|
||||
@ -48,10 +51,6 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
|
||||
if rhost == '-':
|
||||
rhost = None
|
||||
|
||||
ipv6flag = []
|
||||
if rhostIsIPv6:
|
||||
ipv6flag = ['-6']
|
||||
|
||||
z = zlib.compressobj(1)
|
||||
content = readfile('assembler.py')
|
||||
optdata = ''.join("%s=%r\n" % (k, v) for (k, v) in options.items())
|
||||
@ -71,19 +70,25 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
|
||||
""" % (helpers.verbose or 0, len(content))
|
||||
pyscript = re.sub(r'\s+', ' ', pyscript.strip())
|
||||
|
||||
|
||||
if not rhost:
|
||||
argv = [python, '-c', pyscript]
|
||||
# ignore the --python argument when running locally; we already know
|
||||
# which python version works.
|
||||
argv = [sys.argv[1], '-c', pyscript]
|
||||
else:
|
||||
if ssh_cmd:
|
||||
sshl = ssh_cmd.split(' ')
|
||||
else:
|
||||
sshl = ['ssh']
|
||||
if python:
|
||||
pycmd = "'%s' -c '%s'" % (python, pyscript)
|
||||
else:
|
||||
pycmd = ("P=python2; $P -V 2>/dev/null || P=python; "
|
||||
"exec \"$P\" -c '%s'") % pyscript
|
||||
argv = (sshl +
|
||||
portl +
|
||||
ipv6flag +
|
||||
[rhost, '--', "'%s' -c '%s'" % (python, pyscript)])
|
||||
[rhost, '--', pycmd])
|
||||
(s1, s2) = socket.socketpair()
|
||||
|
||||
def setup():
|
||||
# runs in the child process
|
||||
s2.close()
|
12
src/sshuttle
Executable file
12
src/sshuttle
Executable file
@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
EXE=$0
|
||||
for i in 1 2 3 4 5 6 7 8 9 10; do
|
||||
[ -L "$EXE" ] || break
|
||||
EXE=$(readlink "$EXE")
|
||||
done
|
||||
DIR=$(dirname "$EXE")
|
||||
if python2 -V 2>/dev/null; then
|
||||
exec python2 "$DIR/main.py" python2 "$@"
|
||||
else
|
||||
exec python "$DIR/main.py" python "$@"
|
||||
fi
|
@ -71,6 +71,10 @@ entire subnet to the VPN.
|
||||
are taken automatically from the server's routing
|
||||
table.
|
||||
|
||||
--dns
|
||||
: capture local DNS requests and forward to the remote DNS
|
||||
server.
|
||||
|
||||
--python
|
||||
: specify the name/path of the remote python interpreter.
|
||||
The default is just `python`, which means to use the
|
||||
@ -90,6 +94,10 @@ entire subnet to the VPN.
|
||||
`0/0 -x 1.2.3.0/24` to forward everything except the
|
||||
local subnet over the VPN, for example.
|
||||
|
||||
-X, --exclude-from=*file*
|
||||
: exclude the subnets specified in a file, one subnet per
|
||||
line. Useful when you have lots of subnets to exclude.
|
||||
|
||||
-v, --verbose
|
||||
: print more information about the session. This option
|
||||
can be used more than once for increased verbosity. By
|
||||
@ -253,7 +261,24 @@ between the two separate streams, so a tcp-based tunnel is
|
||||
fine.
|
||||
|
||||
|
||||
# BUGS
|
||||
|
||||
On MacOS 10.6 (at least up to 10.6.6), your network will
|
||||
stop responding about 10 minutes after the first time you
|
||||
start sshuttle, because of a MacOS kernel bug relating to
|
||||
arp and the net.inet.ip.scopedroute sysctl. To fix it,
|
||||
just switch your wireless off and on. Sshuttle makes the
|
||||
kernel setting it changes permanent, so this won't happen
|
||||
again, even after a reboot.
|
||||
|
||||
On MacOS, sshuttle will set the kernel boot flag
|
||||
net.inet.ip.scopedroute to 0, which interferes with OS X
|
||||
Internet Sharing and some VPN clients. To reset this flag,
|
||||
you can remove any reference to net.inet.ip.scopedroute from
|
||||
/Library/Preferences/SystemConfiguration/com.apple.Boot.plist
|
||||
and reboot.
|
||||
|
||||
|
||||
# SEE ALSO
|
||||
|
||||
`ssh`(1), `python`(1)
|
||||
|
@ -1,6 +1,12 @@
|
||||
import struct, socket, errno, select
|
||||
import struct
|
||||
import socket
|
||||
import errno
|
||||
import select
|
||||
import os
|
||||
if not globals().get('skip_imports'):
|
||||
from helpers import *
|
||||
from helpers import log, debug1, debug2, debug3, Fatal
|
||||
|
||||
MAX_CHANNEL = 65535
|
||||
|
||||
# these don't exist in the socket module in python 2.3!
|
||||
SHUT_RD = 0
|
||||
@ -14,32 +20,42 @@ HDR_LEN = 8
|
||||
CMD_EXIT = 0x4200
|
||||
CMD_PING = 0x4201
|
||||
CMD_PONG = 0x4202
|
||||
CMD_CONNECT = 0x4203
|
||||
CMD_STOP_SENDING = 0x4204
|
||||
CMD_EOF = 0x4205
|
||||
CMD_DATA = 0x4206
|
||||
CMD_TCP_CONNECT = 0x4203
|
||||
CMD_TCP_STOP_SENDING = 0x4204
|
||||
CMD_TCP_EOF = 0x4205
|
||||
CMD_TCP_DATA = 0x4206
|
||||
CMD_ROUTES = 0x4207
|
||||
CMD_HOST_REQ = 0x4208
|
||||
CMD_HOST_LIST = 0x4209
|
||||
CMD_DNS_REQ = 0x420a
|
||||
CMD_DNS_RESPONSE = 0x420b
|
||||
CMD_UDP_OPEN = 0x420c
|
||||
CMD_UDP_DATA = 0x420d
|
||||
CMD_UDP_CLOSE = 0x420e
|
||||
|
||||
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_TCP_CONNECT: 'TCP_CONNECT',
|
||||
CMD_TCP_STOP_SENDING: 'TCP_STOP_SENDING',
|
||||
CMD_TCP_EOF: 'TCP_EOF',
|
||||
CMD_TCP_DATA: 'TCP_DATA',
|
||||
CMD_ROUTES: 'ROUTES',
|
||||
CMD_HOST_REQ: 'HOST_REQ',
|
||||
CMD_HOST_LIST: 'HOST_LIST',
|
||||
CMD_DNS_REQ: 'DNS_REQ',
|
||||
CMD_DNS_RESPONSE: 'DNS_RESPONSE',
|
||||
CMD_UDP_OPEN: 'UDP_OPEN',
|
||||
CMD_UDP_DATA: 'UDP_DATA',
|
||||
CMD_UDP_CLOSE: 'UDP_CLOSE',
|
||||
}
|
||||
|
||||
|
||||
NET_ERRS = [errno.ECONNREFUSED, errno.ETIMEDOUT,
|
||||
errno.EHOSTUNREACH, errno.ENETUNREACH,
|
||||
errno.EHOSTDOWN, errno.ENETDOWN]
|
||||
|
||||
|
||||
def _add(l, elem):
|
||||
if not elem in l:
|
||||
@ -80,11 +96,14 @@ def _try_peername(sock):
|
||||
|
||||
|
||||
_swcount = 0
|
||||
|
||||
|
||||
class SockWrapper:
|
||||
|
||||
def __init__(self, rsock, wsock, connect_to=None, peername=None):
|
||||
global _swcount
|
||||
_swcount += 1
|
||||
debug3('creating new SockWrapper (%d now exist\n)' % _swcount)
|
||||
debug3('creating new SockWrapper (%d now exist)\n' % _swcount)
|
||||
self.exc = None
|
||||
self.rsock = rsock
|
||||
self.wsock = wsock
|
||||
@ -99,7 +118,7 @@ class SockWrapper:
|
||||
_swcount -= 1
|
||||
debug1('%r: deleting (%d remain)\n' % (self, _swcount))
|
||||
if self.exc:
|
||||
debug1('%r: error was: %r\n' % (self, self.exc))
|
||||
debug1('%r: error was: %s\n' % (self, self.exc))
|
||||
|
||||
def __repr__(self):
|
||||
if self.rsock == self.wsock:
|
||||
@ -127,15 +146,34 @@ class SockWrapper:
|
||||
# connected successfully (Linux)
|
||||
self.connect_to = None
|
||||
except socket.error, e:
|
||||
debug3('%r: connect result: %r\n' % (self, e))
|
||||
debug3('%r: connect result: %s\n' % (self, e))
|
||||
if e.args[0] == errno.EINVAL:
|
||||
# this is what happens when you call connect() on a socket
|
||||
# that is now connected but returned EINPROGRESS last time,
|
||||
# on BSD, on python pre-2.5.1. We need to use getsockopt()
|
||||
# to get the "real" error. Later pythons do this
|
||||
# automatically, so this code won't run.
|
||||
realerr = self.rsock.getsockopt(socket.SOL_SOCKET,
|
||||
socket.SO_ERROR)
|
||||
e = socket.error(realerr, os.strerror(realerr))
|
||||
debug3('%r: fixed connect result: %s\n' % (self, e))
|
||||
if e.args[0] in [errno.EINPROGRESS, errno.EALREADY]:
|
||||
pass # not connected yet
|
||||
elif e.args[0] == 0:
|
||||
# connected successfully (weird Linux bug?)
|
||||
# Sometimes Linux seems to return EINVAL when it isn't
|
||||
# invalid. This *may* be caused by a race condition
|
||||
# between connect() and getsockopt(SO_ERROR) (ie. it
|
||||
# finishes connecting in between the two, so there is no
|
||||
# longer an error). However, I'm not sure of that.
|
||||
#
|
||||
# I did get at least one report that the problem went away
|
||||
# when we added this, however.
|
||||
self.connect_to = None
|
||||
elif e.args[0] == errno.EISCONN:
|
||||
# connected successfully (BSD)
|
||||
self.connect_to = None
|
||||
elif e.args[0] in [errno.ECONNREFUSED, errno.ETIMEDOUT,
|
||||
errno.EHOSTUNREACH, errno.ENETUNREACH,
|
||||
errno.EACCES, errno.EPERM]:
|
||||
elif e.args[0] in NET_ERRS + [errno.EACCES, errno.EPERM]:
|
||||
# a "normal" kind of error
|
||||
self.connect_to = None
|
||||
self.seterr(e)
|
||||
@ -212,6 +250,7 @@ class SockWrapper:
|
||||
|
||||
|
||||
class Handler:
|
||||
|
||||
def __init__(self, socks=None, callback=None):
|
||||
self.ok = True
|
||||
self.socks = socks or []
|
||||
@ -234,6 +273,7 @@ class Handler:
|
||||
|
||||
|
||||
class Proxy(Handler):
|
||||
|
||||
def __init__(self, wrap1, wrap2):
|
||||
Handler.__init__(self, [wrap1.rsock, wrap1.wsock,
|
||||
wrap2.rsock, wrap2.wsock])
|
||||
@ -241,8 +281,10 @@ 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.shut_write:
|
||||
self.wrap2.noread()
|
||||
if self.wrap2.shut_write:
|
||||
self.wrap1.noread()
|
||||
|
||||
if self.wrap1.connect_to:
|
||||
_add(w, self.wrap1.rsock)
|
||||
@ -281,11 +323,13 @@ class Proxy(Handler):
|
||||
|
||||
|
||||
class Mux(Handler):
|
||||
|
||||
def __init__(self, rsock, wsock):
|
||||
Handler.__init__(self, [rsock, wsock])
|
||||
self.rsock = rsock
|
||||
self.wsock = wsock
|
||||
self.new_channel = self.got_dns_req = self.got_routes = None
|
||||
self.got_udp_open = self.got_udp_data = self.got_udp_close = None
|
||||
self.got_host_req = self.got_host_list = None
|
||||
self.channels = {}
|
||||
self.chani = 0
|
||||
@ -300,7 +344,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
|
||||
@ -343,7 +387,7 @@ class Mux(Handler):
|
||||
self.fullness = 0
|
||||
elif cmd == CMD_EXIT:
|
||||
self.ok = False
|
||||
elif cmd == CMD_CONNECT:
|
||||
elif cmd == CMD_TCP_CONNECT:
|
||||
assert(not self.channels.get(channel))
|
||||
if self.new_channel:
|
||||
self.new_channel(channel, data)
|
||||
@ -351,6 +395,10 @@ class Mux(Handler):
|
||||
assert(not self.channels.get(channel))
|
||||
if self.got_dns_req:
|
||||
self.got_dns_req(channel, data)
|
||||
elif cmd == CMD_UDP_OPEN:
|
||||
assert(not self.channels.get(channel))
|
||||
if self.got_udp_open:
|
||||
self.got_udp_open(channel, data)
|
||||
elif cmd == CMD_ROUTES:
|
||||
if self.got_routes:
|
||||
self.got_routes(data)
|
||||
@ -429,6 +477,7 @@ class Mux(Handler):
|
||||
|
||||
|
||||
class MuxWrapper(SockWrapper):
|
||||
|
||||
def __init__(self, mux, channel):
|
||||
SockWrapper.__init__(self, mux.rsock, mux.wsock)
|
||||
self.mux = mux
|
||||
@ -447,13 +496,13 @@ class MuxWrapper(SockWrapper):
|
||||
def noread(self):
|
||||
if not self.shut_read:
|
||||
self.shut_read = True
|
||||
self.mux.send(self.channel, CMD_STOP_SENDING, '')
|
||||
self.mux.send(self.channel, CMD_TCP_STOP_SENDING, '')
|
||||
self.maybe_close()
|
||||
|
||||
def nowrite(self):
|
||||
if not self.shut_write:
|
||||
self.shut_write = True
|
||||
self.mux.send(self.channel, CMD_EOF, '')
|
||||
self.mux.send(self.channel, CMD_TCP_EOF, '')
|
||||
self.maybe_close()
|
||||
|
||||
def maybe_close(self):
|
||||
@ -470,7 +519,7 @@ class MuxWrapper(SockWrapper):
|
||||
return 0 # too much already enqueued
|
||||
if len(buf) > 2048:
|
||||
buf = buf[:2048]
|
||||
self.mux.send(self.channel, CMD_DATA, buf)
|
||||
self.mux.send(self.channel, CMD_TCP_DATA, buf)
|
||||
return len(buf)
|
||||
|
||||
def uread(self):
|
||||
@ -480,20 +529,20 @@ class MuxWrapper(SockWrapper):
|
||||
return None # no data available right now
|
||||
|
||||
def got_packet(self, cmd, data):
|
||||
if cmd == CMD_EOF:
|
||||
if cmd == CMD_TCP_EOF:
|
||||
self.noread()
|
||||
elif cmd == CMD_STOP_SENDING:
|
||||
elif cmd == CMD_TCP_STOP_SENDING:
|
||||
self.nowrite()
|
||||
elif cmd == CMD_DATA:
|
||||
elif cmd == CMD_TCP_DATA:
|
||||
self.buf.append(data)
|
||||
else:
|
||||
raise Exception('unknown command %d (%d bytes)'
|
||||
% (cmd, len(data)))
|
||||
|
||||
|
||||
def connect_dst(ip, port):
|
||||
def connect_dst(family, ip, port):
|
||||
debug2('Connecting to %s:%d\n' % (ip, port))
|
||||
outsock = socket.socket()
|
||||
outsock = socket.socket(family)
|
||||
outsock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
|
||||
return SockWrapper(outsock, outsock,
|
||||
connect_to=(ip, port),
|
@ -1,8 +1,11 @@
|
||||
import sys, os
|
||||
import sys
|
||||
import os
|
||||
from compat import ssubprocess
|
||||
|
||||
|
||||
_p = None
|
||||
|
||||
|
||||
def start_syslog():
|
||||
global _p
|
||||
_p = ssubprocess.Popen(['logger',
|
89
src/stresstest.py
Executable file
89
src/stresstest.py
Executable file
@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python
|
||||
import socket
|
||||
import select
|
||||
import struct
|
||||
import 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()
|
@ -2,17 +2,17 @@
|
||||
<archive type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="7.10">
|
||||
<data>
|
||||
<int key="IBDocument.SystemTarget">1060</int>
|
||||
<string key="IBDocument.SystemVersion">10H574</string>
|
||||
<string key="IBDocument.SystemVersion">10J567</string>
|
||||
<string key="IBDocument.InterfaceBuilderVersion">762</string>
|
||||
<string key="IBDocument.AppKitVersion">1038.35</string>
|
||||
<string key="IBDocument.HIToolboxVersion">461.00</string>
|
||||
<string key="IBDocument.HIToolboxVersion">462.00</string>
|
||||
<object class="NSMutableDictionary" key="IBDocument.PluginVersions">
|
||||
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="NS.object.0">762</string>
|
||||
</object>
|
||||
<object class="NSMutableArray" key="IBDocument.EditedObjectIDs">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<integer value="233"/>
|
||||
<integer value="237"/>
|
||||
</object>
|
||||
<object class="NSArray" key="IBDocument.PluginDependencies">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
@ -41,9 +41,9 @@
|
||||
<string key="NSClassName">NSApplication</string>
|
||||
</object>
|
||||
<object class="NSWindowTemplate" id="411825121">
|
||||
<int key="NSWindowStyleMask">3</int>
|
||||
<int key="NSWindowStyleMask">11</int>
|
||||
<int key="NSWindowBacking">2</int>
|
||||
<string key="NSWindowRect">{{157, 116}, {611, 369}}</string>
|
||||
<string key="NSWindowRect">{{324, 171}, {611, 469}}</string>
|
||||
<int key="NSWTFlags">1886913536</int>
|
||||
<string key="NSWindowTitle">Sshuttle VPN Preferences</string>
|
||||
<string key="NSWindowClass">NSWindow</string>
|
||||
@ -51,16 +51,16 @@
|
||||
<characters key="NS.bytes">View</characters>
|
||||
</object>
|
||||
<string key="NSWindowContentMaxSize">{1.79769e+308, 1.79769e+308}</string>
|
||||
<string key="NSWindowContentMinSize">{213, 107}</string>
|
||||
<string key="NSWindowContentMinSize">{611, 469}</string>
|
||||
<object class="NSView" key="NSWindowView" id="174067038">
|
||||
<reference key="NSNextResponder"/>
|
||||
<int key="NSvFlags">256</int>
|
||||
<int key="NSvFlags">274</int>
|
||||
<object class="NSMutableArray" key="NSSubviews">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSTabView" id="389252804">
|
||||
<reference key="NSNextResponder" ref="174067038"/>
|
||||
<int key="NSvFlags">12</int>
|
||||
<string key="NSFrame">{{-8, -10}, {627, 373}}</string>
|
||||
<int key="NSvFlags">18</int>
|
||||
<string key="NSFrame">{{-8, -10}, {627, 473}}</string>
|
||||
<reference key="NSSuperview" ref="174067038"/>
|
||||
<int key="NSViewLayerContentsRedrawPolicy">2</int>
|
||||
<object class="NSMutableArray" key="NSTabViewItems">
|
||||
@ -68,13 +68,13 @@
|
||||
<object class="NSTabViewItem" id="762265164">
|
||||
<string key="NSIdentifier">1</string>
|
||||
<object class="NSView" key="NSView" id="60314308">
|
||||
<nil key="NSNextResponder"/>
|
||||
<int key="NSvFlags">256</int>
|
||||
<reference key="NSNextResponder" ref="389252804"/>
|
||||
<int key="NSvFlags">274</int>
|
||||
<object class="NSMutableArray" key="NSSubviews">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSButton" id="26015719">
|
||||
<reference key="NSNextResponder" ref="60314308"/>
|
||||
<int key="NSvFlags">268</int>
|
||||
<int key="NSvFlags">292</int>
|
||||
<string key="NSFrame">{{17, 17}, {25, 23}}</string>
|
||||
<reference key="NSSuperview" ref="60314308"/>
|
||||
<int key="NSViewLayerContentsRedrawPolicy">2</int>
|
||||
@ -103,7 +103,7 @@
|
||||
</object>
|
||||
<object class="NSButton" id="244571541">
|
||||
<reference key="NSNextResponder" ref="60314308"/>
|
||||
<int key="NSvFlags">268</int>
|
||||
<int key="NSvFlags">292</int>
|
||||
<string key="NSFrame">{{41, 17}, {25, 23}}</string>
|
||||
<reference key="NSSuperview" ref="60314308"/>
|
||||
<int key="NSViewLayerContentsRedrawPolicy">2</int>
|
||||
@ -128,7 +128,7 @@
|
||||
</object>
|
||||
<object class="NSScrollView" id="776974664">
|
||||
<reference key="NSNextResponder" ref="60314308"/>
|
||||
<int key="NSvFlags">268</int>
|
||||
<int key="NSvFlags">274</int>
|
||||
<object class="NSMutableArray" key="NSSubviews">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSClipView" id="8658574">
|
||||
@ -139,7 +139,7 @@
|
||||
<object class="NSTableView" id="849333466">
|
||||
<reference key="NSNextResponder" ref="8658574"/>
|
||||
<int key="NSvFlags">256</int>
|
||||
<string key="NSFrameSize">{224, 282}</string>
|
||||
<string key="NSFrameSize">{224, 372}</string>
|
||||
<reference key="NSSuperview" ref="8658574"/>
|
||||
<int key="NSViewLayerContentsRedrawPolicy">2</int>
|
||||
<bool key="NSEnabled">YES</bool>
|
||||
@ -275,7 +275,7 @@
|
||||
<int key="NSTableViewDraggingDestinationStyle">0</int>
|
||||
</object>
|
||||
</object>
|
||||
<string key="NSFrame">{{1, 1}, {223, 282}}</string>
|
||||
<string key="NSFrame">{{1, 1}, {223, 372}}</string>
|
||||
<reference key="NSSuperview" ref="776974664"/>
|
||||
<reference key="NSNextKeyView" ref="849333466"/>
|
||||
<int key="NSViewLayerContentsRedrawPolicy">2</int>
|
||||
@ -306,7 +306,7 @@
|
||||
<double key="NSPercent">0.91812865497076024</double>
|
||||
</object>
|
||||
</object>
|
||||
<string key="NSFrame">{{17, 40}, {225, 284}}</string>
|
||||
<string key="NSFrame">{{17, 40}, {225, 374}}</string>
|
||||
<reference key="NSSuperview" ref="60314308"/>
|
||||
<reference key="NSNextKeyView" ref="8658574"/>
|
||||
<int key="NSViewLayerContentsRedrawPolicy">2</int>
|
||||
@ -318,8 +318,8 @@
|
||||
</object>
|
||||
<object class="NSButton" id="538356055">
|
||||
<reference key="NSNextResponder" ref="60314308"/>
|
||||
<int key="NSvFlags">268</int>
|
||||
<string key="NSFrame">{{296, 254}, {273, 18}}</string>
|
||||
<int key="NSvFlags">265</int>
|
||||
<string key="NSFrame">{{291, 356}, {273, 18}}</string>
|
||||
<reference key="NSSuperview" ref="60314308"/>
|
||||
<int key="NSViewLayerContentsRedrawPolicy">2</int>
|
||||
<bool key="NSEnabled">YES</bool>
|
||||
@ -341,8 +341,8 @@
|
||||
</object>
|
||||
<object class="NSTextField" id="889877302">
|
||||
<reference key="NSNextResponder" ref="60314308"/>
|
||||
<int key="NSvFlags">268</int>
|
||||
<string key="NSFrame">{{341, 292}, {249, 22}}</string>
|
||||
<int key="NSvFlags">265</int>
|
||||
<string key="NSFrame">{{341, 392}, {249, 22}}</string>
|
||||
<reference key="NSSuperview" ref="60314308"/>
|
||||
<int key="NSViewLayerContentsRedrawPolicy">2</int>
|
||||
<bool key="NSEnabled">YES</bool>
|
||||
@ -369,8 +369,8 @@
|
||||
</object>
|
||||
<object class="NSPopUpButton" id="801412726">
|
||||
<reference key="NSNextResponder" ref="60314308"/>
|
||||
<int key="NSvFlags">268</int>
|
||||
<string key="NSFrame">{{273, 185}, {320, 26}}</string>
|
||||
<int key="NSvFlags">265</int>
|
||||
<string key="NSFrame">{{273, 193}, {320, 26}}</string>
|
||||
<reference key="NSSuperview" ref="60314308"/>
|
||||
<int key="NSViewLayerContentsRedrawPolicy">2</int>
|
||||
<bool key="NSEnabled">YES</bool>
|
||||
@ -440,10 +440,77 @@
|
||||
<int key="NSArrowPosition">2</int>
|
||||
</object>
|
||||
</object>
|
||||
<object class="NSPopUpButton" id="451647466">
|
||||
<reference key="NSNextResponder" ref="60314308"/>
|
||||
<int key="NSvFlags">265</int>
|
||||
<string key="NSFrame">{{273, 257}, {320, 26}}</string>
|
||||
<reference key="NSSuperview" ref="60314308"/>
|
||||
<int key="NSViewLayerContentsRedrawPolicy">2</int>
|
||||
<bool key="NSEnabled">YES</bool>
|
||||
<object class="NSPopUpButtonCell" key="NSCell" id="970826243">
|
||||
<int key="NSCellFlags">-2076049856</int>
|
||||
<int key="NSCellFlags2">2048</int>
|
||||
<reference key="NSSupport" ref="696441443"/>
|
||||
<reference key="NSControlView" ref="451647466"/>
|
||||
<int key="NSButtonFlags">112869631</int>
|
||||
<int key="NSButtonFlags2">129</int>
|
||||
<string key="NSAlternateContents"/>
|
||||
<string key="NSKeyEquivalent"/>
|
||||
<int key="NSPeriodicDelay">400</int>
|
||||
<int key="NSPeriodicInterval">75</int>
|
||||
<object class="NSMenuItem" key="NSMenuItem" id="903120255">
|
||||
<reference key="NSMenu" ref="790880658"/>
|
||||
<string key="NSTitle">Choose one</string>
|
||||
<string key="NSKeyEquiv"/>
|
||||
<int key="NSKeyEquivModMask">1048576</int>
|
||||
<int key="NSMnemonicLoc">2147483647</int>
|
||||
<int key="NSState">1</int>
|
||||
<reference key="NSOnImage" ref="615977438"/>
|
||||
<reference key="NSMixedImage" ref="445532764"/>
|
||||
<string key="NSAction">_popUpItemAction:</string>
|
||||
<reference key="NSTarget" ref="970826243"/>
|
||||
</object>
|
||||
<bool key="NSMenuItemRespectAlignment">YES</bool>
|
||||
<object class="NSMenu" key="NSMenu" id="790880658">
|
||||
<string key="NSTitle">OtherViews</string>
|
||||
<object class="NSMutableArray" key="NSMenuItems">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference ref="903120255"/>
|
||||
<object class="NSMenuItem" id="778262848">
|
||||
<reference key="NSMenu" ref="790880658"/>
|
||||
<string key="NSTitle">Item 2</string>
|
||||
<string key="NSKeyEquiv"/>
|
||||
<int key="NSKeyEquivModMask">1048576</int>
|
||||
<int key="NSMnemonicLoc">2147483647</int>
|
||||
<reference key="NSOnImage" ref="615977438"/>
|
||||
<reference key="NSMixedImage" ref="445532764"/>
|
||||
<string key="NSAction">_popUpItemAction:</string>
|
||||
<reference key="NSTarget" ref="970826243"/>
|
||||
</object>
|
||||
<object class="NSMenuItem" id="195135854">
|
||||
<reference key="NSMenu" ref="790880658"/>
|
||||
<string key="NSTitle">Item 3</string>
|
||||
<string key="NSKeyEquiv"/>
|
||||
<int key="NSKeyEquivModMask">1048576</int>
|
||||
<int key="NSMnemonicLoc">2147483647</int>
|
||||
<reference key="NSOnImage" ref="615977438"/>
|
||||
<reference key="NSMixedImage" ref="445532764"/>
|
||||
<string key="NSAction">_popUpItemAction:</string>
|
||||
<reference key="NSTarget" ref="970826243"/>
|
||||
</object>
|
||||
</object>
|
||||
<reference key="NSMenuFont" ref="696441443"/>
|
||||
</object>
|
||||
<int key="NSPreferredEdge">1</int>
|
||||
<bool key="NSUsesItemFromMenu">YES</bool>
|
||||
<bool key="NSAltersState">YES</bool>
|
||||
<int key="NSArrowPosition">2</int>
|
||||
</object>
|
||||
</object>
|
||||
<object class="NSTextField" id="753545988">
|
||||
<reference key="NSNextResponder" ref="60314308"/>
|
||||
<int key="NSvFlags">268</int>
|
||||
<string key="NSFrame">{{261, 294}, {75, 17}}</string>
|
||||
<int key="NSvFlags">265</int>
|
||||
<string key="NSFrame">{{261, 394}, {75, 17}}</string>
|
||||
<reference key="NSSuperview" ref="60314308"/>
|
||||
<int key="NSViewLayerContentsRedrawPolicy">2</int>
|
||||
<bool key="NSEnabled">YES</bool>
|
||||
@ -459,8 +526,8 @@
|
||||
</object>
|
||||
<object class="NSTextField" id="840157770">
|
||||
<reference key="NSNextResponder" ref="60314308"/>
|
||||
<int key="NSvFlags">268</int>
|
||||
<string key="NSFrame">{{261, 217}, {105, 17}}</string>
|
||||
<int key="NSvFlags">265</int>
|
||||
<string key="NSFrame">{{261, 226}, {105, 17}}</string>
|
||||
<reference key="NSSuperview" ref="60314308"/>
|
||||
<int key="NSViewLayerContentsRedrawPolicy">2</int>
|
||||
<bool key="NSEnabled">YES</bool>
|
||||
@ -474,9 +541,26 @@
|
||||
<reference key="NSTextColor" ref="399750419"/>
|
||||
</object>
|
||||
</object>
|
||||
<object class="NSTextField" id="459566505">
|
||||
<reference key="NSNextResponder" ref="60314308"/>
|
||||
<int key="NSvFlags">265</int>
|
||||
<string key="NSFrame">{{261, 290}, {105, 17}}</string>
|
||||
<reference key="NSSuperview" ref="60314308"/>
|
||||
<int key="NSViewLayerContentsRedrawPolicy">2</int>
|
||||
<bool key="NSEnabled">YES</bool>
|
||||
<object class="NSTextFieldCell" key="NSCell" id="534245320">
|
||||
<int key="NSCellFlags">68288064</int>
|
||||
<int key="NSCellFlags2">272630784</int>
|
||||
<string key="NSContents">Optimize for:</string>
|
||||
<reference key="NSSupport" ref="696441443"/>
|
||||
<reference key="NSControlView" ref="459566505"/>
|
||||
<reference key="NSBackgroundColor" ref="965844506"/>
|
||||
<reference key="NSTextColor" ref="399750419"/>
|
||||
</object>
|
||||
</object>
|
||||
<object class="NSButton" id="962924480">
|
||||
<reference key="NSNextResponder" ref="60314308"/>
|
||||
<int key="NSvFlags">268</int>
|
||||
<int key="NSvFlags">289</int>
|
||||
<string key="NSFrame">{{276, 40}, {25, 23}}</string>
|
||||
<reference key="NSSuperview" ref="60314308"/>
|
||||
<int key="NSViewLayerContentsRedrawPolicy">2</int>
|
||||
@ -498,7 +582,7 @@
|
||||
</object>
|
||||
<object class="NSButton" id="705215911">
|
||||
<reference key="NSNextResponder" ref="60314308"/>
|
||||
<int key="NSvFlags">268</int>
|
||||
<int key="NSvFlags">289</int>
|
||||
<string key="NSFrame">{{300, 40}, {25, 23}}</string>
|
||||
<reference key="NSSuperview" ref="60314308"/>
|
||||
<int key="NSViewLayerContentsRedrawPolicy">2</int>
|
||||
@ -520,7 +604,7 @@
|
||||
</object>
|
||||
<object class="NSScrollView" id="610703353">
|
||||
<reference key="NSNextResponder" ref="60314308"/>
|
||||
<int key="NSvFlags">268</int>
|
||||
<int key="NSvFlags">273</int>
|
||||
<object class="NSMutableArray" key="NSSubviews">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSClipView" id="844110079">
|
||||
@ -531,7 +615,7 @@
|
||||
<object class="NSTableView" id="714838401">
|
||||
<reference key="NSNextResponder" ref="844110079"/>
|
||||
<int key="NSvFlags">256</int>
|
||||
<string key="NSFrameSize">{312, 95}</string>
|
||||
<string key="NSFrameSize">{312, 102}</string>
|
||||
<reference key="NSSuperview" ref="844110079"/>
|
||||
<int key="NSViewLayerContentsRedrawPolicy">2</int>
|
||||
<bool key="NSEnabled">YES</bool>
|
||||
@ -623,7 +707,7 @@
|
||||
<int key="NSTableViewDraggingDestinationStyle">0</int>
|
||||
</object>
|
||||
</object>
|
||||
<string key="NSFrame">{{1, 17}, {312, 95}}</string>
|
||||
<string key="NSFrame">{{1, 17}, {312, 102}}</string>
|
||||
<reference key="NSSuperview" ref="610703353"/>
|
||||
<reference key="NSNextKeyView" ref="714838401"/>
|
||||
<int key="NSViewLayerContentsRedrawPolicy">2</int>
|
||||
@ -639,7 +723,7 @@
|
||||
<int key="NSViewLayerContentsRedrawPolicy">2</int>
|
||||
<reference key="NSTarget" ref="610703353"/>
|
||||
<string key="NSAction">_doScroller:</string>
|
||||
<double key="NSPercent">0.8529411764705882</double>
|
||||
<double key="NSPercent">0.96938775510204078</double>
|
||||
</object>
|
||||
<object class="NSScroller" id="522064761">
|
||||
<reference key="NSNextResponder" ref="610703353"/>
|
||||
@ -669,7 +753,7 @@
|
||||
</object>
|
||||
<reference ref="188199142"/>
|
||||
</object>
|
||||
<string key="NSFrame">{{276, 63}, {314, 113}}</string>
|
||||
<string key="NSFrame">{{276, 63}, {314, 120}}</string>
|
||||
<reference key="NSSuperview" ref="60314308"/>
|
||||
<reference key="NSNextKeyView" ref="844110079"/>
|
||||
<int key="NSViewLayerContentsRedrawPolicy">2</int>
|
||||
@ -681,8 +765,32 @@
|
||||
<reference key="NSCornerView" ref="188199142"/>
|
||||
<bytes key="NSScrollAmts">QSAAAEEgAABBmAAAQZgAAA</bytes>
|
||||
</object>
|
||||
<object class="NSButton" id="160987209">
|
||||
<reference key="NSNextResponder" ref="60314308"/>
|
||||
<int key="NSvFlags">265</int>
|
||||
<string key="NSFrame">{{291, 323}, {285, 18}}</string>
|
||||
<reference key="NSSuperview" ref="60314308"/>
|
||||
<int key="NSViewLayerContentsRedrawPolicy">2</int>
|
||||
<bool key="NSEnabled">YES</bool>
|
||||
<object class="NSButtonCell" key="NSCell" id="565787077">
|
||||
<int key="NSCellFlags">-2080244224</int>
|
||||
<int key="NSCellFlags2">0</int>
|
||||
<string key="NSContents">Send DNS requests through this server</string>
|
||||
<reference key="NSSupport" ref="696441443"/>
|
||||
<reference key="NSControlView" ref="160987209"/>
|
||||
<int key="NSButtonFlags">1211912703</int>
|
||||
<int key="NSButtonFlags2">2</int>
|
||||
<reference key="NSNormalImage" ref="581816235"/>
|
||||
<reference key="NSAlternateImage" ref="753862261"/>
|
||||
<string key="NSAlternateContents"/>
|
||||
<string key="NSKeyEquivalent"/>
|
||||
<int key="NSPeriodicDelay">200</int>
|
||||
<int key="NSPeriodicInterval">25</int>
|
||||
</object>
|
||||
<string key="NSFrame">{{10, 33}, {607, 327}}</string>
|
||||
</object>
|
||||
</object>
|
||||
<string key="NSFrame">{{10, 33}, {607, 427}}</string>
|
||||
<reference key="NSSuperview" ref="389252804"/>
|
||||
<int key="NSViewLayerContentsRedrawPolicy">2</int>
|
||||
</object>
|
||||
<string key="NSLabel">SSH Servers</string>
|
||||
@ -692,14 +800,14 @@
|
||||
<object class="NSTabViewItem" id="740075218">
|
||||
<string key="NSIdentifier">2</string>
|
||||
<object class="NSView" key="NSView" id="187214803">
|
||||
<reference key="NSNextResponder" ref="389252804"/>
|
||||
<int key="NSvFlags">256</int>
|
||||
<nil key="NSNextResponder"/>
|
||||
<int key="NSvFlags">274</int>
|
||||
<object class="NSMutableArray" key="NSSubviews">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSButton" id="625816566">
|
||||
<reference key="NSNextResponder" ref="187214803"/>
|
||||
<int key="NSvFlags">268</int>
|
||||
<string key="NSFrame">{{189, 287}, {177, 29}}</string>
|
||||
<int key="NSvFlags">269</int>
|
||||
<string key="NSFrame">{{195, 374}, {236, 29}}</string>
|
||||
<reference key="NSSuperview" ref="187214803"/>
|
||||
<int key="NSViewLayerContentsRedrawPolicy">2</int>
|
||||
<bool key="NSEnabled">YES</bool>
|
||||
@ -721,8 +829,8 @@
|
||||
</object>
|
||||
<object class="NSButton" id="633518934">
|
||||
<reference key="NSNextResponder" ref="187214803"/>
|
||||
<int key="NSvFlags">268</int>
|
||||
<string key="NSFrame">{{189, 256}, {270, 29}}</string>
|
||||
<int key="NSvFlags">269</int>
|
||||
<string key="NSFrame">{{195, 343}, {236, 29}}</string>
|
||||
<reference key="NSSuperview" ref="187214803"/>
|
||||
<int key="NSViewLayerContentsRedrawPolicy">2</int>
|
||||
<bool key="NSEnabled">YES</bool>
|
||||
@ -744,15 +852,15 @@
|
||||
</object>
|
||||
<object class="NSButton" id="625121428">
|
||||
<reference key="NSNextResponder" ref="187214803"/>
|
||||
<int key="NSvFlags">268</int>
|
||||
<string key="NSFrame">{{189, 225}, {270, 29}}</string>
|
||||
<int key="NSvFlags">269</int>
|
||||
<string key="NSFrame">{{195, 312}, {236, 29}}</string>
|
||||
<reference key="NSSuperview" ref="187214803"/>
|
||||
<int key="NSViewLayerContentsRedrawPolicy">2</int>
|
||||
<bool key="NSEnabled">YES</bool>
|
||||
<object class="NSButtonCell" key="NSCell" id="132399775">
|
||||
<int key="NSCellFlags">67239424</int>
|
||||
<int key="NSCellFlags2">0</int>
|
||||
<string key="NSContents">Send debug information to log</string>
|
||||
<string key="NSContents">Enable debug messages</string>
|
||||
<reference key="NSSupport" ref="696441443"/>
|
||||
<reference key="NSControlView" ref="625121428"/>
|
||||
<int key="NSButtonFlags">1211912703</int>
|
||||
@ -766,8 +874,7 @@
|
||||
</object>
|
||||
</object>
|
||||
</object>
|
||||
<string key="NSFrame">{{10, 33}, {607, 327}}</string>
|
||||
<reference key="NSSuperview" ref="389252804"/>
|
||||
<string key="NSFrame">{{10, 33}, {607, 427}}</string>
|
||||
<int key="NSViewLayerContentsRedrawPolicy">2</int>
|
||||
</object>
|
||||
<string key="NSLabel">Options</string>
|
||||
@ -783,7 +890,7 @@
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSScrollView" id="486362220">
|
||||
<reference key="NSNextResponder" ref="311013698"/>
|
||||
<int key="NSvFlags">256</int>
|
||||
<int key="NSvFlags">274</int>
|
||||
<object class="NSMutableArray" key="NSSubviews">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSClipView" id="708990027">
|
||||
@ -794,7 +901,7 @@
|
||||
<object class="NSTextView" id="758761310">
|
||||
<reference key="NSNextResponder" ref="708990027"/>
|
||||
<int key="NSvFlags">2322</int>
|
||||
<string key="NSFrameSize">{596, 14}</string>
|
||||
<string key="NSFrameSize">{596, 114}</string>
|
||||
<reference key="NSSuperview" ref="708990027"/>
|
||||
<int key="NSViewLayerContentsRedrawPolicy">2</int>
|
||||
<object class="NSTextContainer" key="NSTextContainer" id="548968659">
|
||||
@ -874,7 +981,7 @@
|
||||
<nil key="NSDelegate"/>
|
||||
</object>
|
||||
</object>
|
||||
<string key="NSFrame">{{1, 1}, {596, 310}}</string>
|
||||
<string key="NSFrame">{{1, 1}, {596, 410}}</string>
|
||||
<reference key="NSSuperview" ref="486362220"/>
|
||||
<reference key="NSNextKeyView" ref="758761310"/>
|
||||
<int key="NSViewLayerContentsRedrawPolicy">2</int>
|
||||
@ -889,7 +996,7 @@
|
||||
<object class="NSScroller" id="794963499">
|
||||
<reference key="NSNextResponder" ref="486362220"/>
|
||||
<int key="NSvFlags">256</int>
|
||||
<string key="NSFrame">{{597, 1}, {15, 310}}</string>
|
||||
<string key="NSFrame">{{597, 1}, {15, 410}}</string>
|
||||
<reference key="NSSuperview" ref="486362220"/>
|
||||
<int key="NSViewLayerContentsRedrawPolicy">2</int>
|
||||
<reference key="NSTarget" ref="486362220"/>
|
||||
@ -900,7 +1007,7 @@
|
||||
<object class="NSScroller" id="324242772">
|
||||
<reference key="NSNextResponder" ref="486362220"/>
|
||||
<int key="NSvFlags">256</int>
|
||||
<string key="NSFrame">{{1, 311}, {596, 15}}</string>
|
||||
<string key="NSFrame">{{1, 411}, {596, 15}}</string>
|
||||
<reference key="NSSuperview" ref="486362220"/>
|
||||
<int key="NSViewLayerContentsRedrawPolicy">2</int>
|
||||
<int key="NSsFlags">1</int>
|
||||
@ -910,7 +1017,7 @@
|
||||
<double key="NSPercent">0.94565218687057495</double>
|
||||
</object>
|
||||
</object>
|
||||
<string key="NSFrame">{{-3, -3}, {613, 327}}</string>
|
||||
<string key="NSFrame">{{-3, -3}, {613, 427}}</string>
|
||||
<reference key="NSSuperview" ref="311013698"/>
|
||||
<reference key="NSNextKeyView" ref="708990027"/>
|
||||
<int key="NSViewLayerContentsRedrawPolicy">2</int>
|
||||
@ -920,7 +1027,7 @@
|
||||
<reference key="NSContentView" ref="708990027"/>
|
||||
</object>
|
||||
</object>
|
||||
<string key="NSFrame">{{10, 33}, {607, 327}}</string>
|
||||
<string key="NSFrame">{{10, 33}, {607, 427}}</string>
|
||||
<int key="NSViewLayerContentsRedrawPolicy">2</int>
|
||||
</object>
|
||||
<string key="NSLabel">Log Messages</string>
|
||||
@ -928,17 +1035,17 @@
|
||||
<reference key="NSTabView" ref="389252804"/>
|
||||
</object>
|
||||
</object>
|
||||
<reference key="NSSelectedTabViewItem" ref="740075218"/>
|
||||
<reference key="NSSelectedTabViewItem" ref="762265164"/>
|
||||
<reference key="NSFont" ref="696441443"/>
|
||||
<int key="NSTvFlags">0</int>
|
||||
<bool key="NSDrawsBackground">YES</bool>
|
||||
<object class="NSMutableArray" key="NSSubviews">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference ref="187214803"/>
|
||||
<reference ref="60314308"/>
|
||||
</object>
|
||||
</object>
|
||||
</object>
|
||||
<string key="NSFrameSize">{611, 369}</string>
|
||||
<string key="NSFrameSize">{611, 469}</string>
|
||||
<reference key="NSSuperview"/>
|
||||
<object class="NSDictionary" key="NSViewAnimations">
|
||||
<string key="NS.key.0">subviews</string>
|
||||
@ -962,8 +1069,8 @@
|
||||
</object>
|
||||
<int key="NSViewLayerContentsRedrawPolicy">2</int>
|
||||
</object>
|
||||
<string key="NSScreenRect">{{0, 0}, {800, 578}}</string>
|
||||
<string key="NSMinSize">{213, 129}</string>
|
||||
<string key="NSScreenRect">{{0, 0}, {1280, 778}}</string>
|
||||
<string key="NSMinSize">{611, 491}</string>
|
||||
<string key="NSMaxSize">{1.79769e+308, 1.79769e+308}</string>
|
||||
</object>
|
||||
<object class="NSArrayController" id="678105904">
|
||||
@ -979,10 +1086,6 @@
|
||||
<string key="NSClassName">SshuttleController</string>
|
||||
</object>
|
||||
<object class="NSUserDefaultsController" id="582889489">
|
||||
<object class="NSMutableArray" key="NSDeclaredKeys">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>debug</string>
|
||||
</object>
|
||||
<bool key="NSSharedInstance">YES</bool>
|
||||
</object>
|
||||
<object class="NSCustomObject" id="735871403">
|
||||
@ -1000,6 +1103,9 @@
|
||||
<string>statusMsg</string>
|
||||
<string>status</string>
|
||||
<string>isValid</string>
|
||||
<string>useDns</string>
|
||||
<string>title</string>
|
||||
<string>latencyControl</string>
|
||||
</object>
|
||||
<string key="NSObjectClassName">SshuttleServer</string>
|
||||
<bool key="NSEditable">YES</bool>
|
||||
@ -1109,35 +1215,6 @@
|
||||
</object>
|
||||
<int key="connectionID">480</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBBindingConnection" key="connection">
|
||||
<string key="label">value: arrangedObjects.host</string>
|
||||
<reference key="source" ref="807059746"/>
|
||||
<reference key="destination" ref="59237012"/>
|
||||
<object class="NSNibBindingConnector" key="connector">
|
||||
<reference key="NSSource" ref="807059746"/>
|
||||
<reference key="NSDestination" ref="59237012"/>
|
||||
<string key="NSLabel">value: arrangedObjects.host</string>
|
||||
<string key="NSBinding">value</string>
|
||||
<string key="NSKeyPath">arrangedObjects.host</string>
|
||||
<object class="NSDictionary" key="NSOptions">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSArray" key="dict.sortedKeys">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>NSConditionallySetsEditable</string>
|
||||
<string>NSNullPlaceholder</string>
|
||||
</object>
|
||||
<object class="NSMutableArray" key="dict.values">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<boolean value="YES"/>
|
||||
<string>Untitled</string>
|
||||
</object>
|
||||
</object>
|
||||
<int key="NSNibBindingConnectorVersion">2</int>
|
||||
</object>
|
||||
</object>
|
||||
<int key="connectionID">495</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBBindingConnection" key="connection">
|
||||
<string key="label">selectedIndex: selection.autoNets</string>
|
||||
@ -1454,6 +1531,83 @@
|
||||
</object>
|
||||
<int key="connectionID">557</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBBindingConnection" key="connection">
|
||||
<string key="label">value: selection.useDns</string>
|
||||
<reference key="source" ref="160987209"/>
|
||||
<reference key="destination" ref="59237012"/>
|
||||
<object class="NSNibBindingConnector" key="connector">
|
||||
<reference key="NSSource" ref="160987209"/>
|
||||
<reference key="NSDestination" ref="59237012"/>
|
||||
<string key="NSLabel">value: selection.useDns</string>
|
||||
<string key="NSBinding">value</string>
|
||||
<string key="NSKeyPath">selection.useDns</string>
|
||||
<object class="NSDictionary" key="NSOptions">
|
||||
<string key="NS.key.0">NSNoSelectionPlaceholder</string>
|
||||
<integer value="0" key="NS.object.0"/>
|
||||
</object>
|
||||
<int key="NSNibBindingConnectorVersion">2</int>
|
||||
</object>
|
||||
</object>
|
||||
<int key="connectionID">572</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBBindingConnection" key="connection">
|
||||
<string key="label">value: arrangedObjects.title</string>
|
||||
<reference key="source" ref="807059746"/>
|
||||
<reference key="destination" ref="59237012"/>
|
||||
<object class="NSNibBindingConnector" key="connector">
|
||||
<reference key="NSSource" ref="807059746"/>
|
||||
<reference key="NSDestination" ref="59237012"/>
|
||||
<string key="NSLabel">value: arrangedObjects.title</string>
|
||||
<string key="NSBinding">value</string>
|
||||
<string key="NSKeyPath">arrangedObjects.title</string>
|
||||
<object class="NSDictionary" key="NSOptions">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSArray" key="dict.sortedKeys">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>NSConditionallySetsEditable</string>
|
||||
<string>NSNullPlaceholder</string>
|
||||
</object>
|
||||
<object class="NSMutableArray" key="dict.values">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<boolean value="YES"/>
|
||||
<string>Untitled</string>
|
||||
</object>
|
||||
</object>
|
||||
<int key="NSNibBindingConnectorVersion">2</int>
|
||||
</object>
|
||||
</object>
|
||||
<int key="connectionID">573</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBBindingConnection" key="connection">
|
||||
<string key="label">selectedIndex: selection.latencyControl</string>
|
||||
<reference key="source" ref="451647466"/>
|
||||
<reference key="destination" ref="59237012"/>
|
||||
<object class="NSNibBindingConnector" key="connector">
|
||||
<reference key="NSSource" ref="451647466"/>
|
||||
<reference key="NSDestination" ref="59237012"/>
|
||||
<string key="NSLabel">selectedIndex: selection.latencyControl</string>
|
||||
<string key="NSBinding">selectedIndex</string>
|
||||
<string key="NSKeyPath">selection.latencyControl</string>
|
||||
<object class="NSDictionary" key="NSOptions">
|
||||
<string key="NS.key.0">NSNoSelectionPlaceholder</string>
|
||||
<real value="1" key="NS.object.0"/>
|
||||
</object>
|
||||
<int key="NSNibBindingConnectorVersion">2</int>
|
||||
</object>
|
||||
</object>
|
||||
<int key="connectionID">581</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBOutletConnection" key="connection">
|
||||
<string key="label">latencyControlField</string>
|
||||
<reference key="source" ref="307402018"/>
|
||||
<reference key="destination" ref="451647466"/>
|
||||
</object>
|
||||
<int key="connectionID">582</int>
|
||||
</object>
|
||||
</object>
|
||||
<object class="IBMutableOrderedSet" key="objectRecords">
|
||||
<object class="NSArray" key="orderedObjects">
|
||||
@ -1554,15 +1708,18 @@
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference ref="889877302"/>
|
||||
<reference ref="753545988"/>
|
||||
<reference ref="26015719"/>
|
||||
<reference ref="244571541"/>
|
||||
<reference ref="776974664"/>
|
||||
<reference ref="962924480"/>
|
||||
<reference ref="705215911"/>
|
||||
<reference ref="610703353"/>
|
||||
<reference ref="160987209"/>
|
||||
<reference ref="801412726"/>
|
||||
<reference ref="840157770"/>
|
||||
<reference ref="538356055"/>
|
||||
<reference ref="776974664"/>
|
||||
<reference ref="610703353"/>
|
||||
<reference ref="26015719"/>
|
||||
<reference ref="244571541"/>
|
||||
<reference ref="962924480"/>
|
||||
<reference ref="705215911"/>
|
||||
<reference ref="451647466"/>
|
||||
<reference ref="459566505"/>
|
||||
</object>
|
||||
<reference key="parent" ref="762265164"/>
|
||||
</object>
|
||||
@ -1962,6 +2119,78 @@
|
||||
<reference key="object" ref="132399775"/>
|
||||
<reference key="parent" ref="625121428"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">561</int>
|
||||
<reference key="object" ref="160987209"/>
|
||||
<object class="NSMutableArray" key="children">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference ref="565787077"/>
|
||||
</object>
|
||||
<reference key="parent" ref="60314308"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">562</int>
|
||||
<reference key="object" ref="565787077"/>
|
||||
<reference key="parent" ref="160987209"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">574</int>
|
||||
<reference key="object" ref="451647466"/>
|
||||
<object class="NSMutableArray" key="children">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference ref="970826243"/>
|
||||
</object>
|
||||
<reference key="parent" ref="60314308"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">575</int>
|
||||
<reference key="object" ref="970826243"/>
|
||||
<object class="NSMutableArray" key="children">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference ref="790880658"/>
|
||||
</object>
|
||||
<reference key="parent" ref="451647466"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">576</int>
|
||||
<reference key="object" ref="790880658"/>
|
||||
<object class="NSMutableArray" key="children">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference ref="195135854"/>
|
||||
<reference ref="778262848"/>
|
||||
<reference ref="903120255"/>
|
||||
</object>
|
||||
<reference key="parent" ref="970826243"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">577</int>
|
||||
<reference key="object" ref="195135854"/>
|
||||
<reference key="parent" ref="790880658"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">578</int>
|
||||
<reference key="object" ref="778262848"/>
|
||||
<reference key="parent" ref="790880658"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">579</int>
|
||||
<reference key="object" ref="903120255"/>
|
||||
<reference key="parent" ref="790880658"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">583</int>
|
||||
<reference key="object" ref="459566505"/>
|
||||
<object class="NSMutableArray" key="children">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference ref="534245320"/>
|
||||
</object>
|
||||
<reference key="parent" ref="60314308"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">584</int>
|
||||
<reference key="object" ref="534245320"/>
|
||||
<reference key="parent" ref="459566505"/>
|
||||
</object>
|
||||
</object>
|
||||
</object>
|
||||
<object class="NSMutableDictionary" key="flattenedProperties">
|
||||
@ -2055,6 +2284,18 @@
|
||||
<string>552.IBAttributePlaceholdersKey</string>
|
||||
<string>552.IBPluginDependency</string>
|
||||
<string>553.IBPluginDependency</string>
|
||||
<string>561.IBAttributePlaceholdersKey</string>
|
||||
<string>561.IBPluginDependency</string>
|
||||
<string>562.IBPluginDependency</string>
|
||||
<string>574.IBAttributePlaceholdersKey</string>
|
||||
<string>574.IBPluginDependency</string>
|
||||
<string>575.IBPluginDependency</string>
|
||||
<string>576.IBPluginDependency</string>
|
||||
<string>577.IBPluginDependency</string>
|
||||
<string>578.IBPluginDependency</string>
|
||||
<string>579.IBPluginDependency</string>
|
||||
<string>583.IBPluginDependency</string>
|
||||
<string>584.IBPluginDependency</string>
|
||||
</object>
|
||||
<object class="NSMutableArray" key="dict.values">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
@ -2062,13 +2303,13 @@
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<boolean value="YES"/>
|
||||
<boolean value="YES"/>
|
||||
<string>{{317, 387}, {611, 369}}</string>
|
||||
<string>{{324, 171}, {611, 469}}</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>{{317, 387}, {611, 369}}</string>
|
||||
<string>{{324, 171}, {611, 469}}</string>
|
||||
<boolean value="YES"/>
|
||||
<boolean value="NO"/>
|
||||
<boolean value="YES"/>
|
||||
<string>{213, 107}</string>
|
||||
<string>{611, 469}</string>
|
||||
<object class="NSMutableDictionary">
|
||||
<string key="NS.key.0">ToolTip</string>
|
||||
<object class="IBToolTipAttribute" key="NS.object.0">
|
||||
@ -2232,6 +2473,32 @@
|
||||
</object>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<object class="NSMutableDictionary">
|
||||
<string key="NS.key.0">ToolTip</string>
|
||||
<object class="IBToolTipAttribute" key="NS.object.0">
|
||||
<string key="name">ToolTip</string>
|
||||
<reference key="object" ref="160987209"/>
|
||||
<string key="toolTip">Search for server names on the remote end and add them to your computer's /etc/hosts file.</string>
|
||||
</object>
|
||||
</object>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<object class="NSMutableDictionary">
|
||||
<string key="NS.key.0">ToolTip</string>
|
||||
<object class="IBToolTipAttribute" key="NS.object.0">
|
||||
<string key="name">ToolTip</string>
|
||||
<reference key="object" ref="451647466"/>
|
||||
<string key="toolTip">Choose which network traffic should be routed over the VPN.</string>
|
||||
</object>
|
||||
</object>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
</object>
|
||||
</object>
|
||||
<object class="NSMutableDictionary" key="unlocalizedProperties">
|
||||
@ -2250,7 +2517,7 @@
|
||||
</object>
|
||||
</object>
|
||||
<nil key="sourceID"/>
|
||||
<int key="maxID">557</int>
|
||||
<int key="maxID">584</int>
|
||||
</object>
|
||||
<object class="IBClassDescriber" key="IBDocument.Classes">
|
||||
<object class="NSMutableArray" key="referencedPartialClassDescriptions">
|
||||
@ -2280,6 +2547,7 @@
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>autoReconnectField</string>
|
||||
<string>debugField</string>
|
||||
<string>latencyControlField</string>
|
||||
<string>logField</string>
|
||||
<string>prefsWindow</string>
|
||||
<string>routingField</string>
|
||||
@ -2295,6 +2563,7 @@
|
||||
<string>id</string>
|
||||
<string>id</string>
|
||||
<string>id</string>
|
||||
<string>id</string>
|
||||
</object>
|
||||
</object>
|
||||
<object class="IBClassDescriptionSource" key="sourceIdentifier">
|
@ -1,4 +1,6 @@
|
||||
import sys, os, re, subprocess
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
|
||||
def askpass(prompt):
|
||||
prompt = prompt.replace('"', "'")
|
@ -6,7 +6,7 @@
|
||||
* 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 <Python/Python.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
20
src/ui-macos/bits/runpython.do
Normal file
20
src/ui-macos/bits/runpython.do
Normal file
@ -0,0 +1,20 @@
|
||||
exec >&2
|
||||
redo-ifchange runpython.c
|
||||
ARCHES=""
|
||||
printf "Platforms: "
|
||||
if [ -d /usr/libexec/gcc/darwin ]; then
|
||||
for d in /usr/libexec/gcc/darwin/*; do
|
||||
PLAT=$(basename "$d")
|
||||
[ "$PLAT" != "ppc64" ] || continue # fails for some reason on my Mac
|
||||
ARCHES="$ARCHES -arch $PLAT"
|
||||
printf "$PLAT "
|
||||
done
|
||||
fi
|
||||
printf "\n"
|
||||
PYTHON_LDFLAGS=$(python-config --ldflags)
|
||||
PYTHON_INCLUDES=$(python-config --includes)
|
||||
gcc $ARCHES \
|
||||
-Wall -o $3 runpython.c \
|
||||
$PYTHON_INCLUDES \
|
||||
$PYTHON_LDFLAGS \
|
||||
-framework Python
|
Before Width: | Height: | Size: 821 B After Width: | Height: | Size: 821 B |
Before Width: | Height: | Size: 789 B After Width: | Height: | Size: 789 B |
Before Width: | Height: | Size: 810 B After Width: | Height: | Size: 810 B |
@ -1,4 +1,4 @@
|
||||
exec >&2
|
||||
find -name '*~' | xargs rm -f
|
||||
find . -name '*~' | xargs rm -f
|
||||
rm -rf *.app *.zip *.tar.gz
|
||||
rm -f bits/runpython *.nib sources.list
|
@ -3,9 +3,9 @@ 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"
|
||||
rm -rf "$2.app"
|
||||
mkdir "$2.app" "$2.app/Contents"
|
||||
cd "$2.app/Contents"
|
||||
|
||||
cp "$TOP/Info.plist" .
|
||||
|
||||
@ -18,11 +18,11 @@ cd "$TOP"
|
||||
while read name newname; do
|
||||
[ -z "$name" ] && continue
|
||||
: "${newname:=$name}"
|
||||
outname=$1.app/Contents/Resources/$newname
|
||||
outname=$2.app/Contents/Resources/$newname
|
||||
outdir=$(dirname "$outname")
|
||||
[ -d "$outdir" ] || mkdir "$outdir"
|
||||
cp "${name-$newname}" "$outname"
|
||||
done <sources.list
|
||||
|
||||
cd "$1.app"
|
||||
cd "$2.app"
|
||||
redo-ifchange $(find . -type f)
|
5
src/ui-macos/default.app.tar.gz.do
Normal file
5
src/ui-macos/default.app.tar.gz.do
Normal file
@ -0,0 +1,5 @@
|
||||
exec >&2
|
||||
IFS="
|
||||
"
|
||||
redo-ifchange $2.app
|
||||
tar -czf $3 $2.app/
|
5
src/ui-macos/default.app.zip.do
Normal file
5
src/ui-macos/default.app.zip.do
Normal file
@ -0,0 +1,5 @@
|
||||
exec >&2
|
||||
IFS="
|
||||
"
|
||||
redo-ifchange $2.app
|
||||
zip -q -r $3 $2.app/
|
2
src/ui-macos/default.nib.do
Normal file
2
src/ui-macos/default.nib.do
Normal file
@ -0,0 +1,2 @@
|
||||
redo-ifchange $2.xib
|
||||
ibtool --compile $3 $2.xib
|
@ -1,8 +1,14 @@
|
||||
import sys, os, pty
|
||||
import sys
|
||||
import os
|
||||
import pty
|
||||
from AppKit import *
|
||||
import my, models, askpass
|
||||
import my
|
||||
import models
|
||||
import 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,26 +17,34 @@ 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
|
||||
|
||||
|
||||
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
|
||||
@ -60,7 +74,8 @@ class Runner:
|
||||
.initWithFileDescriptor_closeOnDealloc_(fd, True)
|
||||
self.cb = Callback(self.gotdata)
|
||||
NSNotificationCenter.defaultCenter()\
|
||||
.addObserver_selector_name_object_(self.cb.obj, self.cb.sel,
|
||||
.addObserver_selector_name_object_(
|
||||
self.cb.obj, self.cb.sel,
|
||||
NSFileHandleDataAvailableNotification, self.file)
|
||||
self.file.waitForDataInBackgroundAndNotify()
|
||||
|
||||
@ -68,7 +83,7 @@ class Runner:
|
||||
self.wait()
|
||||
|
||||
def _try_wait(self, options):
|
||||
if self.rv == None and self.pid > 0:
|
||||
if self.rv is None and self.pid > 0:
|
||||
pid, code = os.waitpid(self.pid, options)
|
||||
if pid == self.pid:
|
||||
if os.WIFEXITED(code):
|
||||
@ -82,7 +97,10 @@ class Runner:
|
||||
return self.rv
|
||||
|
||||
def wait(self):
|
||||
return self._try_wait(0)
|
||||
rv = None
|
||||
while rv is None:
|
||||
self.gotdata(None)
|
||||
rv = self._try_wait(os.WNOHANG)
|
||||
|
||||
def poll(self):
|
||||
return self._try_wait(os.WNOHANG)
|
||||
@ -90,7 +108,7 @@ class Runner:
|
||||
def kill(self):
|
||||
assert(self.pid > 0)
|
||||
print 'killing: pid=%r rv=%r' % (self.pid, self.rv)
|
||||
if self.rv == None:
|
||||
if self.rv is None:
|
||||
self.logfunc('Disconnecting from %s.\n' % self.serverobj.host())
|
||||
os.kill(self.pid, 15)
|
||||
self.wait()
|
||||
@ -117,6 +135,7 @@ class Runner:
|
||||
|
||||
|
||||
class SshuttleApp(NSObject):
|
||||
|
||||
def initialize(self):
|
||||
d = my.PList('UserDefaults')
|
||||
my.Defaults().registerDefaults_(d)
|
||||
@ -131,6 +150,7 @@ class SshuttleController(NSObject):
|
||||
prefsWindow = objc.IBOutlet()
|
||||
serversController = objc.IBOutlet()
|
||||
logField = objc.IBOutlet()
|
||||
latencyControlField = objc.IBOutlet()
|
||||
|
||||
servers = []
|
||||
conns = {}
|
||||
@ -139,12 +159,14 @@ class SshuttleController(NSObject):
|
||||
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()\
|
||||
.appendAttributedString_(NSAttributedString.alloc()
|
||||
.initWithString_(msg))
|
||||
self.logField.didChangeText()
|
||||
|
||||
def promptfunc(prompt):
|
||||
print 'prompt! %r' % prompt
|
||||
return askpass.askpass(prompt)
|
||||
@ -156,11 +178,14 @@ class SshuttleController(NSObject):
|
||||
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()),
|
||||
debug=self.debugField.state(),
|
||||
no_latency_control=noLatencyControl),
|
||||
logfunc=logfunc, promptfunc=promptfunc,
|
||||
serverobj=server)
|
||||
self.conns[host] = conn
|
||||
@ -204,6 +229,7 @@ class SshuttleController(NSObject):
|
||||
it.setRepresentedObject_(obj)
|
||||
it.setTarget_(self)
|
||||
it.setAction_(func)
|
||||
|
||||
def addnote(name):
|
||||
additem(name, None, None)
|
||||
|
||||
@ -213,6 +239,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 +249,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 +263,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')
|
||||
|
||||
@ -267,7 +288,8 @@ class SshuttleController(NSObject):
|
||||
sl = []
|
||||
for s in l:
|
||||
host = s.get('host', None)
|
||||
if not host: continue
|
||||
if not host:
|
||||
continue
|
||||
|
||||
nets = s.get('nets', [])
|
||||
nl = []
|
||||
@ -279,13 +301,17 @@ 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)
|
||||
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)
|
||||
@ -294,16 +320,20 @@ class SshuttleController(NSObject):
|
||||
l = []
|
||||
for s in self.servers:
|
||||
host = s.host()
|
||||
if not host: continue
|
||||
if not host:
|
||||
continue
|
||||
nets = []
|
||||
for n in s.nets():
|
||||
subnet = n.subnet()
|
||||
if not subnet: continue
|
||||
if not subnet:
|
||||
continue
|
||||
nets.append((subnet, n.width()))
|
||||
d = dict(host=s.host(),
|
||||
nets=nets,
|
||||
autoNets=s.autoNets(),
|
||||
autoHosts=s.autoHosts())
|
||||
autoHosts=s.autoHosts(),
|
||||
useDns=s.useDns(),
|
||||
latencyControl=s.latencyControl())
|
||||
l.append(d)
|
||||
my.Defaults().setObject_forKey_(l, 'servers')
|
||||
self.fill_menu()
|
||||
@ -315,6 +345,11 @@ class SshuttleController(NSObject):
|
||||
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).
|
@ -3,6 +3,7 @@ import my
|
||||
|
||||
|
||||
configchange_callback = setconnect_callback = None
|
||||
objc_validator = objc.signature('@@:N^@o^@')
|
||||
|
||||
|
||||
def config_changed():
|
||||
@ -34,22 +35,27 @@ def _validate_width(v):
|
||||
|
||||
|
||||
class SshuttleNet(NSObject):
|
||||
|
||||
def subnet(self):
|
||||
return getattr(self, '_k_subnet', None)
|
||||
|
||||
def setSubnet_(self, v):
|
||||
self._k_subnet = v
|
||||
config_changed()
|
||||
@objc.accessor
|
||||
|
||||
@objc_validator
|
||||
def validateSubnet_error_(self, value, error):
|
||||
# print 'validateSubnet!'
|
||||
return True, _validate_ip(value), error
|
||||
|
||||
def width(self):
|
||||
return getattr(self, '_k_width', 24)
|
||||
|
||||
def setWidth_(self, v):
|
||||
self._k_width = v
|
||||
config_changed()
|
||||
@objc.accessor
|
||||
|
||||
@objc_validator
|
||||
def validateWidth_error_(self, value, error):
|
||||
# print 'validateWidth!'
|
||||
return True, _validate_width(value), error
|
||||
@ -58,7 +64,12 @@ 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()
|
||||
@ -66,22 +77,27 @@ class SshuttleServer(NSObject):
|
||||
|
||||
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)
|
||||
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
|
||||
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()
|
||||
@ -93,12 +109,32 @@ class SshuttleServer(NSObject):
|
||||
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
|
||||
|
||||
@objc_validator
|
||||
def validateHost_error_(self, value, error):
|
||||
# print 'validatehost! %r %r %r' % (self, value, error)
|
||||
while value.startswith('-'):
|
||||
@ -107,25 +143,47 @@ class SshuttleServer(NSObject):
|
||||
|
||||
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()
|
@ -1,4 +1,4 @@
|
||||
import sys, os
|
||||
import os
|
||||
from AppKit import *
|
||||
import PyObjCTools.AppHelper
|
||||
|
||||
@ -44,11 +44,13 @@ def Defaults():
|
||||
#
|
||||
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)
|
4
src/ui-macos/run.do
Normal file
4
src/ui-macos/run.do
Normal file
@ -0,0 +1,4 @@
|
||||
redo-ifchange debug.app
|
||||
exec >&2
|
||||
./debug.app/Contents/MacOS/Sshuttle
|
||||
|
@ -1,5 +0,0 @@
|
||||
exec >&2
|
||||
redo-ifchange runpython.c
|
||||
gcc -Wall -o $3 runpython.c \
|
||||
-I/usr/include/python2.5 \
|
||||
-lpython2.5
|
@ -1,5 +0,0 @@
|
||||
exec >&2
|
||||
IFS="
|
||||
"
|
||||
redo-ifchange $1.app
|
||||
tar -czf $3 $1.app/
|
@ -1,5 +0,0 @@
|
||||
exec >&2
|
||||
IFS="
|
||||
"
|
||||
redo-ifchange $1.app
|
||||
zip -q -r $3 $1.app/
|
@ -1,2 +0,0 @@
|
||||
redo-ifchange $1.xib
|
||||
ibtool --compile $3 $1.xib
|
@ -1,3 +0,0 @@
|
||||
redo-ifchange debug.app
|
||||
exec >&2
|
||||
./debug.app/Contents/MacOS/run
|
Reference in New Issue
Block a user