mirror of
https://github.com/sshuttle/sshuttle.git
synced 2025-07-04 16:50:34 +02:00
Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
a8b288338b | |||
6fd02933dd | |||
7ea2d973c7 | |||
8ec5d1a5ac | |||
4241381d82 | |||
6e15e69029 | |||
8fa45885cc | |||
b8160c4a37 | |||
05bacf6fd6 | |||
dea3f21943 | |||
d522d1e1bd | |||
3541e4bdfe | |||
efdb9b8f94 | |||
7875d1b97a | |||
2b0d0065c7 | |||
9e3f02c199 | |||
8bdefcd10d | |||
29b6e8301f |
@ -1,6 +1,8 @@
|
|||||||
language: python
|
language: python
|
||||||
python:
|
python:
|
||||||
|
- 2.6
|
||||||
- 2.7
|
- 2.7
|
||||||
|
- 3.4
|
||||||
- 3.5
|
- 3.5
|
||||||
- pypy
|
- pypy
|
||||||
|
|
||||||
|
25
CHANGES.rst
25
CHANGES.rst
@ -1,3 +1,28 @@
|
|||||||
|
Release 0.78.0 (Apr 8, 2016)
|
||||||
|
============================
|
||||||
|
|
||||||
|
* Don't force IPv6 if IPv6 nameservers supplied. Fixes #74.
|
||||||
|
* Call /bin/sh as users shell may not be POSIX compliant. Fixes #77.
|
||||||
|
* Use argparse for command line processing. Fixes #75.
|
||||||
|
* Remove useless --server option.
|
||||||
|
* Support multiple -s (subnet) options. Fixes #86.
|
||||||
|
* Make server parts work with old versions of Python. Fixes #81.
|
||||||
|
|
||||||
|
|
||||||
|
Release 0.77.2 (Mar 7, 2016)
|
||||||
|
============================
|
||||||
|
|
||||||
|
* Accidentally switched LGPL2 license with GPL2 license in 0.77.1 - now fixed.
|
||||||
|
|
||||||
|
|
||||||
|
Release 0.77.1 (Mar 7, 2016)
|
||||||
|
============================
|
||||||
|
|
||||||
|
* Use semantic versioning. http://semver.org/
|
||||||
|
* Update GPL 2 license text.
|
||||||
|
* New release to fix PyPI.
|
||||||
|
|
||||||
|
|
||||||
Release 0.77 (Mar 3, 2016)
|
Release 0.77 (Mar 3, 2016)
|
||||||
==========================
|
==========================
|
||||||
|
|
||||||
|
20
LICENSE
20
LICENSE
@ -1,15 +1,15 @@
|
|||||||
GNU LIBRARY GENERAL PUBLIC LICENSE
|
GNU LIBRARY GENERAL PUBLIC LICENSE
|
||||||
Version 2, June 1991
|
Version 2, June 1991
|
||||||
|
|
||||||
Copyright (C) 1991 Free Software Foundation, Inc.
|
Copyright (C) 1991 Free Software Foundation, Inc.
|
||||||
675 Mass Ave, Cambridge, MA 02139, USA
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
of this license document, but changing it is not allowed.
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
[This is the first released version of the library GPL. It is
|
[This is the first released version of the library GPL. It is
|
||||||
numbered 2 because it goes with version 2 of the ordinary GPL.]
|
numbered 2 because it goes with version 2 of the ordinary GPL.]
|
||||||
|
|
||||||
Preamble
|
Preamble
|
||||||
|
|
||||||
The licenses for most software are designed to take away your
|
The licenses for most software are designed to take away your
|
||||||
freedom to share and change it. By contrast, the GNU General Public
|
freedom to share and change it. By contrast, the GNU General Public
|
||||||
@ -99,7 +99,7 @@ works together with the library.
|
|||||||
Note that it is possible for a library to be covered by the ordinary
|
Note that it is possible for a library to be covered by the ordinary
|
||||||
General Public License rather than by this special one.
|
General Public License rather than by this special one.
|
||||||
|
|
||||||
GNU LIBRARY GENERAL PUBLIC LICENSE
|
GNU LIBRARY GENERAL PUBLIC LICENSE
|
||||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
0. This License Agreement applies to any software library which
|
0. This License Agreement applies to any software library which
|
||||||
@ -411,7 +411,7 @@ decision will be guided by the two goals of preserving the free status
|
|||||||
of all derivatives of our free software and of promoting the sharing
|
of all derivatives of our free software and of promoting the sharing
|
||||||
and reuse of software generally.
|
and reuse of software generally.
|
||||||
|
|
||||||
NO WARRANTY
|
NO WARRANTY
|
||||||
|
|
||||||
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
|
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
|
||||||
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
||||||
@ -434,9 +434,9 @@ FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
|
|||||||
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||||
DAMAGES.
|
DAMAGES.
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
Appendix: How to Apply These Terms to Your New Libraries
|
How to Apply These Terms to Your New Libraries
|
||||||
|
|
||||||
If you develop a new library, and you want it to be of the greatest
|
If you develop a new library, and you want it to be of the greatest
|
||||||
possible use to the public, we recommend making it free software that
|
possible use to the public, we recommend making it free software that
|
||||||
@ -463,8 +463,8 @@ convey the exclusion of warranty; and each file should have at least the
|
|||||||
Library General Public License for more details.
|
Library General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU Library General Public
|
You should have received a copy of the GNU Library General Public
|
||||||
License along with this library; if not, write to the Free
|
License along with this library; if not, write to the Free Software
|
||||||
Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
10
conftest.py
Normal file
10
conftest.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 0):
|
||||||
|
good_python = sys.version_info >= (3, 5)
|
||||||
|
else:
|
||||||
|
good_python = sys.version_info >= (2, 7)
|
||||||
|
|
||||||
|
collect_ignore = []
|
||||||
|
if not good_python:
|
||||||
|
collect_ignore.append("sshuttle/tests/client")
|
@ -62,6 +62,7 @@ cmd.exe with Administrator access. See :doc:`windows` for more information.
|
|||||||
|
|
||||||
Server side Requirements
|
Server side Requirements
|
||||||
------------------------
|
------------------------
|
||||||
|
Server requirements are more relaxed, however it is recommended that you use
|
||||||
Python 2.7 or Python 3.5.
|
Python 2.7 or Python 3.5.
|
||||||
|
|
||||||
|
|
||||||
|
2
run
2
run
@ -1,6 +1,8 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
if python3.5 -V 2>/dev/null; then
|
if python3.5 -V 2>/dev/null; then
|
||||||
exec python3.5 -m "sshuttle" "$@"
|
exec python3.5 -m "sshuttle" "$@"
|
||||||
|
elif python2.7 -V 2>/dev/null; then
|
||||||
|
exec python2.7 -m "sshuttle" "$@"
|
||||||
else
|
else
|
||||||
exec python -m "sshuttle" "$@"
|
exec python -m "sshuttle" "$@"
|
||||||
fi
|
fi
|
||||||
|
@ -0,0 +1,4 @@
|
|||||||
|
try:
|
||||||
|
from sshuttle.version import version as __version__
|
||||||
|
except ImportError:
|
||||||
|
__version__ = "unknown"
|
||||||
|
@ -4,19 +4,20 @@ import imp
|
|||||||
|
|
||||||
z = zlib.decompressobj()
|
z = zlib.decompressobj()
|
||||||
while 1:
|
while 1:
|
||||||
name = stdin.readline().strip()
|
name = sys.stdin.readline().strip()
|
||||||
if name:
|
if name:
|
||||||
name = name.decode("ASCII")
|
name = name.decode("ASCII")
|
||||||
|
|
||||||
nbytes = int(stdin.readline())
|
nbytes = int(sys.stdin.readline())
|
||||||
if verbosity >= 2:
|
if verbosity >= 2:
|
||||||
sys.stderr.write('server: assembling %r (%d bytes)\n'
|
sys.stderr.write('server: assembling %r (%d bytes)\n'
|
||||||
% (name, nbytes))
|
% (name, nbytes))
|
||||||
content = z.decompress(stdin.read(nbytes))
|
content = z.decompress(sys.stdin.read(nbytes))
|
||||||
|
|
||||||
module = imp.new_module(name)
|
module = imp.new_module(name)
|
||||||
parent, _, parent_name = name.rpartition(".")
|
parents = name.rsplit(".", 1)
|
||||||
if parent != "":
|
if len(parents) == 2:
|
||||||
|
parent, parent_name = parents
|
||||||
setattr(sys.modules[parent], parent_name, module)
|
setattr(sys.modules[parent], parent_name, module)
|
||||||
|
|
||||||
code = compile(content, name, "exec")
|
code = compile(content, name, "exec")
|
||||||
|
@ -547,11 +547,15 @@ def main(listenip_v6, listenip_v4,
|
|||||||
else:
|
else:
|
||||||
listenip_v6 = None
|
listenip_v6 = None
|
||||||
|
|
||||||
required.ipv6 = len(subnets_v6) > 0 or len(nslist_v6) > 0 \
|
required.ipv6 = len(subnets_v6) > 0 or listenip_v6 is not None
|
||||||
or listenip_v6 is not None
|
|
||||||
required.udp = avail.udp
|
required.udp = avail.udp
|
||||||
required.dns = len(nslist) > 0
|
required.dns = len(nslist) > 0
|
||||||
|
|
||||||
|
# if IPv6 not supported, ignore IPv6 DNS servers
|
||||||
|
if not required.ipv6:
|
||||||
|
nslist_v6 = []
|
||||||
|
nslist = nslist_v4
|
||||||
|
|
||||||
fw.method.assert_features(required)
|
fw.method.assert_features(required)
|
||||||
|
|
||||||
if required.ipv6 and listenip_v6 is None:
|
if required.ipv6 and listenip_v6 is None:
|
||||||
|
@ -1,199 +1,47 @@
|
|||||||
import sys
|
|
||||||
import re
|
import re
|
||||||
import socket
|
|
||||||
import sshuttle.helpers as helpers
|
import sshuttle.helpers as helpers
|
||||||
import sshuttle.options as options
|
|
||||||
import sshuttle.client as client
|
import sshuttle.client as client
|
||||||
import sshuttle.firewall as firewall
|
import sshuttle.firewall as firewall
|
||||||
import sshuttle.hostwatch as hostwatch
|
import sshuttle.hostwatch as hostwatch
|
||||||
import sshuttle.ssyslog as ssyslog
|
import sshuttle.ssyslog as ssyslog
|
||||||
|
from sshuttle.options import parser, parse_ipport6, parse_ipport4
|
||||||
from sshuttle.helpers import family_ip_tuple, log, Fatal
|
from sshuttle.helpers import family_ip_tuple, log, Fatal
|
||||||
|
|
||||||
|
|
||||||
# 1.2.3.4/5 or just 1.2.3.4
|
|
||||||
def parse_subnet4(s):
|
|
||||||
m = re.match(r'(\d+)(?:\.(\d+)\.(\d+)\.(\d+))?(?:/(\d+))?$', s)
|
|
||||||
if not m:
|
|
||||||
raise Fatal('%r is not a valid IP subnet format' % s)
|
|
||||||
(a, b, c, d, width) = m.groups()
|
|
||||||
(a, b, c, d) = (int(a or 0), int(b or 0), int(c or 0), int(d or 0))
|
|
||||||
if width is None:
|
|
||||||
width = 32
|
|
||||||
else:
|
|
||||||
width = int(width)
|
|
||||||
if a > 255 or b > 255 or c > 255 or d > 255:
|
|
||||||
raise Fatal('%d.%d.%d.%d has numbers > 255' % (a, b, c, d))
|
|
||||||
if width > 32:
|
|
||||||
raise Fatal('*/%d is greater than the maximum of 32' % width)
|
|
||||||
return(socket.AF_INET, '%d.%d.%d.%d' % (a, b, c, d), width)
|
|
||||||
|
|
||||||
|
|
||||||
# 1:2::3/64 or just 1:2::3
|
|
||||||
def parse_subnet6(s):
|
|
||||||
m = re.match(r'(?:([a-fA-F\d:]+))?(?:/(\d+))?$', s)
|
|
||||||
if not m:
|
|
||||||
raise Fatal('%r is not a valid IP subnet format' % s)
|
|
||||||
(net, width) = m.groups()
|
|
||||||
if width is None:
|
|
||||||
width = 128
|
|
||||||
else:
|
|
||||||
width = int(width)
|
|
||||||
if width > 128:
|
|
||||||
raise Fatal('*/%d is greater than the maximum of 128' % width)
|
|
||||||
return(socket.AF_INET6, net, width)
|
|
||||||
|
|
||||||
|
|
||||||
# Subnet file, supporting empty lines and hash-started comment lines
|
|
||||||
def parse_subnet_file(s):
|
|
||||||
try:
|
|
||||||
handle = open(s, 'r')
|
|
||||||
except OSError:
|
|
||||||
raise Fatal('Unable to open subnet file: %s' % s)
|
|
||||||
|
|
||||||
raw_config_lines = handle.readlines()
|
|
||||||
config_lines = []
|
|
||||||
for line_no, line in enumerate(raw_config_lines):
|
|
||||||
line = line.strip()
|
|
||||||
if len(line) == 0:
|
|
||||||
continue
|
|
||||||
if line[0] == '#':
|
|
||||||
continue
|
|
||||||
config_lines.append(line)
|
|
||||||
|
|
||||||
return config_lines
|
|
||||||
|
|
||||||
|
|
||||||
# list of:
|
|
||||||
# 1.2.3.4/5 or just 1.2.3.4
|
|
||||||
# 1:2::3/64 or just 1:2::3
|
|
||||||
def parse_subnets(subnets_str):
|
|
||||||
subnets = []
|
|
||||||
for s in subnets_str:
|
|
||||||
if ':' in s:
|
|
||||||
subnet = parse_subnet6(s)
|
|
||||||
else:
|
|
||||||
subnet = parse_subnet4(s)
|
|
||||||
subnets.append(subnet)
|
|
||||||
return subnets
|
|
||||||
|
|
||||||
|
|
||||||
# 1.2.3.4:567 or just 1.2.3.4 or just 567
|
|
||||||
def parse_ipport4(s):
|
|
||||||
s = str(s)
|
|
||||||
m = re.match(r'(?:(\d+)\.(\d+)\.(\d+)\.(\d+))?(?::)?(?:(\d+))?$', s)
|
|
||||||
if not m:
|
|
||||||
raise Fatal('%r is not a valid IP:port format' % s)
|
|
||||||
(a, b, c, d, port) = m.groups()
|
|
||||||
(a, b, c, d, port) = (int(a or 0), int(b or 0), int(c or 0), int(d or 0),
|
|
||||||
int(port or 0))
|
|
||||||
if a > 255 or b > 255 or c > 255 or d > 255:
|
|
||||||
raise Fatal('%d.%d.%d.%d has numbers > 255' % (a, b, c, d))
|
|
||||||
if port > 65535:
|
|
||||||
raise Fatal('*:%d is greater than the maximum of 65535' % port)
|
|
||||||
if a is None:
|
|
||||||
a = b = c = d = 0
|
|
||||||
return ('%d.%d.%d.%d' % (a, b, c, d), port)
|
|
||||||
|
|
||||||
|
|
||||||
# [1:2::3]:456 or [1:2::3] or 456
|
|
||||||
def parse_ipport6(s):
|
|
||||||
s = str(s)
|
|
||||||
m = re.match(r'(?:\[([^]]*)])?(?::)?(?:(\d+))?$', s)
|
|
||||||
if not m:
|
|
||||||
raise Fatal('%s is not a valid IP:port format' % s)
|
|
||||||
(ip, port) = m.groups()
|
|
||||||
(ip, port) = (ip or '::', int(port or 0))
|
|
||||||
return (ip, port)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_list(list):
|
|
||||||
return re.split(r'[\s,]+', list.strip()) if list else []
|
|
||||||
|
|
||||||
|
|
||||||
optspec = """
|
|
||||||
sshuttle [-l [ip:]port] [-r [username@]sshserver[:port]] <subnets...>
|
|
||||||
sshuttle --firewall <port> <subnets...>
|
|
||||||
sshuttle --hostwatch
|
|
||||||
--
|
|
||||||
l,listen= transproxy to this ip address and port number
|
|
||||||
H,auto-hosts scan for remote hostnames and update local /etc/hosts
|
|
||||||
N,auto-nets automatically determine subnets to route
|
|
||||||
dns capture local DNS requests and forward to the remote DNS server
|
|
||||||
ns-hosts= capture and forward remote DNS requests to the following servers
|
|
||||||
method= auto, nat, tproxy or pf
|
|
||||||
python= path to python interpreter on the remote server
|
|
||||||
r,remote= ssh hostname (and optional username) of remote sshuttle server
|
|
||||||
x,exclude= exclude this subnet (can be used more than once)
|
|
||||||
X,exclude-from= exclude the subnets in a file (whitespace separated)
|
|
||||||
v,verbose increase debug message verbosity
|
|
||||||
V,version print the sshuttle version number and exit
|
|
||||||
e,ssh-cmd= the command to use to connect to the remote [ssh]
|
|
||||||
seed-hosts= with -H, use these hostnames for initial scan (comma-separated)
|
|
||||||
no-latency-control sacrifice latency to improve bandwidth benchmarks
|
|
||||||
wrap= restart counting channel numbers after this number (for testing)
|
|
||||||
disable-ipv6 disables ipv6 support
|
|
||||||
D,daemon run in the background as a daemon
|
|
||||||
s,subnets= file where the subnets are stored, instead of on the command line
|
|
||||||
syslog send log messages to syslog (default if you use --daemon)
|
|
||||||
pidfile= pidfile name (only if using --daemon) [./sshuttle.pid]
|
|
||||||
server (internal use only)
|
|
||||||
firewall (internal use only)
|
|
||||||
hostwatch (internal use only)
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
o = options.Options(optspec)
|
opt = parser.parse_args()
|
||||||
(opt, flags, extra) = o.parse(sys.argv[1:])
|
|
||||||
|
|
||||||
if opt.version:
|
|
||||||
from sshuttle.version import version
|
|
||||||
print(version)
|
|
||||||
return 0
|
|
||||||
if opt.daemon:
|
if opt.daemon:
|
||||||
opt.syslog = 1
|
opt.syslog = 1
|
||||||
if opt.wrap:
|
if opt.wrap:
|
||||||
import sshuttle.ssnet as ssnet
|
import sshuttle.ssnet as ssnet
|
||||||
ssnet.MAX_CHANNEL = int(opt.wrap)
|
ssnet.MAX_CHANNEL = opt.wrap
|
||||||
helpers.verbose = opt.verbose or 0
|
helpers.verbose = opt.verbose
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if opt.firewall:
|
if opt.firewall:
|
||||||
if len(extra) != 0:
|
if opt.subnets:
|
||||||
o.fatal('exactly zero arguments expected')
|
parser.error('exactly zero arguments expected')
|
||||||
return firewall.main(opt.method, opt.syslog)
|
return firewall.main(opt.method, opt.syslog)
|
||||||
elif opt.hostwatch:
|
elif opt.hostwatch:
|
||||||
return hostwatch.hw_main(extra)
|
return hostwatch.hw_main(opt.subnets)
|
||||||
else:
|
else:
|
||||||
if len(extra) < 1 and not opt.auto_nets and not opt.subnets:
|
includes = opt.subnets
|
||||||
o.fatal('at least one subnet, subnet file, or -N expected')
|
excludes = opt.exclude
|
||||||
includes = extra
|
if not includes and not opt.auto_nets:
|
||||||
excludes = ['127.0.0.0/8']
|
parser.error('at least one subnet, subnet file, or -N expected')
|
||||||
for k, v in flags:
|
|
||||||
if k in ('-x', '--exclude'):
|
|
||||||
excludes.append(v)
|
|
||||||
if k in ('-X', '--exclude-from'):
|
|
||||||
excludes += open(v).read().split()
|
|
||||||
remotename = opt.remote
|
remotename = opt.remote
|
||||||
if remotename == '' or remotename == '-':
|
if remotename == '' or remotename == '-':
|
||||||
remotename = None
|
remotename = None
|
||||||
nslist = [family_ip_tuple(ns) for ns in parse_list(opt.ns_hosts)]
|
nslist = [family_ip_tuple(ns) for ns in opt.ns_hosts]
|
||||||
if opt.seed_hosts and not opt.auto_hosts:
|
if opt.seed_hosts and not opt.auto_hosts:
|
||||||
o.fatal('--seed-hosts only works if you also use -H')
|
parser.error('--seed-hosts only works if you also use -H')
|
||||||
if opt.seed_hosts:
|
if opt.seed_hosts:
|
||||||
sh = re.split(r'[\s,]+', (opt.seed_hosts or "").strip())
|
sh = re.split(r'[\s,]+', (opt.seed_hosts or "").strip())
|
||||||
elif opt.auto_hosts:
|
elif opt.auto_hosts:
|
||||||
sh = []
|
sh = []
|
||||||
else:
|
else:
|
||||||
sh = None
|
sh = None
|
||||||
if opt.subnets:
|
|
||||||
includes = parse_subnet_file(opt.subnets)
|
|
||||||
if not opt.method:
|
|
||||||
method_name = "auto"
|
|
||||||
elif opt.method in ["auto", "nat", "tproxy", "pf"]:
|
|
||||||
method_name = opt.method
|
|
||||||
else:
|
|
||||||
o.fatal("method_name %s not supported" % opt.method)
|
|
||||||
if opt.listen:
|
if opt.listen:
|
||||||
ipport_v6 = None
|
ipport_v6 = None
|
||||||
ipport_v4 = None
|
ipport_v4 = None
|
||||||
@ -218,11 +66,11 @@ def main():
|
|||||||
opt.latency_control,
|
opt.latency_control,
|
||||||
opt.dns,
|
opt.dns,
|
||||||
nslist,
|
nslist,
|
||||||
method_name,
|
opt.method,
|
||||||
sh,
|
sh,
|
||||||
opt.auto_nets,
|
opt.auto_nets,
|
||||||
parse_subnets(includes),
|
includes,
|
||||||
parse_subnets(excludes),
|
excludes,
|
||||||
opt.daemon, opt.pidfile)
|
opt.daemon, opt.pidfile)
|
||||||
|
|
||||||
if return_code == 0:
|
if return_code == 0:
|
||||||
|
@ -5,6 +5,16 @@ import errno
|
|||||||
logprefix = ''
|
logprefix = ''
|
||||||
verbose = 0
|
verbose = 0
|
||||||
|
|
||||||
|
if sys.version_info[0] == 3:
|
||||||
|
binary_type = bytes
|
||||||
|
|
||||||
|
def b(s):
|
||||||
|
return s.encode("ASCII")
|
||||||
|
else:
|
||||||
|
binary_type = str
|
||||||
|
|
||||||
|
def b(s):
|
||||||
|
return s
|
||||||
|
|
||||||
def log(s):
|
def log(s):
|
||||||
global logprefix
|
global logprefix
|
||||||
@ -70,7 +80,8 @@ def islocal(ip, family):
|
|||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
sock.bind((ip, 0))
|
sock.bind((ip, 0))
|
||||||
except socket.error as e:
|
except socket.error:
|
||||||
|
_, e = sys.exc_info()[:2]
|
||||||
if e.args[0] == errno.EADDRNOTAVAIL:
|
if e.args[0] == errno.EADDRNOTAVAIL:
|
||||||
return False # not a local IP
|
return False # not a local IP
|
||||||
else:
|
else:
|
||||||
|
@ -22,7 +22,8 @@ hostnames = {}
|
|||||||
queue = {}
|
queue = {}
|
||||||
try:
|
try:
|
||||||
null = open('/dev/null', 'wb')
|
null = open('/dev/null', 'wb')
|
||||||
except IOError as e:
|
except IOError:
|
||||||
|
_, e = sys.exc_info()[:2]
|
||||||
log('warning: %s\n' % e)
|
log('warning: %s\n' % e)
|
||||||
null = os.popen("sh -c 'while read x; do :; done'", 'wb', 4096)
|
null = os.popen("sh -c 'while read x; do :; done'", 'wb', 4096)
|
||||||
|
|
||||||
@ -38,7 +39,7 @@ def write_host_cache():
|
|||||||
for name, ip in sorted(hostnames.items()):
|
for name, ip in sorted(hostnames.items()):
|
||||||
f.write(('%s,%s\n' % (name, ip)).encode("ASCII"))
|
f.write(('%s,%s\n' % (name, ip)).encode("ASCII"))
|
||||||
f.close()
|
f.close()
|
||||||
os.chmod(tmpname, 0o600)
|
os.chmod(tmpname, 384) # 600 in octal, 'rw-------'
|
||||||
os.rename(tmpname, CACHEFILE)
|
os.rename(tmpname, CACHEFILE)
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
@ -50,7 +51,8 @@ def write_host_cache():
|
|||||||
def read_host_cache():
|
def read_host_cache():
|
||||||
try:
|
try:
|
||||||
f = open(CACHEFILE)
|
f = open(CACHEFILE)
|
||||||
except IOError as e:
|
except IOError:
|
||||||
|
_, e = sys.exc_info()[:2]
|
||||||
if e.errno == errno.ENOENT:
|
if e.errno == errno.ENOENT:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
@ -124,7 +126,8 @@ def _check_netstat():
|
|||||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null)
|
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null)
|
||||||
content = p.stdout.read().decode("ASCII")
|
content = p.stdout.read().decode("ASCII")
|
||||||
p.wait()
|
p.wait()
|
||||||
except OSError as e:
|
except OSError:
|
||||||
|
_, e = sys.exc_info()[:2]
|
||||||
log('%r failed: %r\n' % (argv, e))
|
log('%r failed: %r\n' % (argv, e))
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -144,7 +147,8 @@ def _check_smb(hostname):
|
|||||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null)
|
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null)
|
||||||
lines = p.stdout.readlines()
|
lines = p.stdout.readlines()
|
||||||
p.wait()
|
p.wait()
|
||||||
except OSError as e:
|
except OSError:
|
||||||
|
_, e = sys.exc_info()[:2]
|
||||||
log('%r failed: %r\n' % (argv, e))
|
log('%r failed: %r\n' % (argv, e))
|
||||||
_smb_ok = False
|
_smb_ok = False
|
||||||
return
|
return
|
||||||
@ -201,7 +205,8 @@ def _check_nmb(hostname, is_workgroup, is_master):
|
|||||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null)
|
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null)
|
||||||
lines = p.stdout.readlines()
|
lines = p.stdout.readlines()
|
||||||
rv = p.wait()
|
rv = p.wait()
|
||||||
except OSError as e:
|
except OSError:
|
||||||
|
_, e = sys.exc_info()[:2]
|
||||||
log('%r failed: %r\n' % (argv, e))
|
log('%r failed: %r\n' % (argv, e))
|
||||||
_nmb_ok = False
|
_nmb_ok = False
|
||||||
return
|
return
|
||||||
|
@ -1,215 +1,306 @@
|
|||||||
"""Command-line options parser.
|
|
||||||
With the help of an options spec string, easily parse command-line options.
|
|
||||||
"""
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import textwrap
|
|
||||||
import getopt
|
|
||||||
import re
|
import re
|
||||||
import struct
|
import socket
|
||||||
|
from argparse import ArgumentParser, Action, ArgumentTypeError as Fatal
|
||||||
|
from sshuttle import __version__
|
||||||
|
|
||||||
|
|
||||||
class OptDict:
|
# 1.2.3.4/5 or just 1.2.3.4
|
||||||
|
def parse_subnet4(s):
|
||||||
def __init__(self):
|
m = re.match(r'(\d+)(?:\.(\d+)\.(\d+)\.(\d+))?(?:/(\d+))?$', s)
|
||||||
self._opts = {}
|
if not m:
|
||||||
|
raise Fatal('%r is not a valid IP subnet format' % s)
|
||||||
def __setitem__(self, k, v):
|
(a, b, c, d, width) = m.groups()
|
||||||
if k.startswith('no-') or k.startswith('no_'):
|
(a, b, c, d) = (int(a or 0), int(b or 0), int(c or 0), int(d or 0))
|
||||||
k = k[3:]
|
if width is None:
|
||||||
v = not v
|
width = 32
|
||||||
self._opts[k] = v
|
else:
|
||||||
|
width = int(width)
|
||||||
def __getitem__(self, k):
|
if a > 255 or b > 255 or c > 255 or d > 255:
|
||||||
if k.startswith('no-') or k.startswith('no_'):
|
raise Fatal('%d.%d.%d.%d has numbers > 255' % (a, b, c, d))
|
||||||
return not self._opts[k[3:]]
|
if width > 32:
|
||||||
return self._opts[k]
|
raise Fatal('*/%d is greater than the maximum of 32' % width)
|
||||||
|
return(socket.AF_INET, '%d.%d.%d.%d' % (a, b, c, d), width)
|
||||||
def __getattr__(self, k):
|
|
||||||
return self[k]
|
|
||||||
|
|
||||||
|
|
||||||
def _default_onabort(msg):
|
# 1:2::3/64 or just 1:2::3
|
||||||
sys.exit(97)
|
def parse_subnet6(s):
|
||||||
|
m = re.match(r'(?:([a-fA-F\d:]+))?(?:/(\d+))?$', s)
|
||||||
|
if not m:
|
||||||
|
raise Fatal('%r is not a valid IP subnet format' % s)
|
||||||
|
(net, width) = m.groups()
|
||||||
|
if width is None:
|
||||||
|
width = 128
|
||||||
|
else:
|
||||||
|
width = int(width)
|
||||||
|
if width > 128:
|
||||||
|
raise Fatal('*/%d is greater than the maximum of 128' % width)
|
||||||
|
return(socket.AF_INET6, net, width)
|
||||||
|
|
||||||
|
|
||||||
def _intify(v):
|
# Subnet file, supporting empty lines and hash-started comment lines
|
||||||
|
def parse_subnet_file(s):
|
||||||
try:
|
try:
|
||||||
vv = int(v or '')
|
handle = open(s, 'r')
|
||||||
if str(vv) == v:
|
except OSError:
|
||||||
return vv
|
raise Fatal('Unable to open subnet file: %s' % s)
|
||||||
except ValueError:
|
|
||||||
pass
|
raw_config_lines = handle.readlines()
|
||||||
return v
|
subnets = []
|
||||||
|
for line_no, line in enumerate(raw_config_lines):
|
||||||
|
line = line.strip()
|
||||||
|
if len(line) == 0:
|
||||||
|
continue
|
||||||
|
if line[0] == '#':
|
||||||
|
continue
|
||||||
|
subnets.append(parse_subnet(line))
|
||||||
|
|
||||||
|
return subnets
|
||||||
|
|
||||||
|
|
||||||
def _atoi(v):
|
# 1.2.3.4/5 or just 1.2.3.4
|
||||||
try:
|
# 1:2::3/64 or just 1:2::3
|
||||||
return int(v or 0)
|
def parse_subnet(subnet_str):
|
||||||
except ValueError:
|
if ':' in subnet_str:
|
||||||
return 0
|
return parse_subnet6(subnet_str)
|
||||||
|
else:
|
||||||
|
return parse_subnet4(subnet_str)
|
||||||
|
|
||||||
|
|
||||||
def _remove_negative_kv(k, v):
|
# 1.2.3.4:567 or just 1.2.3.4 or just 567
|
||||||
if k.startswith('no-') or k.startswith('no_'):
|
def parse_ipport4(s):
|
||||||
return k[3:], not v
|
s = str(s)
|
||||||
return k, v
|
m = re.match(r'(?:(\d+)\.(\d+)\.(\d+)\.(\d+))?(?::)?(?:(\d+))?$', s)
|
||||||
|
if not m:
|
||||||
|
raise Fatal('%r is not a valid IP:port format' % s)
|
||||||
|
(a, b, c, d, port) = m.groups()
|
||||||
|
(a, b, c, d, port) = (int(a or 0), int(b or 0), int(c or 0), int(d or 0),
|
||||||
|
int(port or 0))
|
||||||
|
if a > 255 or b > 255 or c > 255 or d > 255:
|
||||||
|
raise Fatal('%d.%d.%d.%d has numbers > 255' % (a, b, c, d))
|
||||||
|
if port > 65535:
|
||||||
|
raise Fatal('*:%d is greater than the maximum of 65535' % port)
|
||||||
|
if a is None:
|
||||||
|
a = b = c = d = 0
|
||||||
|
return ('%d.%d.%d.%d' % (a, b, c, d), port)
|
||||||
|
|
||||||
|
|
||||||
def _remove_negative_k(k):
|
# [1:2::3]:456 or [1:2::3] or 456
|
||||||
return _remove_negative_kv(k, None)[0]
|
def parse_ipport6(s):
|
||||||
|
s = str(s)
|
||||||
|
m = re.match(r'(?:\[([^]]*)])?(?::)?(?:(\d+))?$', s)
|
||||||
|
if not m:
|
||||||
|
raise Fatal('%s is not a valid IP:port format' % s)
|
||||||
|
(ip, port) = m.groups()
|
||||||
|
(ip, port) = (ip or '::', int(port or 0))
|
||||||
|
return (ip, port)
|
||||||
|
|
||||||
|
|
||||||
def _tty_width():
|
def parse_list(list):
|
||||||
if not hasattr(sys.stderr, "fileno"):
|
return re.split(r'[\s,]+', list.strip()) if list else []
|
||||||
return _atoi(os.environ.get('WIDTH')) or 70
|
|
||||||
s = struct.pack("HHHH", 0, 0, 0, 0)
|
|
||||||
try:
|
|
||||||
import fcntl
|
|
||||||
import 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 or 70
|
|
||||||
|
|
||||||
|
|
||||||
class Options:
|
class Concat(Action):
|
||||||
|
def __init__(self, option_strings, dest, nargs=None, **kwargs):
|
||||||
|
if nargs is not None:
|
||||||
|
raise ValueError("nargs not supported")
|
||||||
|
super(Concat, self).__init__(option_strings, dest, **kwargs)
|
||||||
|
|
||||||
"""Option parser.
|
def __call__(self, parser, namespace, values, option_string=None):
|
||||||
When constructed, two strings are mandatory. The first one is the command
|
curr_value = getattr(namespace, self.dest, [])
|
||||||
name showed before error messages. The second one is a string called an
|
setattr(namespace, self.dest, curr_value + values)
|
||||||
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
|
parser = ArgumentParser(
|
||||||
behaviour is to exit the program.
|
prog="sshuttle",
|
||||||
|
usage="%(prog)s [-l [ip:]port] [-r [user@]sshserver[:port]] <subnets...>"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"subnets",
|
||||||
|
metavar="IP/MASK [IP/MASK...]",
|
||||||
|
nargs="*",
|
||||||
|
type=parse_subnet,
|
||||||
|
help="""
|
||||||
|
capture and forward traffic to these subnets (whitespace separated)
|
||||||
"""
|
"""
|
||||||
|
)
|
||||||
def __init__(self, optspec, optfunc=getopt.gnu_getopt,
|
parser.add_argument(
|
||||||
onabort=_default_onabort):
|
"-l", "--listen",
|
||||||
self.optspec = optspec
|
metavar="[IP:]PORT",
|
||||||
self._onabort = onabort
|
help="""
|
||||||
self.optfunc = optfunc
|
transproxy to this ip address and port number
|
||||||
self._aliases = {}
|
"""
|
||||||
self._shortopts = 'h?'
|
)
|
||||||
self._longopts = ['help']
|
parser.add_argument(
|
||||||
self._hasparms = {}
|
"-H", "--auto-hosts",
|
||||||
self._defaults = {}
|
action="store_true",
|
||||||
self._usagestr = self._gen_usage()
|
help="""
|
||||||
|
scan for remote hostnames and update local /etc/hosts
|
||||||
def _gen_usage(self):
|
"""
|
||||||
out = []
|
)
|
||||||
lines = self.optspec.strip().split('\n')
|
parser.add_argument(
|
||||||
lines.reverse()
|
"-N", "--auto-nets",
|
||||||
first_syn = True
|
action="store_true",
|
||||||
while lines:
|
help="""
|
||||||
l = lines.pop()
|
automatically determine subnets to route
|
||||||
if l == '--':
|
"""
|
||||||
break
|
)
|
||||||
out.append('%s: %s\n' % (first_syn and 'usage' or ' or', l))
|
parser.add_argument(
|
||||||
first_syn = False
|
"--dns",
|
||||||
out.append('\n')
|
action="store_true",
|
||||||
last_was_option = False
|
help="""
|
||||||
while lines:
|
capture local DNS requests and forward to the remote DNS server
|
||||||
l = lines.pop()
|
"""
|
||||||
if l.startswith(' '):
|
)
|
||||||
out.append('%s%s\n' % (last_was_option and '\n' or '',
|
parser.add_argument(
|
||||||
l.lstrip()))
|
"--ns-hosts",
|
||||||
last_was_option = False
|
metavar="IP[,IP]",
|
||||||
elif l:
|
default=[],
|
||||||
(flags, extra) = l.split(' ', 1)
|
type=parse_list,
|
||||||
extra = extra.strip()
|
help="""
|
||||||
if flags.endswith('='):
|
capture and forward DNS requests made to the following servers
|
||||||
flags = flags[:-1]
|
"""
|
||||||
has_parm = 1
|
)
|
||||||
else:
|
parser.add_argument(
|
||||||
has_parm = 0
|
"--method",
|
||||||
g = re.search(r'\[([^\]]*)\]$', extra)
|
choices=["auto", "nat", "tproxy", "pf"],
|
||||||
if g:
|
metavar="TYPE",
|
||||||
defval = g.group(1)
|
default="auto",
|
||||||
else:
|
help="""
|
||||||
defval = None
|
%(choices)s
|
||||||
flagl = flags.split(',')
|
"""
|
||||||
flagl_nice = []
|
)
|
||||||
for _f in flagl:
|
parser.add_argument(
|
||||||
f, dvi = _remove_negative_kv(_f, _intify(defval))
|
"--python",
|
||||||
self._aliases[f] = _remove_negative_k(flagl[0])
|
metavar="PATH",
|
||||||
self._hasparms[f] = has_parm
|
help="""
|
||||||
self._defaults[f] = dvi
|
path to python interpreter on the remote server
|
||||||
if len(f) == 1:
|
"""
|
||||||
self._shortopts += f + (has_parm and ':' or '')
|
)
|
||||||
flagl_nice.append('-' + f)
|
parser.add_argument(
|
||||||
else:
|
"-r", "--remote",
|
||||||
f_nice = re.sub(r'\W', '_', f)
|
metavar="[USERNAME@]ADDR[:PORT]",
|
||||||
self._aliases[f_nice] = _remove_negative_k(flagl[0])
|
help="""
|
||||||
self._longopts.append(f + (has_parm and '=' or ''))
|
ssh hostname (and optional username) of remote %(prog)s server
|
||||||
self._longopts.append('no-' + f)
|
"""
|
||||||
flagl_nice.append('--' + _f)
|
)
|
||||||
flags_nice = ', '.join(flagl_nice)
|
parser.add_argument(
|
||||||
if has_parm:
|
"-x", "--exclude",
|
||||||
flags_nice += ' ...'
|
metavar="IP/MASK",
|
||||||
prefix = ' %-20s ' % flags_nice
|
action="append",
|
||||||
argtext = '\n'.join(textwrap.wrap(extra, width=_tty_width(),
|
default=[parse_subnet('127.0.0.1/8')],
|
||||||
initial_indent=prefix,
|
type=parse_subnet,
|
||||||
subsequent_indent=' ' * 28))
|
help="""
|
||||||
out.append(argtext + '\n')
|
exclude this subnet (can be used more than once)
|
||||||
last_was_option = True
|
"""
|
||||||
else:
|
)
|
||||||
out.append('\n')
|
parser.add_argument(
|
||||||
last_was_option = False
|
"-X", "--exclude-from",
|
||||||
return ''.join(out).rstrip() + '\n'
|
metavar="PATH",
|
||||||
|
action=Concat,
|
||||||
def usage(self, msg=""):
|
dest="exclude",
|
||||||
"""Print usage string to stderr and abort."""
|
type=parse_subnet_file,
|
||||||
sys.stderr.write(self._usagestr)
|
help="""
|
||||||
e = self._onabort and self._onabort(msg) or None
|
exclude the subnets in a file (whitespace separated)
|
||||||
if e:
|
"""
|
||||||
raise e
|
)
|
||||||
|
parser.add_argument(
|
||||||
def fatal(self, s):
|
"-v", "--verbose",
|
||||||
"""Print an error message to stderr and abort with usage string."""
|
action="count",
|
||||||
msg = 'error: %s\n' % s
|
default=0,
|
||||||
sys.stderr.write(msg)
|
help="""
|
||||||
return self.usage(msg)
|
increase debug message verbosity
|
||||||
|
"""
|
||||||
def parse(self, args):
|
)
|
||||||
"""Parse a list of arguments and return (options, flags, extra).
|
parser.add_argument(
|
||||||
|
"-V", "--version",
|
||||||
In the returned tuple, "options" is an OptDict with known options,
|
action="version",
|
||||||
"flags" is a list of option flags that were used on the command-line,
|
version=__version__,
|
||||||
and "extra" is a list of positional arguments.
|
help="""
|
||||||
"""
|
print the %(prog)s version number and exit
|
||||||
try:
|
"""
|
||||||
(flags, extra) = self.optfunc(
|
)
|
||||||
args, self._shortopts, self._longopts)
|
parser.add_argument(
|
||||||
except getopt.GetoptError as e:
|
"-e", "--ssh-cmd",
|
||||||
self.fatal(e)
|
metavar="CMD",
|
||||||
|
default="ssh",
|
||||||
opt = OptDict()
|
help="""
|
||||||
|
the command to use to connect to the remote [%(default)s]
|
||||||
for k, v in self._defaults.items():
|
"""
|
||||||
k = self._aliases[k]
|
)
|
||||||
opt[k] = v
|
parser.add_argument(
|
||||||
|
"--seed-hosts",
|
||||||
for (k, v) in flags:
|
metavar="HOSTNAME[,HOSTNAME]",
|
||||||
k = k.lstrip('-')
|
default=[],
|
||||||
if k in ('h', '?', 'help'):
|
help="""
|
||||||
self.usage()
|
with -H, use these hostnames for initial scan (comma-separated)
|
||||||
if k.startswith('no-'):
|
"""
|
||||||
k = self._aliases[k[3:]]
|
)
|
||||||
v = 0
|
parser.add_argument(
|
||||||
else:
|
"--no-latency-control",
|
||||||
k = self._aliases[k]
|
action="store_false",
|
||||||
if not self._hasparms[k]:
|
dest="latency_control",
|
||||||
assert(v == '')
|
help="""
|
||||||
v = (opt._opts.get(k) or 0) + 1
|
sacrifice latency to improve bandwidth benchmarks
|
||||||
else:
|
"""
|
||||||
v = _intify(v)
|
)
|
||||||
opt[k] = v
|
parser.add_argument(
|
||||||
for (f1, f2) in self._aliases.items():
|
"--wrap",
|
||||||
opt[f1] = opt._opts.get(f2)
|
metavar="NUM",
|
||||||
return (opt, flags, extra)
|
type=int,
|
||||||
|
help="""
|
||||||
|
restart counting channel numbers after this number (for testing)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--disable-ipv6",
|
||||||
|
action="store_true",
|
||||||
|
help="""
|
||||||
|
disable IPv6 support
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-D", "--daemon",
|
||||||
|
action="store_true",
|
||||||
|
help="""
|
||||||
|
run in the background as a daemon
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-s", "--subnets",
|
||||||
|
metavar="PATH",
|
||||||
|
action=Concat,
|
||||||
|
dest="subnets",
|
||||||
|
type=parse_subnet_file,
|
||||||
|
help="""
|
||||||
|
file where the subnets are stored, instead of on the command line
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--syslog",
|
||||||
|
action="store_true",
|
||||||
|
help="""
|
||||||
|
send log messages to syslog (default if you use --daemon)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--pidfile",
|
||||||
|
metavar="PATH",
|
||||||
|
default="./sshuttle.pid",
|
||||||
|
help="""
|
||||||
|
pidfile name (only if using --daemon) [%(default)s]
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--firewall",
|
||||||
|
action="store_true",
|
||||||
|
help="""
|
||||||
|
(internal use only)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--hostwatch",
|
||||||
|
action="store_true",
|
||||||
|
help="""
|
||||||
|
(internal use only)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
@ -12,32 +12,34 @@ import sshuttle.helpers as helpers
|
|||||||
import sshuttle.hostwatch as hostwatch
|
import sshuttle.hostwatch as hostwatch
|
||||||
import subprocess as ssubprocess
|
import subprocess as ssubprocess
|
||||||
from sshuttle.ssnet import Handler, Proxy, Mux, MuxWrapper
|
from sshuttle.ssnet import Handler, Proxy, Mux, MuxWrapper
|
||||||
from sshuttle.helpers import log, debug1, debug2, debug3, Fatal, \
|
from sshuttle.helpers import b, log, debug1, debug2, debug3, Fatal, \
|
||||||
resolvconf_random_nameserver
|
resolvconf_random_nameserver
|
||||||
|
|
||||||
|
|
||||||
def _ipmatch(ipstr):
|
def _ipmatch(ipstr):
|
||||||
if ipstr == b'default':
|
# FIXME: IPv4 only
|
||||||
ipstr = b'0.0.0.0/0'
|
if ipstr == 'default':
|
||||||
m = re.match(b'^(\d+(\.\d+(\.\d+(\.\d+)?)?)?)(?:/(\d+))?$', ipstr)
|
ipstr = '0.0.0.0/0'
|
||||||
|
m = re.match('^(\d+(\.\d+(\.\d+(\.\d+)?)?)?)(?:/(\d+))?$', ipstr)
|
||||||
if m:
|
if m:
|
||||||
g = m.groups()
|
g = m.groups()
|
||||||
ips = g[0]
|
ips = g[0]
|
||||||
width = int(g[4] or 32)
|
width = int(g[4] or 32)
|
||||||
if g[1] is None:
|
if g[1] is None:
|
||||||
ips += b'.0.0.0'
|
ips += '.0.0.0'
|
||||||
width = min(width, 8)
|
width = min(width, 8)
|
||||||
elif g[2] is None:
|
elif g[2] is None:
|
||||||
ips += b'.0.0'
|
ips += '.0.0'
|
||||||
width = min(width, 16)
|
width = min(width, 16)
|
||||||
elif g[3] is None:
|
elif g[3] is None:
|
||||||
ips += b'.0'
|
ips += '.0'
|
||||||
width = min(width, 24)
|
width = min(width, 24)
|
||||||
ips = ips.decode("ASCII")
|
ips = ips
|
||||||
return (struct.unpack('!I', socket.inet_aton(ips))[0], width)
|
return (struct.unpack('!I', socket.inet_aton(ips))[0], width)
|
||||||
|
|
||||||
|
|
||||||
def _ipstr(ip, width):
|
def _ipstr(ip, width):
|
||||||
|
# FIXME: IPv4 only
|
||||||
if width >= 32:
|
if width >= 32:
|
||||||
return ip
|
return ip
|
||||||
else:
|
else:
|
||||||
@ -45,6 +47,7 @@ def _ipstr(ip, width):
|
|||||||
|
|
||||||
|
|
||||||
def _maskbits(netmask):
|
def _maskbits(netmask):
|
||||||
|
# FIXME: IPv4 only
|
||||||
if not netmask:
|
if not netmask:
|
||||||
return 32
|
return 32
|
||||||
for i in range(32):
|
for i in range(32):
|
||||||
@ -63,7 +66,7 @@ def _list_routes():
|
|||||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE)
|
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE)
|
||||||
routes = []
|
routes = []
|
||||||
for line in p.stdout:
|
for line in p.stdout:
|
||||||
cols = re.split(b'\s+', line)
|
cols = re.split(r'\s+', line.decode("ASCII"))
|
||||||
ipw = _ipmatch(cols[0])
|
ipw = _ipmatch(cols[0])
|
||||||
if not ipw:
|
if not ipw:
|
||||||
continue # some lines won't be parseable; never mind
|
continue # some lines won't be parseable; never mind
|
||||||
@ -149,7 +152,8 @@ class DnsProxy(Handler):
|
|||||||
try:
|
try:
|
||||||
sock.send(self.request)
|
sock.send(self.request)
|
||||||
self.socks.append(sock)
|
self.socks.append(sock)
|
||||||
except socket.error as e:
|
except socket.error:
|
||||||
|
_, e = sys.exc_info()[:2]
|
||||||
if e.args[0] in ssnet.NET_ERRS:
|
if e.args[0] in ssnet.NET_ERRS:
|
||||||
# might have been spurious; try again.
|
# might have been spurious; try again.
|
||||||
# Note: these errors sometimes are reported by recv(),
|
# Note: these errors sometimes are reported by recv(),
|
||||||
@ -166,7 +170,8 @@ class DnsProxy(Handler):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
data = sock.recv(4096)
|
data = sock.recv(4096)
|
||||||
except socket.error as e:
|
except socket.error:
|
||||||
|
_, e = sys.exc_info()[:2]
|
||||||
self.socks.remove(sock)
|
self.socks.remove(sock)
|
||||||
del self.peers[sock]
|
del self.peers[sock]
|
||||||
|
|
||||||
@ -201,14 +206,16 @@ class UdpProxy(Handler):
|
|||||||
debug2('UDP: sending to %r port %d\n' % dstip)
|
debug2('UDP: sending to %r port %d\n' % dstip)
|
||||||
try:
|
try:
|
||||||
self.sock.sendto(data, dstip)
|
self.sock.sendto(data, dstip)
|
||||||
except socket.error as e:
|
except socket.error:
|
||||||
|
_, e = sys.exc_info()[:2]
|
||||||
log('UDP send to %r port %d: %s\n' % (dstip[0], dstip[1], e))
|
log('UDP send to %r port %d: %s\n' % (dstip[0], dstip[1], e))
|
||||||
return
|
return
|
||||||
|
|
||||||
def callback(self, sock):
|
def callback(self, sock):
|
||||||
try:
|
try:
|
||||||
data, peer = sock.recvfrom(4096)
|
data, peer = sock.recvfrom(4096)
|
||||||
except socket.error as e:
|
except socket.error:
|
||||||
|
_, e = sys.exc_info()[:2]
|
||||||
log('UDP recv from %r port %d: %s\n' % (peer[0], peer[1], e))
|
log('UDP recv from %r port %d: %s\n' % (peer[0], peer[1], e))
|
||||||
return
|
return
|
||||||
debug2('UDP response: %d bytes\n' % len(data))
|
debug2('UDP response: %d bytes\n' % len(data))
|
||||||
@ -241,26 +248,26 @@ def main(latency_control):
|
|||||||
socket.fromfd(sys.stdout.fileno(),
|
socket.fromfd(sys.stdout.fileno(),
|
||||||
socket.AF_INET, socket.SOCK_STREAM))
|
socket.AF_INET, socket.SOCK_STREAM))
|
||||||
handlers.append(mux)
|
handlers.append(mux)
|
||||||
routepkt = b''
|
routepkt = ''
|
||||||
for r in routes:
|
for r in routes:
|
||||||
routepkt += b'%d,%s,%d\n' % (r[0], r[1].encode("ASCII"), r[2])
|
routepkt += '%d,%s,%d\n' % r
|
||||||
mux.send(0, ssnet.CMD_ROUTES, routepkt)
|
mux.send(0, ssnet.CMD_ROUTES, b(routepkt))
|
||||||
|
|
||||||
hw = Hostwatch()
|
hw = Hostwatch()
|
||||||
hw.leftover = b''
|
hw.leftover = b('')
|
||||||
|
|
||||||
def hostwatch_ready(sock):
|
def hostwatch_ready(sock):
|
||||||
assert(hw.pid)
|
assert(hw.pid)
|
||||||
content = hw.sock.recv(4096)
|
content = hw.sock.recv(4096)
|
||||||
if content:
|
if content:
|
||||||
lines = (hw.leftover + content).split(b'\n')
|
lines = (hw.leftover + content).split(b('\n'))
|
||||||
if lines[-1]:
|
if lines[-1]:
|
||||||
# no terminating newline: entry isn't complete yet!
|
# no terminating newline: entry isn't complete yet!
|
||||||
hw.leftover = lines.pop()
|
hw.leftover = lines.pop()
|
||||||
lines.append(b'')
|
lines.append(b(''))
|
||||||
else:
|
else:
|
||||||
hw.leftover = b''
|
hw.leftover = b('')
|
||||||
mux.send(0, ssnet.CMD_HOST_LIST, b'\n'.join(lines))
|
mux.send(0, ssnet.CMD_HOST_LIST, b('\n').join(lines))
|
||||||
else:
|
else:
|
||||||
raise Fatal('hostwatch process died')
|
raise Fatal('hostwatch process died')
|
||||||
|
|
||||||
@ -272,7 +279,7 @@ def main(latency_control):
|
|||||||
mux.got_host_req = got_host_req
|
mux.got_host_req = got_host_req
|
||||||
|
|
||||||
def new_channel(channel, data):
|
def new_channel(channel, data):
|
||||||
(family, dstip, dstport) = data.split(b',', 2)
|
(family, dstip, dstport) = data.decode("ASCII").split(',', 2)
|
||||||
family = int(family)
|
family = int(family)
|
||||||
dstport = int(dstport)
|
dstport = int(dstport)
|
||||||
outwrap = ssnet.connect_dst(family, dstip, dstport)
|
outwrap = ssnet.connect_dst(family, dstip, dstport)
|
||||||
|
@ -8,6 +8,12 @@ import subprocess as ssubprocess
|
|||||||
import sshuttle.helpers as helpers
|
import sshuttle.helpers as helpers
|
||||||
from sshuttle.helpers import debug2
|
from sshuttle.helpers import debug2
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Python >= 3.5
|
||||||
|
from shlex import quote
|
||||||
|
except ImportError:
|
||||||
|
# Python 2.x
|
||||||
|
from pipes import quote
|
||||||
|
|
||||||
def readfile(name):
|
def readfile(name):
|
||||||
tokens = name.split(".")
|
tokens = name.split(".")
|
||||||
@ -89,10 +95,10 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
|
|||||||
b"\n")
|
b"\n")
|
||||||
|
|
||||||
pyscript = r"""
|
pyscript = r"""
|
||||||
import sys;
|
import sys, os;
|
||||||
verbosity=%d;
|
verbosity=%d;
|
||||||
stdin=getattr(sys.stdin,"buffer",sys.stdin);
|
sys.stdin = os.fdopen(0, "rb");
|
||||||
exec(compile(stdin.read(%d), "assembler.py", "exec"))
|
exec(compile(sys.stdin.read(%d), "assembler.py", "exec"))
|
||||||
""" % (helpers.verbose or 0, len(content))
|
""" % (helpers.verbose or 0, len(content))
|
||||||
pyscript = re.sub(r'\s+', ' ', pyscript.strip())
|
pyscript = re.sub(r'\s+', ' ', pyscript.strip())
|
||||||
|
|
||||||
@ -109,7 +115,8 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
|
|||||||
pycmd = "'%s' -c '%s'" % (python, pyscript)
|
pycmd = "'%s' -c '%s'" % (python, pyscript)
|
||||||
else:
|
else:
|
||||||
pycmd = ("P=python3.5; $P -V 2>/dev/null || P=python; "
|
pycmd = ("P=python3.5; $P -V 2>/dev/null || P=python; "
|
||||||
"exec \"$P\" -c '%s'") % pyscript
|
"exec \"$P\" -c %s") % quote(pyscript)
|
||||||
|
pycmd = ("exec /bin/sh -c %s" % quote(pycmd))
|
||||||
argv = (sshl +
|
argv = (sshl +
|
||||||
portl +
|
portl +
|
||||||
[rhost, '--', pycmd])
|
[rhost, '--', pycmd])
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
|
import sys
|
||||||
import struct
|
import struct
|
||||||
import socket
|
import socket
|
||||||
import errno
|
import errno
|
||||||
import select
|
import select
|
||||||
import os
|
import os
|
||||||
from sshuttle.helpers import log, debug1, debug2, debug3, Fatal
|
from sshuttle.helpers import b, binary_type, log, debug1, debug2, debug3, Fatal
|
||||||
|
|
||||||
MAX_CHANNEL = 65535
|
MAX_CHANNEL = 65535
|
||||||
|
|
||||||
@ -75,7 +76,8 @@ def _fds(l):
|
|||||||
def _nb_clean(func, *args):
|
def _nb_clean(func, *args):
|
||||||
try:
|
try:
|
||||||
return func(*args)
|
return func(*args)
|
||||||
except OSError as e:
|
except OSError:
|
||||||
|
_, e = sys.exc_info()[:2]
|
||||||
if e.errno not in (errno.EWOULDBLOCK, errno.EAGAIN):
|
if e.errno not in (errno.EWOULDBLOCK, errno.EAGAIN):
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
@ -88,7 +90,8 @@ def _try_peername(sock):
|
|||||||
pn = sock.getpeername()
|
pn = sock.getpeername()
|
||||||
if pn:
|
if pn:
|
||||||
return '%s:%s' % (pn[0], pn[1])
|
return '%s:%s' % (pn[0], pn[1])
|
||||||
except socket.error as e:
|
except socket.error:
|
||||||
|
_, e = sys.exc_info()[:2]
|
||||||
if e.args[0] not in (errno.ENOTCONN, errno.ENOTSOCK):
|
if e.args[0] not in (errno.ENOTCONN, errno.ENOTSOCK):
|
||||||
raise
|
raise
|
||||||
return 'unknown'
|
return 'unknown'
|
||||||
@ -144,7 +147,8 @@ class SockWrapper:
|
|||||||
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 as e:
|
except socket.error:
|
||||||
|
_, e = sys.exc_info()[:2]
|
||||||
debug3('%r: connect result: %s\n' % (self, e))
|
debug3('%r: connect result: %s\n' % (self, e))
|
||||||
if e.args[0] == errno.EINVAL:
|
if e.args[0] == errno.EINVAL:
|
||||||
# this is what happens when you call connect() on a socket
|
# this is what happens when you call connect() on a socket
|
||||||
@ -191,7 +195,8 @@ class SockWrapper:
|
|||||||
self.shut_write = True
|
self.shut_write = True
|
||||||
try:
|
try:
|
||||||
self.wsock.shutdown(SHUT_WR)
|
self.wsock.shutdown(SHUT_WR)
|
||||||
except socket.error as e:
|
except socket.error:
|
||||||
|
_, e = sys.exc_info()[:2]
|
||||||
self.seterr('nowrite: %s' % e)
|
self.seterr('nowrite: %s' % e)
|
||||||
|
|
||||||
def too_full(self):
|
def too_full(self):
|
||||||
@ -203,7 +208,8 @@ class SockWrapper:
|
|||||||
self.wsock.setblocking(False)
|
self.wsock.setblocking(False)
|
||||||
try:
|
try:
|
||||||
return _nb_clean(os.write, self.wsock.fileno(), buf)
|
return _nb_clean(os.write, self.wsock.fileno(), buf)
|
||||||
except OSError as e:
|
except OSError:
|
||||||
|
_, e = sys.exc_info()[:2]
|
||||||
if e.errno == errno.EPIPE:
|
if e.errno == errno.EPIPE:
|
||||||
debug1('%r: uwrite: got EPIPE\n' % self)
|
debug1('%r: uwrite: got EPIPE\n' % self)
|
||||||
self.nowrite()
|
self.nowrite()
|
||||||
@ -225,9 +231,10 @@ class SockWrapper:
|
|||||||
self.rsock.setblocking(False)
|
self.rsock.setblocking(False)
|
||||||
try:
|
try:
|
||||||
return _nb_clean(os.read, self.rsock.fileno(), 65536)
|
return _nb_clean(os.read, self.rsock.fileno(), 65536)
|
||||||
except OSError as e:
|
except OSError:
|
||||||
|
_, e = sys.exc_info()[:2]
|
||||||
self.seterr('uread: %s' % e)
|
self.seterr('uread: %s' % e)
|
||||||
return b'' # unexpected error... we'll call it EOF
|
return b('') # unexpected error... we'll call it EOF
|
||||||
|
|
||||||
def fill(self):
|
def fill(self):
|
||||||
if self.buf:
|
if self.buf:
|
||||||
@ -235,7 +242,7 @@ class SockWrapper:
|
|||||||
rb = self.uread()
|
rb = self.uread()
|
||||||
if rb:
|
if rb:
|
||||||
self.buf.append(rb)
|
self.buf.append(rb)
|
||||||
if rb == b'': # empty string means EOF; None means temporarily empty
|
if rb == b(''): # empty string means EOF; None means temporarily empty
|
||||||
self.noread()
|
self.noread()
|
||||||
|
|
||||||
def copy_to(self, outwrap):
|
def copy_to(self, outwrap):
|
||||||
@ -333,11 +340,11 @@ class Mux(Handler):
|
|||||||
self.channels = {}
|
self.channels = {}
|
||||||
self.chani = 0
|
self.chani = 0
|
||||||
self.want = 0
|
self.want = 0
|
||||||
self.inbuf = b''
|
self.inbuf = b('')
|
||||||
self.outbuf = []
|
self.outbuf = []
|
||||||
self.fullness = 0
|
self.fullness = 0
|
||||||
self.too_full = False
|
self.too_full = False
|
||||||
self.send(0, CMD_PING, b'chicken')
|
self.send(0, CMD_PING, b('chicken'))
|
||||||
|
|
||||||
def next_channel(self):
|
def next_channel(self):
|
||||||
# channel 0 is special, so we never allocate it
|
# channel 0 is special, so we never allocate it
|
||||||
@ -357,7 +364,7 @@ class Mux(Handler):
|
|||||||
def check_fullness(self):
|
def check_fullness(self):
|
||||||
if self.fullness > 32768:
|
if self.fullness > 32768:
|
||||||
if not self.too_full:
|
if not self.too_full:
|
||||||
self.send(0, CMD_PING, b'rttest')
|
self.send(0, CMD_PING, b('rttest'))
|
||||||
self.too_full = True
|
self.too_full = True
|
||||||
# ob = []
|
# ob = []
|
||||||
# for b in self.outbuf:
|
# for b in self.outbuf:
|
||||||
@ -366,9 +373,9 @@ class Mux(Handler):
|
|||||||
# log('outbuf: %d %r\n' % (self.amount_queued(), ob))
|
# log('outbuf: %d %r\n' % (self.amount_queued(), ob))
|
||||||
|
|
||||||
def send(self, channel, cmd, data):
|
def send(self, channel, cmd, data):
|
||||||
assert isinstance(data, bytes)
|
assert isinstance(data, binary_type)
|
||||||
assert len(data) <= 65535
|
assert len(data) <= 65535
|
||||||
p = struct.pack('!ccHHH', b'S', b'S', channel, cmd, len(data)) + data
|
p = struct.pack('!ccHHH', b('S'), b('S'), channel, cmd, len(data)) + data
|
||||||
self.outbuf.append(p)
|
self.outbuf.append(p)
|
||||||
debug2(' > channel=%d cmd=%s len=%d (fullness=%d)\n'
|
debug2(' > channel=%d cmd=%s len=%d (fullness=%d)\n'
|
||||||
% (channel, cmd_to_name.get(cmd, hex(cmd)),
|
% (channel, cmd_to_name.get(cmd, hex(cmd)),
|
||||||
@ -434,14 +441,15 @@ class Mux(Handler):
|
|||||||
def fill(self):
|
def fill(self):
|
||||||
self.rsock.setblocking(False)
|
self.rsock.setblocking(False)
|
||||||
try:
|
try:
|
||||||
b = _nb_clean(os.read, self.rsock.fileno(), 32768)
|
read = _nb_clean(os.read, self.rsock.fileno(), 32768)
|
||||||
except OSError as e:
|
except OSError:
|
||||||
|
_, e = sys.exc_info()[:2]
|
||||||
raise Fatal('other end: %r' % e)
|
raise Fatal('other end: %r' % e)
|
||||||
# log('<<< %r\n' % b)
|
# log('<<< %r\n' % b)
|
||||||
if b == b'': # EOF
|
if read == b(''): # EOF
|
||||||
self.ok = False
|
self.ok = False
|
||||||
if b:
|
if read:
|
||||||
self.inbuf += b
|
self.inbuf += read
|
||||||
|
|
||||||
def handle(self):
|
def handle(self):
|
||||||
self.fill()
|
self.fill()
|
||||||
@ -451,8 +459,8 @@ class Mux(Handler):
|
|||||||
if len(self.inbuf) >= (self.want or HDR_LEN):
|
if len(self.inbuf) >= (self.want or HDR_LEN):
|
||||||
(s1, s2, channel, cmd, datalen) = \
|
(s1, s2, channel, cmd, datalen) = \
|
||||||
struct.unpack('!ccHHH', self.inbuf[:HDR_LEN])
|
struct.unpack('!ccHHH', self.inbuf[:HDR_LEN])
|
||||||
assert(s1 == b'S')
|
assert(s1 == b('S'))
|
||||||
assert(s2 == b'S')
|
assert(s2 == b('S'))
|
||||||
self.want = datalen + HDR_LEN
|
self.want = datalen + HDR_LEN
|
||||||
if self.want and len(self.inbuf) >= self.want:
|
if self.want and len(self.inbuf) >= self.want:
|
||||||
data = self.inbuf[HDR_LEN:self.want]
|
data = self.inbuf[HDR_LEN:self.want]
|
||||||
@ -496,14 +504,14 @@ class MuxWrapper(SockWrapper):
|
|||||||
if not self.shut_read:
|
if not self.shut_read:
|
||||||
debug2('%r: done reading\n' % self)
|
debug2('%r: done reading\n' % self)
|
||||||
self.shut_read = True
|
self.shut_read = True
|
||||||
self.mux.send(self.channel, CMD_TCP_STOP_SENDING, b'')
|
self.mux.send(self.channel, CMD_TCP_STOP_SENDING, b(''))
|
||||||
self.maybe_close()
|
self.maybe_close()
|
||||||
|
|
||||||
def nowrite(self):
|
def nowrite(self):
|
||||||
if not self.shut_write:
|
if not self.shut_write:
|
||||||
debug2('%r: done writing\n' % self)
|
debug2('%r: done writing\n' % self)
|
||||||
self.shut_write = True
|
self.shut_write = True
|
||||||
self.mux.send(self.channel, CMD_TCP_EOF, b'')
|
self.mux.send(self.channel, CMD_TCP_EOF, b(''))
|
||||||
self.maybe_close()
|
self.maybe_close()
|
||||||
|
|
||||||
def maybe_close(self):
|
def maybe_close(self):
|
||||||
@ -526,7 +534,7 @@ class MuxWrapper(SockWrapper):
|
|||||||
|
|
||||||
def uread(self):
|
def uread(self):
|
||||||
if self.shut_read:
|
if self.shut_read:
|
||||||
return b'' # EOF
|
return b('') # EOF
|
||||||
else:
|
else:
|
||||||
return None # no data available right now
|
return None # no data available right now
|
||||||
|
|
||||||
|
63
sshuttle/tests/server/test_server.py
Normal file
63
sshuttle/tests/server/test_server.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import io
|
||||||
|
import socket
|
||||||
|
import sshuttle.server
|
||||||
|
from mock import patch, Mock, call
|
||||||
|
|
||||||
|
|
||||||
|
def test__ipmatch():
|
||||||
|
assert sshuttle.server._ipmatch("1.2.3.4") is not None
|
||||||
|
assert sshuttle.server._ipmatch("::1") is None # ipv6 not supported
|
||||||
|
assert sshuttle.server._ipmatch("42 Example Street, Melbourne") is None
|
||||||
|
|
||||||
|
|
||||||
|
def test__ipstr():
|
||||||
|
assert sshuttle.server._ipstr("1.2.3.4", 24) == "1.2.3.4/24"
|
||||||
|
assert sshuttle.server._ipstr("1.2.3.4", 32) == "1.2.3.4"
|
||||||
|
|
||||||
|
|
||||||
|
def test__maskbits():
|
||||||
|
netmask = sshuttle.server._ipmatch("255.255.255.0")
|
||||||
|
sshuttle.server._maskbits(netmask)
|
||||||
|
|
||||||
|
|
||||||
|
@patch('sshuttle.server.ssubprocess.Popen')
|
||||||
|
def test__listroutes(mock_popen):
|
||||||
|
mock_pobj = Mock()
|
||||||
|
mock_pobj.stdout = io.BytesIO(b"""
|
||||||
|
Kernel IP routing table
|
||||||
|
Destination Gateway Genmask Flags MSS Window irtt Iface
|
||||||
|
0.0.0.0 192.168.1.1 0.0.0.0 UG 0 0 0 wlan0
|
||||||
|
192.168.1.0 0.0.0.0 255.255.255.0 U 0 0 0 wlan0
|
||||||
|
""")
|
||||||
|
mock_pobj.wait.return_value = 0
|
||||||
|
mock_popen.return_value = mock_pobj
|
||||||
|
|
||||||
|
routes = sshuttle.server._list_routes()
|
||||||
|
|
||||||
|
assert mock_popen.mock_calls == [
|
||||||
|
call(['netstat', '-rn'], stdout=-1),
|
||||||
|
call().wait()
|
||||||
|
]
|
||||||
|
assert routes == [
|
||||||
|
(socket.AF_INET, '0.0.0.0', 0),
|
||||||
|
(socket.AF_INET, '192.168.1.0', 24)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@patch('sshuttle.server.ssubprocess.Popen')
|
||||||
|
def test_listroutes(mock_popen):
|
||||||
|
mock_pobj = Mock()
|
||||||
|
mock_pobj.stdout = io.BytesIO(b"""
|
||||||
|
Kernel IP routing table
|
||||||
|
Destination Gateway Genmask Flags MSS Window irtt Iface
|
||||||
|
0.0.0.0 192.168.1.1 0.0.0.0 UG 0 0 0 wlan0
|
||||||
|
192.168.1.0 0.0.0.0 255.255.255.0 U 0 0 0 wlan0
|
||||||
|
""")
|
||||||
|
mock_pobj.wait.return_value = 0
|
||||||
|
mock_popen.return_value = mock_pobj
|
||||||
|
|
||||||
|
routes = sshuttle.server.list_routes()
|
||||||
|
|
||||||
|
assert list(routes) == [
|
||||||
|
(socket.AF_INET, '192.168.1.0', 24)
|
||||||
|
]
|
4
tox.ini
4
tox.ini
@ -1,12 +1,16 @@
|
|||||||
[tox]
|
[tox]
|
||||||
downloadcache = {toxworkdir}/cache/
|
downloadcache = {toxworkdir}/cache/
|
||||||
envlist =
|
envlist =
|
||||||
|
py26,
|
||||||
py27,
|
py27,
|
||||||
|
py34,
|
||||||
py35,
|
py35,
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
basepython =
|
basepython =
|
||||||
|
py26: python2.6
|
||||||
py27: python2.7
|
py27: python2.7
|
||||||
|
py34: python3.4
|
||||||
py35: python3.5
|
py35: python3.5
|
||||||
commands =
|
commands =
|
||||||
py.test
|
py.test
|
||||||
|
Reference in New Issue
Block a user