mirror of
https://github.com/sshuttle/sshuttle.git
synced 2025-07-08 02:27:27 +02:00
Compare commits
11 Commits
sshuttle-0
...
sshuttle-0
Author | SHA1 | Date | |
---|---|---|---|
6e336c09bf | |||
f950a3800b | |||
8b4466b802 | |||
4bf4f70c67 | |||
410b9d4229 | |||
2ef1c6a4c4 | |||
b35cfbd022 | |||
dcba684766 | |||
ee74110cff | |||
5bf8687ce3 | |||
6bdb9517fd |
12
README.md
12
README.md
@ -1,14 +1,10 @@
|
||||
sshuttle: where transparent proxy meets VPN meets ssh
|
||||
=====================================================
|
||||
|
||||
I just spent an afternoon working on a new kind of VPN. You can get
|
||||
the first release, <a href="http://github.com/apenwarr/sshuttle">sshuttle
|
||||
0.10, on github</a>.
|
||||
|
||||
As far as I know, sshuttle is the only program that solves the following
|
||||
common case:
|
||||
|
||||
- Your client machine (or router) is Linux.
|
||||
- Your client machine (or router) is Linux, FreeBSD, or MacOS.
|
||||
|
||||
- You have access to a remote network via ssh.
|
||||
|
||||
@ -53,10 +49,8 @@ This is how you use it:
|
||||
-----------------------
|
||||
|
||||
- <tt>git clone git://github.com/apenwarr/sshuttle</tt>
|
||||
on your client and server machines. The server can be
|
||||
any ssh server with python available; the client must
|
||||
be Linux with iptables, and you'll need root or sudo
|
||||
access.
|
||||
on your client machine. You'll need root or sudo
|
||||
access, and python needs to be installed.
|
||||
|
||||
- <tt>./sshuttle -r username@sshserver 0.0.0.0/0 -vv</tt>
|
||||
|
||||
|
@ -92,7 +92,7 @@ class FirewallClient:
|
||||
raise Fatal('cleanup: %r returned %d' % (self.argv, rv))
|
||||
|
||||
|
||||
def _main(listener, fw, use_server, remotename, seed_hosts, auto_nets):
|
||||
def _main(listener, fw, use_server, remotename, python, seed_hosts, auto_nets):
|
||||
handlers = []
|
||||
if use_server:
|
||||
if helpers.verbose >= 1:
|
||||
@ -100,7 +100,7 @@ def _main(listener, fw, use_server, remotename, seed_hosts, auto_nets):
|
||||
else:
|
||||
helpers.logprefix = 'client: '
|
||||
debug1('connecting to server...\n')
|
||||
(serverproc, serversock) = ssh.connect(remotename)
|
||||
(serverproc, serversock) = ssh.connect(remotename, python)
|
||||
mux = Mux(serversock, serversock)
|
||||
handlers.append(mux)
|
||||
|
||||
@ -188,7 +188,7 @@ def _main(listener, fw, use_server, remotename, seed_hosts, auto_nets):
|
||||
mux.check_fullness()
|
||||
|
||||
|
||||
def main(listenip, use_server, remotename, seed_hosts, auto_nets,
|
||||
def main(listenip, use_server, remotename, python, seed_hosts, auto_nets,
|
||||
subnets_include, subnets_exclude):
|
||||
debug1('Starting sshuttle proxy.\n')
|
||||
listener = socket.socket()
|
||||
@ -220,6 +220,6 @@ def main(listenip, use_server, remotename, seed_hosts, auto_nets,
|
||||
|
||||
try:
|
||||
return _main(listener, fw, use_server, remotename,
|
||||
seed_hosts, auto_nets)
|
||||
python, seed_hosts, auto_nets)
|
||||
finally:
|
||||
fw.done()
|
||||
|
17
firewall.py
17
firewall.py
@ -65,14 +65,19 @@ def do_iptables(port, subnets):
|
||||
def ipfw_rule_exists(n):
|
||||
argv = ['ipfw', 'list']
|
||||
p = subprocess.Popen(argv, stdout = subprocess.PIPE)
|
||||
found = False
|
||||
for line in p.stdout:
|
||||
if line.startswith('%05d ' % n):
|
||||
if line.find('ipttl 42') < 0 and line.find('established') < 0:
|
||||
if not ('ipttl 42 setup keep-state' in line
|
||||
or ('skipto %d' % (n+1)) in line
|
||||
or 'check-state' in line):
|
||||
log('non-sshuttle ipfw rule: %r\n' % line.strip())
|
||||
raise Fatal('non-sshuttle ipfw rule #%d already exists!' % n)
|
||||
return True
|
||||
found = True
|
||||
rv = p.wait()
|
||||
if rv:
|
||||
raise Fatal('%r returned %d' % (argv, rv))
|
||||
return found
|
||||
|
||||
|
||||
def sysctl_get(name):
|
||||
@ -124,10 +129,10 @@ def do_ipfw(port, subnets):
|
||||
|
||||
if subnets:
|
||||
sysctl_set('net.inet.ip.fw.enable', 1)
|
||||
sysctl_set('net.inet.ip.forwarding', 1)
|
||||
sysctl_set('net.inet.ip.scopedroute', 0)
|
||||
|
||||
ipfw('add', sport, 'accept', 'ip',
|
||||
'from', 'any', 'to', 'any', 'established')
|
||||
ipfw('add', sport, 'check-state', 'ip',
|
||||
'from', 'any', 'to', 'any')
|
||||
|
||||
# create new subnet entries
|
||||
for swidth,sexclude,snet in sorted(subnets, reverse=True):
|
||||
@ -139,7 +144,7 @@ def do_ipfw(port, subnets):
|
||||
ipfw('add', sport, 'fwd', '127.0.0.1,%d' % port,
|
||||
'log', 'tcp',
|
||||
'from', 'any', 'to', '%s/%s' % (snet,swidth),
|
||||
'not', 'ipttl', '42')
|
||||
'not', 'ipttl', '42', 'keep-state', 'setup')
|
||||
|
||||
|
||||
def program_exists(name):
|
||||
|
@ -1,4 +1,4 @@
|
||||
import subprocess, time, socket, re, select
|
||||
import subprocess, time, socket, re, select, errno
|
||||
if not globals().get('skip_imports'):
|
||||
import helpers
|
||||
from helpers import *
|
||||
|
10
main.py
10
main.py
@ -49,17 +49,18 @@ sshuttle [-l [ip:]port] [-r [username@]sshserver[:port]] <subnets...>
|
||||
sshuttle --firewall <port> <subnets...>
|
||||
sshuttle --server
|
||||
--
|
||||
l,listen= transproxy to this ip address and port number [default=0]
|
||||
l,listen= transproxy to this ip address and port number [0.0.0.0:0]
|
||||
H,auto-hosts scan for remote hostnames and update local /etc/hosts
|
||||
N,auto-nets automatically determine subnets to route
|
||||
python= specify the name/path of the python interpreter on the remote server [python]
|
||||
r,remote= ssh hostname (and optional username) of remote sshuttle server
|
||||
x,exclude= exclude this subnet (can be used more than once)
|
||||
v,verbose increase debug message verbosity
|
||||
seed-hosts= with -H, use these hostnames for initial scan (comma-separated)
|
||||
noserver don't use a separate server process (mostly for debugging)
|
||||
server [internal use only]
|
||||
firewall [internal use only]
|
||||
hostwatch [internal use only]
|
||||
server (internal use only)
|
||||
firewall (internal use only)
|
||||
hostwatch (internal use only)
|
||||
"""
|
||||
o = options.Options('sshuttle', optspec)
|
||||
(opt, flags, extra) = o.parse(sys.argv[1:])
|
||||
@ -99,6 +100,7 @@ try:
|
||||
sys.exit(client.main(parse_ipport(opt.listen or '0.0.0.0:0'),
|
||||
not opt.noserver,
|
||||
remotename,
|
||||
(opt.python or "python"),
|
||||
sh,
|
||||
opt.auto_nets,
|
||||
parse_subnets(includes),
|
||||
|
147
options.py
147
options.py
@ -1,30 +1,94 @@
|
||||
import sys, textwrap, getopt, re
|
||||
"""Command-line options parser.
|
||||
With the help of an options spec string, easily parse command-line options.
|
||||
"""
|
||||
import sys, os, textwrap, getopt, re, struct
|
||||
|
||||
class OptDict:
|
||||
def __init__(self):
|
||||
self._opts = {}
|
||||
|
||||
def __setitem__(self, k, v):
|
||||
if k.startswith('no-') or k.startswith('no_'):
|
||||
k = k[3:]
|
||||
v = not v
|
||||
self._opts[k] = v
|
||||
|
||||
|
||||
def __getitem__(self, k):
|
||||
if k.startswith('no-') or k.startswith('no_'):
|
||||
return not self._opts[k[3:]]
|
||||
return self._opts[k]
|
||||
|
||||
def __getattr__(self, k):
|
||||
return self[k]
|
||||
|
||||
|
||||
def _default_onabort(msg):
|
||||
sys.exit(97)
|
||||
|
||||
|
||||
def _intify(v):
|
||||
try:
|
||||
vv = int(v or '')
|
||||
if str(vv) == v:
|
||||
return vv
|
||||
except ValueError:
|
||||
pass
|
||||
return v
|
||||
|
||||
|
||||
def _atoi(v):
|
||||
try:
|
||||
return int(v or 0)
|
||||
except ValueError:
|
||||
return 0
|
||||
|
||||
|
||||
def _remove_negative_kv(k, v):
|
||||
if k.startswith('no-') or k.startswith('no_'):
|
||||
return k[3:], not v
|
||||
return k,v
|
||||
|
||||
def _remove_negative_k(k):
|
||||
return _remove_negative_kv(k, None)[0]
|
||||
|
||||
|
||||
def _tty_width():
|
||||
s = struct.pack("HHHH", 0, 0, 0, 0)
|
||||
try:
|
||||
import fcntl, termios
|
||||
s = fcntl.ioctl(sys.stderr.fileno(), termios.TIOCGWINSZ, s)
|
||||
except (IOError, ImportError):
|
||||
return _atoi(os.environ.get('WIDTH')) or 70
|
||||
(ysize,xsize,ypix,xpix) = struct.unpack('HHHH', s)
|
||||
return xsize
|
||||
|
||||
|
||||
class Options:
|
||||
def __init__(self, exe, optspec, optfunc=getopt.gnu_getopt):
|
||||
"""Option parser.
|
||||
When constructed, two strings are mandatory. The first one is the command
|
||||
name showed before error messages. The second one is a string called an
|
||||
optspec that specifies the synopsis and option flags and their description.
|
||||
For more information about optspecs, consult the bup-options(1) man page.
|
||||
|
||||
Two optional arguments specify an alternative parsing function and an
|
||||
alternative behaviour on abort (after having output the usage string).
|
||||
|
||||
By default, the parser function is getopt.gnu_getopt, and the abort
|
||||
behaviour is to exit the program.
|
||||
"""
|
||||
def __init__(self, exe, optspec, optfunc=getopt.gnu_getopt,
|
||||
onabort=_default_onabort):
|
||||
self.exe = exe
|
||||
self.optspec = optspec
|
||||
self._onabort = onabort
|
||||
self.optfunc = optfunc
|
||||
self._aliases = {}
|
||||
self._shortopts = 'h?'
|
||||
self._longopts = ['help']
|
||||
self._hasparms = {}
|
||||
self._defaults = {}
|
||||
self._usagestr = self._gen_usage()
|
||||
|
||||
|
||||
def _gen_usage(self):
|
||||
out = []
|
||||
lines = self.optspec.strip().split('\n')
|
||||
@ -36,10 +100,13 @@ class Options:
|
||||
out.append('%s: %s\n' % (first_syn and 'usage' or ' or', l))
|
||||
first_syn = False
|
||||
out.append('\n')
|
||||
last_was_option = False
|
||||
while lines:
|
||||
l = lines.pop()
|
||||
if l.startswith(' '):
|
||||
out.append('\n%s\n' % l.lstrip())
|
||||
out.append('%s%s\n' % (last_was_option and '\n' or '',
|
||||
l.lstrip()))
|
||||
last_was_option = False
|
||||
elif l:
|
||||
(flags, extra) = l.split(' ', 1)
|
||||
extra = extra.strip()
|
||||
@ -48,18 +115,24 @@ class Options:
|
||||
has_parm = 1
|
||||
else:
|
||||
has_parm = 0
|
||||
g = re.search(r'\[([^\]]*)\]$', extra)
|
||||
if g:
|
||||
defval = g.group(1)
|
||||
else:
|
||||
defval = None
|
||||
flagl = flags.split(',')
|
||||
flagl_nice = []
|
||||
for f in flagl:
|
||||
f_nice = re.sub(r'\W', '_', f)
|
||||
self._aliases[f] = flagl[0]
|
||||
self._aliases[f_nice] = flagl[0]
|
||||
f,dvi = _remove_negative_kv(f, _intify(defval))
|
||||
self._aliases[f] = _remove_negative_k(flagl[0])
|
||||
self._hasparms[f] = has_parm
|
||||
self._defaults[f] = dvi
|
||||
if len(f) == 1:
|
||||
self._shortopts += f + (has_parm and ':' or '')
|
||||
flagl_nice.append('-' + f)
|
||||
else:
|
||||
assert(not f.startswith('no-')) # supported implicitly
|
||||
f_nice = re.sub(r'\W', '_', f)
|
||||
self._aliases[f_nice] = _remove_negative_k(flagl[0])
|
||||
self._longopts.append(f + (has_parm and '=' or ''))
|
||||
self._longopts.append('no-' + f)
|
||||
flagl_nice.append('--' + f)
|
||||
@ -67,52 +140,62 @@ class Options:
|
||||
if has_parm:
|
||||
flags_nice += ' ...'
|
||||
prefix = ' %-20s ' % flags_nice
|
||||
argtext = '\n'.join(textwrap.wrap(extra, width=70,
|
||||
argtext = '\n'.join(textwrap.wrap(extra, width=_tty_width(),
|
||||
initial_indent=prefix,
|
||||
subsequent_indent=' '*28))
|
||||
out.append(argtext + '\n')
|
||||
last_was_option = True
|
||||
else:
|
||||
out.append('\n')
|
||||
last_was_option = False
|
||||
return ''.join(out).rstrip() + '\n'
|
||||
|
||||
def usage(self):
|
||||
|
||||
def usage(self, msg=""):
|
||||
"""Print usage string to stderr and abort."""
|
||||
sys.stderr.write(self._usagestr)
|
||||
sys.exit(97)
|
||||
e = self._onabort and self._onabort(msg) or None
|
||||
if e:
|
||||
raise e
|
||||
|
||||
def fatal(self, s):
|
||||
sys.stderr.write('error: %s\n' % s)
|
||||
return self.usage()
|
||||
|
||||
"""Print an error message to stderr and abort with usage string."""
|
||||
msg = 'error: %s\n' % s
|
||||
sys.stderr.write(msg)
|
||||
return self.usage(msg)
|
||||
|
||||
def parse(self, args):
|
||||
"""Parse a list of arguments and return (options, flags, extra).
|
||||
|
||||
In the returned tuple, "options" is an OptDict with known options,
|
||||
"flags" is a list of option flags that were used on the command-line,
|
||||
and "extra" is a list of positional arguments.
|
||||
"""
|
||||
try:
|
||||
(flags,extra) = self.optfunc(args, self._shortopts, self._longopts)
|
||||
except getopt.GetoptError, e:
|
||||
self.fatal(e)
|
||||
|
||||
opt = OptDict()
|
||||
for f in self._aliases.values():
|
||||
opt[f] = None
|
||||
|
||||
for k,v in self._defaults.iteritems():
|
||||
k = self._aliases[k]
|
||||
opt[k] = v
|
||||
|
||||
for (k,v) in flags:
|
||||
while k.startswith('-'):
|
||||
k = k[1:]
|
||||
if k in ['h', '?', 'help']:
|
||||
k = k.lstrip('-')
|
||||
if k in ('h', '?', 'help'):
|
||||
self.usage()
|
||||
if k.startswith('no-'):
|
||||
k = self._aliases[k[3:]]
|
||||
opt[k] = None
|
||||
v = 0
|
||||
else:
|
||||
k = self._aliases[k]
|
||||
if not self._hasparms[k]:
|
||||
assert(v == '')
|
||||
opt[k] = (opt._opts.get(k) or 0) + 1
|
||||
v = (opt._opts.get(k) or 0) + 1
|
||||
else:
|
||||
try:
|
||||
vv = int(v)
|
||||
if str(vv) == v:
|
||||
v = vv
|
||||
except ValueError:
|
||||
pass
|
||||
opt[k] = v
|
||||
for (f1,f2) in self._aliases.items():
|
||||
opt[f1] = opt[f2]
|
||||
v = _intify(v)
|
||||
opt[k] = v
|
||||
for (f1,f2) in self._aliases.iteritems():
|
||||
opt[f1] = opt._opts.get(f2)
|
||||
return (opt,flags,extra)
|
||||
|
@ -57,7 +57,8 @@ def _list_routes():
|
||||
routes.append((socket.inet_ntoa(struct.pack('!I', ip)), width))
|
||||
rv = p.wait()
|
||||
if rv != 0:
|
||||
raise Fatal('%r returned %d' % (argv, rv))
|
||||
log('WARNING: %r returned %d\n' % (argv, rv))
|
||||
log('WARNING: That prevents --auto-nets from working.\n')
|
||||
return routes
|
||||
|
||||
|
||||
|
6
ssh.py
6
ssh.py
@ -19,7 +19,7 @@ def empackage(z, filename):
|
||||
return '%s\n%d\n%s' % (filename,len(content), content)
|
||||
|
||||
|
||||
def connect(rhostport):
|
||||
def connect(rhostport, python):
|
||||
main_exe = sys.argv[0]
|
||||
l = (rhostport or '').split(':', 1)
|
||||
rhost = l[0]
|
||||
@ -48,9 +48,9 @@ def connect(rhostport):
|
||||
|
||||
|
||||
if not rhost:
|
||||
argv = ['python', '-c', pyscript]
|
||||
argv = [python, '-c', pyscript]
|
||||
else:
|
||||
argv = ['ssh'] + portl + [rhost, '--', "python -c '%s'" % pyscript]
|
||||
argv = ['ssh'] + portl + [rhost, '--', "'%s' -c '%s'" % (python, pyscript)]
|
||||
(s1,s2) = socket.socketpair()
|
||||
def setup():
|
||||
# runs in the child process
|
||||
|
4
ssnet.py
4
ssnet.py
@ -35,7 +35,7 @@ def _nb_clean(func, *args):
|
||||
try:
|
||||
return func(*args)
|
||||
except OSError, e:
|
||||
if e.errno not in (errno.EWOULDBLOCK, errno.EAGAIN):
|
||||
if e.errno not in (errno.EWOULDBLOCK, errno.EAGAIN, errno.EPIPE):
|
||||
raise
|
||||
else:
|
||||
return None
|
||||
@ -308,7 +308,7 @@ class Mux(Handler):
|
||||
self.wsock.setblocking(False)
|
||||
if self.outbuf and self.outbuf[0]:
|
||||
wrote = _nb_clean(os.write, self.wsock.fileno(), self.outbuf[0])
|
||||
debug2('mux wrote: %d/%d\n' % (wrote, len(self.outbuf[0])))
|
||||
debug2('mux wrote: %r/%d\n' % (wrote, len(self.outbuf[0])))
|
||||
if wrote:
|
||||
self.outbuf[0] = self.outbuf[0][wrote:]
|
||||
while self.outbuf and not self.outbuf[0]:
|
||||
|
Reference in New Issue
Block a user