mirror of
https://github.com/sshuttle/sshuttle.git
synced 2025-07-04 16:50:34 +02:00
Compare commits
115 Commits
sshuttle-0
...
sshuttle-0
Author | SHA1 | Date | |
---|---|---|---|
29d2e06bf5 | |||
bff1610050 | |||
cce6a9d96d | |||
5743f29ed6 | |||
42bc6d62db | |||
274ee854d4 | |||
12f6a52ec6 | |||
e737f4b944 | |||
d9f761a8a3 | |||
bd20841782 | |||
4c1a505e37 | |||
41d1f73dc2 | |||
cbc32ff8d8 | |||
6698992f4f | |||
e2c682084c | |||
89e914e9d1 | |||
2268e76771 | |||
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 | |||
f2297066e7 | |||
0bf0351d9b | |||
9731680d2e | |||
88937e148e | |||
7f3c522c56 | |||
ebfc3703ec | |||
760740e9aa | |||
b570778894 | |||
4c5185dc55 | |||
a2fcb08a2d | |||
e7a19890aa | |||
d9b1bb52e5 | |||
a30c4d7ccb | |||
9877a8d6fb | |||
8fde1155da | |||
fdb7c9b995 | |||
675f19f57e | |||
049a0c40ac | |||
668441adb6 | |||
d4ccd30c24 | |||
522bc23854 | |||
191d97a897 | |||
da7a490cd7 | |||
342ce12840 | |||
701f59a5b8 | |||
9f7b2bb4f6 | |||
d301184184 | |||
659a57beb2 | |||
c70b9937df | |||
415be935d4 | |||
d4c9d31068 | |||
38bb7f3c21 | |||
b7f1530aef | |||
973d5a95a1 | |||
95ab6e7119 | |||
e6d7c44e27 | |||
5bf6e40682 | |||
8a5ae1a40a | |||
651b607361 | |||
dc9a5e63c7 | |||
33bc55be27 | |||
c3204d2728 | |||
b1edb226a5 | |||
7fa1c3c4e4 | |||
cca69eb496 | |||
91f65132be | |||
2ef3a301fb | |||
41fd0348eb | |||
1907048dad | |||
82e1d1c166 | |||
a497132c01 | |||
7354600849 | |||
918725c485 | |||
95c9b788a0 | |||
ef71751846 | |||
32b4defa9b | |||
8b7605cc5d | |||
bcf1892305 | |||
fe742c928d | |||
10ce1ee5d4 | |||
a32305a275 |
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,2 +1,6 @@
|
|||||||
*.pyc
|
*.pyc
|
||||||
*~
|
*~
|
||||||
|
*.8
|
||||||
|
/.do_built
|
||||||
|
/.do_built.dir
|
||||||
|
/.redo
|
||||||
|
3
Documentation/.gitignore
vendored
Normal file
3
Documentation/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
*.8
|
||||||
|
/md-to-man
|
||||||
|
/*.md.tmp
|
5
Documentation/all.do
Normal file
5
Documentation/all.do
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/bin/ls *.md |
|
||||||
|
sed 's/\.md/.8/' |
|
||||||
|
xargs redo-ifchange
|
||||||
|
|
||||||
|
redo-always
|
1
Documentation/clean.do
Normal file
1
Documentation/clean.do
Normal file
@ -0,0 +1 @@
|
|||||||
|
rm -f *~ .*~ *.8 t/*.8 md-to-man *.tmp t/*.tmp
|
2
Documentation/default.8.do
Normal file
2
Documentation/default.8.do
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
redo-ifchange md-to-man $2.md.tmp
|
||||||
|
. ./md-to-man $1 $2 $3
|
3
Documentation/default.md.tmp.do
Normal file
3
Documentation/default.md.tmp.do
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
redo-ifchange ../version/vars $2.md
|
||||||
|
. ../version/vars
|
||||||
|
sed -e "s/%VERSION%/$TAG/" -e "s/%DATE%/$DATE/" $2.md
|
8
Documentation/md-to-man.do
Normal file
8
Documentation/md-to-man.do
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
redo-ifchange md2man.py
|
||||||
|
if ./md2man.py </dev/null >/dev/null; then
|
||||||
|
echo './md2man.py $2.md.tmp'
|
||||||
|
else
|
||||||
|
echo "Warning: md2man.py missing modules; can't generate manpages." >&2
|
||||||
|
echo "Warning: try this: sudo easy_install markdown BeautifulSoup" >&2
|
||||||
|
echo 'echo Skipping: $2.1 >&2'
|
||||||
|
fi
|
278
Documentation/md2man.py
Executable file
278
Documentation/md2man.py
Executable file
@ -0,0 +1,278 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
import sys, os, markdown, re
|
||||||
|
from BeautifulSoup import BeautifulSoup
|
||||||
|
|
||||||
|
def _split_lines(s):
|
||||||
|
return re.findall(r'([^\n]*\n?)', s)
|
||||||
|
|
||||||
|
|
||||||
|
class Writer:
|
||||||
|
def __init__(self):
|
||||||
|
self.started = False
|
||||||
|
self.indent = 0
|
||||||
|
self.last_wrote = '\n'
|
||||||
|
|
||||||
|
def _write(self, s):
|
||||||
|
if s:
|
||||||
|
self.last_wrote = s
|
||||||
|
sys.stdout.write(s)
|
||||||
|
|
||||||
|
def writeln(self, s):
|
||||||
|
if s:
|
||||||
|
self.linebreak()
|
||||||
|
self._write('%s\n' % s)
|
||||||
|
|
||||||
|
def write(self, s):
|
||||||
|
if s:
|
||||||
|
self.para()
|
||||||
|
for line in _split_lines(s):
|
||||||
|
if line.startswith('.'):
|
||||||
|
self._write('\\&' + line)
|
||||||
|
else:
|
||||||
|
self._write(line)
|
||||||
|
|
||||||
|
def linebreak(self):
|
||||||
|
if not self.last_wrote.endswith('\n'):
|
||||||
|
self._write('\n')
|
||||||
|
|
||||||
|
def para(self, bullet=None):
|
||||||
|
if not self.started:
|
||||||
|
if not bullet:
|
||||||
|
bullet = ' '
|
||||||
|
if not self.indent:
|
||||||
|
self.writeln(_macro('.PP'))
|
||||||
|
else:
|
||||||
|
assert(self.indent >= 2)
|
||||||
|
prefix = ' '*(self.indent-2) + bullet + ' '
|
||||||
|
self.writeln('.IP "%s" %d' % (prefix, self.indent))
|
||||||
|
self.started = True
|
||||||
|
|
||||||
|
def end_para(self):
|
||||||
|
self.linebreak()
|
||||||
|
self.started = False
|
||||||
|
|
||||||
|
def start_bullet(self):
|
||||||
|
self.indent += 3
|
||||||
|
self.para(bullet='\\[bu]')
|
||||||
|
|
||||||
|
def end_bullet(self):
|
||||||
|
self.indent -= 3
|
||||||
|
self.end_para()
|
||||||
|
|
||||||
|
w = Writer()
|
||||||
|
|
||||||
|
|
||||||
|
def _macro(name, *args):
|
||||||
|
if not name.startswith('.'):
|
||||||
|
raise ValueError('macro names must start with "."')
|
||||||
|
fixargs = []
|
||||||
|
for i in args:
|
||||||
|
i = str(i)
|
||||||
|
i = i.replace('\\', '')
|
||||||
|
i = i.replace('"', "'")
|
||||||
|
if (' ' in i) or not i:
|
||||||
|
i = '"%s"' % i
|
||||||
|
fixargs.append(i)
|
||||||
|
return ' '.join([name] + list(fixargs))
|
||||||
|
|
||||||
|
|
||||||
|
def macro(name, *args):
|
||||||
|
w.writeln(_macro(name, *args))
|
||||||
|
|
||||||
|
|
||||||
|
def _force_string(owner, tag):
|
||||||
|
if tag.string:
|
||||||
|
return tag.string
|
||||||
|
else:
|
||||||
|
out = ''
|
||||||
|
for i in tag:
|
||||||
|
if not (i.string or i.name in ['a', 'br']):
|
||||||
|
raise ValueError('"%s" tags must contain only strings: '
|
||||||
|
'got %r: %r' % (owner.name, tag.name, tag))
|
||||||
|
out += _force_string(owner, i)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _clean(s):
|
||||||
|
s = s.replace('\\', '\\\\')
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def _bitlist(tag):
|
||||||
|
if getattr(tag, 'contents', None) == None:
|
||||||
|
for i in _split_lines(str(tag)):
|
||||||
|
yield None,_clean(i)
|
||||||
|
else:
|
||||||
|
for e in tag:
|
||||||
|
name = getattr(e, 'name', None)
|
||||||
|
if name in ['a', 'br']:
|
||||||
|
name = None # just treat as simple text
|
||||||
|
s = _force_string(tag, e)
|
||||||
|
if name:
|
||||||
|
yield name,_clean(s)
|
||||||
|
else:
|
||||||
|
for i in _split_lines(s):
|
||||||
|
yield None,_clean(i)
|
||||||
|
|
||||||
|
|
||||||
|
def _bitlist_simple(tag):
|
||||||
|
for typ,text in _bitlist(tag):
|
||||||
|
if typ and not typ in ['em', 'strong', 'code']:
|
||||||
|
raise ValueError('unexpected tag %r inside %r' % (typ, tag.name))
|
||||||
|
yield text
|
||||||
|
|
||||||
|
|
||||||
|
def _text(bitlist):
|
||||||
|
out = ''
|
||||||
|
for typ,text in bitlist:
|
||||||
|
if not typ:
|
||||||
|
out += text
|
||||||
|
elif typ == 'em':
|
||||||
|
out += '\\fI%s\\fR' % text
|
||||||
|
elif typ in ['strong', 'code']:
|
||||||
|
out += '\\fB%s\\fR' % text
|
||||||
|
else:
|
||||||
|
raise ValueError('unexpected tag %r inside %r' % (typ, tag.name))
|
||||||
|
out = out.strip()
|
||||||
|
out = re.sub(re.compile(r'^\s+', re.M), '', out)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def text(tag):
|
||||||
|
w.write(_text(_bitlist(tag)))
|
||||||
|
|
||||||
|
|
||||||
|
# This is needed because .BI (and .BR, .RB, etc) are weird little state
|
||||||
|
# machines that alternate between two fonts. So if someone says something
|
||||||
|
# like foo<b>chicken</b><b>wicken</b>dicken we have to convert that to
|
||||||
|
# .BI foo chickenwicken dicken
|
||||||
|
def _boldline(l):
|
||||||
|
out = ['']
|
||||||
|
last_bold = False
|
||||||
|
for typ,text in l:
|
||||||
|
nonzero = not not typ
|
||||||
|
if nonzero != last_bold:
|
||||||
|
last_bold = not last_bold
|
||||||
|
out.append('')
|
||||||
|
out[-1] += re.sub(r'\s+', ' ', text)
|
||||||
|
macro('.BI', *out)
|
||||||
|
|
||||||
|
|
||||||
|
def do_definition(tag):
|
||||||
|
w.end_para()
|
||||||
|
macro('.TP')
|
||||||
|
w.started = True
|
||||||
|
split = 0
|
||||||
|
pre = []
|
||||||
|
post = []
|
||||||
|
for typ,text in _bitlist(tag):
|
||||||
|
if split:
|
||||||
|
post.append((typ,text))
|
||||||
|
elif text.lstrip().startswith(': '):
|
||||||
|
split = 1
|
||||||
|
post.append((typ,text.lstrip()[2:].lstrip()))
|
||||||
|
else:
|
||||||
|
pre.append((typ,text))
|
||||||
|
_boldline(pre)
|
||||||
|
w.write(_text(post))
|
||||||
|
|
||||||
|
|
||||||
|
def do_list(tag):
|
||||||
|
for i in tag:
|
||||||
|
name = getattr(i, 'name', '').lower()
|
||||||
|
if not name and not str(i).strip():
|
||||||
|
pass
|
||||||
|
elif name != 'li':
|
||||||
|
raise ValueError('only <li> is allowed inside <ul>: got %r' % i)
|
||||||
|
else:
|
||||||
|
w.start_bullet()
|
||||||
|
for xi in i:
|
||||||
|
do(xi)
|
||||||
|
w.end_para()
|
||||||
|
w.end_bullet()
|
||||||
|
|
||||||
|
|
||||||
|
def do(tag):
|
||||||
|
name = getattr(tag, 'name', '').lower()
|
||||||
|
if not name:
|
||||||
|
text(tag)
|
||||||
|
elif name == 'h1':
|
||||||
|
macro('.SH', _force_string(tag, tag).upper())
|
||||||
|
w.started = True
|
||||||
|
elif name == 'h2':
|
||||||
|
macro('.SS', _force_string(tag, tag))
|
||||||
|
w.started = True
|
||||||
|
elif name.startswith('h') and len(name)==2:
|
||||||
|
raise ValueError('%r invalid - man page headers must be h1 or h2'
|
||||||
|
% name)
|
||||||
|
elif name == 'pre':
|
||||||
|
t = _force_string(tag.code, tag.code)
|
||||||
|
if t.strip():
|
||||||
|
macro('.RS', '+4n')
|
||||||
|
macro('.nf')
|
||||||
|
w.write(_clean(t).rstrip())
|
||||||
|
macro('.fi')
|
||||||
|
macro('.RE')
|
||||||
|
w.end_para()
|
||||||
|
elif name == 'p' or name == 'br':
|
||||||
|
g = re.match(re.compile(r'([^\n]*)\n +: +(.*)', re.S), str(tag))
|
||||||
|
if g:
|
||||||
|
# it's a definition list (which some versions of python-markdown
|
||||||
|
# don't support, including the one in Debian-lenny, so we can't
|
||||||
|
# enable that markdown extension). Fake it up.
|
||||||
|
do_definition(tag)
|
||||||
|
else:
|
||||||
|
text(tag)
|
||||||
|
w.end_para()
|
||||||
|
elif name == 'ul':
|
||||||
|
do_list(tag)
|
||||||
|
else:
|
||||||
|
raise ValueError('non-man-compatible html tag %r' % name)
|
||||||
|
|
||||||
|
|
||||||
|
PROD='Untitled'
|
||||||
|
VENDOR='Vendor Name'
|
||||||
|
SECTION='9'
|
||||||
|
GROUPNAME='User Commands'
|
||||||
|
DATE=''
|
||||||
|
AUTHOR=''
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
for n in sys.argv[1:]:
|
||||||
|
lines += open(n).read().decode('utf8').split('\n')
|
||||||
|
else:
|
||||||
|
lines += sys.stdin.read().decode('utf8').split('\n')
|
||||||
|
|
||||||
|
# parse pandoc-style document headers (not part of markdown)
|
||||||
|
g = re.match(r'^%\s+(.*?)\((.*?)\)\s+(.*)$', lines[0])
|
||||||
|
if g:
|
||||||
|
PROD = g.group(1)
|
||||||
|
SECTION = g.group(2)
|
||||||
|
VENDOR = g.group(3)
|
||||||
|
lines.pop(0)
|
||||||
|
g = re.match(r'^%\s+(.*?)$', lines[0])
|
||||||
|
if g:
|
||||||
|
AUTHOR = g.group(1)
|
||||||
|
lines.pop(0)
|
||||||
|
g = re.match(r'^%\s+(.*?)$', lines[0])
|
||||||
|
if g:
|
||||||
|
DATE = g.group(1)
|
||||||
|
lines.pop(0)
|
||||||
|
g = re.match(r'^%\s+(.*?)$', lines[0])
|
||||||
|
if g:
|
||||||
|
GROUPNAME = g.group(1)
|
||||||
|
lines.pop(0)
|
||||||
|
|
||||||
|
inp = '\n'.join(lines)
|
||||||
|
if AUTHOR:
|
||||||
|
inp += ('\n# AUTHOR\n\n%s\n' % AUTHOR).replace('<', '\\<')
|
||||||
|
|
||||||
|
html = markdown.markdown(inp)
|
||||||
|
soup = BeautifulSoup(html, convertEntities=BeautifulSoup.HTML_ENTITIES)
|
||||||
|
|
||||||
|
macro('.TH', PROD.upper(), SECTION, DATE, VENDOR, GROUPNAME)
|
||||||
|
macro('.ad', 'l') # left justified
|
||||||
|
macro('.nh') # disable hyphenation
|
||||||
|
for e in soup:
|
||||||
|
do(e)
|
278
Documentation/sshuttle.md
Normal file
278
Documentation/sshuttle.md
Normal file
@ -0,0 +1,278 @@
|
|||||||
|
% sshuttle(8) Sshuttle %VERSION%
|
||||||
|
% Avery Pennarun <apenwarr@gmail.com>
|
||||||
|
% %DATE%
|
||||||
|
|
||||||
|
# NAME
|
||||||
|
|
||||||
|
sshuttle - a transparent proxy-based VPN using ssh
|
||||||
|
|
||||||
|
# SYNOPSIS
|
||||||
|
|
||||||
|
sshuttle [options...] [-r [username@]sshserver[:port]] \<subnets...\>
|
||||||
|
|
||||||
|
|
||||||
|
# DESCRIPTION
|
||||||
|
|
||||||
|
sshuttle allows you to create a VPN connection from your
|
||||||
|
machine to any remote server that you can connect to via
|
||||||
|
ssh, as long as that server has python 2.3 or higher.
|
||||||
|
|
||||||
|
To work, you must have root access on the local machine,
|
||||||
|
but you can have a normal account on the server.
|
||||||
|
|
||||||
|
It's valid to run sshuttle more than once simultaneously on
|
||||||
|
a single client machine, connecting to a different server
|
||||||
|
every time, so you can be on more than one VPN at once.
|
||||||
|
|
||||||
|
If run on a router, sshuttle can forward traffic for your
|
||||||
|
entire subnet to the VPN.
|
||||||
|
|
||||||
|
|
||||||
|
# OPTIONS
|
||||||
|
|
||||||
|
\<subnets...\>
|
||||||
|
: a list of subnets to route over the VPN, in the form
|
||||||
|
`a.b.c.d[/width]`. Valid examples are 1.2.3.4 (a
|
||||||
|
single IP address), 1.2.3.4/32 (equivalent to 1.2.3.4),
|
||||||
|
1.2.3.0/24 (a 24-bit subnet, ie. with a 255.255.255.0
|
||||||
|
netmask), and 0/0 ('just route everything through the
|
||||||
|
VPN').
|
||||||
|
|
||||||
|
-l, --listen=*[ip:]port*
|
||||||
|
: use this ip address and port number as the transparent
|
||||||
|
proxy port. By default sshuttle finds an available
|
||||||
|
port automatically and listens on IP 127.0.0.1
|
||||||
|
(localhost), so you don't need to override it, and
|
||||||
|
connections are only proxied from the local machine,
|
||||||
|
not from outside machines. If you want to accept
|
||||||
|
connections from other machines on your network (ie. to
|
||||||
|
run sshuttle on a router) try enabling IP Forwarding in
|
||||||
|
your kernel, then using `--listen 0.0.0.0:0`.
|
||||||
|
|
||||||
|
-H, --auto-hosts
|
||||||
|
: scan for remote hostnames and update the local /etc/hosts
|
||||||
|
file with matching entries for as long as the VPN is
|
||||||
|
open. This is nicer than changing your system's DNS
|
||||||
|
(/etc/resolv.conf) settings, for several reasons. First,
|
||||||
|
hostnames are added without domain names attached, so
|
||||||
|
you can `ssh thatserver` without worrying if your local
|
||||||
|
domain matches the remote one. Second, if you sshuttle
|
||||||
|
into more than one VPN at a time, it's impossible to
|
||||||
|
use more than one DNS server at once anyway, but
|
||||||
|
sshuttle correctly merges /etc/hosts entries between
|
||||||
|
all running copies. Third, if you're only routing a
|
||||||
|
few subnets over the VPN, you probably would prefer to
|
||||||
|
keep using your local DNS server for everything else.
|
||||||
|
|
||||||
|
-N, --auto-nets
|
||||||
|
: in addition to the subnets provided on the command
|
||||||
|
line, ask the server which subnets it thinks we should
|
||||||
|
route, and route those automatically. The suggestions
|
||||||
|
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
|
||||||
|
default python interpreter on the remote system's PATH.
|
||||||
|
|
||||||
|
-r, --remote=*[username@]sshserver[:port]*
|
||||||
|
: the remote hostname and optional username and ssh
|
||||||
|
port number to use for connecting to the remote server.
|
||||||
|
For example, example.com, testuser@example.com,
|
||||||
|
testuser@example.com:2222, or example.com:2244.
|
||||||
|
|
||||||
|
-x, --exclude=*subnet*
|
||||||
|
: explicitly exclude this subnet from forwarding. The
|
||||||
|
format of this option is the same as the `<subnets>`
|
||||||
|
option. To exclude more than one subnet, specify the
|
||||||
|
`-x` option more than once. You can say something like
|
||||||
|
`0/0 -x 1.2.3.0/24` to forward everything except the
|
||||||
|
local subnet over the VPN, for example.
|
||||||
|
|
||||||
|
--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
|
||||||
|
default, sshuttle prints only error messages.
|
||||||
|
|
||||||
|
-e, --ssh-cmd
|
||||||
|
: the command to use to connect to the remote server. The
|
||||||
|
default is just `ssh`. Use this if your ssh client is
|
||||||
|
in a non-standard location or you want to provide extra
|
||||||
|
options to the ssh command, for example, `-e 'ssh -v'`.
|
||||||
|
|
||||||
|
--seed-hosts
|
||||||
|
: a comma-separated list of hostnames to use to
|
||||||
|
initialize the `--auto-hosts` scan algorithm.
|
||||||
|
`--auto-hosts` does things like poll local SMB servers
|
||||||
|
for lists of local hostnames, but can speed things up
|
||||||
|
if you use this option to give it a few names to start
|
||||||
|
from.
|
||||||
|
|
||||||
|
--no-latency-control
|
||||||
|
: sacrifice latency to improve bandwidth benchmarks. ssh
|
||||||
|
uses really big socket buffers, which can overload the
|
||||||
|
connection if you start doing large file transfers,
|
||||||
|
thus making all your other sessions inside the same
|
||||||
|
tunnel go slowly. Normally, sshuttle tries to avoid
|
||||||
|
this problem using a "fullness check" that allows only
|
||||||
|
a certain amount of outstanding data to be buffered at
|
||||||
|
a time. But on high-bandwidth links, this can leave a
|
||||||
|
lot of your bandwidth underutilized. It also makes
|
||||||
|
sshuttle seem slow in bandwidth benchmarks (benchmarks
|
||||||
|
rarely test ping latency, which is what sshuttle is
|
||||||
|
trying to control). This option disables the latency
|
||||||
|
control feature, maximizing bandwidth usage. Use at
|
||||||
|
your own risk.
|
||||||
|
|
||||||
|
-D, --daemon
|
||||||
|
: automatically fork into the background after connecting
|
||||||
|
to the remote server. Implies `--syslog`.
|
||||||
|
|
||||||
|
--syslog
|
||||||
|
: after connecting, send all log messages to the
|
||||||
|
`syslog`(3) service instead of stderr. This is
|
||||||
|
implicit if you use `--daemon`.
|
||||||
|
|
||||||
|
--pidfile=*pidfilename*
|
||||||
|
: when using `--daemon`, save sshuttle's pid to
|
||||||
|
*pidfilename*. The default is `sshuttle.pid` in the
|
||||||
|
current directory.
|
||||||
|
|
||||||
|
--server
|
||||||
|
: (internal use only) run the sshuttle server on
|
||||||
|
stdin/stdout. This is what the client runs on
|
||||||
|
the remote end.
|
||||||
|
|
||||||
|
--firewall
|
||||||
|
: (internal use only) run the firewall manager. This is
|
||||||
|
the only part of sshuttle that must run as root. If
|
||||||
|
you start sshuttle as a non-root user, it will
|
||||||
|
automatically run `sudo` or `su` to start the firewall
|
||||||
|
manager, but the core of sshuttle still runs as a
|
||||||
|
normal user.
|
||||||
|
|
||||||
|
--hostwatch
|
||||||
|
: (internal use only) run the hostwatch daemon. This
|
||||||
|
process runs on the server side and collects hostnames for
|
||||||
|
the `--auto-hosts` option. Using this option by itself
|
||||||
|
makes it a lot easier to debug and test the `--auto-hosts`
|
||||||
|
feature.
|
||||||
|
|
||||||
|
|
||||||
|
# EXAMPLES
|
||||||
|
|
||||||
|
Test locally by proxying all local connections, without using ssh:
|
||||||
|
|
||||||
|
$ sshuttle -v 0/0
|
||||||
|
|
||||||
|
Starting sshuttle proxy.
|
||||||
|
Listening on ('0.0.0.0', 12300).
|
||||||
|
[local sudo] Password:
|
||||||
|
firewall manager ready.
|
||||||
|
c : connecting to server...
|
||||||
|
s: available routes:
|
||||||
|
s: 192.168.42.0/24
|
||||||
|
c : connected.
|
||||||
|
firewall manager: starting transproxy.
|
||||||
|
c : Accept: 192.168.42.106:50035 -> 192.168.42.121:139.
|
||||||
|
c : Accept: 192.168.42.121:47523 -> 77.141.99.22:443.
|
||||||
|
...etc...
|
||||||
|
^C
|
||||||
|
firewall manager: undoing changes.
|
||||||
|
KeyboardInterrupt
|
||||||
|
c : Keyboard interrupt: exiting.
|
||||||
|
c : SW#8:192.168.42.121:47523: deleting
|
||||||
|
c : SW#6:192.168.42.106:50035: deleting
|
||||||
|
|
||||||
|
Test connection to a remote server, with automatic hostname
|
||||||
|
and subnet guessing:
|
||||||
|
|
||||||
|
$ sshuttle -vNHr example.org
|
||||||
|
|
||||||
|
Starting sshuttle proxy.
|
||||||
|
Listening on ('0.0.0.0', 12300).
|
||||||
|
firewall manager ready.
|
||||||
|
c : connecting to server...
|
||||||
|
s: available routes:
|
||||||
|
s: 77.141.99.0/24
|
||||||
|
c : connected.
|
||||||
|
c : seed_hosts: []
|
||||||
|
firewall manager: starting transproxy.
|
||||||
|
hostwatch: Found: testbox1: 1.2.3.4
|
||||||
|
hostwatch: Found: mytest2: 5.6.7.8
|
||||||
|
hostwatch: Found: domaincontroller: 99.1.2.3
|
||||||
|
c : Accept: 192.168.42.121:60554 -> 77.141.99.22:22.
|
||||||
|
^C
|
||||||
|
firewall manager: undoing changes.
|
||||||
|
c : Keyboard interrupt: exiting.
|
||||||
|
c : SW#6:192.168.42.121:60554: deleting
|
||||||
|
|
||||||
|
|
||||||
|
# DISCUSSION
|
||||||
|
|
||||||
|
When it starts, sshuttle creates an ssh session to the
|
||||||
|
server specified by the `-r` option. If `-r` is omitted,
|
||||||
|
it will start both its client and server locally, which is
|
||||||
|
sometimes useful for testing.
|
||||||
|
|
||||||
|
After connecting to the remote server, sshuttle uploads its
|
||||||
|
(python) source code to the remote end and executes it
|
||||||
|
there. Thus, you don't need to install sshuttle on the
|
||||||
|
remote server, and there are never sshuttle version
|
||||||
|
conflicts between client and server.
|
||||||
|
|
||||||
|
Unlike most VPNs, sshuttle forwards sessions, not packets.
|
||||||
|
That is, it uses kernel transparent proxying (`iptables
|
||||||
|
REDIRECT` rules on Linux, or `ipfw fwd` rules on BSD) to
|
||||||
|
capture outgoing TCP sessions, then creates entirely
|
||||||
|
separate TCP sessions out to the original destination at
|
||||||
|
the other end of the tunnel.
|
||||||
|
|
||||||
|
Packet-level forwarding (eg. using the tun/tap devices on
|
||||||
|
Linux) seems elegant at first, but it results in
|
||||||
|
several problems, notably the 'tcp over tcp' problem. The
|
||||||
|
tcp protocol depends fundamentally on packets being dropped
|
||||||
|
in order to implement its congestion control agorithm; if
|
||||||
|
you pass tcp packets through a tcp-based tunnel (such as
|
||||||
|
ssh), the inner tcp packets will never be dropped, and so
|
||||||
|
the inner tcp stream's congestion control will be
|
||||||
|
completely broken, and performance will be terrible. Thus,
|
||||||
|
packet-based VPNs (such as IPsec and openvpn) cannot use
|
||||||
|
tcp-based encrypted streams like ssh or ssl, and have to
|
||||||
|
implement their own encryption from scratch, which is very
|
||||||
|
complex and error prone.
|
||||||
|
|
||||||
|
sshuttle's simplicity comes from the fact that it can
|
||||||
|
safely use the existing ssh encrypted tunnel without
|
||||||
|
incurring a performance penalty. It does this by letting
|
||||||
|
the client-side kernel manage the incoming tcp stream, and
|
||||||
|
the server-side kernel manage the outgoing tcp stream;
|
||||||
|
there is no need for congestion control to be shared
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
|
# SEE ALSO
|
||||||
|
|
||||||
|
`ssh`(1), `python`(1)
|
||||||
|
|
33
README.md
33
README.md
@ -1,3 +1,14 @@
|
|||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
sshuttle: where transparent proxy meets VPN meets ssh
|
sshuttle: where transparent proxy meets VPN meets ssh
|
||||||
=====================================================
|
=====================================================
|
||||||
|
|
||||||
@ -52,10 +63,28 @@ This is how you use it:
|
|||||||
on your client machine. You'll need root or sudo
|
on your client machine. You'll need root or sudo
|
||||||
access, and python needs to be installed.
|
access, and python needs to be installed.
|
||||||
|
|
||||||
- <tt>./sshuttle -r username@sshserver 0.0.0.0/0 -vv</tt>
|
- The most basic use of sshuttle looks like:
|
||||||
|
<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
|
||||||
|
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.)
|
||||||
|
|
||||||
That's it! Now your local machine can access the remote network as if you
|
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
|
were right there. And if your "client" machine is a router, everyone on
|
||||||
your local network can make connections to your remote network.
|
your local network can make connections to your remote network.
|
||||||
|
|
||||||
You don't need to install sshuttle on the remote server;
|
You don't need to install sshuttle on the remote server;
|
||||||
|
11
all.do
Normal file
11
all.do
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
exec >&2
|
||||||
|
UI=
|
||||||
|
[ "$(uname)" = "Darwin" ] && UI=ui-macos/all
|
||||||
|
redo-ifchange Documentation/all version/all $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 Documentation/sshuttle.md"
|
2
clean.do
Normal file
2
clean.do
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
redo ui-macos/clean Documentation/clean version/clean
|
||||||
|
rm -f *~ */*~ .*~ */.*~ *.8 *.tmp */*.tmp *.pyc */*.pyc
|
306
client.py
306
client.py
@ -1,9 +1,83 @@
|
|||||||
import struct, socket, select, errno, re
|
import struct, socket, select, errno, re, signal, time
|
||||||
import compat.ssubprocess as ssubprocess
|
import compat.ssubprocess as ssubprocess
|
||||||
import helpers, ssnet, ssh
|
import helpers, ssnet, ssh, ssyslog
|
||||||
from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
|
from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
|
||||||
from helpers import *
|
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):
|
def original_dst(sock):
|
||||||
try:
|
try:
|
||||||
@ -22,16 +96,19 @@ def original_dst(sock):
|
|||||||
|
|
||||||
|
|
||||||
class FirewallClient:
|
class FirewallClient:
|
||||||
def __init__(self, port, subnets_include, subnets_exclude):
|
def __init__(self, port, subnets_include, subnets_exclude, dnsport):
|
||||||
self.port = port
|
self.port = port
|
||||||
self.auto_nets = []
|
self.auto_nets = []
|
||||||
self.subnets_include = subnets_include
|
self.subnets_include = subnets_include
|
||||||
self.subnets_exclude = subnets_exclude
|
self.subnets_exclude = subnets_exclude
|
||||||
argvbase = ([sys.argv[0]] +
|
self.dnsport = dnsport
|
||||||
|
argvbase = ([sys.argv[1], sys.argv[0], sys.argv[1]] +
|
||||||
['-v'] * (helpers.verbose or 0) +
|
['-v'] * (helpers.verbose or 0) +
|
||||||
['--firewall', str(port)])
|
['--firewall', str(port), str(dnsport)])
|
||||||
|
if ssyslog._p:
|
||||||
|
argvbase += ['--syslog']
|
||||||
argv_tries = [
|
argv_tries = [
|
||||||
['sudo'] + argvbase,
|
['sudo', '-p', '[local sudo] Password: '] + argvbase,
|
||||||
['su', '-c', ' '.join(argvbase)],
|
['su', '-c', ' '.join(argvbase)],
|
||||||
argvbase
|
argvbase
|
||||||
]
|
]
|
||||||
@ -45,8 +122,12 @@ class FirewallClient:
|
|||||||
# run in the child process
|
# run in the child process
|
||||||
s2.close()
|
s2.close()
|
||||||
e = None
|
e = None
|
||||||
|
if os.getuid() == 0:
|
||||||
|
argv_tries = argv_tries[-1:] # last entry only
|
||||||
for argv in argv_tries:
|
for argv in argv_tries:
|
||||||
try:
|
try:
|
||||||
|
if argv[0] == 'su':
|
||||||
|
sys.stderr.write('[local su] ')
|
||||||
self.p = ssubprocess.Popen(argv, stdout=s1, preexec_fn=setup)
|
self.p = ssubprocess.Popen(argv, stdout=s1, preexec_fn=setup)
|
||||||
e = None
|
e = None
|
||||||
break
|
break
|
||||||
@ -90,39 +171,125 @@ class FirewallClient:
|
|||||||
def done(self):
|
def done(self):
|
||||||
self.pfile.close()
|
self.pfile.close()
|
||||||
rv = self.p.wait()
|
rv = self.p.wait()
|
||||||
if rv:
|
if rv == EXITCODE_NEEDS_REBOOT:
|
||||||
|
raise FatalNeedsReboot()
|
||||||
|
elif rv:
|
||||||
raise Fatal('cleanup: %r returned %d' % (self.argv, rv))
|
raise Fatal('cleanup: %r returned %d' % (self.argv, rv))
|
||||||
|
|
||||||
|
|
||||||
def _main(listener, fw, use_server, remotename, python, seed_hosts, auto_nets):
|
def onaccept(listener, mux, handlers):
|
||||||
handlers = []
|
global _extra_fd
|
||||||
if use_server:
|
try:
|
||||||
if helpers.verbose >= 1:
|
sock,srcip = listener.accept()
|
||||||
helpers.logprefix = 'c : '
|
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:
|
else:
|
||||||
helpers.logprefix = 'client: '
|
raise
|
||||||
debug1('connecting to server...\n')
|
dstip = original_dst(sock)
|
||||||
try:
|
debug1('Accept: %s:%r -> %s:%r.\n' % (srcip[0],srcip[1],
|
||||||
(serverproc, serversock) = ssh.connect(remotename, python)
|
dstip[0],dstip[1]))
|
||||||
except socket.error, e:
|
if dstip[1] == listener.getsockname()[1] and islocal(dstip[0]):
|
||||||
if e.errno == errno.EPIPE:
|
debug1("-- ignored: that's my address!\n")
|
||||||
raise Fatal("failed to establish ssh session")
|
sock.close()
|
||||||
else:
|
return
|
||||||
raise
|
chan = mux.next_channel()
|
||||||
mux = Mux(serversock, serversock)
|
if not chan:
|
||||||
handlers.append(mux)
|
log('warning: too many open channels. Discarded connection.\n')
|
||||||
|
sock.close()
|
||||||
|
return
|
||||||
|
mux.send(chan, ssnet.CMD_CONNECT, '%s,%s' % dstip)
|
||||||
|
outwrap = MuxWrapper(mux, chan)
|
||||||
|
handlers.append(Proxy(SockWrapper(sock, sock), outwrap))
|
||||||
|
|
||||||
expected = 'SSHUTTLE0001'
|
|
||||||
|
dnsreqs = {}
|
||||||
|
def dns_done(chan, data):
|
||||||
|
peer,sock,timeout = dnsreqs.get(chan) or (None,None,None)
|
||||||
|
debug3('dns_done: channel=%r peer=%r\n' % (chan, peer))
|
||||||
|
if peer:
|
||||||
|
del dnsreqs[chan]
|
||||||
|
debug3('doing sendto %r\n' % (peer,))
|
||||||
|
sock.sendto(data, peer)
|
||||||
|
|
||||||
|
|
||||||
|
def ondns(listener, mux, handlers):
|
||||||
|
pkt,peer = listener.recvfrom(4096)
|
||||||
|
now = time.time()
|
||||||
|
if pkt:
|
||||||
|
debug1('DNS request from %r: %d bytes\n' % (peer, len(pkt)))
|
||||||
|
chan = mux.next_channel()
|
||||||
|
dnsreqs[chan] = peer,listener,now+30
|
||||||
|
mux.send(chan, ssnet.CMD_DNS_REQ, pkt)
|
||||||
|
mux.channels[chan] = lambda cmd,data: dns_done(chan,data)
|
||||||
|
for chan,(peer,sock,timeout) in dnsreqs.items():
|
||||||
|
if timeout < now:
|
||||||
|
del dnsreqs[chan]
|
||||||
|
debug3('Remaining DNS requests: %d\n' % len(dnsreqs))
|
||||||
|
|
||||||
|
|
||||||
|
def _main(listener, fw, ssh_cmd, remotename, python, latency_control,
|
||||||
|
dnslistener, seed_hosts, auto_nets,
|
||||||
|
syslog, daemon):
|
||||||
|
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:
|
||||||
|
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))
|
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()
|
rv = serverproc.poll()
|
||||||
if rv:
|
if rv:
|
||||||
raise Fatal('server died with error code %d' % rv)
|
raise Fatal('server died with error code %d' % rv)
|
||||||
|
|
||||||
if initstring != expected:
|
if initstring != expected:
|
||||||
raise Fatal('expected server init string %r; got %r'
|
raise Fatal('expected server init string %r; got %r'
|
||||||
% (expected, initstring))
|
% (expected, initstring))
|
||||||
debug1('connected.\n')
|
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):
|
def onroutes(routestr):
|
||||||
if auto_nets:
|
if auto_nets:
|
||||||
@ -149,45 +316,39 @@ def _main(listener, fw, use_server, remotename, python, seed_hosts, auto_nets):
|
|||||||
fw.sethostip(name, ip)
|
fw.sethostip(name, ip)
|
||||||
mux.got_host_list = onhostlist
|
mux.got_host_list = onhostlist
|
||||||
|
|
||||||
def onaccept():
|
handlers.append(Handler([listener], lambda: onaccept(listener, mux, handlers)))
|
||||||
sock,srcip = listener.accept()
|
|
||||||
dstip = original_dst(sock)
|
if dnslistener:
|
||||||
debug1('Accept: %r:%r -> %r:%r.\n' % (srcip[0],srcip[1],
|
handlers.append(Handler([dnslistener], lambda: ondns(dnslistener, mux, handlers)))
|
||||||
dstip[0],dstip[1]))
|
|
||||||
if dstip == listener.getsockname():
|
|
||||||
debug1("-- ignored: that's my address!\n")
|
|
||||||
sock.close()
|
|
||||||
return
|
|
||||||
if use_server:
|
|
||||||
chan = mux.next_channel()
|
|
||||||
mux.send(chan, ssnet.CMD_CONNECT, '%s,%s' % dstip)
|
|
||||||
outwrap = MuxWrapper(mux, chan)
|
|
||||||
else:
|
|
||||||
outwrap = ssnet.connect_dst(dstip[0], dstip[1])
|
|
||||||
handlers.append(Proxy(SockWrapper(sock, sock), outwrap))
|
|
||||||
handlers.append(Handler([listener], onaccept))
|
|
||||||
|
|
||||||
if seed_hosts != None:
|
if seed_hosts != None:
|
||||||
debug1('seed_hosts: %r\n' % seed_hosts)
|
debug1('seed_hosts: %r\n' % seed_hosts)
|
||||||
mux.send(0, ssnet.CMD_HOST_REQ, '\n'.join(seed_hosts))
|
mux.send(0, ssnet.CMD_HOST_REQ, '\n'.join(seed_hosts))
|
||||||
|
|
||||||
while 1:
|
while 1:
|
||||||
if use_server:
|
rv = serverproc.poll()
|
||||||
rv = serverproc.poll()
|
if rv:
|
||||||
if rv:
|
raise Fatal('server died with error code %d' % rv)
|
||||||
raise Fatal('server died with error code %d' % rv)
|
|
||||||
|
|
||||||
ssnet.runonce(handlers, mux)
|
ssnet.runonce(handlers, mux)
|
||||||
if use_server:
|
if latency_control:
|
||||||
mux.callback()
|
|
||||||
mux.check_fullness()
|
mux.check_fullness()
|
||||||
|
mux.callback()
|
||||||
|
|
||||||
|
|
||||||
def main(listenip, use_server, remotename, python, seed_hosts, auto_nets,
|
def main(listenip, ssh_cmd, remotename, python, latency_control, dns,
|
||||||
subnets_include, subnets_exclude):
|
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')
|
debug1('Starting sshuttle proxy.\n')
|
||||||
listener = socket.socket()
|
|
||||||
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
||||||
if listenip[1]:
|
if listenip[1]:
|
||||||
ports = [listenip[1]]
|
ports = [listenip[1]]
|
||||||
else:
|
else:
|
||||||
@ -197,8 +358,13 @@ def main(listenip, use_server, remotename, python, seed_hosts, auto_nets,
|
|||||||
debug2('Binding:')
|
debug2('Binding:')
|
||||||
for port in ports:
|
for port in ports:
|
||||||
debug2(' %d' % port)
|
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:
|
try:
|
||||||
listener.bind((listenip[0], port))
|
listener.bind((listenip[0], port))
|
||||||
|
dnslistener.bind((listenip[0], port))
|
||||||
bound = True
|
bound = True
|
||||||
break
|
break
|
||||||
except socket.error, e:
|
except socket.error, e:
|
||||||
@ -211,10 +377,26 @@ def main(listenip, use_server, remotename, python, seed_hosts, auto_nets,
|
|||||||
listenip = listener.getsockname()
|
listenip = listener.getsockname()
|
||||||
debug1('Listening on %r.\n' % (listenip,))
|
debug1('Listening on %r.\n' % (listenip,))
|
||||||
|
|
||||||
fw = FirewallClient(listenip[1], subnets_include, subnets_exclude)
|
if dns:
|
||||||
|
dnsip = dnslistener.getsockname()
|
||||||
|
debug1('DNS listening on %r.\n' % (dnsip,))
|
||||||
|
dnsport = dnsip[1]
|
||||||
|
else:
|
||||||
|
dnsport = 0
|
||||||
|
dnslistener = None
|
||||||
|
|
||||||
|
fw = FirewallClient(listenip[1], subnets_include, subnets_exclude, dnsport)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return _main(listener, fw, use_server, remotename,
|
return _main(listener, fw, ssh_cmd, remotename,
|
||||||
python, seed_hosts, auto_nets)
|
python, latency_control, dnslistener,
|
||||||
|
seed_hosts, auto_nets, syslog, daemon)
|
||||||
finally:
|
finally:
|
||||||
fw.done()
|
try:
|
||||||
|
if daemon:
|
||||||
|
# it's not our child anymore; can't waitpid
|
||||||
|
fw.p.returncode = 0
|
||||||
|
fw.done()
|
||||||
|
finally:
|
||||||
|
if daemon:
|
||||||
|
daemon_cleanup()
|
||||||
|
7
default.8.do
Normal file
7
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
do
Executable file
175
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
|
337
firewall.py
337
firewall.py
@ -1,8 +1,32 @@
|
|||||||
import re, errno
|
import re, errno, socket, select, signal, struct
|
||||||
import compat.ssubprocess as ssubprocess
|
import compat.ssubprocess as ssubprocess
|
||||||
import helpers
|
import helpers, ssyslog
|
||||||
from helpers import *
|
from helpers import *
|
||||||
|
|
||||||
|
# python doesn't have a definition for this
|
||||||
|
IPPROTO_DIVERT = 254
|
||||||
|
|
||||||
|
# return values from sysctl_set
|
||||||
|
SUCCESS = 0
|
||||||
|
SAME = 1
|
||||||
|
FAILED = -1
|
||||||
|
NONEXIST = -2
|
||||||
|
|
||||||
|
|
||||||
|
def nonfatal(func, *args):
|
||||||
|
try:
|
||||||
|
func(*args)
|
||||||
|
except Fatal, e:
|
||||||
|
log('error: %s\n' % e)
|
||||||
|
|
||||||
|
|
||||||
|
def _call(argv):
|
||||||
|
debug1('>> %s\n' % ' '.join(argv))
|
||||||
|
rv = ssubprocess.call(argv)
|
||||||
|
if rv:
|
||||||
|
raise Fatal('%r returned %d' % (argv, rv))
|
||||||
|
return rv
|
||||||
|
|
||||||
|
|
||||||
def ipt_chain_exists(name):
|
def ipt_chain_exists(name):
|
||||||
argv = ['iptables', '-t', 'nat', '-nL']
|
argv = ['iptables', '-t', 'nat', '-nL']
|
||||||
@ -17,10 +41,28 @@ def ipt_chain_exists(name):
|
|||||||
|
|
||||||
def ipt(*args):
|
def ipt(*args):
|
||||||
argv = ['iptables', '-t', 'nat'] + list(args)
|
argv = ['iptables', '-t', 'nat'] + list(args)
|
||||||
debug1('>> %s\n' % ' '.join(argv))
|
_call(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
|
# We name the chain based on the transproxy port number so that it's possible
|
||||||
@ -28,22 +70,23 @@ def ipt(*args):
|
|||||||
# multiple copies shouldn't have overlapping subnets, or only the most-
|
# multiple copies shouldn't have overlapping subnets, or only the most-
|
||||||
# recently-started one will win (because we use "-I OUTPUT 1" instead of
|
# recently-started one will win (because we use "-I OUTPUT 1" instead of
|
||||||
# "-A OUTPUT").
|
# "-A OUTPUT").
|
||||||
def do_iptables(port, subnets):
|
def do_iptables(port, dnsport, subnets):
|
||||||
chain = 'sshuttle-%s' % port
|
chain = 'sshuttle-%s' % port
|
||||||
|
|
||||||
# basic cleanup/setup of chains
|
# basic cleanup/setup of chains
|
||||||
if ipt_chain_exists(chain):
|
if ipt_chain_exists(chain):
|
||||||
ipt('-D', 'OUTPUT', '-j', chain)
|
nonfatal(ipt, '-D', 'OUTPUT', '-j', chain)
|
||||||
ipt('-D', 'PREROUTING', '-j', chain)
|
nonfatal(ipt, '-D', 'PREROUTING', '-j', chain)
|
||||||
ipt('-F', chain)
|
nonfatal(ipt, '-F', chain)
|
||||||
ipt('-X', chain)
|
ipt('-X', chain)
|
||||||
|
|
||||||
if subnets:
|
if subnets or dnsport:
|
||||||
ipt('-N', chain)
|
ipt('-N', chain)
|
||||||
ipt('-F', chain)
|
ipt('-F', chain)
|
||||||
ipt('-I', 'OUTPUT', '1', '-j', chain)
|
ipt('-I', 'OUTPUT', '1', '-j', chain)
|
||||||
ipt('-I', 'PREROUTING', '1', '-j', chain)
|
ipt('-I', 'PREROUTING', '1', '-j', chain)
|
||||||
|
|
||||||
|
if subnets:
|
||||||
# create new subnet entries. Note that we're sorting in a very
|
# create new subnet entries. Note that we're sorting in a very
|
||||||
# particular order: we need to go from most-specific (largest swidth)
|
# particular order: we need to go from most-specific (largest swidth)
|
||||||
# to least-specific, and at any given level of specificity, we want
|
# to least-specific, and at any given level of specificity, we want
|
||||||
@ -55,12 +98,19 @@ def do_iptables(port, subnets):
|
|||||||
'--dest', '%s/%s' % (snet,swidth),
|
'--dest', '%s/%s' % (snet,swidth),
|
||||||
'-p', 'tcp')
|
'-p', 'tcp')
|
||||||
else:
|
else:
|
||||||
ipt('-A', chain, '-j', 'REDIRECT',
|
ipt_ttl('-A', chain, '-j', 'REDIRECT',
|
||||||
'--dest', '%s/%s' % (snet,swidth),
|
'--dest', '%s/%s' % (snet,swidth),
|
||||||
'-p', 'tcp',
|
'-p', 'tcp',
|
||||||
'--to-ports', str(port),
|
'--to-ports', str(port))
|
||||||
'-m', 'ttl', '!', '--ttl', '42' # to prevent infinite loops
|
|
||||||
)
|
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):
|
def ipfw_rule_exists(n):
|
||||||
@ -69,7 +119,7 @@ def ipfw_rule_exists(n):
|
|||||||
found = False
|
found = False
|
||||||
for line in p.stdout:
|
for line in p.stdout:
|
||||||
if line.startswith('%05d ' % n):
|
if line.startswith('%05d ' % n):
|
||||||
if not ('ipttl 42 setup keep-state' in line
|
if not ('ipttl 42' in line
|
||||||
or ('skipto %d' % (n+1)) in line
|
or ('skipto %d' % (n+1)) in line
|
||||||
or 'check-state' in line):
|
or 'check-state' in line):
|
||||||
log('non-sshuttle ipfw rule: %r\n' % line.strip())
|
log('non-sshuttle ipfw rule: %r\n' % line.strip())
|
||||||
@ -81,72 +131,254 @@ def ipfw_rule_exists(n):
|
|||||||
return found
|
return found
|
||||||
|
|
||||||
|
|
||||||
def sysctl_get(name):
|
_oldctls = {}
|
||||||
argv = ['sysctl', '-n', name]
|
def _fill_oldctls(prefix):
|
||||||
|
argv = ['sysctl', prefix]
|
||||||
p = ssubprocess.Popen(argv, stdout = ssubprocess.PIPE)
|
p = ssubprocess.Popen(argv, stdout = ssubprocess.PIPE)
|
||||||
line = p.stdout.readline()
|
for line in p.stdout:
|
||||||
|
assert(line[-1] == '\n')
|
||||||
|
(k,v) = line[:-1].split(': ', 1)
|
||||||
|
_oldctls[k] = v
|
||||||
rv = p.wait()
|
rv = p.wait()
|
||||||
if rv:
|
if rv:
|
||||||
raise Fatal('%r returned %d' % (argv, rv))
|
raise Fatal('%r returned %d' % (argv, rv))
|
||||||
if not line:
|
if not line:
|
||||||
raise Fatal('%r returned no data' % (argv,))
|
raise Fatal('%r returned no data' % (argv,))
|
||||||
assert(line[-1] == '\n')
|
|
||||||
return line[:-1]
|
|
||||||
|
KERNEL_FLAGS_PATH = '/Library/Preferences/SystemConfiguration/com.apple.Boot'
|
||||||
|
KERNEL_FLAGS_NAME = 'Kernel Flags'
|
||||||
|
def _defaults_read_kernel_flags():
|
||||||
|
argv = ['defaults', 'read', KERNEL_FLAGS_PATH, KERNEL_FLAGS_NAME]
|
||||||
|
debug1('>> %s\n' % ' '.join(argv))
|
||||||
|
p = ssubprocess.Popen(argv, stdout = ssubprocess.PIPE)
|
||||||
|
flagstr = p.stdout.read().strip()
|
||||||
|
rv = p.wait()
|
||||||
|
if rv:
|
||||||
|
raise Fatal('%r returned %d' % (argv, rv))
|
||||||
|
flags = flagstr and flagstr.split(' ') or []
|
||||||
|
return flags
|
||||||
|
|
||||||
|
|
||||||
|
def _defaults_write_kernel_flags(flags):
|
||||||
|
flagstr = ' '.join(flags)
|
||||||
|
argv = ['defaults', 'write', KERNEL_FLAGS_PATH, KERNEL_FLAGS_NAME,
|
||||||
|
flagstr]
|
||||||
|
_call(argv)
|
||||||
|
argv = ['plutil', '-convert', 'xml1', KERNEL_FLAGS_PATH + '.plist']
|
||||||
|
_call(argv)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def defaults_write_kernel_flag(name, val):
|
||||||
|
flags = _defaults_read_kernel_flags()
|
||||||
|
found = 0
|
||||||
|
for i in range(len(flags)):
|
||||||
|
if flags[i].startswith('%s=' % name):
|
||||||
|
found += 1
|
||||||
|
flags[i] = '%s=%s' % (name, val)
|
||||||
|
if not found:
|
||||||
|
flags.insert(0, '%s=%s' % (name, val))
|
||||||
|
_defaults_write_kernel_flags(flags)
|
||||||
|
|
||||||
|
|
||||||
def _sysctl_set(name, val):
|
def _sysctl_set(name, val):
|
||||||
argv = ['sysctl', '-w', '%s=%s' % (name, val)]
|
argv = ['sysctl', '-w', '%s=%s' % (name, val)]
|
||||||
debug1('>> %s\n' % ' '.join(argv))
|
debug1('>> %s\n' % ' '.join(argv))
|
||||||
rv = ssubprocess.call(argv, stdout = open('/dev/null', 'w'))
|
return ssubprocess.call(argv, stdout = open('/dev/null', 'w'))
|
||||||
|
|
||||||
|
|
||||||
_oldctls = []
|
_changedctls = []
|
||||||
def sysctl_set(name, val):
|
def sysctl_set(name, val, permanent=False):
|
||||||
oldval = sysctl_get(name)
|
PREFIX = 'net.inet.ip'
|
||||||
if str(val) != str(oldval):
|
assert(name.startswith(PREFIX + '.'))
|
||||||
_oldctls.append((name, oldval))
|
val = str(val)
|
||||||
return _sysctl_set(name, val)
|
if not _oldctls:
|
||||||
|
_fill_oldctls(PREFIX)
|
||||||
|
if not (name in _oldctls):
|
||||||
|
debug1('>> No such sysctl: %r\n' % name)
|
||||||
|
return NONEXIST
|
||||||
|
oldval = _oldctls[name]
|
||||||
|
if val == oldval:
|
||||||
|
return SAME
|
||||||
|
|
||||||
|
rv = _sysctl_set(name, val)
|
||||||
|
if rv != 0:
|
||||||
|
return FAILED
|
||||||
|
if permanent:
|
||||||
|
debug1('>> ...saving permanently in /etc/sysctl.conf\n')
|
||||||
|
f = open('/etc/sysctl.conf', 'a')
|
||||||
|
f.write('\n'
|
||||||
|
'# Added by sshuttle\n'
|
||||||
|
'%s=%s\n' % (name, val))
|
||||||
|
f.close()
|
||||||
|
else:
|
||||||
|
_changedctls.append(name)
|
||||||
|
return SUCCESS
|
||||||
|
|
||||||
|
|
||||||
|
def _udp_unpack(p):
|
||||||
|
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):
|
def ipfw(*args):
|
||||||
argv = ['ipfw', '-q'] + list(args)
|
argv = ['ipfw', '-q'] + list(args)
|
||||||
debug1('>> %s\n' % ' '.join(argv))
|
_call(argv)
|
||||||
rv = ssubprocess.call(argv)
|
|
||||||
if rv:
|
|
||||||
raise Fatal('%r returned %d' % (argv, rv))
|
|
||||||
|
|
||||||
|
|
||||||
def do_ipfw(port, subnets):
|
def do_ipfw(port, dnsport, subnets):
|
||||||
sport = str(port)
|
sport = str(port)
|
||||||
xsport = str(port+1)
|
xsport = str(port+1)
|
||||||
|
|
||||||
# cleanup any existing rules
|
# cleanup any existing rules
|
||||||
if ipfw_rule_exists(port):
|
if ipfw_rule_exists(port):
|
||||||
ipfw('del', sport)
|
ipfw('delete', sport)
|
||||||
|
|
||||||
while _oldctls:
|
while _changedctls:
|
||||||
(name,oldval) = _oldctls.pop()
|
name = _changedctls.pop()
|
||||||
|
oldval = _oldctls[name]
|
||||||
_sysctl_set(name, oldval)
|
_sysctl_set(name, oldval)
|
||||||
|
|
||||||
if subnets:
|
if subnets or dnsport:
|
||||||
sysctl_set('net.inet.ip.fw.enable', 1)
|
sysctl_set('net.inet.ip.fw.enable', 1)
|
||||||
sysctl_set('net.inet.ip.scopedroute', 0)
|
|
||||||
|
# This seems to be needed on MacOS 10.6 and 10.7. For more
|
||||||
|
# information, see:
|
||||||
|
# http://groups.google.com/group/sshuttle/browse_thread/thread/bc32562e17987b25/6d3aa2bb30a1edab
|
||||||
|
# and
|
||||||
|
# http://serverfault.com/questions/138622/transparent-proxying-leaves-sockets-with-syn-rcvd-in-macos-x-10-6-snow-leopard
|
||||||
|
changeflag = sysctl_set('net.inet.ip.scopedroute', 0, permanent=True)
|
||||||
|
if changeflag == SUCCESS:
|
||||||
|
log("\n"
|
||||||
|
" WARNING: ONE-TIME NETWORK DISRUPTION:\n"
|
||||||
|
" =====================================\n"
|
||||||
|
"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)
|
||||||
|
elif changeflag == FAILED:
|
||||||
|
# On MacOS 10.7, the scopedroute sysctl became read-only, so
|
||||||
|
# we have to fix it using a kernel boot parameter instead,
|
||||||
|
# which requires rebooting. For more, see:
|
||||||
|
# http://groups.google.com/group/sshuttle/browse_thread/thread/a42505ca33e1de80/e5e8f3e5a92d25f7
|
||||||
|
log('Updating kernel boot flags.\n')
|
||||||
|
defaults_write_kernel_flag('net.inet.ip.scopedroute', 0)
|
||||||
|
log("\n"
|
||||||
|
" YOU MUST REBOOT TO USE SSHUTTLE\n"
|
||||||
|
" ===============================\n"
|
||||||
|
"sshuttle has changed a MacOS kernel boot-time setting\n"
|
||||||
|
"to work around a bug in MacOS 10.7 Lion. You will need\n"
|
||||||
|
"to reboot before it takes effect. You only have to\n"
|
||||||
|
"do this once.\n\n")
|
||||||
|
sys.exit(EXITCODE_NEEDS_REBOOT)
|
||||||
|
|
||||||
ipfw('add', sport, 'check-state', 'ip',
|
ipfw('add', sport, 'check-state', 'ip',
|
||||||
'from', 'any', 'to', 'any')
|
'from', 'any', 'to', 'any')
|
||||||
|
|
||||||
|
if subnets:
|
||||||
# create new subnet entries
|
# create new subnet entries
|
||||||
for swidth,sexclude,snet in sorted(subnets, reverse=True):
|
for swidth,sexclude,snet in sorted(subnets, reverse=True):
|
||||||
if sexclude:
|
if sexclude:
|
||||||
ipfw('add', sport, 'skipto', xsport,
|
ipfw('add', sport, 'skipto', xsport,
|
||||||
'log', 'tcp',
|
'tcp',
|
||||||
'from', 'any', 'to', '%s/%s' % (snet,swidth))
|
'from', 'any', 'to', '%s/%s' % (snet,swidth))
|
||||||
else:
|
else:
|
||||||
ipfw('add', sport, 'fwd', '127.0.0.1,%d' % port,
|
ipfw('add', sport, 'fwd', '127.0.0.1,%d' % port,
|
||||||
'log', 'tcp',
|
'tcp',
|
||||||
'from', 'any', 'to', '%s/%s' % (snet,swidth),
|
'from', 'any', 'to', '%s/%s' % (snet,swidth),
|
||||||
'not', 'ipttl', '42', 'keep-state', 'setup')
|
'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,
|
||||||
|
'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 program_exists(name):
|
def program_exists(name):
|
||||||
paths = (os.getenv('PATH') or os.defpath).split(os.pathsep)
|
paths = (os.getenv('PATH') or os.defpath).split(os.pathsep)
|
||||||
@ -155,6 +387,7 @@ def program_exists(name):
|
|||||||
if os.path.exists(fn):
|
if os.path.exists(fn):
|
||||||
return not os.path.isdir(fn) and os.access(fn, os.X_OK)
|
return not os.path.isdir(fn) and os.access(fn, os.X_OK)
|
||||||
|
|
||||||
|
|
||||||
hostmap = {}
|
hostmap = {}
|
||||||
def rewrite_etc_hosts(port):
|
def rewrite_etc_hosts(port):
|
||||||
HOSTSFILE='/etc/hosts'
|
HOSTSFILE='/etc/hosts'
|
||||||
@ -205,9 +438,11 @@ def restore_etc_hosts(port):
|
|||||||
# exit. In case that fails, it's not the end of the world; future runs will
|
# 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
|
# supercede it in the transproxy list, at least, so the leftover rules
|
||||||
# are hopefully harmless.
|
# are hopefully harmless.
|
||||||
def main(port):
|
def main(port, dnsport, syslog):
|
||||||
assert(port > 0)
|
assert(port > 0)
|
||||||
assert(port <= 65535)
|
assert(port <= 65535)
|
||||||
|
assert(dnsport >= 0)
|
||||||
|
assert(dnsport <= 65535)
|
||||||
|
|
||||||
if os.getuid() != 0:
|
if os.getuid() != 0:
|
||||||
raise Fatal('you must be root (or enable su/sudo) to set the firewall')
|
raise Fatal('you must be root (or enable su/sudo) to set the firewall')
|
||||||
@ -224,10 +459,21 @@ def main(port):
|
|||||||
# can read from it.
|
# can read from it.
|
||||||
os.dup2(1, 0)
|
os.dup2(1, 0)
|
||||||
|
|
||||||
|
if syslog:
|
||||||
|
ssyslog.start_syslog()
|
||||||
|
ssyslog.stderr_to_syslog()
|
||||||
|
|
||||||
debug1('firewall manager ready.\n')
|
debug1('firewall manager ready.\n')
|
||||||
sys.stdout.write('READY\n')
|
sys.stdout.write('READY\n')
|
||||||
sys.stdout.flush()
|
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,
|
# ctrl-c shouldn't be passed along to me. When the main sshuttle dies,
|
||||||
# I'll die automatically.
|
# I'll die automatically.
|
||||||
os.setsid()
|
os.setsid()
|
||||||
@ -257,7 +503,7 @@ def main(port):
|
|||||||
try:
|
try:
|
||||||
if line:
|
if line:
|
||||||
debug1('firewall manager: starting transproxy.\n')
|
debug1('firewall manager: starting transproxy.\n')
|
||||||
do_it(port, subnets)
|
do_wait = do_it(port, dnsport, subnets)
|
||||||
sys.stdout.write('STARTED\n')
|
sys.stdout.write('STARTED\n')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -271,6 +517,7 @@ def main(port):
|
|||||||
# to stay running so that we don't need a *second* password
|
# to stay running so that we don't need a *second* password
|
||||||
# authentication at shutdown time - that cleanup is important!
|
# authentication at shutdown time - that cleanup is important!
|
||||||
while 1:
|
while 1:
|
||||||
|
if do_wait: do_wait()
|
||||||
line = sys.stdin.readline(128)
|
line = sys.stdin.readline(128)
|
||||||
if line.startswith('HOST '):
|
if line.startswith('HOST '):
|
||||||
(name,ip) = line[5:].strip().split(',', 1)
|
(name,ip) = line[5:].strip().split(',', 1)
|
||||||
@ -285,5 +532,5 @@ def main(port):
|
|||||||
debug1('firewall manager: undoing changes.\n')
|
debug1('firewall manager: undoing changes.\n')
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
do_it(port, [])
|
do_it(port, 0, [])
|
||||||
restore_etc_hosts(port)
|
restore_etc_hosts(port)
|
||||||
|
45
helpers.py
45
helpers.py
@ -1,4 +1,4 @@
|
|||||||
import sys, os
|
import sys, os, socket, errno
|
||||||
|
|
||||||
logprefix = ''
|
logprefix = ''
|
||||||
verbose = 0
|
verbose = 0
|
||||||
@ -30,8 +30,51 @@ class Fatal(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
EXITCODE_NEEDS_REBOOT = 111
|
||||||
|
class FatalNeedsReboot(Fatal):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def list_contains_any(l, sub):
|
def list_contains_any(l, sub):
|
||||||
for i in sub:
|
for i in sub:
|
||||||
if i in l:
|
if i in l:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def resolvconf_nameservers():
|
||||||
|
l = []
|
||||||
|
for line in open('/etc/resolv.conf'):
|
||||||
|
words = line.lower().split()
|
||||||
|
if len(words) >= 2 and words[0] == 'nameserver':
|
||||||
|
l.append(words[1])
|
||||||
|
return l
|
||||||
|
|
||||||
|
|
||||||
|
def resolvconf_random_nameserver():
|
||||||
|
l = resolvconf_nameservers()
|
||||||
|
if l:
|
||||||
|
if len(l) > 1:
|
||||||
|
# don't import this unless we really need it
|
||||||
|
import random
|
||||||
|
random.shuffle(l)
|
||||||
|
return l[0]
|
||||||
|
else:
|
||||||
|
return '127.0.0.1'
|
||||||
|
|
||||||
|
|
||||||
|
def islocal(ip):
|
||||||
|
sock = socket.socket()
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
sock.bind((ip, 0))
|
||||||
|
except socket.error, e:
|
||||||
|
if e.args[0] == errno.EADDRNOTAVAIL:
|
||||||
|
return False # not a local IP
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
sock.close()
|
||||||
|
return True # it's a local IP, or there would have been an error
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,7 +13,11 @@ _nmb_ok = True
|
|||||||
_smb_ok = True
|
_smb_ok = True
|
||||||
hostnames = {}
|
hostnames = {}
|
||||||
queue = {}
|
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):
|
def _is_ip(s):
|
||||||
|
53
main.py
53
main.py
@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
import sys, os, re
|
import sys, os, re
|
||||||
import helpers, options, client, server, firewall, hostwatch
|
import helpers, options, client, server, firewall, hostwatch
|
||||||
|
import compat.ssubprocess as ssubprocess
|
||||||
from helpers import *
|
from helpers import *
|
||||||
|
|
||||||
|
|
||||||
@ -46,36 +46,55 @@ def parse_ipport(s):
|
|||||||
|
|
||||||
optspec = """
|
optspec = """
|
||||||
sshuttle [-l [ip:]port] [-r [username@]sshserver[:port]] <subnets...>
|
sshuttle [-l [ip:]port] [-r [username@]sshserver[:port]] <subnets...>
|
||||||
sshuttle --firewall <port> <subnets...>
|
|
||||||
sshuttle --server
|
sshuttle --server
|
||||||
|
sshuttle --firewall <port> <subnets...>
|
||||||
|
sshuttle --hostwatch
|
||||||
--
|
--
|
||||||
l,listen= transproxy to this ip address and port number [0.0.0.0:0]
|
l,listen= transproxy to this ip address and port number [127.0.0.1:0]
|
||||||
H,auto-hosts scan for remote hostnames and update local /etc/hosts
|
H,auto-hosts scan for remote hostnames and update local /etc/hosts
|
||||||
N,auto-nets automatically determine subnets to route
|
N,auto-nets automatically determine subnets to route
|
||||||
python= specify the name/path of the python interpreter on the remote server [python]
|
dns capture local DNS requests and forward to the remote DNS server
|
||||||
|
python= path to python interpreter on the remote server
|
||||||
r,remote= ssh hostname (and optional username) of remote sshuttle 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= exclude this subnet (can be used more than once)
|
||||||
|
exclude-from= exclude the subnets in a file (whitespace separated)
|
||||||
v,verbose increase debug message verbosity
|
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)
|
seed-hosts= with -H, use these hostnames for initial scan (comma-separated)
|
||||||
noserver don't use a separate server process (mostly for debugging)
|
no-latency-control sacrifice latency to improve bandwidth benchmarks
|
||||||
|
wrap= restart counting channel numbers after this number (for testing)
|
||||||
|
D,daemon run in the background as a daemon
|
||||||
|
V,version print sshuttle's version number
|
||||||
|
syslog send log messages to syslog (default if you use --daemon)
|
||||||
|
pidfile= pidfile name (only if using --daemon) [./sshuttle.pid]
|
||||||
server (internal use only)
|
server (internal use only)
|
||||||
firewall (internal use only)
|
firewall (internal use only)
|
||||||
hostwatch (internal use only)
|
hostwatch (internal use only)
|
||||||
"""
|
"""
|
||||||
o = options.Options('sshuttle', optspec)
|
o = options.Options(optspec)
|
||||||
(opt, flags, extra) = o.parse(sys.argv[1:])
|
(opt, flags, extra) = o.parse(sys.argv[2:])
|
||||||
|
|
||||||
|
if opt.version:
|
||||||
|
import version
|
||||||
|
print version.TAG
|
||||||
|
sys.exit(0)
|
||||||
|
if opt.daemon:
|
||||||
|
opt.syslog = 1
|
||||||
|
if opt.wrap:
|
||||||
|
import ssnet
|
||||||
|
ssnet.MAX_CHANNEL = int(opt.wrap)
|
||||||
helpers.verbose = opt.verbose
|
helpers.verbose = opt.verbose
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if opt.server:
|
if opt.server:
|
||||||
if len(extra) != 0:
|
if len(extra) != 0:
|
||||||
o.fatal('no arguments expected')
|
o.fatal('no arguments expected')
|
||||||
|
server.latency_control = opt.latency_control
|
||||||
sys.exit(server.main())
|
sys.exit(server.main())
|
||||||
elif opt.firewall:
|
elif opt.firewall:
|
||||||
if len(extra) != 1:
|
if len(extra) != 2:
|
||||||
o.fatal('exactly one argument expected')
|
o.fatal('exactly two arguments expected')
|
||||||
sys.exit(firewall.main(int(extra[0])))
|
sys.exit(firewall.main(int(extra[0]), int(extra[1]), opt.syslog))
|
||||||
elif opt.hostwatch:
|
elif opt.hostwatch:
|
||||||
sys.exit(hostwatch.hw_main(extra))
|
sys.exit(hostwatch.hw_main(extra))
|
||||||
else:
|
else:
|
||||||
@ -86,6 +105,8 @@ try:
|
|||||||
for k,v in flags:
|
for k,v in flags:
|
||||||
if k in ('-x','--exclude'):
|
if k in ('-x','--exclude'):
|
||||||
excludes.append(v)
|
excludes.append(v)
|
||||||
|
if k in ('-X', '--exclude-from'):
|
||||||
|
excludes += open(v).read().split()
|
||||||
remotename = opt.remote
|
remotename = opt.remote
|
||||||
if remotename == '' or remotename == '-':
|
if remotename == '' or remotename == '-':
|
||||||
remotename = None
|
remotename = None
|
||||||
@ -98,13 +119,19 @@ try:
|
|||||||
else:
|
else:
|
||||||
sh = None
|
sh = None
|
||||||
sys.exit(client.main(parse_ipport(opt.listen or '0.0.0.0:0'),
|
sys.exit(client.main(parse_ipport(opt.listen or '0.0.0.0:0'),
|
||||||
not opt.noserver,
|
opt.ssh_cmd,
|
||||||
remotename,
|
remotename,
|
||||||
(opt.python or "python"),
|
opt.python,
|
||||||
|
opt.latency_control,
|
||||||
|
opt.dns,
|
||||||
sh,
|
sh,
|
||||||
opt.auto_nets,
|
opt.auto_nets,
|
||||||
parse_subnets(includes),
|
parse_subnets(includes),
|
||||||
parse_subnets(excludes)))
|
parse_subnets(excludes),
|
||||||
|
opt.syslog, opt.daemon, opt.pidfile))
|
||||||
|
except FatalNeedsReboot, e:
|
||||||
|
log('You must reboot before using sshuttle.\n')
|
||||||
|
sys.exit(EXITCODE_NEEDS_REBOOT)
|
||||||
except Fatal, e:
|
except Fatal, e:
|
||||||
log('fatal: %s\n' % e)
|
log('fatal: %s\n' % e)
|
||||||
sys.exit(99)
|
sys.exit(99)
|
||||||
|
11
options.py
11
options.py
@ -60,7 +60,7 @@ def _tty_width():
|
|||||||
except (IOError, ImportError):
|
except (IOError, ImportError):
|
||||||
return _atoi(os.environ.get('WIDTH')) or 70
|
return _atoi(os.environ.get('WIDTH')) or 70
|
||||||
(ysize,xsize,ypix,xpix) = struct.unpack('HHHH', s)
|
(ysize,xsize,ypix,xpix) = struct.unpack('HHHH', s)
|
||||||
return xsize
|
return xsize or 70
|
||||||
|
|
||||||
|
|
||||||
class Options:
|
class Options:
|
||||||
@ -76,9 +76,8 @@ class Options:
|
|||||||
By default, the parser function is getopt.gnu_getopt, and the abort
|
By default, the parser function is getopt.gnu_getopt, and the abort
|
||||||
behaviour is to exit the program.
|
behaviour is to exit the program.
|
||||||
"""
|
"""
|
||||||
def __init__(self, exe, optspec, optfunc=getopt.gnu_getopt,
|
def __init__(self, optspec, optfunc=getopt.gnu_getopt,
|
||||||
onabort=_default_onabort):
|
onabort=_default_onabort):
|
||||||
self.exe = exe
|
|
||||||
self.optspec = optspec
|
self.optspec = optspec
|
||||||
self._onabort = onabort
|
self._onabort = onabort
|
||||||
self.optfunc = optfunc
|
self.optfunc = optfunc
|
||||||
@ -122,8 +121,8 @@ class Options:
|
|||||||
defval = None
|
defval = None
|
||||||
flagl = flags.split(',')
|
flagl = flags.split(',')
|
||||||
flagl_nice = []
|
flagl_nice = []
|
||||||
for f in flagl:
|
for _f in flagl:
|
||||||
f,dvi = _remove_negative_kv(f, _intify(defval))
|
f,dvi = _remove_negative_kv(_f, _intify(defval))
|
||||||
self._aliases[f] = _remove_negative_k(flagl[0])
|
self._aliases[f] = _remove_negative_k(flagl[0])
|
||||||
self._hasparms[f] = has_parm
|
self._hasparms[f] = has_parm
|
||||||
self._defaults[f] = dvi
|
self._defaults[f] = dvi
|
||||||
@ -135,7 +134,7 @@ class Options:
|
|||||||
self._aliases[f_nice] = _remove_negative_k(flagl[0])
|
self._aliases[f_nice] = _remove_negative_k(flagl[0])
|
||||||
self._longopts.append(f + (has_parm and '=' or ''))
|
self._longopts.append(f + (has_parm and '=' or ''))
|
||||||
self._longopts.append('no-' + f)
|
self._longopts.append('no-' + f)
|
||||||
flagl_nice.append('--' + f)
|
flagl_nice.append('--' + _f)
|
||||||
flags_nice = ', '.join(flagl_nice)
|
flags_nice = ', '.join(flagl_nice)
|
||||||
if has_parm:
|
if has_parm:
|
||||||
flags_nice += ' ...'
|
flags_nice += ' ...'
|
||||||
|
99
server.py
99
server.py
@ -1,4 +1,4 @@
|
|||||||
import re, struct, socket, select, traceback
|
import re, struct, socket, select, traceback, time
|
||||||
if not globals().get('skip_imports'):
|
if not globals().get('skip_imports'):
|
||||||
import ssnet, helpers, hostwatch
|
import ssnet, helpers, hostwatch
|
||||||
import compat.ssubprocess as ssubprocess
|
import compat.ssubprocess as ssubprocess
|
||||||
@ -43,7 +43,12 @@ def _maskbits(netmask):
|
|||||||
|
|
||||||
|
|
||||||
def _shl(n, bits):
|
def _shl(n, bits):
|
||||||
return n * int(2**bits)
|
# we use our own implementation of left-shift because
|
||||||
|
# results may be different between older and newer versions
|
||||||
|
# of python for numbers like 1<<32. We use long() because
|
||||||
|
# int(2**32) doesn't work in older python, which has limited
|
||||||
|
# int sizes.
|
||||||
|
return n * long(2**bits)
|
||||||
|
|
||||||
|
|
||||||
def _list_routes():
|
def _list_routes():
|
||||||
@ -68,9 +73,11 @@ def _list_routes():
|
|||||||
|
|
||||||
|
|
||||||
def list_routes():
|
def list_routes():
|
||||||
|
l = []
|
||||||
for (ip,width) in _list_routes():
|
for (ip,width) in _list_routes():
|
||||||
if not ip.startswith('0.') and not ip.startswith('127.'):
|
if not ip.startswith('0.') and not ip.startswith('127.'):
|
||||||
yield (ip,width)
|
l.append((ip,width))
|
||||||
|
return l
|
||||||
|
|
||||||
|
|
||||||
def _exc_dump():
|
def _exc_dump():
|
||||||
@ -106,11 +113,66 @@ class Hostwatch:
|
|||||||
self.sock = None
|
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.timeout = time.time()+30
|
||||||
|
self.mux = mux
|
||||||
|
self.chan = chan
|
||||||
|
self.tries = 0
|
||||||
|
self.peer = None
|
||||||
|
self.request = request
|
||||||
|
self.sock = sock
|
||||||
|
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
|
||||||
|
self.peer = resolvconf_random_nameserver()
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
if helpers.verbose >= 1:
|
if helpers.verbose >= 1:
|
||||||
helpers.logprefix = ' s: '
|
helpers.logprefix = ' s: '
|
||||||
else:
|
else:
|
||||||
helpers.logprefix = 'server: '
|
helpers.logprefix = 'server: '
|
||||||
|
debug1('latency control setting = %r\n' % latency_control)
|
||||||
|
|
||||||
routes = list(list_routes())
|
routes = list(list_routes())
|
||||||
debug1('available routes:\n')
|
debug1('available routes:\n')
|
||||||
@ -118,7 +180,7 @@ def main():
|
|||||||
debug1(' %s/%d\n' % r)
|
debug1(' %s/%d\n' % r)
|
||||||
|
|
||||||
# synchronization header
|
# synchronization header
|
||||||
sys.stdout.write('SSHUTTLE0001')
|
sys.stdout.write('\0\0SSHUTTLE0001')
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
||||||
handlers = []
|
handlers = []
|
||||||
@ -133,12 +195,20 @@ def main():
|
|||||||
mux.send(0, ssnet.CMD_ROUTES, routepkt)
|
mux.send(0, ssnet.CMD_ROUTES, routepkt)
|
||||||
|
|
||||||
hw = Hostwatch()
|
hw = Hostwatch()
|
||||||
|
hw.leftover = ''
|
||||||
|
|
||||||
def hostwatch_ready():
|
def hostwatch_ready():
|
||||||
assert(hw.pid)
|
assert(hw.pid)
|
||||||
content = hw.sock.recv(4096)
|
content = hw.sock.recv(4096)
|
||||||
if content:
|
if content:
|
||||||
mux.send(0, ssnet.CMD_HOST_LIST, 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:
|
else:
|
||||||
raise Fatal('hostwatch process died')
|
raise Fatal('hostwatch process died')
|
||||||
|
|
||||||
@ -156,12 +226,29 @@ def main():
|
|||||||
handlers.append(Proxy(MuxWrapper(mux, channel), outwrap))
|
handlers.append(Proxy(MuxWrapper(mux, channel), outwrap))
|
||||||
mux.new_channel = new_channel
|
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:
|
while mux.ok:
|
||||||
if hw.pid:
|
if hw.pid:
|
||||||
|
assert(hw.pid > 0)
|
||||||
(rpid, rv) = os.waitpid(hw.pid, os.WNOHANG)
|
(rpid, rv) = os.waitpid(hw.pid, os.WNOHANG)
|
||||||
if rpid:
|
if rpid:
|
||||||
raise Fatal('hostwatch exited unexpectedly: code 0x%04x\n' % rv)
|
raise Fatal('hostwatch exited unexpectedly: code 0x%04x\n' % rv)
|
||||||
|
|
||||||
ssnet.runonce(handlers, mux)
|
ssnet.runonce(handlers, mux)
|
||||||
mux.check_fullness()
|
if latency_control:
|
||||||
|
mux.check_fullness()
|
||||||
mux.callback()
|
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
|
||||||
|
60
ssh.py
60
ssh.py
@ -14,27 +14,49 @@ def readfile(name):
|
|||||||
raise Exception("can't find file %r in any of %r" % (name, path))
|
raise Exception("can't find file %r in any of %r" % (name, path))
|
||||||
|
|
||||||
|
|
||||||
def empackage(z, filename):
|
def empackage(z, filename, data=None):
|
||||||
(path,basename) = os.path.split(filename)
|
(path,basename) = os.path.split(filename)
|
||||||
content = z.compress(readfile(filename))
|
if not data:
|
||||||
|
data = readfile(filename)
|
||||||
|
content = z.compress(data)
|
||||||
content += z.flush(zlib.Z_SYNC_FLUSH)
|
content += z.flush(zlib.Z_SYNC_FLUSH)
|
||||||
return '%s\n%d\n%s' % (basename,len(content), content)
|
return '%s\n%d\n%s' % (basename, len(content), content)
|
||||||
|
|
||||||
|
|
||||||
def connect(rhostport, python):
|
def connect(ssh_cmd, rhostport, python, stderr, options):
|
||||||
main_exe = sys.argv[0]
|
main_exe = sys.argv[0]
|
||||||
l = (rhostport or '').split(':', 1)
|
|
||||||
rhost = l[0]
|
|
||||||
portl = []
|
portl = []
|
||||||
if len(l) > 1:
|
|
||||||
portl = ['-p', str(int(l[1]))]
|
rhostIsIPv6 = False
|
||||||
|
if (rhostport or '').count(':') > 1:
|
||||||
|
rhostIsIPv6 = True
|
||||||
|
if rhostport.count(']') or rhostport.count('['):
|
||||||
|
result = rhostport.split(']')
|
||||||
|
rhost = result[0].strip('[')
|
||||||
|
if len(result) > 1:
|
||||||
|
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.
|
||||||
|
rhost = rhostport
|
||||||
|
else: # IPv4
|
||||||
|
l = (rhostport or '').split(':', 1)
|
||||||
|
rhost = l[0]
|
||||||
|
if len(l) > 1:
|
||||||
|
portl = ['-p', str(int(l[1]))]
|
||||||
|
|
||||||
if rhost == '-':
|
if rhost == '-':
|
||||||
rhost = None
|
rhost = None
|
||||||
|
|
||||||
|
ipv6flag = []
|
||||||
|
if rhostIsIPv6:
|
||||||
|
ipv6flag = ['-6']
|
||||||
|
|
||||||
z = zlib.compressobj(1)
|
z = zlib.compressobj(1)
|
||||||
content = readfile('assembler.py')
|
content = readfile('assembler.py')
|
||||||
content2 = (empackage(z, 'helpers.py') +
|
optdata = ''.join("%s=%r\n" % (k,v) for (k,v) in options.items())
|
||||||
|
content2 = (empackage(z, 'cmdline_options.py', optdata) +
|
||||||
|
empackage(z, 'helpers.py') +
|
||||||
empackage(z, 'compat/ssubprocess.py') +
|
empackage(z, 'compat/ssubprocess.py') +
|
||||||
empackage(z, 'ssnet.py') +
|
empackage(z, 'ssnet.py') +
|
||||||
empackage(z, 'hostwatch.py') +
|
empackage(z, 'hostwatch.py') +
|
||||||
@ -51,9 +73,23 @@ def connect(rhostport, python):
|
|||||||
|
|
||||||
|
|
||||||
if not rhost:
|
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:
|
else:
|
||||||
argv = ['ssh'] + portl + [rhost, '--', "'%s' -c '%s'" % (python, pyscript)]
|
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, '--', pycmd])
|
||||||
(s1,s2) = socket.socketpair()
|
(s1,s2) = socket.socketpair()
|
||||||
def setup():
|
def setup():
|
||||||
# runs in the child process
|
# runs in the child process
|
||||||
@ -62,7 +98,7 @@ def connect(rhostport, python):
|
|||||||
s1.close()
|
s1.close()
|
||||||
debug2('executing: %r\n' % argv)
|
debug2('executing: %r\n' % argv)
|
||||||
p = ssubprocess.Popen(argv, stdin=s1a, stdout=s1b, preexec_fn=setup,
|
p = ssubprocess.Popen(argv, stdin=s1a, stdout=s1b, preexec_fn=setup,
|
||||||
close_fds=True)
|
close_fds=True, stderr=stderr)
|
||||||
os.close(s1a)
|
os.close(s1a)
|
||||||
os.close(s1b)
|
os.close(s1b)
|
||||||
s2.sendall(content)
|
s2.sendall(content)
|
||||||
|
12
sshuttle
Executable file
12
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
|
116
ssnet.py
116
ssnet.py
@ -2,6 +2,8 @@ import struct, socket, errno, select
|
|||||||
if not globals().get('skip_imports'):
|
if not globals().get('skip_imports'):
|
||||||
from helpers import *
|
from helpers import *
|
||||||
|
|
||||||
|
MAX_CHANNEL = 65535
|
||||||
|
|
||||||
# these don't exist in the socket module in python 2.3!
|
# these don't exist in the socket module in python 2.3!
|
||||||
SHUT_RD = 0
|
SHUT_RD = 0
|
||||||
SHUT_WR = 1
|
SHUT_WR = 1
|
||||||
@ -15,27 +17,35 @@ CMD_EXIT = 0x4200
|
|||||||
CMD_PING = 0x4201
|
CMD_PING = 0x4201
|
||||||
CMD_PONG = 0x4202
|
CMD_PONG = 0x4202
|
||||||
CMD_CONNECT = 0x4203
|
CMD_CONNECT = 0x4203
|
||||||
CMD_CLOSE = 0x4204
|
CMD_STOP_SENDING = 0x4204
|
||||||
CMD_EOF = 0x4205
|
CMD_EOF = 0x4205
|
||||||
CMD_DATA = 0x4206
|
CMD_DATA = 0x4206
|
||||||
CMD_ROUTES = 0x4207
|
CMD_ROUTES = 0x4207
|
||||||
CMD_HOST_REQ = 0x4208
|
CMD_HOST_REQ = 0x4208
|
||||||
CMD_HOST_LIST = 0x4209
|
CMD_HOST_LIST = 0x4209
|
||||||
|
CMD_DNS_REQ = 0x420a
|
||||||
|
CMD_DNS_RESPONSE = 0x420b
|
||||||
|
|
||||||
cmd_to_name = {
|
cmd_to_name = {
|
||||||
CMD_EXIT: 'EXIT',
|
CMD_EXIT: 'EXIT',
|
||||||
CMD_PING: 'PING',
|
CMD_PING: 'PING',
|
||||||
CMD_PONG: 'PONG',
|
CMD_PONG: 'PONG',
|
||||||
CMD_CONNECT: 'CONNECT',
|
CMD_CONNECT: 'CONNECT',
|
||||||
CMD_CLOSE: 'CLOSE',
|
CMD_STOP_SENDING: 'STOP_SENDING',
|
||||||
CMD_EOF: 'EOF',
|
CMD_EOF: 'EOF',
|
||||||
CMD_DATA: 'DATA',
|
CMD_DATA: 'DATA',
|
||||||
CMD_ROUTES: 'ROUTES',
|
CMD_ROUTES: 'ROUTES',
|
||||||
CMD_HOST_REQ: 'HOST_REQ',
|
CMD_HOST_REQ: 'HOST_REQ',
|
||||||
CMD_HOST_LIST: 'HOST_LIST',
|
CMD_HOST_LIST: 'HOST_LIST',
|
||||||
|
CMD_DNS_REQ: 'DNS_REQ',
|
||||||
|
CMD_DNS_RESPONSE: 'DNS_RESPONSE',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
NET_ERRS = [errno.ECONNREFUSED, errno.ETIMEDOUT,
|
||||||
|
errno.EHOSTUNREACH, errno.ENETUNREACH,
|
||||||
|
errno.EHOSTDOWN, errno.ENETDOWN]
|
||||||
|
|
||||||
|
|
||||||
def _add(l, elem):
|
def _add(l, elem):
|
||||||
if not elem in l:
|
if not elem in l:
|
||||||
@ -75,8 +85,12 @@ def _try_peername(sock):
|
|||||||
return 'unknown'
|
return 'unknown'
|
||||||
|
|
||||||
|
|
||||||
|
_swcount = 0
|
||||||
class SockWrapper:
|
class SockWrapper:
|
||||||
def __init__(self, rsock, wsock, connect_to=None, peername=None):
|
def __init__(self, rsock, wsock, connect_to=None, peername=None):
|
||||||
|
global _swcount
|
||||||
|
_swcount += 1
|
||||||
|
debug3('creating new SockWrapper (%d now exist)\n' % _swcount)
|
||||||
self.exc = None
|
self.exc = None
|
||||||
self.rsock = rsock
|
self.rsock = rsock
|
||||||
self.wsock = wsock
|
self.wsock = wsock
|
||||||
@ -87,9 +101,11 @@ class SockWrapper:
|
|||||||
self.try_connect()
|
self.try_connect()
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
debug1('%r: deleting\n' % self)
|
global _swcount
|
||||||
|
_swcount -= 1
|
||||||
|
debug1('%r: deleting (%d remain)\n' % (self, _swcount))
|
||||||
if self.exc:
|
if self.exc:
|
||||||
debug1('%r: error was: %r\n' % (self, self.exc))
|
debug1('%r: error was: %s\n' % (self, self.exc))
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
if self.rsock == self.wsock:
|
if self.rsock == self.wsock:
|
||||||
@ -101,6 +117,8 @@ class SockWrapper:
|
|||||||
def seterr(self, e):
|
def seterr(self, e):
|
||||||
if not self.exc:
|
if not self.exc:
|
||||||
self.exc = e
|
self.exc = e
|
||||||
|
self.nowrite()
|
||||||
|
self.noread()
|
||||||
|
|
||||||
def try_connect(self):
|
def try_connect(self):
|
||||||
if self.connect_to and self.shut_write:
|
if self.connect_to and self.shut_write:
|
||||||
@ -110,20 +128,45 @@ class SockWrapper:
|
|||||||
return # already connected
|
return # already connected
|
||||||
self.rsock.setblocking(False)
|
self.rsock.setblocking(False)
|
||||||
debug3('%r: trying connect to %r\n' % (self, self.connect_to))
|
debug3('%r: trying connect to %r\n' % (self, self.connect_to))
|
||||||
|
if socket.inet_aton(self.connect_to[0])[0] == '\0':
|
||||||
|
self.seterr(Exception("Can't connect to %r: "
|
||||||
|
"IP address starts with zero\n"
|
||||||
|
% (self.connect_to,)))
|
||||||
|
self.connect_to = None
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
self.rsock.connect(self.connect_to)
|
self.rsock.connect(self.connect_to)
|
||||||
# connected successfully (Linux)
|
# connected successfully (Linux)
|
||||||
self.connect_to = None
|
self.connect_to = None
|
||||||
except socket.error, e:
|
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]:
|
if e.args[0] in [errno.EINPROGRESS, errno.EALREADY]:
|
||||||
pass # not connected yet
|
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:
|
elif e.args[0] == errno.EISCONN:
|
||||||
# connected successfully (BSD)
|
# connected successfully (BSD)
|
||||||
self.connect_to = None
|
self.connect_to = None
|
||||||
elif e.args[0] in [errno.ECONNREFUSED, errno.ETIMEDOUT,
|
elif e.args[0] in NET_ERRS + [errno.EACCES, errno.EPERM]:
|
||||||
errno.EHOSTUNREACH, errno.ENETUNREACH,
|
|
||||||
errno.EACCES, errno.EPERM]:
|
|
||||||
# a "normal" kind of error
|
# a "normal" kind of error
|
||||||
self.connect_to = None
|
self.connect_to = None
|
||||||
self.seterr(e)
|
self.seterr(e)
|
||||||
@ -143,7 +186,7 @@ class SockWrapper:
|
|||||||
try:
|
try:
|
||||||
self.wsock.shutdown(SHUT_WR)
|
self.wsock.shutdown(SHUT_WR)
|
||||||
except socket.error, e:
|
except socket.error, e:
|
||||||
self.seterr(e)
|
self.seterr('nowrite: %s' % e)
|
||||||
|
|
||||||
def too_full(self):
|
def too_full(self):
|
||||||
return False # fullness is determined by the socket's select() state
|
return False # fullness is determined by the socket's select() state
|
||||||
@ -155,11 +198,14 @@ class SockWrapper:
|
|||||||
try:
|
try:
|
||||||
return _nb_clean(os.write, self.wsock.fileno(), buf)
|
return _nb_clean(os.write, self.wsock.fileno(), buf)
|
||||||
except OSError, e:
|
except OSError, e:
|
||||||
# unexpected error... stream is dead
|
if e.errno == errno.EPIPE:
|
||||||
self.seterr(e)
|
debug1('%r: uwrite: got EPIPE\n' % self)
|
||||||
self.nowrite()
|
self.nowrite()
|
||||||
self.noread()
|
return 0
|
||||||
return 0
|
else:
|
||||||
|
# unexpected error... stream is dead
|
||||||
|
self.seterr('uwrite: %s' % e)
|
||||||
|
return 0
|
||||||
|
|
||||||
def write(self, buf):
|
def write(self, buf):
|
||||||
assert(buf)
|
assert(buf)
|
||||||
@ -174,7 +220,7 @@ class SockWrapper:
|
|||||||
try:
|
try:
|
||||||
return _nb_clean(os.read, self.rsock.fileno(), 65536)
|
return _nb_clean(os.read, self.rsock.fileno(), 65536)
|
||||||
except OSError, e:
|
except OSError, e:
|
||||||
self.seterr(e)
|
self.seterr('uread: %s' % e)
|
||||||
return '' # unexpected error... we'll call it EOF
|
return '' # unexpected error... we'll call it EOF
|
||||||
|
|
||||||
def fill(self):
|
def fill(self):
|
||||||
@ -226,6 +272,9 @@ class Proxy(Handler):
|
|||||||
self.wrap2 = wrap2
|
self.wrap2 = wrap2
|
||||||
|
|
||||||
def pre_select(self, r, w, x):
|
def pre_select(self, r, w, x):
|
||||||
|
if self.wrap1.shut_write: self.wrap2.noread()
|
||||||
|
if self.wrap2.shut_write: self.wrap1.noread()
|
||||||
|
|
||||||
if self.wrap1.connect_to:
|
if self.wrap1.connect_to:
|
||||||
_add(w, self.wrap1.rsock)
|
_add(w, self.wrap1.rsock)
|
||||||
elif self.wrap1.buf:
|
elif self.wrap1.buf:
|
||||||
@ -258,6 +307,8 @@ class Proxy(Handler):
|
|||||||
if (self.wrap1.shut_read and self.wrap2.shut_read and
|
if (self.wrap1.shut_read and self.wrap2.shut_read and
|
||||||
not self.wrap1.buf and not self.wrap2.buf):
|
not self.wrap1.buf and not self.wrap2.buf):
|
||||||
self.ok = False
|
self.ok = False
|
||||||
|
self.wrap1.nowrite()
|
||||||
|
self.wrap2.nowrite()
|
||||||
|
|
||||||
|
|
||||||
class Mux(Handler):
|
class Mux(Handler):
|
||||||
@ -265,7 +316,7 @@ class Mux(Handler):
|
|||||||
Handler.__init__(self, [rsock, wsock])
|
Handler.__init__(self, [rsock, wsock])
|
||||||
self.rsock = rsock
|
self.rsock = rsock
|
||||||
self.wsock = wsock
|
self.wsock = wsock
|
||||||
self.new_channel = self.got_routes = None
|
self.new_channel = self.got_dns_req = self.got_routes = None
|
||||||
self.got_host_req = self.got_host_list = None
|
self.got_host_req = self.got_host_list = None
|
||||||
self.channels = {}
|
self.channels = {}
|
||||||
self.chani = 0
|
self.chani = 0
|
||||||
@ -280,7 +331,7 @@ class Mux(Handler):
|
|||||||
# channel 0 is special, so we never allocate it
|
# channel 0 is special, so we never allocate it
|
||||||
for timeout in xrange(1024):
|
for timeout in xrange(1024):
|
||||||
self.chani += 1
|
self.chani += 1
|
||||||
if self.chani > 65535:
|
if self.chani > MAX_CHANNEL:
|
||||||
self.chani = 1
|
self.chani = 1
|
||||||
if not self.channels.get(self.chani):
|
if not self.channels.get(self.chani):
|
||||||
return self.chani
|
return self.chani
|
||||||
@ -327,6 +378,10 @@ class Mux(Handler):
|
|||||||
assert(not self.channels.get(channel))
|
assert(not self.channels.get(channel))
|
||||||
if self.new_channel:
|
if self.new_channel:
|
||||||
self.new_channel(channel, data)
|
self.new_channel(channel, data)
|
||||||
|
elif cmd == CMD_DNS_REQ:
|
||||||
|
assert(not self.channels.get(channel))
|
||||||
|
if self.got_dns_req:
|
||||||
|
self.got_dns_req(channel, data)
|
||||||
elif cmd == CMD_ROUTES:
|
elif cmd == CMD_ROUTES:
|
||||||
if self.got_routes:
|
if self.got_routes:
|
||||||
self.got_routes(data)
|
self.got_routes(data)
|
||||||
@ -343,8 +398,12 @@ class Mux(Handler):
|
|||||||
else:
|
else:
|
||||||
raise Exception('got CMD_HOST_LIST without got_host_list?')
|
raise Exception('got CMD_HOST_LIST without got_host_list?')
|
||||||
else:
|
else:
|
||||||
callback = self.channels[channel]
|
callback = self.channels.get(channel)
|
||||||
callback(cmd, data)
|
if not callback:
|
||||||
|
log('warning: closed channel %d got cmd=%s len=%d\n'
|
||||||
|
% (channel, cmd_to_name.get(cmd,hex(cmd)), len(data)))
|
||||||
|
else:
|
||||||
|
callback(cmd, data)
|
||||||
|
|
||||||
def flush(self):
|
def flush(self):
|
||||||
self.wsock.setblocking(False)
|
self.wsock.setblocking(False)
|
||||||
@ -419,11 +478,20 @@ class MuxWrapper(SockWrapper):
|
|||||||
def noread(self):
|
def noread(self):
|
||||||
if not self.shut_read:
|
if not self.shut_read:
|
||||||
self.shut_read = True
|
self.shut_read = True
|
||||||
|
self.mux.send(self.channel, CMD_STOP_SENDING, '')
|
||||||
|
self.maybe_close()
|
||||||
|
|
||||||
def nowrite(self):
|
def nowrite(self):
|
||||||
if not self.shut_write:
|
if not self.shut_write:
|
||||||
self.shut_write = True
|
self.shut_write = True
|
||||||
self.mux.send(self.channel, CMD_EOF, '')
|
self.mux.send(self.channel, CMD_EOF, '')
|
||||||
|
self.maybe_close()
|
||||||
|
|
||||||
|
def maybe_close(self):
|
||||||
|
if self.shut_read and self.shut_write:
|
||||||
|
# remove the mux's reference to us. The python garbage collector
|
||||||
|
# will then be able to reap our object.
|
||||||
|
self.mux.channels[self.channel] = None
|
||||||
|
|
||||||
def too_full(self):
|
def too_full(self):
|
||||||
return self.mux.too_full
|
return self.mux.too_full
|
||||||
@ -443,11 +511,10 @@ class MuxWrapper(SockWrapper):
|
|||||||
return None # no data available right now
|
return None # no data available right now
|
||||||
|
|
||||||
def got_packet(self, cmd, data):
|
def got_packet(self, cmd, data):
|
||||||
if cmd == CMD_CLOSE:
|
if cmd == CMD_EOF:
|
||||||
self.noread()
|
self.noread()
|
||||||
|
elif cmd == CMD_STOP_SENDING:
|
||||||
self.nowrite()
|
self.nowrite()
|
||||||
elif cmd == CMD_EOF:
|
|
||||||
self.noread()
|
|
||||||
elif cmd == CMD_DATA:
|
elif cmd == CMD_DATA:
|
||||||
self.buf.append(data)
|
self.buf.append(data)
|
||||||
else:
|
else:
|
||||||
@ -468,7 +535,10 @@ def runonce(handlers, mux):
|
|||||||
r = []
|
r = []
|
||||||
w = []
|
w = []
|
||||||
x = []
|
x = []
|
||||||
handlers = filter(lambda s: s.ok, handlers)
|
to_remove = filter(lambda s: not s.ok, handlers)
|
||||||
|
for h in to_remove:
|
||||||
|
handlers.remove(h)
|
||||||
|
|
||||||
for s in handlers:
|
for s in handlers:
|
||||||
s.pre_select(r,w,x)
|
s.pre_select(r,w,x)
|
||||||
debug2('Waiting: %d r=%r w=%r x=%r (fullness=%d/%d)\n'
|
debug2('Waiting: %d r=%r w=%r x=%r (fullness=%d/%d)\n'
|
||||||
|
16
ssyslog.py
Normal file
16
ssyslog.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import sys, os
|
||||||
|
from compat import ssubprocess
|
||||||
|
|
||||||
|
|
||||||
|
_p = None
|
||||||
|
def start_syslog():
|
||||||
|
global _p
|
||||||
|
_p = ssubprocess.Popen(['logger',
|
||||||
|
'-p', 'daemon.notice',
|
||||||
|
'-t', 'sshuttle'], stdin=ssubprocess.PIPE)
|
||||||
|
|
||||||
|
|
||||||
|
def stderr_to_syslog():
|
||||||
|
sys.stdout.flush()
|
||||||
|
sys.stderr.flush()
|
||||||
|
os.dup2(_p.stdin.fileno(), 2)
|
86
stresstest.py
Executable file
86
stresstest.py
Executable file
@ -0,0 +1,86 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
import sys, os, socket, select, struct, time
|
||||||
|
|
||||||
|
listener = socket.socket()
|
||||||
|
listener.bind(('127.0.0.1', 0))
|
||||||
|
listener.listen(500)
|
||||||
|
|
||||||
|
servers = []
|
||||||
|
clients = []
|
||||||
|
remain = {}
|
||||||
|
|
||||||
|
NUMCLIENTS = 50
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
|
||||||
|
while 1:
|
||||||
|
if len(clients) < NUMCLIENTS:
|
||||||
|
c = socket.socket()
|
||||||
|
c.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
c.bind(('0.0.0.0', 0))
|
||||||
|
c.connect(listener.getsockname())
|
||||||
|
count += 1
|
||||||
|
if count >= 16384:
|
||||||
|
count = 1
|
||||||
|
print 'cli CREATING %d' % count
|
||||||
|
b = struct.pack('I', count) + 'x'*count
|
||||||
|
remain[c] = count
|
||||||
|
print 'cli >> %r' % len(b)
|
||||||
|
c.send(b)
|
||||||
|
c.shutdown(socket.SHUT_WR)
|
||||||
|
clients.append(c)
|
||||||
|
r = [listener]
|
||||||
|
time.sleep(0.1)
|
||||||
|
else:
|
||||||
|
r = [listener]+servers+clients
|
||||||
|
print 'select(%d)' % len(r)
|
||||||
|
r,w,x = select.select(r, [], [], 5)
|
||||||
|
assert(r)
|
||||||
|
for i in r:
|
||||||
|
if i == listener:
|
||||||
|
s,addr = listener.accept()
|
||||||
|
servers.append(s)
|
||||||
|
elif i in servers:
|
||||||
|
b = i.recv(4096)
|
||||||
|
print 'srv << %r' % len(b)
|
||||||
|
if not i in remain:
|
||||||
|
assert(len(b) >= 4)
|
||||||
|
want = struct.unpack('I', b[:4])[0]
|
||||||
|
b = b[4:]
|
||||||
|
#i.send('y'*want)
|
||||||
|
else:
|
||||||
|
want = remain[i]
|
||||||
|
if want < len(b):
|
||||||
|
print 'weird wanted %d bytes, got %d: %r' % (want, len(b), b)
|
||||||
|
assert(want >= len(b))
|
||||||
|
want -= len(b)
|
||||||
|
remain[i] = want
|
||||||
|
if not b: # EOF
|
||||||
|
if want:
|
||||||
|
print 'weird: eof but wanted %d more' % want
|
||||||
|
assert(want == 0)
|
||||||
|
i.close()
|
||||||
|
servers.remove(i)
|
||||||
|
del remain[i]
|
||||||
|
else:
|
||||||
|
print 'srv >> %r' % len(b)
|
||||||
|
i.send('y'*len(b))
|
||||||
|
if not want:
|
||||||
|
i.shutdown(socket.SHUT_WR)
|
||||||
|
elif i in clients:
|
||||||
|
b = i.recv(4096)
|
||||||
|
print 'cli << %r' % len(b)
|
||||||
|
want = remain[i]
|
||||||
|
if want < len(b):
|
||||||
|
print 'weird wanted %d bytes, got %d: %r' % (want, len(b), b)
|
||||||
|
assert(want >= len(b))
|
||||||
|
want -= len(b)
|
||||||
|
remain[i] = want
|
||||||
|
if not b: # EOF
|
||||||
|
if want:
|
||||||
|
print 'weird: eof but wanted %d more' % want
|
||||||
|
assert(want == 0)
|
||||||
|
i.close()
|
||||||
|
clients.remove(i)
|
||||||
|
del remain[i]
|
||||||
|
listener.accept()
|
8
ui-macos/.gitignore
vendored
Normal file
8
ui-macos/.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
*.pyc
|
||||||
|
*~
|
||||||
|
/*.nib
|
||||||
|
/debug.app
|
||||||
|
/sources.list
|
||||||
|
/Sshuttle VPN.app
|
||||||
|
/*.tar.gz
|
||||||
|
/*.zip
|
40
ui-macos/Info.plist
Normal file
40
ui-macos/Info.plist
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>English</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Sshuttle VPN</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>Sshuttle</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>app.icns</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>ca.apenwarr.Sshuttle</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>Sshuttle VPN</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>0.0.0</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>0.0.0</string>
|
||||||
|
<key>LSUIElement</key>
|
||||||
|
<string>1</string>
|
||||||
|
<key>LSHasLocalizedDisplayName</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSAppleScriptEnabled</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>GNU LGPL Version 2</string>
|
||||||
|
<key>NSMainNibFile</key>
|
||||||
|
<string>MainMenu</string>
|
||||||
|
<key>NSPrincipalClass</key>
|
||||||
|
<string>NSApplication</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
2609
ui-macos/MainMenu.xib
Normal file
2609
ui-macos/MainMenu.xib
Normal file
File diff suppressed because it is too large
Load Diff
10
ui-macos/UserDefaults.plist
Normal file
10
ui-macos/UserDefaults.plist
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>startAtLogin</key>
|
||||||
|
<false/>
|
||||||
|
<key>autoReconnect</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
1
ui-macos/all.do
Normal file
1
ui-macos/all.do
Normal file
@ -0,0 +1 @@
|
|||||||
|
redo-ifchange debug.app dist
|
BIN
ui-macos/app.icns
Normal file
BIN
ui-macos/app.icns
Normal file
Binary file not shown.
28
ui-macos/askpass.py
Normal file
28
ui-macos/askpass.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import sys, os, re, subprocess
|
||||||
|
|
||||||
|
def askpass(prompt):
|
||||||
|
prompt = prompt.replace('"', "'")
|
||||||
|
|
||||||
|
if 'yes/no' in prompt:
|
||||||
|
return "yes"
|
||||||
|
|
||||||
|
script="""
|
||||||
|
tell application "Finder"
|
||||||
|
activate
|
||||||
|
display dialog "%s" \
|
||||||
|
with title "Sshuttle SSH Connection" \
|
||||||
|
default answer "" \
|
||||||
|
with icon caution \
|
||||||
|
with hidden answer
|
||||||
|
end tell
|
||||||
|
""" % prompt
|
||||||
|
|
||||||
|
p = subprocess.Popen(['osascript', '-e', script], stdout=subprocess.PIPE)
|
||||||
|
out = p.stdout.read()
|
||||||
|
rv = p.wait()
|
||||||
|
if rv:
|
||||||
|
return None
|
||||||
|
g = re.match("text returned:(.*), button returned:.*", out)
|
||||||
|
if not g:
|
||||||
|
return None
|
||||||
|
return g.group(1)
|
1
ui-macos/bits/.gitignore
vendored
Normal file
1
ui-macos/bits/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/runpython
|
1
ui-macos/bits/PkgInfo
Normal file
1
ui-macos/bits/PkgInfo
Normal file
@ -0,0 +1 @@
|
|||||||
|
APPL????
|
23
ui-macos/bits/runpython.c
Normal file
23
ui-macos/bits/runpython.c
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* This rather pointless program acts like the python interpreter, except
|
||||||
|
* it's intended to sit inside a MacOS .app package, so that its argv[0]
|
||||||
|
* will point inside the package.
|
||||||
|
*
|
||||||
|
* NSApplicationMain() looks for Info.plist using the path in argv[0], which
|
||||||
|
* goes wrong if your interpreter is /usr/bin/python.
|
||||||
|
*/
|
||||||
|
#include <Python.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
char *path = strdup(argv[0]), *cptr;
|
||||||
|
char *args[] = {argv[0], "../Resources/main.py", NULL};
|
||||||
|
cptr = strrchr(path, '/');
|
||||||
|
if (cptr)
|
||||||
|
*cptr = 0;
|
||||||
|
chdir(path);
|
||||||
|
free(path);
|
||||||
|
return Py_Main(2, args);
|
||||||
|
}
|
17
ui-macos/bits/runpython.do
Normal file
17
ui-macos/bits/runpython.do
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
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"
|
||||||
|
gcc $ARCHES \
|
||||||
|
-Wall -o $3 runpython.c \
|
||||||
|
-I/usr/include/python2.5 \
|
||||||
|
-lpython2.5
|
BIN
ui-macos/chicken-tiny-bw.png
Normal file
BIN
ui-macos/chicken-tiny-bw.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 821 B |
BIN
ui-macos/chicken-tiny-err.png
Normal file
BIN
ui-macos/chicken-tiny-err.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 789 B |
BIN
ui-macos/chicken-tiny.png
Normal file
BIN
ui-macos/chicken-tiny.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 810 B |
4
ui-macos/clean.do
Normal file
4
ui-macos/clean.do
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
exec >&2
|
||||||
|
find . -name '*~' | xargs rm -f
|
||||||
|
rm -rf *.app *.zip *.tar.gz
|
||||||
|
rm -f bits/runpython *.nib sources.list
|
15
ui-macos/debug.app.do
Normal file
15
ui-macos/debug.app.do
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
redo-ifchange bits/runpython MainMenu.nib
|
||||||
|
rm -rf debug.app
|
||||||
|
mkdir debug.app debug.app/Contents
|
||||||
|
cd debug.app/Contents
|
||||||
|
ln -s ../.. Resources
|
||||||
|
ln -s ../.. English.lproj
|
||||||
|
ln -s ../../Info.plist .
|
||||||
|
ln -s ../../app.icns .
|
||||||
|
|
||||||
|
mkdir MacOS
|
||||||
|
cd MacOS
|
||||||
|
ln -s ../../../bits/runpython Sshuttle
|
||||||
|
|
||||||
|
cd ../../..
|
||||||
|
redo-ifchange $(find debug.app -type f)
|
28
ui-macos/default.app.do
Normal file
28
ui-macos/default.app.do
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
TOP=$PWD
|
||||||
|
redo-ifchange sources.list
|
||||||
|
redo-ifchange Info.plist bits/runpython \
|
||||||
|
$(while read name newname; do echo "$name"; done <sources.list)
|
||||||
|
|
||||||
|
rm -rf "$2.app"
|
||||||
|
mkdir "$2.app" "$2.app/Contents"
|
||||||
|
cd "$2.app/Contents"
|
||||||
|
|
||||||
|
cp "$TOP/Info.plist" .
|
||||||
|
|
||||||
|
mkdir MacOS
|
||||||
|
cp "$TOP/bits/runpython" MacOS/Sshuttle
|
||||||
|
|
||||||
|
mkdir Resources
|
||||||
|
|
||||||
|
cd "$TOP"
|
||||||
|
while read name newname; do
|
||||||
|
[ -z "$name" ] && continue
|
||||||
|
: "${newname:=$name}"
|
||||||
|
outname=$2.app/Contents/Resources/$newname
|
||||||
|
outdir=$(dirname "$outname")
|
||||||
|
[ -d "$outdir" ] || mkdir "$outdir"
|
||||||
|
cp "${name-$newname}" "$outname"
|
||||||
|
done <sources.list
|
||||||
|
|
||||||
|
cd "$2.app"
|
||||||
|
redo-ifchange $(find . -type f)
|
5
ui-macos/default.app.tar.gz.do
Normal file
5
ui-macos/default.app.tar.gz.do
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
exec >&2
|
||||||
|
IFS="
|
||||||
|
"
|
||||||
|
redo-ifchange $2.app
|
||||||
|
tar -czf $3 $2.app/
|
5
ui-macos/default.app.zip.do
Normal file
5
ui-macos/default.app.zip.do
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
exec >&2
|
||||||
|
IFS="
|
||||||
|
"
|
||||||
|
redo-ifchange $2.app
|
||||||
|
zip -q -r $3 $2.app/
|
2
ui-macos/default.nib.do
Normal file
2
ui-macos/default.nib.do
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
redo-ifchange $2.xib
|
||||||
|
ibtool --compile $3 $2.xib
|
1
ui-macos/dist.do
Normal file
1
ui-macos/dist.do
Normal file
@ -0,0 +1 @@
|
|||||||
|
redo-ifchange "Sshuttle VPN.app.zip" "Sshuttle VPN.app.tar.gz"
|
19
ui-macos/git-export.do
Normal file
19
ui-macos/git-export.do
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# update a local branch with pregenerated output files, so people can download
|
||||||
|
# the completed tarballs from github. Since we don't have any real binaries,
|
||||||
|
# our final distribution package contains mostly blobs from the source code,
|
||||||
|
# so this doesn't cost us much extra space in the repo.
|
||||||
|
BRANCH=dist/macos
|
||||||
|
redo-ifchange 'Sshuttle VPN.app'
|
||||||
|
git update-ref refs/heads/$BRANCH origin/$BRANCH '' 2>/dev/null || true
|
||||||
|
|
||||||
|
export GIT_INDEX_FILE=$PWD/gitindex.tmp
|
||||||
|
rm -f "$GIT_INDEX_FILE"
|
||||||
|
git add -f 'Sshuttle VPN.app'
|
||||||
|
|
||||||
|
MSG="MacOS precompiled app package for $(git describe)"
|
||||||
|
TREE=$(git write-tree --prefix=ui-macos)
|
||||||
|
git show-ref refs/heads/$BRANCH >/dev/null && PARENT="-p refs/heads/$BRANCH"
|
||||||
|
COMMITID=$(echo "$MSG" | git commit-tree $TREE $PARENT)
|
||||||
|
|
||||||
|
git update-ref refs/heads/$BRANCH $COMMITID
|
||||||
|
rm -f "$GIT_INDEX_FILE"
|
375
ui-macos/main.py
Normal file
375
ui-macos/main.py
Normal file
@ -0,0 +1,375 @@
|
|||||||
|
import sys, os, pty
|
||||||
|
from AppKit import *
|
||||||
|
import my, models, askpass
|
||||||
|
|
||||||
|
def sshuttle_args(host, auto_nets, auto_hosts, dns, nets, debug,
|
||||||
|
no_latency_control):
|
||||||
|
argv = [my.bundle_path('sshuttle/sshuttle', ''), '-r', host]
|
||||||
|
assert(argv[0])
|
||||||
|
if debug:
|
||||||
|
argv.append('-v')
|
||||||
|
if auto_nets:
|
||||||
|
argv.append('--auto-nets')
|
||||||
|
if auto_hosts:
|
||||||
|
argv.append('--auto-hosts')
|
||||||
|
if dns:
|
||||||
|
argv.append('--dns')
|
||||||
|
if no_latency_control:
|
||||||
|
argv.append('--no-latency-control')
|
||||||
|
argv += nets
|
||||||
|
return argv
|
||||||
|
|
||||||
|
|
||||||
|
class _Callback(NSObject):
|
||||||
|
def initWithFunc_(self, func):
|
||||||
|
self = super(_Callback, self).init()
|
||||||
|
self.func = func
|
||||||
|
return self
|
||||||
|
def func_(self, obj):
|
||||||
|
return self.func(obj)
|
||||||
|
|
||||||
|
|
||||||
|
class Callback:
|
||||||
|
def __init__(self, func):
|
||||||
|
self.obj = _Callback.alloc().initWithFunc_(func)
|
||||||
|
self.sel = self.obj.func_
|
||||||
|
|
||||||
|
|
||||||
|
class Runner:
|
||||||
|
def __init__(self, argv, logfunc, promptfunc, serverobj):
|
||||||
|
print 'in __init__'
|
||||||
|
self.id = argv
|
||||||
|
self.rv = None
|
||||||
|
self.pid = None
|
||||||
|
self.fd = None
|
||||||
|
self.logfunc = logfunc
|
||||||
|
self.promptfunc = promptfunc
|
||||||
|
self.serverobj = serverobj
|
||||||
|
self.buf = ''
|
||||||
|
self.logfunc('\nConnecting to %s.\n' % self.serverobj.host())
|
||||||
|
print 'will run: %r' % argv
|
||||||
|
self.serverobj.setConnected_(False)
|
||||||
|
pid,fd = pty.fork()
|
||||||
|
if pid == 0:
|
||||||
|
# child
|
||||||
|
try:
|
||||||
|
os.execvp(argv[0], argv)
|
||||||
|
except Exception, e:
|
||||||
|
sys.stderr.write('failed to start: %r\n' % e)
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
os._exit(42)
|
||||||
|
# parent
|
||||||
|
self.pid = pid
|
||||||
|
self.file = NSFileHandle.alloc()\
|
||||||
|
.initWithFileDescriptor_closeOnDealloc_(fd, True)
|
||||||
|
self.cb = Callback(self.gotdata)
|
||||||
|
NSNotificationCenter.defaultCenter()\
|
||||||
|
.addObserver_selector_name_object_(self.cb.obj, self.cb.sel,
|
||||||
|
NSFileHandleDataAvailableNotification, self.file)
|
||||||
|
self.file.waitForDataInBackgroundAndNotify()
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
self.wait()
|
||||||
|
|
||||||
|
def _try_wait(self, options):
|
||||||
|
if self.rv == None and self.pid > 0:
|
||||||
|
pid,code = os.waitpid(self.pid, options)
|
||||||
|
if pid == self.pid:
|
||||||
|
if os.WIFEXITED(code):
|
||||||
|
self.rv = os.WEXITSTATUS(code)
|
||||||
|
if self.rv == 111:
|
||||||
|
NSRunAlertPanel('Sshuttle',
|
||||||
|
'Please restart your computer to finish '
|
||||||
|
'installing Sshuttle.',
|
||||||
|
'Restart Later', None, None)
|
||||||
|
else:
|
||||||
|
self.rv = -os.WSTOPSIG(code)
|
||||||
|
self.serverobj.setConnected_(False)
|
||||||
|
self.serverobj.setError_('VPN process died')
|
||||||
|
self.logfunc('Disconnected.\n')
|
||||||
|
print 'wait_result: %r' % self.rv
|
||||||
|
return self.rv
|
||||||
|
|
||||||
|
def wait(self):
|
||||||
|
rv = None
|
||||||
|
while rv is None:
|
||||||
|
self.gotdata(None)
|
||||||
|
rv = self._try_wait(os.WNOHANG)
|
||||||
|
|
||||||
|
def poll(self):
|
||||||
|
return self._try_wait(os.WNOHANG)
|
||||||
|
|
||||||
|
def kill(self):
|
||||||
|
assert(self.pid > 0)
|
||||||
|
print 'killing: pid=%r rv=%r' % (self.pid, self.rv)
|
||||||
|
if self.rv == None:
|
||||||
|
self.logfunc('Disconnecting from %s.\n' % self.serverobj.host())
|
||||||
|
os.kill(self.pid, 15)
|
||||||
|
self.wait()
|
||||||
|
|
||||||
|
def gotdata(self, notification):
|
||||||
|
print 'gotdata!'
|
||||||
|
d = str(self.file.availableData())
|
||||||
|
if d:
|
||||||
|
self.logfunc(d)
|
||||||
|
self.buf = self.buf + d
|
||||||
|
if 'Connected.\r\n' in self.buf:
|
||||||
|
self.serverobj.setConnected_(True)
|
||||||
|
self.buf = self.buf[-4096:]
|
||||||
|
if self.buf.strip().endswith(':'):
|
||||||
|
lastline = self.buf.rstrip().split('\n')[-1]
|
||||||
|
resp = self.promptfunc(lastline)
|
||||||
|
add = ' (response)\n'
|
||||||
|
self.buf += add
|
||||||
|
self.logfunc(add)
|
||||||
|
self.file.writeData_(my.Data(resp + '\n'))
|
||||||
|
self.file.waitForDataInBackgroundAndNotify()
|
||||||
|
self.poll()
|
||||||
|
#print 'gotdata done!'
|
||||||
|
|
||||||
|
|
||||||
|
class SshuttleApp(NSObject):
|
||||||
|
def initialize(self):
|
||||||
|
d = my.PList('UserDefaults')
|
||||||
|
my.Defaults().registerDefaults_(d)
|
||||||
|
|
||||||
|
|
||||||
|
class SshuttleController(NSObject):
|
||||||
|
# Interface builder outlets
|
||||||
|
startAtLoginField = objc.IBOutlet()
|
||||||
|
autoReconnectField = objc.IBOutlet()
|
||||||
|
debugField = objc.IBOutlet()
|
||||||
|
routingField = objc.IBOutlet()
|
||||||
|
prefsWindow = objc.IBOutlet()
|
||||||
|
serversController = objc.IBOutlet()
|
||||||
|
logField = objc.IBOutlet()
|
||||||
|
latencyControlField = objc.IBOutlet()
|
||||||
|
|
||||||
|
servers = []
|
||||||
|
conns = {}
|
||||||
|
|
||||||
|
def _connect(self, server):
|
||||||
|
host = server.host()
|
||||||
|
print 'connecting %r' % host
|
||||||
|
self.fill_menu()
|
||||||
|
def logfunc(msg):
|
||||||
|
print 'log! (%d bytes)' % len(msg)
|
||||||
|
self.logField.textStorage()\
|
||||||
|
.appendAttributedString_(NSAttributedString.alloc()\
|
||||||
|
.initWithString_(msg))
|
||||||
|
self.logField.didChangeText()
|
||||||
|
def promptfunc(prompt):
|
||||||
|
print 'prompt! %r' % prompt
|
||||||
|
return askpass.askpass(prompt)
|
||||||
|
nets_mode = server.autoNets()
|
||||||
|
if nets_mode == models.NET_MANUAL:
|
||||||
|
manual_nets = ["%s/%d" % (i.subnet(), i.width())
|
||||||
|
for i in server.nets()]
|
||||||
|
elif nets_mode == models.NET_ALL:
|
||||||
|
manual_nets = ['0/0']
|
||||||
|
else:
|
||||||
|
manual_nets = []
|
||||||
|
noLatencyControl = (server.latencyControl() != models.LAT_INTERACTIVE)
|
||||||
|
conn = Runner(sshuttle_args(host,
|
||||||
|
auto_nets = nets_mode == models.NET_AUTO,
|
||||||
|
auto_hosts = server.autoHosts(),
|
||||||
|
dns = server.useDns(),
|
||||||
|
nets = manual_nets,
|
||||||
|
debug = self.debugField.state(),
|
||||||
|
no_latency_control = noLatencyControl),
|
||||||
|
logfunc=logfunc, promptfunc=promptfunc,
|
||||||
|
serverobj=server)
|
||||||
|
self.conns[host] = conn
|
||||||
|
|
||||||
|
def _disconnect(self, server):
|
||||||
|
host = server.host()
|
||||||
|
print 'disconnecting %r' % host
|
||||||
|
conn = self.conns.get(host)
|
||||||
|
if conn:
|
||||||
|
conn.kill()
|
||||||
|
self.fill_menu()
|
||||||
|
self.logField.textStorage().setAttributedString_(
|
||||||
|
NSAttributedString.alloc().initWithString_(''))
|
||||||
|
|
||||||
|
@objc.IBAction
|
||||||
|
def cmd_connect(self, sender):
|
||||||
|
server = sender.representedObject()
|
||||||
|
server.setWantConnect_(True)
|
||||||
|
|
||||||
|
@objc.IBAction
|
||||||
|
def cmd_disconnect(self, sender):
|
||||||
|
server = sender.representedObject()
|
||||||
|
server.setWantConnect_(False)
|
||||||
|
|
||||||
|
@objc.IBAction
|
||||||
|
def cmd_show(self, sender):
|
||||||
|
self.prefsWindow.makeKeyAndOrderFront_(self)
|
||||||
|
NSApp.activateIgnoringOtherApps_(True)
|
||||||
|
|
||||||
|
@objc.IBAction
|
||||||
|
def cmd_quit(self, sender):
|
||||||
|
NSApp.performSelector_withObject_afterDelay_(NSApp.terminate_,
|
||||||
|
None, 0.0)
|
||||||
|
|
||||||
|
def fill_menu(self):
|
||||||
|
menu = self.menu
|
||||||
|
menu.removeAllItems()
|
||||||
|
|
||||||
|
def additem(name, func, obj):
|
||||||
|
it = menu.addItemWithTitle_action_keyEquivalent_(name, None, "")
|
||||||
|
it.setRepresentedObject_(obj)
|
||||||
|
it.setTarget_(self)
|
||||||
|
it.setAction_(func)
|
||||||
|
def addnote(name):
|
||||||
|
additem(name, None, None)
|
||||||
|
|
||||||
|
any_inprogress = None
|
||||||
|
any_conn = None
|
||||||
|
any_err = None
|
||||||
|
if len(self.servers):
|
||||||
|
for i in self.servers:
|
||||||
|
host = i.host()
|
||||||
|
title = i.title()
|
||||||
|
want = i.wantConnect()
|
||||||
|
connected = i.connected()
|
||||||
|
numnets = len(list(i.nets()))
|
||||||
|
if not host:
|
||||||
|
additem('Connect Untitled', None, i)
|
||||||
|
elif i.autoNets() == models.NET_MANUAL and not numnets:
|
||||||
|
additem('Connect %s (no routes)' % host, None, i)
|
||||||
|
elif want:
|
||||||
|
any_conn = i
|
||||||
|
additem('Disconnect %s' % title, self.cmd_disconnect, i)
|
||||||
|
else:
|
||||||
|
additem('Connect %s' % title, self.cmd_connect, i)
|
||||||
|
if not want:
|
||||||
|
msg = 'Off'
|
||||||
|
elif i.error():
|
||||||
|
msg = 'ERROR - try reconnecting'
|
||||||
|
any_err = i
|
||||||
|
elif connected:
|
||||||
|
msg = 'Connected'
|
||||||
|
else:
|
||||||
|
msg = 'Connecting...'
|
||||||
|
any_inprogress = i
|
||||||
|
addnote(' State: %s' % msg)
|
||||||
|
else:
|
||||||
|
addnote('No servers defined yet')
|
||||||
|
|
||||||
|
menu.addItem_(NSMenuItem.separatorItem())
|
||||||
|
additem('Preferences...', self.cmd_show, None)
|
||||||
|
additem('Quit Sshuttle VPN', self.cmd_quit, None)
|
||||||
|
|
||||||
|
if any_err:
|
||||||
|
self.statusitem.setImage_(self.img_err)
|
||||||
|
self.statusitem.setTitle_('Error!')
|
||||||
|
elif any_conn:
|
||||||
|
self.statusitem.setImage_(self.img_running)
|
||||||
|
if any_inprogress:
|
||||||
|
self.statusitem.setTitle_('Connecting...')
|
||||||
|
else:
|
||||||
|
self.statusitem.setTitle_('')
|
||||||
|
else:
|
||||||
|
self.statusitem.setImage_(self.img_idle)
|
||||||
|
self.statusitem.setTitle_('')
|
||||||
|
|
||||||
|
def load_servers(self):
|
||||||
|
l = my.Defaults().arrayForKey_('servers') or []
|
||||||
|
sl = []
|
||||||
|
for s in l:
|
||||||
|
host = s.get('host', None)
|
||||||
|
if not host: continue
|
||||||
|
|
||||||
|
nets = s.get('nets', [])
|
||||||
|
nl = []
|
||||||
|
for n in nets:
|
||||||
|
subnet = n[0]
|
||||||
|
width = n[1]
|
||||||
|
net = models.SshuttleNet.alloc().init()
|
||||||
|
net.setSubnet_(subnet)
|
||||||
|
net.setWidth_(width)
|
||||||
|
nl.append(net)
|
||||||
|
|
||||||
|
autoNets = s.get('autoNets', models.NET_AUTO)
|
||||||
|
autoHosts = s.get('autoHosts', True)
|
||||||
|
useDns = s.get('useDns', autoNets == models.NET_ALL)
|
||||||
|
latencyControl = s.get('latencyControl', models.LAT_INTERACTIVE)
|
||||||
|
srv = models.SshuttleServer.alloc().init()
|
||||||
|
srv.setHost_(host)
|
||||||
|
srv.setAutoNets_(autoNets)
|
||||||
|
srv.setAutoHosts_(autoHosts)
|
||||||
|
srv.setNets_(nl)
|
||||||
|
srv.setUseDns_(useDns)
|
||||||
|
srv.setLatencyControl_(latencyControl)
|
||||||
|
sl.append(srv)
|
||||||
|
self.serversController.addObjects_(sl)
|
||||||
|
self.serversController.setSelectionIndex_(0)
|
||||||
|
|
||||||
|
def save_servers(self):
|
||||||
|
l = []
|
||||||
|
for s in self.servers:
|
||||||
|
host = s.host()
|
||||||
|
if not host: continue
|
||||||
|
nets = []
|
||||||
|
for n in s.nets():
|
||||||
|
subnet = n.subnet()
|
||||||
|
if not subnet: continue
|
||||||
|
nets.append((subnet, n.width()))
|
||||||
|
d = dict(host=s.host(),
|
||||||
|
nets=nets,
|
||||||
|
autoNets=s.autoNets(),
|
||||||
|
autoHosts=s.autoHosts(),
|
||||||
|
useDns=s.useDns(),
|
||||||
|
latencyControl=s.latencyControl())
|
||||||
|
l.append(d)
|
||||||
|
my.Defaults().setObject_forKey_(l, 'servers')
|
||||||
|
self.fill_menu()
|
||||||
|
|
||||||
|
def awakeFromNib(self):
|
||||||
|
self.routingField.removeAllItems()
|
||||||
|
tf = self.routingField.addItemWithTitle_
|
||||||
|
tf('Send all traffic through this server')
|
||||||
|
tf('Determine automatically')
|
||||||
|
tf('Custom...')
|
||||||
|
|
||||||
|
self.latencyControlField.removeAllItems()
|
||||||
|
tf = self.latencyControlField.addItemWithTitle_
|
||||||
|
tf('Fast transfer')
|
||||||
|
tf('Low latency')
|
||||||
|
|
||||||
|
# Hmm, even when I mark this as !enabled in the .nib, it still comes
|
||||||
|
# through as enabled. So let's just disable it here (since we don't
|
||||||
|
# support this feature yet).
|
||||||
|
self.startAtLoginField.setEnabled_(False)
|
||||||
|
self.startAtLoginField.setState_(False)
|
||||||
|
self.autoReconnectField.setEnabled_(False)
|
||||||
|
self.autoReconnectField.setState_(False)
|
||||||
|
|
||||||
|
self.load_servers()
|
||||||
|
|
||||||
|
# Initialize our menu item
|
||||||
|
self.menu = NSMenu.alloc().initWithTitle_('Sshuttle')
|
||||||
|
bar = NSStatusBar.systemStatusBar()
|
||||||
|
statusitem = bar.statusItemWithLength_(NSVariableStatusItemLength)
|
||||||
|
self.statusitem = statusitem
|
||||||
|
self.img_idle = my.Image('chicken-tiny-bw', 'png')
|
||||||
|
self.img_running = my.Image('chicken-tiny', 'png')
|
||||||
|
self.img_err = my.Image('chicken-tiny-err', 'png')
|
||||||
|
statusitem.setImage_(self.img_idle)
|
||||||
|
statusitem.setHighlightMode_(True)
|
||||||
|
statusitem.setMenu_(self.menu)
|
||||||
|
self.fill_menu()
|
||||||
|
|
||||||
|
models.configchange_callback = my.DelayedCallback(self.save_servers)
|
||||||
|
|
||||||
|
def sc(server):
|
||||||
|
if server.wantConnect():
|
||||||
|
self._connect(server)
|
||||||
|
else:
|
||||||
|
self._disconnect(server)
|
||||||
|
models.setconnect_callback = sc
|
||||||
|
|
||||||
|
|
||||||
|
# Note: NSApplicationMain calls sys.exit(), so this never returns.
|
||||||
|
NSApplicationMain(sys.argv)
|
167
ui-macos/models.py
Normal file
167
ui-macos/models.py
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
from AppKit import *
|
||||||
|
import my
|
||||||
|
|
||||||
|
|
||||||
|
configchange_callback = setconnect_callback = None
|
||||||
|
objc_validator = objc.signature('@@:N^@o^@')
|
||||||
|
|
||||||
|
|
||||||
|
def config_changed():
|
||||||
|
if configchange_callback:
|
||||||
|
configchange_callback()
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_ip(v):
|
||||||
|
parts = v.split('.')[:4]
|
||||||
|
if len(parts) < 4:
|
||||||
|
parts += ['0'] * (4 - len(parts))
|
||||||
|
for i in range(4):
|
||||||
|
n = my.atoi(parts[i])
|
||||||
|
if n < 0:
|
||||||
|
n = 0
|
||||||
|
elif n > 255:
|
||||||
|
n = 255
|
||||||
|
parts[i] = str(n)
|
||||||
|
return '.'.join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_width(v):
|
||||||
|
n = my.atoi(v)
|
||||||
|
if n < 0:
|
||||||
|
n = 0
|
||||||
|
elif n > 32:
|
||||||
|
n = 32
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
class SshuttleNet(NSObject):
|
||||||
|
def subnet(self):
|
||||||
|
return getattr(self, '_k_subnet', None)
|
||||||
|
def setSubnet_(self, v):
|
||||||
|
self._k_subnet = v
|
||||||
|
config_changed()
|
||||||
|
@objc_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_validator
|
||||||
|
def validateWidth_error_(self, value, error):
|
||||||
|
#print 'validateWidth!'
|
||||||
|
return True, _validate_width(value), error
|
||||||
|
|
||||||
|
NET_ALL = 0
|
||||||
|
NET_AUTO = 1
|
||||||
|
NET_MANUAL = 2
|
||||||
|
|
||||||
|
LAT_BANDWIDTH = 0
|
||||||
|
LAT_INTERACTIVE = 1
|
||||||
|
|
||||||
|
class SshuttleServer(NSObject):
|
||||||
|
def init(self):
|
||||||
|
self = super(SshuttleServer, self).init()
|
||||||
|
config_changed()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def wantConnect(self):
|
||||||
|
return getattr(self, '_k_wantconnect', False)
|
||||||
|
def setWantConnect_(self, v):
|
||||||
|
self._k_wantconnect = v
|
||||||
|
self.setError_(None)
|
||||||
|
config_changed()
|
||||||
|
if setconnect_callback: setconnect_callback(self)
|
||||||
|
|
||||||
|
def connected(self):
|
||||||
|
return getattr(self, '_k_connected', False)
|
||||||
|
def setConnected_(self, v):
|
||||||
|
print 'setConnected of %r to %r' % (self, v)
|
||||||
|
self._k_connected = v
|
||||||
|
if v: self.setError_(None) # connected ok, so no error
|
||||||
|
config_changed()
|
||||||
|
|
||||||
|
def error(self):
|
||||||
|
return getattr(self, '_k_error', None)
|
||||||
|
def setError_(self, v):
|
||||||
|
self._k_error = v
|
||||||
|
config_changed()
|
||||||
|
|
||||||
|
def isValid(self):
|
||||||
|
if not self.host():
|
||||||
|
return False
|
||||||
|
if self.autoNets() == NET_MANUAL and not len(list(self.nets())):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def title(self):
|
||||||
|
host = self.host()
|
||||||
|
if not host:
|
||||||
|
return host
|
||||||
|
an = self.autoNets()
|
||||||
|
suffix = ""
|
||||||
|
if an == NET_ALL:
|
||||||
|
suffix = " (all traffic)"
|
||||||
|
elif an == NET_MANUAL:
|
||||||
|
n = self.nets()
|
||||||
|
suffix = ' (%d subnet%s)' % (len(n), len(n)!=1 and 's' or '')
|
||||||
|
return self.host() + suffix
|
||||||
|
def setTitle_(self, v):
|
||||||
|
# title is always auto-generated
|
||||||
|
config_changed()
|
||||||
|
|
||||||
|
def host(self):
|
||||||
|
return getattr(self, '_k_host', None)
|
||||||
|
def setHost_(self, v):
|
||||||
|
self._k_host = v
|
||||||
|
self.setTitle_(None)
|
||||||
|
config_changed()
|
||||||
|
@objc_validator
|
||||||
|
def validateHost_error_(self, value, error):
|
||||||
|
#print 'validatehost! %r %r %r' % (self, value, error)
|
||||||
|
while value.startswith('-'):
|
||||||
|
value = value[1:]
|
||||||
|
return True, value, error
|
||||||
|
|
||||||
|
def nets(self):
|
||||||
|
return getattr(self, '_k_nets', [])
|
||||||
|
def setNets_(self, v):
|
||||||
|
self._k_nets = v
|
||||||
|
self.setTitle_(None)
|
||||||
|
config_changed()
|
||||||
|
def netsHidden(self):
|
||||||
|
#print 'checking netsHidden'
|
||||||
|
return self.autoNets() != NET_MANUAL
|
||||||
|
def setNetsHidden_(self, v):
|
||||||
|
config_changed()
|
||||||
|
#print 'setting netsHidden to %r' % v
|
||||||
|
|
||||||
|
def autoNets(self):
|
||||||
|
return getattr(self, '_k_autoNets', NET_AUTO)
|
||||||
|
def setAutoNets_(self, v):
|
||||||
|
self._k_autoNets = v
|
||||||
|
self.setNetsHidden_(-1)
|
||||||
|
self.setUseDns_(v == NET_ALL)
|
||||||
|
self.setTitle_(None)
|
||||||
|
config_changed()
|
||||||
|
|
||||||
|
def autoHosts(self):
|
||||||
|
return getattr(self, '_k_autoHosts', True)
|
||||||
|
def setAutoHosts_(self, v):
|
||||||
|
self._k_autoHosts = v
|
||||||
|
config_changed()
|
||||||
|
|
||||||
|
def useDns(self):
|
||||||
|
return getattr(self, '_k_useDns', False)
|
||||||
|
def setUseDns_(self, v):
|
||||||
|
self._k_useDns = v
|
||||||
|
config_changed()
|
||||||
|
|
||||||
|
def latencyControl(self):
|
||||||
|
return getattr(self, '_k_latencyControl', LAT_INTERACTIVE)
|
||||||
|
def setLatencyControl_(self, v):
|
||||||
|
self._k_latencyControl = v
|
||||||
|
config_changed()
|
62
ui-macos/my.py
Normal file
62
ui-macos/my.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import sys, os
|
||||||
|
from AppKit import *
|
||||||
|
import PyObjCTools.AppHelper
|
||||||
|
|
||||||
|
|
||||||
|
def bundle_path(name, typ):
|
||||||
|
if typ:
|
||||||
|
return NSBundle.mainBundle().pathForResource_ofType_(name, typ)
|
||||||
|
else:
|
||||||
|
return os.path.join(NSBundle.mainBundle().resourcePath(), name)
|
||||||
|
|
||||||
|
|
||||||
|
# Load an NSData using a python string
|
||||||
|
def Data(s):
|
||||||
|
return NSData.alloc().initWithBytes_length_(s, len(s))
|
||||||
|
|
||||||
|
|
||||||
|
# Load a property list from a file in the application bundle.
|
||||||
|
def PList(name):
|
||||||
|
path = bundle_path(name, 'plist')
|
||||||
|
return NSDictionary.dictionaryWithContentsOfFile_(path)
|
||||||
|
|
||||||
|
|
||||||
|
# Load an NSImage from a file in the application bundle.
|
||||||
|
def Image(name, ext):
|
||||||
|
bytes = open(bundle_path(name, ext)).read()
|
||||||
|
img = NSImage.alloc().initWithData_(Data(bytes))
|
||||||
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
# Return the NSUserDefaults shared object.
|
||||||
|
def Defaults():
|
||||||
|
return NSUserDefaults.standardUserDefaults()
|
||||||
|
|
||||||
|
|
||||||
|
# Usage:
|
||||||
|
# f = DelayedCallback(func, args...)
|
||||||
|
# later:
|
||||||
|
# f()
|
||||||
|
#
|
||||||
|
# When you call f(), it will schedule a call to func() next time the
|
||||||
|
# ObjC event loop iterates. Multiple calls to f() in a single iteration
|
||||||
|
# will only result in one call to func().
|
||||||
|
#
|
||||||
|
def DelayedCallback(func, *args, **kwargs):
|
||||||
|
flag = [0]
|
||||||
|
def _go():
|
||||||
|
if flag[0]:
|
||||||
|
print 'running %r (flag=%r)' % (func, flag)
|
||||||
|
flag[0] = 0
|
||||||
|
func(*args, **kwargs)
|
||||||
|
def call():
|
||||||
|
flag[0] += 1
|
||||||
|
PyObjCTools.AppHelper.callAfter(_go)
|
||||||
|
return call
|
||||||
|
|
||||||
|
|
||||||
|
def atoi(s):
|
||||||
|
try:
|
||||||
|
return int(s)
|
||||||
|
except ValueError:
|
||||||
|
return 0
|
4
ui-macos/run.do
Normal file
4
ui-macos/run.do
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
redo-ifchange debug.app
|
||||||
|
exec >&2
|
||||||
|
./debug.app/Contents/MacOS/Sshuttle
|
||||||
|
|
14
ui-macos/sources.list.do
Normal file
14
ui-macos/sources.list.do
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
redo-always
|
||||||
|
exec >$3
|
||||||
|
cat <<-EOF
|
||||||
|
app.icns
|
||||||
|
MainMenu.nib English.lproj/MainMenu.nib
|
||||||
|
UserDefaults.plist
|
||||||
|
chicken-tiny.png
|
||||||
|
chicken-tiny-bw.png
|
||||||
|
chicken-tiny-err.png
|
||||||
|
EOF
|
||||||
|
for d in *.py sshuttle/*.py sshuttle/sshuttle sshuttle/compat/*.py; do
|
||||||
|
echo $d
|
||||||
|
done
|
||||||
|
redo-stamp <$3
|
1
ui-macos/sshuttle
Symbolic link
1
ui-macos/sshuttle
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
..
|
1
version/.gitattributes
vendored
Normal file
1
version/.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
gitvars.pre export-subst
|
3
version/.gitignore
vendored
Normal file
3
version/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/vars
|
||||||
|
/gitvars
|
||||||
|
/_version.py
|
1
version/__init__.py
Normal file
1
version/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from _version import COMMIT, TAG, DATE
|
3
version/_version.py.do
Normal file
3
version/_version.py.do
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
redo-ifchange vars
|
||||||
|
cat vars
|
||||||
|
|
2
version/all.do
Normal file
2
version/all.do
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
redo-ifchange vars _version.py
|
||||||
|
|
3
version/clean.do
Normal file
3
version/clean.do
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
rm -f *~ .*~ *.pyc _version.py vars gitvars
|
||||||
|
|
||||||
|
|
28
version/gitvars.do
Normal file
28
version/gitvars.do
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
redo-ifchange gitvars.pre prodname
|
||||||
|
|
||||||
|
read PROD <prodname
|
||||||
|
exec >$3
|
||||||
|
|
||||||
|
# Fix each line from gitvars.pre where git may or may not have already
|
||||||
|
# substituted the variables. If someone generated a tarball with 'git archive',
|
||||||
|
# then the data will have been substituted already. If we're in a checkout of
|
||||||
|
# the git repo, then it won't, but we can just ask git to do the substitutions
|
||||||
|
# right now.
|
||||||
|
while read line; do
|
||||||
|
# Lines *may* be of the form: $Format: ... $
|
||||||
|
x=${line#\$Format:} # remove prefix
|
||||||
|
if [ "$x" != "$line" ]; then
|
||||||
|
# git didn't substitute it
|
||||||
|
redo-always # git this from the git repo
|
||||||
|
x=${x%\$} # remove trailing $
|
||||||
|
if [ "$x" = "%d" ]; then
|
||||||
|
tag=$(git describe --match="$PROD-*")
|
||||||
|
x="(tag: $tag)"
|
||||||
|
else
|
||||||
|
x=$(git log -1 --pretty=format:"$x")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo "$x"
|
||||||
|
done <gitvars.pre
|
||||||
|
|
||||||
|
redo-stamp <$3
|
3
version/gitvars.pre
Normal file
3
version/gitvars.pre
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
$Format:%H$
|
||||||
|
$Format:%d$
|
||||||
|
$Format:%ci$
|
1
version/prodname
Normal file
1
version/prodname
Normal file
@ -0,0 +1 @@
|
|||||||
|
sshuttle
|
40
version/vars.do
Normal file
40
version/vars.do
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
redo-ifchange gitvars prodname
|
||||||
|
|
||||||
|
read PROD <prodname
|
||||||
|
|
||||||
|
exec <gitvars
|
||||||
|
read COMMIT
|
||||||
|
read NAMES
|
||||||
|
read DATE
|
||||||
|
|
||||||
|
# the list of names is of the form:
|
||||||
|
# (x,y,tag: $PROD-####,tag: $PROD-####,a,b)
|
||||||
|
# The entries we want are the ones starting with "tag: $PROD-" since those
|
||||||
|
# refer to the right actual git tags.
|
||||||
|
names_to_tag()
|
||||||
|
{
|
||||||
|
x=${1#\(}
|
||||||
|
x=${x%\)}
|
||||||
|
cur=
|
||||||
|
while [ "$cur" != "$x" ]; do
|
||||||
|
x=${x# }
|
||||||
|
x=${x#tag: }
|
||||||
|
cur=${x%%,*}
|
||||||
|
tagpost=${cur#$PROD-}
|
||||||
|
if [ "$cur" != "$tagpost" ]; then
|
||||||
|
echo "$tagpost"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
x=${x#*,}
|
||||||
|
done
|
||||||
|
commitpost=${COMMIT#???????}
|
||||||
|
commitpre=${COMMIT%$commitpost}
|
||||||
|
echo "unknown-$commitpre"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
sTAG=$(names_to_tag "$NAMES")
|
||||||
|
|
||||||
|
echo "COMMIT='$COMMIT'"
|
||||||
|
echo "TAG='$sTAG'"
|
||||||
|
echo "DATE='${DATE%% *}'"
|
Reference in New Issue
Block a user