mirror of
https://github.com/sshuttle/sshuttle.git
synced 2025-07-04 08:40:30 +02:00
Compare commits
61 Commits
Author | SHA1 | Date | |
---|---|---|---|
2c20a1fd5a | |||
915f72de35 | |||
1ffc3f52a1 | |||
8520ea2787 | |||
6a394deaf2 | |||
83d5c59a57 | |||
1cfd9eb9d7 | |||
f8d58fa4f0 | |||
d2d5a37541 | |||
e9be2deea0 | |||
22b1b54bfd | |||
a43c668dde | |||
e0dfb95596 | |||
5d28ce8272 | |||
f876c5db5e | |||
2e1beefc9a | |||
5a20783baa | |||
495b3c39ea | |||
f3cbc5018a | |||
e73e797f33 | |||
1d64879613 | |||
8fad282bfd | |||
1dda9dd621 | |||
74e308a29f | |||
516ff7bc4a | |||
89c5b57019 | |||
a8b288338b | |||
6fd02933dd | |||
7ea2d973c7 | |||
8ec5d1a5ac | |||
4241381d82 | |||
6e15e69029 | |||
8fa45885cc | |||
b8160c4a37 | |||
05bacf6fd6 | |||
dea3f21943 | |||
d522d1e1bd | |||
3541e4bdfe | |||
efdb9b8f94 | |||
7875d1b97a | |||
2b0d0065c7 | |||
9e3f02c199 | |||
8bdefcd10d | |||
29b6e8301f | |||
083293ea0d | |||
2c07985924 | |||
756025b1bc | |||
cedc8dc146 | |||
e8047ce3a9 | |||
fae4cb1dbf | |||
7d8309ef05 | |||
b7d37e44fb | |||
4a954c547a | |||
4fcf7c73da | |||
ba8e948c0d | |||
e06f0240cb | |||
517fc2c930 | |||
11533869a8 | |||
0392a779a2 | |||
ee26157faa | |||
0bdfb883aa |
11
.gitignore
vendored
11
.gitignore
vendored
@ -1,4 +1,13 @@
|
|||||||
sshuttle/version.py
|
/sshuttle/version.py
|
||||||
|
/docs/conf.py
|
||||||
|
/tmp/
|
||||||
|
/.cache/
|
||||||
|
/.eggs/
|
||||||
|
/.tox/
|
||||||
|
/build/
|
||||||
|
/dist/
|
||||||
|
/sshuttle.egg-info/
|
||||||
|
/docs/_build/
|
||||||
*.pyc
|
*.pyc
|
||||||
*~
|
*~
|
||||||
*.8
|
*.8
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
language: python
|
language: python
|
||||||
python:
|
python:
|
||||||
|
- 2.6
|
||||||
- 2.7
|
- 2.7
|
||||||
|
- 3.4
|
||||||
- 3.5
|
- 3.5
|
||||||
- pypy
|
- pypy
|
||||||
|
|
||||||
|
42
CHANGES.rst
42
CHANGES.rst
@ -1,3 +1,45 @@
|
|||||||
|
Release 0.78.1 (6th August, 2016)
|
||||||
|
=================================
|
||||||
|
* Fix readthedocs versioning.
|
||||||
|
* Don't crash on ENETUNREACH.
|
||||||
|
* Various bug fixes.
|
||||||
|
* Improvements to BSD and OSX support.
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
==========================
|
||||||
|
|
||||||
|
* Various bug fixes.
|
||||||
|
* Fix Documentation.
|
||||||
|
* Add fix for MacOS X issue.
|
||||||
|
* Add support for OpenBSD.
|
||||||
|
|
||||||
|
|
||||||
Release 0.76 (Jan 17, 2016)
|
Release 0.76 (Jan 17, 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.
|
||||||
|
|
||||||
|
@ -5,8 +5,11 @@ include MANIFEST.in
|
|||||||
include LICENSE
|
include LICENSE
|
||||||
include run
|
include run
|
||||||
include tox.ini
|
include tox.ini
|
||||||
|
exclude sshuttle/version.py
|
||||||
|
exclude docs/conf.py
|
||||||
recursive-include docs *.bat
|
recursive-include docs *.bat
|
||||||
recursive-include docs *.py
|
recursive-include docs *.py
|
||||||
recursive-include docs *.rst
|
recursive-include docs *.rst
|
||||||
recursive-include docs Makefile
|
recursive-include docs Makefile
|
||||||
recursive-include sshuttle *.py
|
recursive-include sshuttle *.py
|
||||||
|
recursive-exclude docs/_build *
|
||||||
|
@ -1 +0,0 @@
|
|||||||
0.75
|
|
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")
|
@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
# import sys
|
# import sys
|
||||||
# import os
|
# import os
|
||||||
|
from setuptools_scm import get_version
|
||||||
|
|
||||||
# If extensions (or modules to document with autodoc) are in another directory,
|
# If extensions (or modules to document with autodoc) are in another directory,
|
||||||
# add these directories to sys.path here. If the directory is relative to the
|
# add these directories to sys.path here. If the directory is relative to the
|
||||||
@ -54,7 +55,6 @@ copyright = '2016, Brian May'
|
|||||||
# built documents.
|
# built documents.
|
||||||
#
|
#
|
||||||
# The short X.Y version.
|
# The short X.Y version.
|
||||||
from setuptools_scm import get_version
|
|
||||||
version = get_version(root="..")
|
version = get_version(root="..")
|
||||||
# The full version, including alpha/beta/rc tags.
|
# The full version, including alpha/beta/rc tags.
|
||||||
release = version
|
release = version
|
@ -1,6 +1,9 @@
|
|||||||
sshuttle: where transparent proxy meets VPN meets ssh
|
sshuttle: where transparent proxy meets VPN meets ssh
|
||||||
=====================================================
|
=====================================================
|
||||||
|
|
||||||
|
:Date: |today|
|
||||||
|
:Version: |version|
|
||||||
|
|
||||||
Contents:
|
Contents:
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
@ -10,6 +13,7 @@ Contents:
|
|||||||
requirements
|
requirements
|
||||||
installation
|
installation
|
||||||
usage
|
usage
|
||||||
|
platform
|
||||||
Man Page <manpage>
|
Man Page <manpage>
|
||||||
how-it-works
|
how-it-works
|
||||||
support
|
support
|
||||||
|
@ -37,6 +37,12 @@ Options
|
|||||||
netmask), and 0/0 ('just route everything through the
|
netmask), and 0/0 ('just route everything through the
|
||||||
VPN').
|
VPN').
|
||||||
|
|
||||||
|
.. option:: --method [auto|nat|tproxy|pf]
|
||||||
|
|
||||||
|
Which firewall method should sshuttle use? For auto, sshuttle attempts to
|
||||||
|
guess the appropriate method depending on what it can find in PATH. The
|
||||||
|
default value is auto.
|
||||||
|
|
||||||
.. option:: -l, --listen=[ip:]port
|
.. option:: -l, --listen=[ip:]port
|
||||||
|
|
||||||
Use this ip address and port number as the transparent
|
Use this ip address and port number as the transparent
|
||||||
@ -130,6 +136,10 @@ Options
|
|||||||
if you use this option to give it a few names to start
|
if you use this option to give it a few names to start
|
||||||
from.
|
from.
|
||||||
|
|
||||||
|
If this option is used *without* :option:`--auto-hosts`,
|
||||||
|
then the listed hostnames will be scanned and added, but
|
||||||
|
no further hostnames will be added.
|
||||||
|
|
||||||
.. option:: --no-latency-control
|
.. option:: --no-latency-control
|
||||||
|
|
||||||
Sacrifice latency to improve bandwidth benchmarks. ssh
|
Sacrifice latency to improve bandwidth benchmarks. ssh
|
||||||
@ -278,7 +288,6 @@ there is no need for congestion control to be shared
|
|||||||
between the two separate streams, so a tcp-based tunnel is
|
between the two separate streams, so a tcp-based tunnel is
|
||||||
fine.
|
fine.
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
See Also
|
:manpage:`ssh(1)`, :manpage:`python(1)`
|
||||||
--------
|
|
||||||
:manpage:`ssh(1)`, :manpage:`python(1)`
|
|
||||||
|
10
docs/platform.rst
Normal file
10
docs/platform.rst
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
Platform Specific Notes
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Contents:
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
tproxy
|
||||||
|
windows
|
@ -37,11 +37,13 @@ Supports:
|
|||||||
Full UDP or DNS support with the TPROXY method requires the ``recvmsg()``
|
Full UDP or DNS support with the TPROXY method requires the ``recvmsg()``
|
||||||
syscall. This is not available in Python 2, however is in Python 3.5 and
|
syscall. This is not available in Python 2, however is in Python 3.5 and
|
||||||
later. Under Python 2 you might find it sufficient installing PyXAPI_ to get
|
later. Under Python 2 you might find it sufficient installing PyXAPI_ to get
|
||||||
the ``recvmsg()`` function.
|
the ``recvmsg()`` function. See :doc:`tproxy` for more information.
|
||||||
|
|
||||||
|
|
||||||
MacOS with PF method
|
MacOS / FreeBSD / OpenBSD
|
||||||
~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Method: pf
|
||||||
|
|
||||||
Supports:
|
Supports:
|
||||||
|
|
||||||
* IPv4 TCP
|
* IPv4 TCP
|
||||||
@ -51,9 +53,16 @@ Requires:
|
|||||||
|
|
||||||
* You need to have the pfctl command.
|
* You need to have the pfctl command.
|
||||||
|
|
||||||
|
Windows
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
Not officially supported, however can be made to work with Vagrant. Requires
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
|
42
docs/tproxy.rst
Normal file
42
docs/tproxy.rst
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
TPROXY
|
||||||
|
======
|
||||||
|
TPROXY is the only method that has full support of IPv6 and UDP.
|
||||||
|
|
||||||
|
There are some things you need to consider for TPROXY to work:
|
||||||
|
|
||||||
|
- The following commands need to be run first as root. This only needs to be
|
||||||
|
done once after booting up::
|
||||||
|
|
||||||
|
ip route add local default dev lo table 100
|
||||||
|
ip rule add fwmark 1 lookup 100
|
||||||
|
ip -6 route add local default dev lo table 100
|
||||||
|
ip -6 rule add fwmark 1 lookup 100
|
||||||
|
|
||||||
|
- The ``--auto-nets`` feature does not detect IPv6 routes automatically. Add IPv6
|
||||||
|
routes manually. e.g. by adding ``'::/0'`` to the end of the command line.
|
||||||
|
|
||||||
|
- The client needs to be run as root. e.g.::
|
||||||
|
|
||||||
|
sudo SSH_AUTH_SOCK="$SSH_AUTH_SOCK" $HOME/tree/sshuttle.tproxy/sshuttle --method=tproxy ...
|
||||||
|
|
||||||
|
- You may need to exclude the IP address of the server you are connecting to.
|
||||||
|
Otherwise sshuttle may attempt to intercept the ssh packets, which will not
|
||||||
|
work. Use the ``--exclude`` parameter for this.
|
||||||
|
|
||||||
|
- Similarly, UDP return packets (including DNS) could get intercepted and
|
||||||
|
bounced back. This is the case if you have a broad subnet such as
|
||||||
|
``0.0.0.0/0`` or ``::/0`` that includes the IP address of the client. Use the
|
||||||
|
``--exclude`` parameter for this.
|
||||||
|
|
||||||
|
- You need the ``--method=tproxy`` parameter, as above.
|
||||||
|
|
||||||
|
- The routes for the outgoing packets must already exist. For example, if your
|
||||||
|
connection does not have IPv6 support, no IPv6 routes will exist, IPv6
|
||||||
|
packets will not be generated and sshuttle cannot intercept them::
|
||||||
|
|
||||||
|
telnet -6 www.google.com 80
|
||||||
|
Trying 2404:6800:4001:805::1010...
|
||||||
|
telnet: Unable to connect to remote host: Network is unreachable
|
||||||
|
|
||||||
|
Add some dummy routes to external interfaces. Make sure they get removed
|
||||||
|
however after sshuttle exits.
|
@ -3,7 +3,7 @@ Useless Trivia
|
|||||||
This section written by the original author, Avery Pennarun
|
This section written by the original author, Avery Pennarun
|
||||||
<apenwarr@gmail.com>.
|
<apenwarr@gmail.com>.
|
||||||
|
|
||||||
Back in 1998 (12 years ago! Yikes!), I released the first version of `Tunnel
|
Back in 1998, I released the first version of `Tunnel
|
||||||
Vision <http://alumnit.ca/wiki/?TunnelVisionReadMe>`_, a semi-intelligent VPN
|
Vision <http://alumnit.ca/wiki/?TunnelVisionReadMe>`_, a semi-intelligent VPN
|
||||||
client for Linux. Unfortunately, I made two big mistakes: I implemented the
|
client for Linux. Unfortunately, I made two big mistakes: I implemented the
|
||||||
key exchange myself (oops), and I ended up doing TCP-over-TCP (double oops).
|
key exchange myself (oops), and I ended up doing TCP-over-TCP (double oops).
|
||||||
@ -31,6 +31,6 @@ intended.)
|
|||||||
|
|
||||||
That project I did for Slipstream was what first gave me the idea to merge
|
That project I did for Slipstream was what first gave me the idea to merge
|
||||||
the concepts of Fast Forward, Double Vision, and Tunnel Vision into a single
|
the concepts of Fast Forward, Double Vision, and Tunnel Vision into a single
|
||||||
program that was the best of all worlds. And here we are, at last, 10 years
|
program that was the best of all worlds. And here we are, at last.
|
||||||
later. You're welcome.
|
You're welcome.
|
||||||
|
|
||||||
|
@ -1,24 +1,33 @@
|
|||||||
Usage
|
Usage
|
||||||
=====
|
=====
|
||||||
- Forward all traffic::
|
|
||||||
|
|
||||||
sshuttle -r username@sshserver 0.0.0.0/0
|
.. note::
|
||||||
|
|
||||||
|
For information on usage with Windows, see the :doc:`windows` section.
|
||||||
|
For information on using the TProxy method, see the :doc:`tproxy` section.
|
||||||
|
|
||||||
|
Forward all traffic::
|
||||||
|
|
||||||
|
sshuttle -r username@sshserver 0.0.0.0/0
|
||||||
|
|
||||||
|
- Use the :option:`sshuttle -r` parameter to specify a remote server.
|
||||||
|
|
||||||
- By default sshuttle will automatically choose a method to use. Override with
|
- By default sshuttle will automatically choose a method to use. Override with
|
||||||
the ``--method=`` parameter.
|
the :option:`sshuttle --method` parameter.
|
||||||
|
|
||||||
- There is a shortcut for 0.0.0.0/0 for those that value
|
- There is a shortcut for 0.0.0.0/0 for those that value
|
||||||
their wrists::
|
their wrists::
|
||||||
|
|
||||||
sshuttle -r username@sshserver 0/0
|
sshuttle -r username@sshserver 0/0
|
||||||
|
|
||||||
- If you would also like your DNS queries to be proxied
|
If you would also like your DNS queries to be proxied
|
||||||
through the DNS server of the server you are connect to::
|
through the DNS server of the server you are connect to::
|
||||||
|
|
||||||
sshuttle --dns -r username@sshserver 0/0
|
sshuttle --dns -r username@sshserver 0/0
|
||||||
|
|
||||||
The above is probably what you want to use to prevent
|
The above is probably what you want to use to prevent
|
||||||
local network attacks such as Firesheep and friends.
|
local network attacks such as Firesheep and friends.
|
||||||
|
See the documentation for the :option:`sshuttle --dns` parameter.
|
||||||
|
|
||||||
(You may be prompted for one or more passwords; first, the local password to
|
(You may be prompted for one or more passwords; first, the local password to
|
||||||
become root using sudo, and then the remote ssh password. Or you might have
|
become root using sudo, and then the remote ssh password. Or you might have
|
||||||
@ -51,45 +60,3 @@ the data back and forth through ssh.
|
|||||||
Fun, right? A poor man's instant VPN, and you don't even have to have
|
Fun, right? A poor man's instant VPN, and you don't even have to have
|
||||||
admin access on the server.
|
admin access on the server.
|
||||||
|
|
||||||
Additional information for TPROXY
|
|
||||||
---------------------------------
|
|
||||||
TPROXY is the only method that supports full support of IPv6 and UDP.
|
|
||||||
|
|
||||||
There are some things you need to consider for TPROXY to work:
|
|
||||||
|
|
||||||
- The following commands need to be run first as root. This only needs to be
|
|
||||||
done once after booting up::
|
|
||||||
|
|
||||||
ip route add local default dev lo table 100
|
|
||||||
ip rule add fwmark 1 lookup 100
|
|
||||||
ip -6 route add local default dev lo table 100
|
|
||||||
ip -6 rule add fwmark 1 lookup 100
|
|
||||||
|
|
||||||
- The ``--auto-nets`` feature does not detect IPv6 routes automatically. Add IPv6
|
|
||||||
routes manually. e.g. by adding ``'::/0'`` to the end of the command line.
|
|
||||||
|
|
||||||
- The client needs to be run as root. e.g.::
|
|
||||||
|
|
||||||
sudo SSH_AUTH_SOCK="$SSH_AUTH_SOCK" $HOME/tree/sshuttle.tproxy/sshuttle --method=tproxy ...
|
|
||||||
|
|
||||||
- You may need to exclude the IP address of the server you are connecting to.
|
|
||||||
Otherwise sshuttle may attempt to intercept the ssh packets, which will not
|
|
||||||
work. Use the ``--exclude`` parameter for this.
|
|
||||||
|
|
||||||
- Similarly, UDP return packets (including DNS) could get intercepted and
|
|
||||||
bounced back. This is the case if you have a broad subnet such as
|
|
||||||
``0.0.0.0/0`` or ``::/0`` that includes the IP address of the client. Use the
|
|
||||||
``--exclude`` parameter for this.
|
|
||||||
|
|
||||||
- You need the ``--method=tproxy`` parameter, as above.
|
|
||||||
|
|
||||||
- The routes for the outgoing packets must already exist. For example, if your
|
|
||||||
connection does not have IPv6 support, no IPv6 routes will exist, IPv6
|
|
||||||
packets will not be generated and sshuttle cannot intercept them::
|
|
||||||
|
|
||||||
telnet -6 www.google.com 80
|
|
||||||
Trying 2404:6800:4001:805::1010...
|
|
||||||
telnet: Unable to connect to remote host: Network is unreachable
|
|
||||||
|
|
||||||
Add some dummy routes to external interfaces. Make sure they get removed
|
|
||||||
however after sshuttle exits.
|
|
||||||
|
19
docs/windows.rst
Normal file
19
docs/windows.rst
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
Microsoft Windows
|
||||||
|
=================
|
||||||
|
Currently there is no built in support for running sshuttle directly on
|
||||||
|
Microsoft Windows.
|
||||||
|
|
||||||
|
What we can really do is to create a Linux VM with Vagrant (or simply
|
||||||
|
Virtualbox if you like). In the Vagrant settings, remember to turn on bridged
|
||||||
|
NIC. Then, run sshuttle inside the VM like below::
|
||||||
|
|
||||||
|
sshuttle -l 0.0.0.0 -x 10.0.0.0/8 -x 192.168.0.0/16 0/0
|
||||||
|
|
||||||
|
10.0.0.0/8 excludes NAT traffic of Vagrant and 192.168.0.0/16 excludes
|
||||||
|
traffic to local area network (assuming that we're using 192.168.0.0 subnet).
|
||||||
|
|
||||||
|
Assuming the VM has the IP 192.168.1.200 obtained on the bridge NIC (we can
|
||||||
|
configure that in Vagrant), we can then ask Windows to route all its traffic
|
||||||
|
via the VM by running the following in cmd.exe with admin right::
|
||||||
|
|
||||||
|
route add 0.0.0.0 mask 0.0.0.0 192.168.1.200
|
4
run
4
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 -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
|
||||||
|
7
setup.py
7
setup.py
@ -18,7 +18,12 @@
|
|||||||
# along with python-tldap If not, see <http://www.gnu.org/licenses/>.
|
# along with python-tldap If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
with open("./docs/conf.orig.py", "r") as src:
|
||||||
|
with open("./docs/conf.py", "w") as dst:
|
||||||
|
dst.write("# FILE COPIED FROM conf.orig.py; DO NOT CHANGE\n")
|
||||||
|
shutil.copyfileobj(src, dst)
|
||||||
|
|
||||||
def version_scheme(version):
|
def version_scheme(version):
|
||||||
from setuptools_scm.version import guess_next_dev_version
|
from setuptools_scm.version import guess_next_dev_version
|
||||||
@ -31,7 +36,7 @@ setup(
|
|||||||
'write_to': "sshuttle/version.py",
|
'write_to': "sshuttle/version.py",
|
||||||
'version_scheme': version_scheme,
|
'version_scheme': version_scheme,
|
||||||
},
|
},
|
||||||
setup_requires=['setuptools_scm'],
|
setup_requires=['setuptools_scm', 'pytest-runner'],
|
||||||
# version=version,
|
# version=version,
|
||||||
url='https://github.com/sshuttle/sshuttle',
|
url='https://github.com/sshuttle/sshuttle',
|
||||||
author='Brian May',
|
author='Brian May',
|
||||||
|
@ -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")
|
||||||
@ -33,4 +34,4 @@ sshuttle.helpers.verbose = verbosity
|
|||||||
|
|
||||||
import sshuttle.cmdline_options as options
|
import sshuttle.cmdline_options as options
|
||||||
from sshuttle.server import main
|
from sshuttle.server import main
|
||||||
main(options.latency_control)
|
main(options.latency_control, options.auto_hosts)
|
||||||
|
@ -95,16 +95,21 @@ def daemon_cleanup():
|
|||||||
class MultiListener:
|
class MultiListener:
|
||||||
|
|
||||||
def __init__(self, type=socket.SOCK_STREAM, proto=0):
|
def __init__(self, type=socket.SOCK_STREAM, proto=0):
|
||||||
self.v6 = socket.socket(socket.AF_INET6, type, proto)
|
self.type = type
|
||||||
self.v4 = socket.socket(socket.AF_INET, type, proto)
|
self.proto = proto
|
||||||
|
self.v6 = None
|
||||||
|
self.v4 = None
|
||||||
|
self.bind_called = False
|
||||||
|
|
||||||
def setsockopt(self, level, optname, value):
|
def setsockopt(self, level, optname, value):
|
||||||
|
assert(self.bind_called)
|
||||||
if self.v6:
|
if self.v6:
|
||||||
self.v6.setsockopt(level, optname, value)
|
self.v6.setsockopt(level, optname, value)
|
||||||
if self.v4:
|
if self.v4:
|
||||||
self.v4.setsockopt(level, optname, value)
|
self.v4.setsockopt(level, optname, value)
|
||||||
|
|
||||||
def add_handler(self, handlers, callback, method, mux):
|
def add_handler(self, handlers, callback, method, mux):
|
||||||
|
assert(self.bind_called)
|
||||||
socks = []
|
socks = []
|
||||||
if self.v6:
|
if self.v6:
|
||||||
socks.append(self.v6)
|
socks.append(self.v6)
|
||||||
@ -119,6 +124,7 @@ class MultiListener:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def listen(self, backlog):
|
def listen(self, backlog):
|
||||||
|
assert(self.bind_called)
|
||||||
if self.v6:
|
if self.v6:
|
||||||
self.v6.listen(backlog)
|
self.v6.listen(backlog)
|
||||||
if self.v4:
|
if self.v4:
|
||||||
@ -133,16 +139,23 @@ class MultiListener:
|
|||||||
raise e
|
raise e
|
||||||
|
|
||||||
def bind(self, address_v6, address_v4):
|
def bind(self, address_v6, address_v4):
|
||||||
if address_v6 and self.v6:
|
assert(not self.bind_called)
|
||||||
|
self.bind_called = True
|
||||||
|
if address_v6 is not None:
|
||||||
|
self.v6 = socket.socket(socket.AF_INET6, self.type, self.proto)
|
||||||
|
self.v6.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
self.v6.bind(address_v6)
|
self.v6.bind(address_v6)
|
||||||
else:
|
else:
|
||||||
self.v6 = None
|
self.v6 = None
|
||||||
if address_v4 and self.v4:
|
if address_v4 is not None:
|
||||||
|
self.v4 = socket.socket(socket.AF_INET, self.type, self.proto)
|
||||||
|
self.v4.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
self.v4.bind(address_v4)
|
self.v4.bind(address_v4)
|
||||||
else:
|
else:
|
||||||
self.v4 = None
|
self.v4 = None
|
||||||
|
|
||||||
def print_listening(self, what):
|
def print_listening(self, what):
|
||||||
|
assert(self.bind_called)
|
||||||
if self.v6:
|
if self.v6:
|
||||||
listenip = self.v6.getsockname()
|
listenip = self.v6.getsockname()
|
||||||
debug1('%s listening on %r.\n' % (what, listenip))
|
debug1('%s listening on %r.\n' % (what, listenip))
|
||||||
@ -387,7 +400,7 @@ def ondns(listener, method, mux, handlers):
|
|||||||
|
|
||||||
def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
||||||
python, latency_control,
|
python, latency_control,
|
||||||
dns_listener, seed_hosts, auto_nets, daemon):
|
dns_listener, seed_hosts, auto_hosts, auto_nets, daemon):
|
||||||
|
|
||||||
debug1('Starting client with Python version %s\n'
|
debug1('Starting client with Python version %s\n'
|
||||||
% platform.python_version())
|
% platform.python_version())
|
||||||
@ -405,7 +418,8 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
|||||||
(serverproc, serversock) = ssh.connect(
|
(serverproc, serversock) = ssh.connect(
|
||||||
ssh_cmd, remotename, python,
|
ssh_cmd, remotename, python,
|
||||||
stderr=ssyslog._p and ssyslog._p.stdin,
|
stderr=ssyslog._p and ssyslog._p.stdin,
|
||||||
options=dict(latency_control=latency_control))
|
options=dict(latency_control=latency_control,
|
||||||
|
auto_hosts=auto_hosts))
|
||||||
except socket.error as e:
|
except socket.error as e:
|
||||||
if e.args[0] == errno.EPIPE:
|
if e.args[0] == errno.EPIPE:
|
||||||
raise Fatal("failed to establish ssh session (1)")
|
raise Fatal("failed to establish ssh session (1)")
|
||||||
@ -501,7 +515,7 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
|||||||
|
|
||||||
def main(listenip_v6, listenip_v4,
|
def main(listenip_v6, listenip_v4,
|
||||||
ssh_cmd, remotename, python, latency_control, dns, nslist,
|
ssh_cmd, remotename, python, latency_control, dns, nslist,
|
||||||
method_name, seed_hosts, auto_nets,
|
method_name, seed_hosts, auto_hosts, auto_nets,
|
||||||
subnets_include, subnets_exclude, daemon, pidfile):
|
subnets_include, subnets_exclude, daemon, pidfile):
|
||||||
|
|
||||||
if daemon:
|
if daemon:
|
||||||
@ -534,11 +548,16 @@ 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.ipv4 = len(subnets_v4) > 0 or listenip_v4 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:
|
||||||
@ -553,6 +572,14 @@ def main(listenip_v6, listenip_v4,
|
|||||||
if listenip_v4 == "auto":
|
if listenip_v4 == "auto":
|
||||||
listenip_v4 = ('127.0.0.1', 0)
|
listenip_v4 = ('127.0.0.1', 0)
|
||||||
|
|
||||||
|
if required.ipv4 and \
|
||||||
|
not any(listenip_v4[0] == sex[1] for sex in subnets_v4):
|
||||||
|
subnets_exclude.append((socket.AF_INET, listenip_v4[0], 32))
|
||||||
|
|
||||||
|
if required.ipv6 and \
|
||||||
|
not any(listenip_v6[0] == sex[1] for sex in subnets_v6):
|
||||||
|
subnets_exclude.append((socket.AF_INET6, listenip_v6[0], 128))
|
||||||
|
|
||||||
if listenip_v6 and listenip_v6[1] and listenip_v4 and listenip_v4[1]:
|
if listenip_v6 and listenip_v6[1] and listenip_v4 and listenip_v4[1]:
|
||||||
# if both ports given, no need to search for a spare port
|
# if both ports given, no need to search for a spare port
|
||||||
ports = [0, ]
|
ports = [0, ]
|
||||||
@ -569,11 +596,9 @@ def main(listenip_v6, listenip_v4,
|
|||||||
for port in ports:
|
for port in ports:
|
||||||
debug2(' %d' % port)
|
debug2(' %d' % port)
|
||||||
tcp_listener = MultiListener()
|
tcp_listener = MultiListener()
|
||||||
tcp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
||||||
|
|
||||||
if required.udp:
|
if required.udp:
|
||||||
udp_listener = MultiListener(socket.SOCK_DGRAM)
|
udp_listener = MultiListener(socket.SOCK_DGRAM)
|
||||||
udp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
||||||
else:
|
else:
|
||||||
udp_listener = None
|
udp_listener = None
|
||||||
|
|
||||||
@ -698,7 +723,7 @@ def main(listenip_v6, listenip_v4,
|
|||||||
try:
|
try:
|
||||||
return _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
return _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
||||||
python, latency_control, dns_listener,
|
python, latency_control, dns_listener,
|
||||||
seed_hosts, auto_nets, daemon)
|
seed_hosts, auto_hosts, auto_nets, daemon)
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
if daemon:
|
if daemon:
|
||||||
|
@ -1,199 +1,46 @@
|
|||||||
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, '
|
||||||
for k, v in flags:
|
'or -N expected')
|
||||||
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:
|
|
||||||
o.fatal('--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 +65,12 @@ def main():
|
|||||||
opt.latency_control,
|
opt.latency_control,
|
||||||
opt.dns,
|
opt.dns,
|
||||||
nslist,
|
nslist,
|
||||||
method_name,
|
opt.method,
|
||||||
sh,
|
sh,
|
||||||
|
opt.auto_hosts,
|
||||||
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,17 @@ 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 +81,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:
|
||||||
@ -68,8 +70,8 @@ def read_host_cache():
|
|||||||
def found_host(hostname, ip):
|
def found_host(hostname, ip):
|
||||||
hostname = re.sub(r'\..*', '', hostname)
|
hostname = re.sub(r'\..*', '', hostname)
|
||||||
hostname = re.sub(r'[^-\w]', '_', hostname)
|
hostname = re.sub(r'[^-\w]', '_', hostname)
|
||||||
if (ip.startswith('127.') or ip.startswith('255.')
|
if (ip.startswith('127.') or ip.startswith('255.') or
|
||||||
or hostname == 'localhost'):
|
hostname == 'localhost'):
|
||||||
return
|
return
|
||||||
oldip = hostnames.get(hostname)
|
oldip = hostnames.get(hostname)
|
||||||
if oldip != ip:
|
if oldip != ip:
|
||||||
@ -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
|
||||||
@ -250,7 +255,7 @@ def _stdin_still_ok(timeout):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def hw_main(seed_hosts):
|
def hw_main(seed_hosts, auto_hosts):
|
||||||
if helpers.verbose >= 2:
|
if helpers.verbose >= 2:
|
||||||
helpers.logprefix = 'HH: '
|
helpers.logprefix = 'HH: '
|
||||||
else:
|
else:
|
||||||
@ -259,17 +264,18 @@ def hw_main(seed_hosts):
|
|||||||
debug1('Starting hostwatch with Python version %s\n'
|
debug1('Starting hostwatch with Python version %s\n'
|
||||||
% platform.python_version())
|
% platform.python_version())
|
||||||
|
|
||||||
read_host_cache()
|
|
||||||
|
|
||||||
_enqueue(_check_etc_hosts)
|
|
||||||
_enqueue(_check_netstat)
|
|
||||||
check_host('localhost')
|
|
||||||
check_host(socket.gethostname())
|
|
||||||
check_workgroup('workgroup')
|
|
||||||
check_workgroup('-')
|
|
||||||
for h in seed_hosts:
|
for h in seed_hosts:
|
||||||
check_host(h)
|
check_host(h)
|
||||||
|
|
||||||
|
if auto_hosts:
|
||||||
|
read_host_cache()
|
||||||
|
_enqueue(_check_etc_hosts)
|
||||||
|
_enqueue(_check_netstat)
|
||||||
|
check_host('localhost')
|
||||||
|
check_host(socket.gethostname())
|
||||||
|
check_workgroup('workgroup')
|
||||||
|
check_workgroup('-')
|
||||||
|
|
||||||
while 1:
|
while 1:
|
||||||
now = time.time()
|
now = time.time()
|
||||||
for t, last_polled in list(queue.items()):
|
for t, last_polled in list(queue.items()):
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import os
|
||||||
import socket
|
import socket
|
||||||
import subprocess as ssubprocess
|
import subprocess as ssubprocess
|
||||||
from sshuttle.helpers import log, debug1, Fatal, family_to_string
|
from sshuttle.helpers import log, debug1, Fatal, family_to_string
|
||||||
@ -18,7 +19,11 @@ def ipt_chain_exists(family, table, name):
|
|||||||
else:
|
else:
|
||||||
raise Exception('Unsupported family "%s"' % family_to_string(family))
|
raise Exception('Unsupported family "%s"' % family_to_string(family))
|
||||||
argv = [cmd, '-t', table, '-nL']
|
argv = [cmd, '-t', table, '-nL']
|
||||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE)
|
env = {
|
||||||
|
'PATH': os.environ['PATH'],
|
||||||
|
'LC_ALL': "C",
|
||||||
|
}
|
||||||
|
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=env)
|
||||||
for line in p.stdout:
|
for line in p.stdout:
|
||||||
if line.startswith(b'Chain %s ' % name.encode("ASCII")):
|
if line.startswith(b'Chain %s ' % name.encode("ASCII")):
|
||||||
return True
|
return True
|
||||||
@ -35,7 +40,11 @@ def ipt(family, table, *args):
|
|||||||
else:
|
else:
|
||||||
raise Exception('Unsupported family "%s"' % family_to_string(family))
|
raise Exception('Unsupported family "%s"' % family_to_string(family))
|
||||||
debug1('>> %s\n' % ' '.join(argv))
|
debug1('>> %s\n' % ' '.join(argv))
|
||||||
rv = ssubprocess.call(argv)
|
env = {
|
||||||
|
'PATH': os.environ['PATH'],
|
||||||
|
'LC_ALL': "C",
|
||||||
|
}
|
||||||
|
rv = ssubprocess.call(argv, env=env)
|
||||||
if rv:
|
if rv:
|
||||||
raise Fatal('%r returned %d' % (argv, rv))
|
raise Fatal('%r returned %d' % (argv, rv))
|
||||||
|
|
||||||
|
@ -11,54 +11,286 @@ from sshuttle.helpers import debug1, debug2, debug3, Fatal, family_to_string
|
|||||||
from sshuttle.methods import BaseMethod
|
from sshuttle.methods import BaseMethod
|
||||||
|
|
||||||
|
|
||||||
def pfctl(args, stdin=None):
|
_pf_context = {'started_by_sshuttle': False, 'Xtoken': []}
|
||||||
argv = ['pfctl'] + list(args.split(" "))
|
|
||||||
debug1('>> %s\n' % ' '.join(argv))
|
|
||||||
|
|
||||||
p = ssubprocess.Popen(argv, stdin=ssubprocess.PIPE,
|
|
||||||
stdout=ssubprocess.PIPE,
|
|
||||||
stderr=ssubprocess.PIPE)
|
|
||||||
o = p.communicate(stdin)
|
|
||||||
if p.returncode:
|
|
||||||
raise Fatal('%r returned %d' % (argv, p.returncode))
|
|
||||||
|
|
||||||
return o
|
|
||||||
|
|
||||||
_pf_context = {'started_by_sshuttle': False, 'Xtoken': None}
|
|
||||||
_pf_fd = None
|
_pf_fd = None
|
||||||
|
|
||||||
|
|
||||||
class OsDefs(object):
|
class Generic(object):
|
||||||
|
MAXPATHLEN = 1024
|
||||||
|
PF_CHANGE_ADD_TAIL = 2
|
||||||
|
PF_CHANGE_GET_TICKET = 6
|
||||||
|
PF_PASS = 0
|
||||||
|
PF_RDR = 8
|
||||||
|
PF_OUT = 2
|
||||||
|
ACTION_OFFSET = 0
|
||||||
|
POOL_TICKET_OFFSET = 8
|
||||||
|
ANCHOR_CALL_OFFSET = 1040
|
||||||
|
|
||||||
def __init__(self, platform=None):
|
class pf_addr(Structure):
|
||||||
if platform is None:
|
class _pfa(Union):
|
||||||
platform = sys.platform
|
_fields_ = [("v4", c_uint32), # struct in_addr
|
||||||
self.platform = platform
|
("v6", c_uint32 * 4), # struct in6_addr
|
||||||
|
("addr8", c_uint8 * 16),
|
||||||
|
("addr16", c_uint16 * 8),
|
||||||
|
("addr32", c_uint32 * 4)]
|
||||||
|
|
||||||
# This are some classes and functions used to support pf in yosemite.
|
_fields_ = [("pfa", _pfa)]
|
||||||
if platform == 'darwin':
|
_anonymous_ = ("pfa",)
|
||||||
class pf_state_xport(Union):
|
|
||||||
_fields_ = [("port", c_uint16),
|
|
||||||
("call_id", c_uint16),
|
|
||||||
("spi", c_uint32)]
|
|
||||||
else:
|
|
||||||
class pf_state_xport(Union):
|
|
||||||
_fields_ = [("port", c_uint16),
|
|
||||||
("call_id", c_uint16)]
|
|
||||||
|
|
||||||
class pf_addr(Structure):
|
def __init__(self):
|
||||||
|
self.status = b''
|
||||||
|
self.pfioc_pooladdr = c_char * 1136
|
||||||
|
|
||||||
class _pfa(Union):
|
self.DIOCNATLOOK = (
|
||||||
_fields_ = [("v4", c_uint32), # struct in_addr
|
(0x40000000 | 0x80000000) |
|
||||||
("v6", c_uint32 * 4), # struct in6_addr
|
((sizeof(self.pfioc_natlook) & 0x1fff) << 16) |
|
||||||
("addr8", c_uint8 * 16),
|
((ord('D')) << 8) | (23))
|
||||||
("addr16", c_uint16 * 8),
|
self.DIOCCHANGERULE = (
|
||||||
("addr32", c_uint32 * 4)]
|
(0x40000000 | 0x80000000) |
|
||||||
|
((sizeof(self.pfioc_rule) & 0x1fff) << 16) |
|
||||||
|
((ord('D')) << 8) | (26))
|
||||||
|
self.DIOCBEGINADDRS = (
|
||||||
|
(0x40000000 | 0x80000000) |
|
||||||
|
((sizeof(self.pfioc_pooladdr) & 0x1fff) << 16) |
|
||||||
|
((ord('D')) << 8) | (51))
|
||||||
|
|
||||||
_fields_ = [("pfa", _pfa)]
|
def enable(self):
|
||||||
_anonymous_ = ("pfa",)
|
if b'INFO:\nStatus: Disabled' in self.status:
|
||||||
|
pfctl('-e')
|
||||||
|
_pf_context['started_by_sshuttle'] = True
|
||||||
|
|
||||||
|
def disable(self, anchor):
|
||||||
|
pfctl('-a %s -F all' % anchor)
|
||||||
|
if _pf_context['started_by_sshuttle']:
|
||||||
|
pfctl('-d')
|
||||||
|
|
||||||
|
def query_nat(self, family, proto, src_ip, src_port, dst_ip, dst_port):
|
||||||
|
[proto, family, src_port, dst_port] = [
|
||||||
|
int(v) for v in [proto, family, src_port, dst_port]]
|
||||||
|
|
||||||
|
packed_src_ip = socket.inet_pton(family, src_ip)
|
||||||
|
packed_dst_ip = socket.inet_pton(family, dst_ip)
|
||||||
|
|
||||||
|
assert len(packed_src_ip) == len(packed_dst_ip)
|
||||||
|
length = len(packed_src_ip)
|
||||||
|
|
||||||
|
pnl = self.pfioc_natlook()
|
||||||
|
pnl.proto = proto
|
||||||
|
pnl.direction = self.PF_OUT
|
||||||
|
pnl.af = family
|
||||||
|
memmove(addressof(pnl.saddr), packed_src_ip, length)
|
||||||
|
memmove(addressof(pnl.daddr), packed_dst_ip, length)
|
||||||
|
self._add_natlook_ports(pnl, src_port, dst_port)
|
||||||
|
|
||||||
|
ioctl(pf_get_dev(), self.DIOCNATLOOK,
|
||||||
|
(c_char * sizeof(pnl)).from_address(addressof(pnl)))
|
||||||
|
|
||||||
|
ip = socket.inet_ntop(
|
||||||
|
pnl.af, (c_char * length).from_address(addressof(pnl.rdaddr)).raw)
|
||||||
|
port = socket.ntohs(self._get_natlook_port(pnl.rdxport))
|
||||||
|
return (ip, port)
|
||||||
|
|
||||||
|
def _add_natlook_ports(self, pnl, src_port, dst_port):
|
||||||
|
pnl.sxport = socket.htons(src_port)
|
||||||
|
pnl.dxport = socket.htons(dst_port)
|
||||||
|
|
||||||
|
def _get_natlook_port(self, xport):
|
||||||
|
return xport
|
||||||
|
|
||||||
|
def add_anchors(self, anchor, status=None):
|
||||||
|
if status is None:
|
||||||
|
status = pfctl('-s all')[0]
|
||||||
|
self.status = status
|
||||||
|
if ('\nanchor "%s"' % anchor).encode('ASCII') not in status:
|
||||||
|
self._add_anchor_rule(self.PF_PASS, anchor.encode('ASCII'))
|
||||||
|
|
||||||
|
def _add_anchor_rule(self, type, name, pr=None):
|
||||||
|
if pr is None:
|
||||||
|
pr = self.pfioc_rule()
|
||||||
|
|
||||||
|
memmove(addressof(pr) + self.ANCHOR_CALL_OFFSET, name,
|
||||||
|
min(self.MAXPATHLEN, len(name))) # anchor_call = name
|
||||||
|
memmove(addressof(pr) + self.RULE_ACTION_OFFSET,
|
||||||
|
struct.pack('I', type), 4) # rule.action = type
|
||||||
|
|
||||||
|
memmove(addressof(pr) + self.ACTION_OFFSET, struct.pack(
|
||||||
|
'I', self.PF_CHANGE_GET_TICKET), 4) # action = PF_CHANGE_GET_TICKET
|
||||||
|
ioctl(pf_get_dev(), pf.DIOCCHANGERULE, pr)
|
||||||
|
|
||||||
|
memmove(addressof(pr) + self.ACTION_OFFSET, struct.pack(
|
||||||
|
'I', self.PF_CHANGE_ADD_TAIL), 4) # action = PF_CHANGE_ADD_TAIL
|
||||||
|
ioctl(pf_get_dev(), pf.DIOCCHANGERULE, pr)
|
||||||
|
|
||||||
|
def _inet_version(self, family):
|
||||||
|
return b'inet' if family == socket.AF_INET else b'inet6'
|
||||||
|
|
||||||
|
def _lo_addr(self, family):
|
||||||
|
return b'127.0.0.1' if family == socket.AF_INET else b'::1'
|
||||||
|
|
||||||
|
def add_rules(self, anchor, rules):
|
||||||
|
assert isinstance(rules, bytes)
|
||||||
|
debug3("rules:\n" + rules.decode("ASCII"))
|
||||||
|
pfctl('-a %s -f /dev/stdin' % anchor, rules)
|
||||||
|
|
||||||
|
def has_skip_loopback(self):
|
||||||
|
return b'skip' in pfctl('-s Interfaces -i lo -v')[0]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class FreeBsd(Generic):
|
||||||
|
RULE_ACTION_OFFSET = 2968
|
||||||
|
|
||||||
|
def __new__(cls):
|
||||||
|
class pfioc_natlook(Structure):
|
||||||
|
pf_addr = Generic.pf_addr
|
||||||
|
_fields_ = [("saddr", pf_addr),
|
||||||
|
("daddr", pf_addr),
|
||||||
|
("rsaddr", pf_addr),
|
||||||
|
("rdaddr", pf_addr),
|
||||||
|
("sxport", c_uint16),
|
||||||
|
("dxport", c_uint16),
|
||||||
|
("rsxport", c_uint16),
|
||||||
|
("rdxport", c_uint16),
|
||||||
|
("af", c_uint8), # sa_family_t
|
||||||
|
("proto", c_uint8),
|
||||||
|
("proto_variant", c_uint8),
|
||||||
|
("direction", c_uint8)]
|
||||||
|
|
||||||
|
freebsd = Generic.__new__(cls)
|
||||||
|
freebsd.pfioc_rule = c_char * 3040
|
||||||
|
freebsd.pfioc_natlook = pfioc_natlook
|
||||||
|
return freebsd
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(FreeBsd, self).__init__()
|
||||||
|
|
||||||
|
def add_anchors(self, anchor):
|
||||||
|
status = pfctl('-s all')[0]
|
||||||
|
if ('\nrdr-anchor "%s"' % anchor).encode('ASCII') not in status:
|
||||||
|
self._add_anchor_rule(self.PF_RDR, anchor.encode('ASCII'))
|
||||||
|
super(FreeBsd, self).add_anchors(anchor, status=status)
|
||||||
|
|
||||||
|
def _add_anchor_rule(self, type, name):
|
||||||
|
pr = self.pfioc_rule()
|
||||||
|
ppa = self.pfioc_pooladdr()
|
||||||
|
|
||||||
|
ioctl(pf_get_dev(), self.DIOCBEGINADDRS, ppa)
|
||||||
|
# pool ticket
|
||||||
|
memmove(addressof(pr) + self.POOL_TICKET_OFFSET, ppa[4:8], 4)
|
||||||
|
super(FreeBsd, self)._add_anchor_rule(type, name, pr=pr)
|
||||||
|
|
||||||
|
def add_rules(self, anchor, includes, port, dnsport, nslist, family):
|
||||||
|
inet_version = self._inet_version(family)
|
||||||
|
lo_addr = self._lo_addr(family)
|
||||||
|
|
||||||
|
tables = [
|
||||||
|
b'table <forward_subnets> {%s}' % b','.join(includes)
|
||||||
|
]
|
||||||
|
translating_rules = [
|
||||||
|
b'rdr pass on lo0 %s proto tcp to <forward_subnets> '
|
||||||
|
b'-> %s port %r' % (inet_version, lo_addr, port)
|
||||||
|
]
|
||||||
|
filtering_rules = [
|
||||||
|
b'pass out route-to lo0 %s proto tcp '
|
||||||
|
b'to <forward_subnets> keep state' % inet_version
|
||||||
|
]
|
||||||
|
|
||||||
|
if len(nslist) > 0:
|
||||||
|
tables.append(
|
||||||
|
b'table <dns_servers> {%s}' %
|
||||||
|
b','.join([ns[1].encode("ASCII") for ns in nslist]))
|
||||||
|
translating_rules.append(
|
||||||
|
b'rdr pass on lo0 %s proto udp to <dns_servers> '
|
||||||
|
b'port 53 -> %s port %r' % (inet_version, lo_addr, dnsport))
|
||||||
|
filtering_rules.append(
|
||||||
|
b'pass out route-to lo0 %s proto udp to '
|
||||||
|
b'<dns_servers> port 53 keep state' % inet_version)
|
||||||
|
|
||||||
|
rules = b'\n'.join(tables + translating_rules + filtering_rules) \
|
||||||
|
+ b'\n'
|
||||||
|
|
||||||
|
super(FreeBsd, self).add_rules(anchor, rules)
|
||||||
|
|
||||||
|
|
||||||
|
class OpenBsd(Generic):
|
||||||
|
POOL_TICKET_OFFSET = 4
|
||||||
|
RULE_ACTION_OFFSET = 3324
|
||||||
|
ANCHOR_CALL_OFFSET = 1036
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
class pfioc_natlook(Structure):
|
||||||
|
pf_addr = Generic.pf_addr
|
||||||
|
_fields_ = [("saddr", pf_addr),
|
||||||
|
("daddr", pf_addr),
|
||||||
|
("rsaddr", pf_addr),
|
||||||
|
("rdaddr", pf_addr),
|
||||||
|
("rdomain", c_uint16),
|
||||||
|
("rrdomain", c_uint16),
|
||||||
|
("sxport", c_uint16),
|
||||||
|
("dxport", c_uint16),
|
||||||
|
("rsxport", c_uint16),
|
||||||
|
("rdxport", c_uint16),
|
||||||
|
("af", c_uint8), # sa_family_t
|
||||||
|
("proto", c_uint8),
|
||||||
|
("proto_variant", c_uint8),
|
||||||
|
("direction", c_uint8)]
|
||||||
|
|
||||||
|
self.pfioc_rule = c_char * 3400
|
||||||
|
self.pfioc_natlook = pfioc_natlook
|
||||||
|
super(OpenBsd, self).__init__()
|
||||||
|
|
||||||
|
def add_anchors(self, anchor):
|
||||||
|
# before adding anchors and rules we must override the skip lo
|
||||||
|
# that comes by default in openbsd pf.conf so the rules we will add,
|
||||||
|
# which rely on translating/filtering packets on lo, can work
|
||||||
|
if self.has_skip_loopback():
|
||||||
|
pfctl('-f /dev/stdin', b'match on lo\n')
|
||||||
|
super(OpenBsd, self).add_anchors(anchor)
|
||||||
|
|
||||||
|
def add_rules(self, anchor, includes, port, dnsport, nslist, family):
|
||||||
|
inet_version = self._inet_version(family)
|
||||||
|
lo_addr = self._lo_addr(family)
|
||||||
|
|
||||||
|
tables = [
|
||||||
|
b'table <forward_subnets> {%s}' % b','.join(includes)
|
||||||
|
]
|
||||||
|
translating_rules = [
|
||||||
|
b'pass in on lo0 %s proto tcp to <forward_subnets> '
|
||||||
|
b'divert-to %s port %r' % (inet_version, lo_addr, port)
|
||||||
|
]
|
||||||
|
filtering_rules = [
|
||||||
|
b'pass out %s proto tcp to <forward_subnets> '
|
||||||
|
b'route-to lo0 keep state' % inet_version
|
||||||
|
]
|
||||||
|
|
||||||
|
if len(nslist) > 0:
|
||||||
|
tables.append(
|
||||||
|
b'table <dns_servers> {%s}' %
|
||||||
|
b','.join([ns[1].encode("ASCII") for ns in nslist]))
|
||||||
|
translating_rules.append(
|
||||||
|
b'pass in on lo0 %s proto udp to <dns_servers> port 53 '
|
||||||
|
b'rdr-to %s port %r' % (inet_version, lo_addr, dnsport))
|
||||||
|
filtering_rules.append(
|
||||||
|
b'pass out %s proto udp to <dns_servers> port 53 '
|
||||||
|
b'route-to lo0 keep state' % inet_version)
|
||||||
|
|
||||||
|
rules = b'\n'.join(tables + translating_rules + filtering_rules) \
|
||||||
|
+ b'\n'
|
||||||
|
|
||||||
|
super(OpenBsd, self).add_rules(anchor, rules)
|
||||||
|
|
||||||
|
|
||||||
|
class Darwin(FreeBsd):
|
||||||
|
RULE_ACTION_OFFSET = 3068
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
class pf_state_xport(Union):
|
||||||
|
_fields_ = [("port", c_uint16),
|
||||||
|
("call_id", c_uint16),
|
||||||
|
("spi", c_uint32)]
|
||||||
|
|
||||||
class pfioc_natlook(Structure):
|
class pfioc_natlook(Structure):
|
||||||
|
pf_addr = Generic.pf_addr
|
||||||
_fields_ = [("saddr", pf_addr),
|
_fields_ = [("saddr", pf_addr),
|
||||||
("daddr", pf_addr),
|
("daddr", pf_addr),
|
||||||
("rsaddr", pf_addr),
|
("rsaddr", pf_addr),
|
||||||
@ -71,39 +303,61 @@ class OsDefs(object):
|
|||||||
("proto", c_uint8),
|
("proto", c_uint8),
|
||||||
("proto_variant", c_uint8),
|
("proto_variant", c_uint8),
|
||||||
("direction", c_uint8)]
|
("direction", c_uint8)]
|
||||||
|
|
||||||
|
self.pfioc_rule = c_char * 3104
|
||||||
self.pfioc_natlook = pfioc_natlook
|
self.pfioc_natlook = pfioc_natlook
|
||||||
|
super(Darwin, self).__init__()
|
||||||
|
|
||||||
# sizeof(struct pfioc_rule)
|
def enable(self):
|
||||||
self.pfioc_rule = c_char * \
|
o = pfctl('-E')
|
||||||
(3104 if platform == 'darwin' else 3040)
|
_pf_context['Xtoken'].append(re.search(b'Token : (.+)', o[1]).group(1))
|
||||||
|
|
||||||
# sizeof(struct pfioc_pooladdr)
|
def disable(self, anchor):
|
||||||
self.pfioc_pooladdr = c_char * 1136
|
pfctl('-a %s -F all' % anchor)
|
||||||
|
if _pf_context['Xtoken']:
|
||||||
|
pfctl('-X %s' % _pf_context['Xtoken'].pop().decode("ASCII"))
|
||||||
|
|
||||||
self.MAXPATHLEN = 1024
|
def add_anchors(self, anchor):
|
||||||
|
# before adding anchors and rules we must override the skip lo
|
||||||
|
# that in some cases ends up in the chain so the rules we will add,
|
||||||
|
# which rely on translating/filtering packets on lo, can work
|
||||||
|
if self.has_skip_loopback():
|
||||||
|
pfctl('-f /dev/stdin', b'pass on lo\n')
|
||||||
|
super(Darwin, self).add_anchors(anchor)
|
||||||
|
|
||||||
self.DIOCNATLOOK = (
|
def _add_natlook_ports(self, pnl, src_port, dst_port):
|
||||||
(0x40000000 | 0x80000000) |
|
pnl.sxport.port = socket.htons(src_port)
|
||||||
((sizeof(pfioc_natlook) & 0x1fff) << 16) |
|
pnl.dxport.port = socket.htons(dst_port)
|
||||||
((ord('D')) << 8) | (23))
|
|
||||||
self.DIOCCHANGERULE = (
|
|
||||||
(0x40000000 | 0x80000000) |
|
|
||||||
((sizeof(self.pfioc_rule) & 0x1fff) << 16) |
|
|
||||||
((ord('D')) << 8) | (26))
|
|
||||||
self.DIOCBEGINADDRS = (
|
|
||||||
(0x40000000 | 0x80000000) |
|
|
||||||
((sizeof(self.pfioc_pooladdr) & 0x1fff) << 16) |
|
|
||||||
((ord('D')) << 8) | (51))
|
|
||||||
|
|
||||||
self.PF_CHANGE_ADD_TAIL = 2
|
def _get_natlook_port(self, xport):
|
||||||
self.PF_CHANGE_GET_TICKET = 6
|
return xport.port
|
||||||
|
|
||||||
self.PF_PASS = 0
|
|
||||||
self.PF_RDR = 8
|
|
||||||
|
|
||||||
self.PF_OUT = 2
|
if sys.platform == 'darwin':
|
||||||
|
pf = Darwin()
|
||||||
|
elif sys.platform.startswith('openbsd'):
|
||||||
|
pf = OpenBsd()
|
||||||
|
else:
|
||||||
|
pf = FreeBsd()
|
||||||
|
|
||||||
osdefs = OsDefs()
|
|
||||||
|
def pfctl(args, stdin=None):
|
||||||
|
argv = ['pfctl'] + list(args.split(" "))
|
||||||
|
debug1('>> %s\n' % ' '.join(argv))
|
||||||
|
|
||||||
|
env = {
|
||||||
|
'PATH': os.environ['PATH'],
|
||||||
|
'LC_ALL': "C",
|
||||||
|
}
|
||||||
|
p = ssubprocess.Popen(argv, stdin=ssubprocess.PIPE,
|
||||||
|
stdout=ssubprocess.PIPE,
|
||||||
|
stderr=ssubprocess.PIPE,
|
||||||
|
env=env)
|
||||||
|
o = p.communicate(stdin)
|
||||||
|
if p.returncode:
|
||||||
|
raise Fatal('%r returned %d' % (argv, p.returncode))
|
||||||
|
|
||||||
|
return o
|
||||||
|
|
||||||
|
|
||||||
def pf_get_dev():
|
def pf_get_dev():
|
||||||
@ -114,62 +368,17 @@ def pf_get_dev():
|
|||||||
return _pf_fd
|
return _pf_fd
|
||||||
|
|
||||||
|
|
||||||
def pf_query_nat(family, proto, src_ip, src_port, dst_ip, dst_port):
|
def pf_get_anchor(family, port):
|
||||||
[proto, family, src_port, dst_port] = [
|
return 'sshuttle%s-%d' % ('' if family == socket.AF_INET else '6', port)
|
||||||
int(v) for v in [proto, family, src_port, dst_port]]
|
|
||||||
|
|
||||||
packed_src_ip = socket.inet_pton(family, src_ip)
|
|
||||||
packed_dst_ip = socket.inet_pton(family, dst_ip)
|
|
||||||
|
|
||||||
assert len(packed_src_ip) == len(packed_dst_ip)
|
|
||||||
length = len(packed_src_ip)
|
|
||||||
|
|
||||||
pnl = osdefs.pfioc_natlook()
|
|
||||||
pnl.proto = proto
|
|
||||||
pnl.direction = osdefs.PF_OUT
|
|
||||||
pnl.af = family
|
|
||||||
memmove(addressof(pnl.saddr), packed_src_ip, length)
|
|
||||||
memmove(addressof(pnl.daddr), packed_dst_ip, length)
|
|
||||||
pnl.sxport.port = socket.htons(src_port)
|
|
||||||
pnl.dxport.port = socket.htons(dst_port)
|
|
||||||
|
|
||||||
ioctl(pf_get_dev(), osdefs.DIOCNATLOOK,
|
|
||||||
(c_char * sizeof(pnl)).from_address(addressof(pnl)))
|
|
||||||
|
|
||||||
ip = socket.inet_ntop(
|
|
||||||
pnl.af, (c_char * length).from_address(addressof(pnl.rdaddr)).raw)
|
|
||||||
port = socket.ntohs(pnl.rdxport.port)
|
|
||||||
return (ip, port)
|
|
||||||
|
|
||||||
|
|
||||||
def pf_add_anchor_rule(type, name):
|
|
||||||
ACTION_OFFSET = 0
|
|
||||||
POOL_TICKET_OFFSET = 8
|
|
||||||
ANCHOR_CALL_OFFSET = 1040
|
|
||||||
RULE_ACTION_OFFSET = 3068 if osdefs.platform == 'darwin' else 2968
|
|
||||||
|
|
||||||
pr = osdefs.pfioc_rule()
|
|
||||||
ppa = osdefs.pfioc_pooladdr()
|
|
||||||
|
|
||||||
ioctl(pf_get_dev(), osdefs.DIOCBEGINADDRS, ppa)
|
|
||||||
|
|
||||||
memmove(addressof(pr) + POOL_TICKET_OFFSET, ppa[4:8], 4) # pool_ticket
|
|
||||||
memmove(addressof(pr) + ANCHOR_CALL_OFFSET, name,
|
|
||||||
min(osdefs.MAXPATHLEN, len(name))) # anchor_call = name
|
|
||||||
memmove(addressof(pr) + RULE_ACTION_OFFSET,
|
|
||||||
struct.pack('I', type), 4) # rule.action = type
|
|
||||||
|
|
||||||
memmove(addressof(pr) + ACTION_OFFSET, struct.pack(
|
|
||||||
'I', osdefs.PF_CHANGE_GET_TICKET), 4) # action = PF_CHANGE_GET_TICKET
|
|
||||||
ioctl(pf_get_dev(), osdefs.DIOCCHANGERULE, pr)
|
|
||||||
|
|
||||||
memmove(addressof(pr) + ACTION_OFFSET, struct.pack(
|
|
||||||
'I', osdefs.PF_CHANGE_ADD_TAIL), 4) # action = PF_CHANGE_ADD_TAIL
|
|
||||||
ioctl(pf_get_dev(), osdefs.DIOCCHANGERULE, pr)
|
|
||||||
|
|
||||||
|
|
||||||
class Method(BaseMethod):
|
class Method(BaseMethod):
|
||||||
|
|
||||||
|
def get_supported_features(self):
|
||||||
|
result = super(Method, self).get_supported_features()
|
||||||
|
result.ipv6 = True
|
||||||
|
return result
|
||||||
|
|
||||||
def get_tcp_dstip(self, sock):
|
def get_tcp_dstip(self, sock):
|
||||||
pfile = self.firewall.pfile
|
pfile = self.firewall.pfile
|
||||||
|
|
||||||
@ -195,7 +404,7 @@ class Method(BaseMethod):
|
|||||||
translating_rules = []
|
translating_rules = []
|
||||||
filtering_rules = []
|
filtering_rules = []
|
||||||
|
|
||||||
if family != socket.AF_INET:
|
if family not in [socket.AF_INET, socket.AF_INET6]:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
'Address family "%s" unsupported by pf method_name'
|
'Address family "%s" unsupported by pf method_name'
|
||||||
% family_to_string(family))
|
% family_to_string(family))
|
||||||
@ -214,65 +423,25 @@ class Method(BaseMethod):
|
|||||||
snet.encode("ASCII"),
|
snet.encode("ASCII"),
|
||||||
swidth))
|
swidth))
|
||||||
|
|
||||||
tables.append(
|
anchor = pf_get_anchor(family, port)
|
||||||
b'table <forward_subnets> {%s}' % b','.join(includes))
|
pf.add_anchors(anchor)
|
||||||
translating_rules.append(
|
pf.add_rules(anchor, includes, port, dnsport, nslist, family)
|
||||||
b'rdr pass on lo0 proto tcp '
|
pf.enable()
|
||||||
b'to <forward_subnets> -> 127.0.0.1 port %r' % port)
|
|
||||||
filtering_rules.append(
|
|
||||||
b'pass out route-to lo0 inet proto tcp '
|
|
||||||
b'to <forward_subnets> keep state')
|
|
||||||
|
|
||||||
if len(nslist) > 0:
|
|
||||||
tables.append(
|
|
||||||
b'table <dns_servers> {%s}' %
|
|
||||||
b','.join([ns[1].encode("ASCII") for ns in nslist]))
|
|
||||||
translating_rules.append(
|
|
||||||
b'rdr pass on lo0 proto udp to '
|
|
||||||
b'<dns_servers> port 53 -> 127.0.0.1 port %r' % dnsport)
|
|
||||||
filtering_rules.append(
|
|
||||||
b'pass out route-to lo0 inet proto udp to '
|
|
||||||
b'<dns_servers> port 53 keep state')
|
|
||||||
|
|
||||||
rules = b'\n'.join(tables + translating_rules + filtering_rules) \
|
|
||||||
+ b'\n'
|
|
||||||
assert isinstance(rules, bytes)
|
|
||||||
debug3("rules:\n" + rules.decode("ASCII"))
|
|
||||||
|
|
||||||
pf_status = pfctl('-s all')[0]
|
|
||||||
if b'\nrdr-anchor "sshuttle" all\n' not in pf_status:
|
|
||||||
pf_add_anchor_rule(osdefs.PF_RDR, b"sshuttle")
|
|
||||||
if b'\nanchor "sshuttle" all\n' not in pf_status:
|
|
||||||
pf_add_anchor_rule(osdefs.PF_PASS, b"sshuttle")
|
|
||||||
|
|
||||||
pfctl('-a sshuttle -f /dev/stdin', rules)
|
|
||||||
if osdefs.platform == "darwin":
|
|
||||||
o = pfctl('-E')
|
|
||||||
_pf_context['Xtoken'] = \
|
|
||||||
re.search(b'Token : (.+)', o[1]).group(1)
|
|
||||||
elif b'INFO:\nStatus: Disabled' in pf_status:
|
|
||||||
pfctl('-e')
|
|
||||||
_pf_context['started_by_sshuttle'] = True
|
|
||||||
|
|
||||||
def restore_firewall(self, port, family, udp):
|
def restore_firewall(self, port, family, udp):
|
||||||
if family != socket.AF_INET:
|
if family not in [socket.AF_INET, socket.AF_INET6]:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
'Address family "%s" unsupported by pf method_name'
|
'Address family "%s" unsupported by pf method_name'
|
||||||
% family_to_string(family))
|
% family_to_string(family))
|
||||||
if udp:
|
if udp:
|
||||||
raise Exception("UDP not supported by pf method_name")
|
raise Exception("UDP not supported by pf method_name")
|
||||||
|
|
||||||
pfctl('-a sshuttle -F all')
|
pf.disable(pf_get_anchor(family, port))
|
||||||
if osdefs.platform == "darwin":
|
|
||||||
if _pf_context['Xtoken'] is not None:
|
|
||||||
pfctl('-X %s' % _pf_context['Xtoken'].decode("ASCII"))
|
|
||||||
elif _pf_context['started_by_sshuttle']:
|
|
||||||
pfctl('-d')
|
|
||||||
|
|
||||||
def firewall_command(self, line):
|
def firewall_command(self, line):
|
||||||
if line.startswith('QUERY_PF_NAT '):
|
if line.startswith('QUERY_PF_NAT '):
|
||||||
try:
|
try:
|
||||||
dst = pf_query_nat(*(line[13:].split(',')))
|
dst = pf.query_nat(*(line[13:].split(',')))
|
||||||
sys.stdout.write('QUERY_PF_NAT_SUCCESS %s,%r\n' % dst)
|
sys.stdout.write('QUERY_PF_NAT_SUCCESS %s,%r\n' % dst)
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
sys.stdout.write('QUERY_PF_NAT_FAILURE %s\n' % e)
|
sys.stdout.write('QUERY_PF_NAT_FAILURE %s\n' % e)
|
||||||
|
@ -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="""
|
||||||
|
continuously scan for remote hostnames and update local /etc/hosts as they are found
|
||||||
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=[],
|
||||||
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()
|
comma-separated list of hostnames for initial scan (may be used with or without --auto-hosts)
|
||||||
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):
|
||||||
@ -60,10 +63,14 @@ def _shl(n, bits):
|
|||||||
def _list_routes():
|
def _list_routes():
|
||||||
# FIXME: IPv4 only
|
# FIXME: IPv4 only
|
||||||
argv = ['netstat', '-rn']
|
argv = ['netstat', '-rn']
|
||||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE)
|
env = {
|
||||||
|
'PATH': os.environ['PATH'],
|
||||||
|
'LC_ALL': "C",
|
||||||
|
}
|
||||||
|
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=env)
|
||||||
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
|
||||||
@ -91,7 +98,7 @@ def _exc_dump():
|
|||||||
return ''.join(traceback.format_exception(*exc_info))
|
return ''.join(traceback.format_exception(*exc_info))
|
||||||
|
|
||||||
|
|
||||||
def start_hostwatch(seed_hosts):
|
def start_hostwatch(seed_hosts, auto_hosts):
|
||||||
s1, s2 = socket.socketpair()
|
s1, s2 = socket.socketpair()
|
||||||
pid = os.fork()
|
pid = os.fork()
|
||||||
if not pid:
|
if not pid:
|
||||||
@ -103,7 +110,7 @@ def start_hostwatch(seed_hosts):
|
|||||||
os.dup2(s1.fileno(), 1)
|
os.dup2(s1.fileno(), 1)
|
||||||
os.dup2(s1.fileno(), 0)
|
os.dup2(s1.fileno(), 0)
|
||||||
s1.close()
|
s1.close()
|
||||||
rv = hostwatch.hw_main(seed_hosts) or 0
|
rv = hostwatch.hw_main(seed_hosts, auto_hosts) or 0
|
||||||
except Exception:
|
except Exception:
|
||||||
log('%s\n' % _exc_dump())
|
log('%s\n' % _exc_dump())
|
||||||
rv = 98
|
rv = 98
|
||||||
@ -149,7 +156,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 +174,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,22 +210,24 @@ 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))
|
||||||
hdr = "%s,%r," % (peer[0], peer[1])
|
hdr = b("%s,%r," % (peer[0], peer[1]))
|
||||||
self.mux.send(self.chan, ssnet.CMD_UDP_DATA, hdr + data)
|
self.mux.send(self.chan, ssnet.CMD_UDP_DATA, hdr + data)
|
||||||
|
|
||||||
|
|
||||||
def main(latency_control):
|
def main(latency_control, auto_hosts):
|
||||||
debug1('Starting server with Python version %s\n'
|
debug1('Starting server with Python version %s\n'
|
||||||
% platform.python_version())
|
% platform.python_version())
|
||||||
|
|
||||||
@ -241,39 +252,46 @@ 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('')
|
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')
|
||||||
|
|
||||||
def got_host_req(data):
|
def got_host_req(data):
|
||||||
if not hw.pid:
|
if not hw.pid:
|
||||||
(hw.pid, hw.sock) = start_hostwatch(data.strip().split())
|
(hw.pid, hw.sock) = start_hostwatch(
|
||||||
|
data.strip().split(), auto_hosts)
|
||||||
handlers.append(Handler(socks=[hw.sock],
|
handlers.append(Handler(socks=[hw.sock],
|
||||||
callback=hostwatch_ready))
|
callback=hostwatch_ready))
|
||||||
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)
|
||||||
|
# AF_INET is the same constant on Linux and BSD but AF_INET6
|
||||||
|
# is different. As the client and server can be running on
|
||||||
|
# different platforms we can not just set the socket family
|
||||||
|
# to what comes in the wire.
|
||||||
|
if family != socket.AF_INET:
|
||||||
|
family = socket.AF_INET6
|
||||||
dstport = int(dstport)
|
dstport = int(dstport)
|
||||||
outwrap = ssnet.connect_dst(family, dstip, dstport)
|
outwrap = ssnet.connect_dst(family, dstip, dstport)
|
||||||
handlers.append(Proxy(MuxWrapper(mux, channel), outwrap))
|
handlers.append(Proxy(MuxWrapper(mux, channel), outwrap))
|
||||||
@ -293,7 +311,7 @@ def main(latency_control):
|
|||||||
def udp_req(channel, cmd, data):
|
def udp_req(channel, cmd, data):
|
||||||
debug2('Incoming UDP request channel=%d, cmd=%d\n' % (channel, cmd))
|
debug2('Incoming UDP request channel=%d, cmd=%d\n' % (channel, cmd))
|
||||||
if cmd == ssnet.CMD_UDP_DATA:
|
if cmd == ssnet.CMD_UDP_DATA:
|
||||||
(dstip, dstport, data) = data.split(",", 2)
|
(dstip, dstport, data) = data.split(b(','), 2)
|
||||||
dstport = int(dstport)
|
dstport = int(dstport)
|
||||||
debug2('is incoming UDP data. %r %d.\n' % (dstip, dstport))
|
debug2('is incoming UDP data. %r %d.\n' % (dstip, dstport))
|
||||||
h = udphandlers[channel]
|
h = udphandlers[channel]
|
||||||
|
@ -8,6 +8,13 @@ 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,17 +96,17 @@ 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())
|
||||||
|
|
||||||
if not rhost:
|
if not rhost:
|
||||||
# ignore the --python argument when running locally; we already know
|
# ignore the --python argument when running locally; we already know
|
||||||
# which python version works.
|
# which python version works.
|
||||||
argv = [sys.argv[1], '-c', pyscript]
|
argv = [sys.executable, '-c', pyscript]
|
||||||
else:
|
else:
|
||||||
if ssh_cmd:
|
if ssh_cmd:
|
||||||
sshl = ssh_cmd.split(' ')
|
sshl = ssh_cmd.split(' ')
|
||||||
@ -109,7 +116,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
|
||||||
|
|
||||||
@ -53,7 +54,8 @@ cmd_to_name = {
|
|||||||
|
|
||||||
NET_ERRS = [errno.ECONNREFUSED, errno.ETIMEDOUT,
|
NET_ERRS = [errno.ECONNREFUSED, errno.ETIMEDOUT,
|
||||||
errno.EHOSTUNREACH, errno.ENETUNREACH,
|
errno.EHOSTUNREACH, errno.ENETUNREACH,
|
||||||
errno.EHOSTDOWN, errno.ENETDOWN]
|
errno.EHOSTDOWN, errno.ENETDOWN,
|
||||||
|
errno.ENETUNREACH]
|
||||||
|
|
||||||
|
|
||||||
def _add(l, elem):
|
def _add(l, elem):
|
||||||
@ -75,7 +77,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 +91,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 +148,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 +196,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 +209,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 +232,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 +243,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 +341,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
|
||||||
@ -350,14 +358,14 @@ class Mux(Handler):
|
|||||||
|
|
||||||
def amount_queued(self):
|
def amount_queued(self):
|
||||||
total = 0
|
total = 0
|
||||||
for b in self.outbuf:
|
for byte in self.outbuf:
|
||||||
total += len(b)
|
total += len(byte)
|
||||||
return total
|
return total
|
||||||
|
|
||||||
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 +374,10 @@ 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 +443,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 +461,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 +506,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 +536,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
|
||||||
|
|
||||||
@ -548,7 +558,7 @@ def connect_dst(family, ip, port):
|
|||||||
outsock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
|
outsock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
|
||||||
return SockWrapper(outsock, outsock,
|
return SockWrapper(outsock, outsock,
|
||||||
connect_to=(ip, port),
|
connect_to=(ip, port),
|
||||||
peername = '%s:%d' % (ip, port))
|
peername='%s:%d' % (ip, port))
|
||||||
|
|
||||||
|
|
||||||
def runonce(handlers, mux):
|
def runonce(handlers, mux):
|
||||||
|
@ -4,13 +4,13 @@ import socket
|
|||||||
|
|
||||||
from sshuttle.methods import get_method
|
from sshuttle.methods import get_method
|
||||||
from sshuttle.helpers import Fatal
|
from sshuttle.helpers import Fatal
|
||||||
from sshuttle.methods.pf import OsDefs
|
from sshuttle.methods.pf import FreeBsd, Darwin, OpenBsd
|
||||||
|
|
||||||
|
|
||||||
def test_get_supported_features():
|
def test_get_supported_features():
|
||||||
method = get_method('pf')
|
method = get_method('pf')
|
||||||
features = method.get_supported_features()
|
features = method.get_supported_features()
|
||||||
assert not features.ipv6
|
assert features.ipv6
|
||||||
assert not features.udp
|
assert not features.udp
|
||||||
assert features.dns
|
assert features.dns
|
||||||
|
|
||||||
@ -85,7 +85,7 @@ def test_assert_features():
|
|||||||
method.assert_features(features)
|
method.assert_features(features)
|
||||||
|
|
||||||
|
|
||||||
@patch('sshuttle.methods.pf.osdefs', OsDefs('darwin'))
|
@patch('sshuttle.methods.pf.pf', Darwin())
|
||||||
@patch('sshuttle.methods.pf.sys.stdout')
|
@patch('sshuttle.methods.pf.sys.stdout')
|
||||||
@patch('sshuttle.methods.pf.ioctl')
|
@patch('sshuttle.methods.pf.ioctl')
|
||||||
@patch('sshuttle.methods.pf.pf_get_dev')
|
@patch('sshuttle.methods.pf.pf_get_dev')
|
||||||
@ -108,11 +108,11 @@ def test_firewall_command_darwin(mock_pf_get_dev, mock_ioctl, mock_stdout):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@patch('sshuttle.methods.pf.osdefs', OsDefs('notdarwin'))
|
@patch('sshuttle.methods.pf.pf', FreeBsd())
|
||||||
@patch('sshuttle.methods.pf.sys.stdout')
|
@patch('sshuttle.methods.pf.sys.stdout')
|
||||||
@patch('sshuttle.methods.pf.ioctl')
|
@patch('sshuttle.methods.pf.ioctl')
|
||||||
@patch('sshuttle.methods.pf.pf_get_dev')
|
@patch('sshuttle.methods.pf.pf_get_dev')
|
||||||
def test_firewall_command_notdarwin(mock_pf_get_dev, mock_ioctl, mock_stdout):
|
def test_firewall_command_freebsd(mock_pf_get_dev, mock_ioctl, mock_stdout):
|
||||||
method = get_method('pf')
|
method = get_method('pf')
|
||||||
assert not method.firewall_command("somthing")
|
assert not method.firewall_command("somthing")
|
||||||
|
|
||||||
@ -131,7 +131,32 @@ def test_firewall_command_notdarwin(mock_pf_get_dev, mock_ioctl, mock_stdout):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@patch('sshuttle.methods.pf.pf', OpenBsd())
|
||||||
|
@patch('sshuttle.methods.pf.sys.stdout')
|
||||||
|
@patch('sshuttle.methods.pf.ioctl')
|
||||||
|
@patch('sshuttle.methods.pf.pf_get_dev')
|
||||||
|
def test_firewall_command_openbsd(mock_pf_get_dev, mock_ioctl, mock_stdout):
|
||||||
|
method = get_method('pf')
|
||||||
|
assert not method.firewall_command("somthing")
|
||||||
|
|
||||||
|
command = "QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % (
|
||||||
|
socket.AF_INET, socket.IPPROTO_TCP,
|
||||||
|
"127.0.0.1", 1025, "127.0.0.2", 1024)
|
||||||
|
assert method.firewall_command(command)
|
||||||
|
|
||||||
|
assert mock_pf_get_dev.mock_calls == [call()]
|
||||||
|
assert mock_ioctl.mock_calls == [
|
||||||
|
call(mock_pf_get_dev(), 0xc0504417, ANY),
|
||||||
|
]
|
||||||
|
assert mock_stdout.mock_calls == [
|
||||||
|
call.write('QUERY_PF_NAT_SUCCESS 0.0.0.0,0\n'),
|
||||||
|
call.flush(),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def pfctl(args, stdin=None):
|
def pfctl(args, stdin=None):
|
||||||
|
if args == '-s Interfaces -i lo -v':
|
||||||
|
return (b'lo0 (skip)',)
|
||||||
if args == '-s all':
|
if args == '-s all':
|
||||||
return (b'INFO:\nStatus: Disabled\nanother mary had a little lamb\n',
|
return (b'INFO:\nStatus: Disabled\nanother mary had a little lamb\n',
|
||||||
b'little lamb\n')
|
b'little lamb\n')
|
||||||
@ -141,7 +166,7 @@ def pfctl(args, stdin=None):
|
|||||||
|
|
||||||
|
|
||||||
@patch('sshuttle.helpers.verbose', new=3)
|
@patch('sshuttle.helpers.verbose', new=3)
|
||||||
@patch('sshuttle.methods.pf.osdefs', OsDefs('darwin'))
|
@patch('sshuttle.methods.pf.pf', Darwin())
|
||||||
@patch('sshuttle.methods.pf.pfctl')
|
@patch('sshuttle.methods.pf.pfctl')
|
||||||
@patch('sshuttle.methods.pf.ioctl')
|
@patch('sshuttle.methods.pf.ioctl')
|
||||||
@patch('sshuttle.methods.pf.pf_get_dev')
|
@patch('sshuttle.methods.pf.pf_get_dev')
|
||||||
@ -151,19 +176,45 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
|||||||
method = get_method('pf')
|
method = get_method('pf')
|
||||||
assert method.name == 'pf'
|
assert method.name == 'pf'
|
||||||
|
|
||||||
with pytest.raises(Exception) as excinfo:
|
# IPV6
|
||||||
method.setup_firewall(
|
|
||||||
1024, 1026,
|
method.setup_firewall(
|
||||||
[(10, u'2404:6800:4004:80c::33')],
|
1024, 1026,
|
||||||
10,
|
[(10, u'2404:6800:4004:80c::33')],
|
||||||
[(10, 64, False, u'2404:6800:4004:80c::'),
|
10,
|
||||||
(10, 128, True, u'2404:6800:4004:80c::101f')],
|
[(10, 64, False, u'2404:6800:4004:80c::'),
|
||||||
True)
|
(10, 128, True, u'2404:6800:4004:80c::101f')],
|
||||||
assert str(excinfo.value) \
|
False)
|
||||||
== 'Address family "AF_INET6" unsupported by pf method_name'
|
assert mock_ioctl.mock_calls == [
|
||||||
assert mock_pf_get_dev.mock_calls == []
|
call(mock_pf_get_dev(), 0xC4704433, ANY),
|
||||||
assert mock_ioctl.mock_calls == []
|
call(mock_pf_get_dev(), 0xCC20441A, ANY),
|
||||||
assert mock_pfctl.mock_calls == []
|
call(mock_pf_get_dev(), 0xCC20441A, ANY),
|
||||||
|
call(mock_pf_get_dev(), 0xC4704433, ANY),
|
||||||
|
call(mock_pf_get_dev(), 0xCC20441A, ANY),
|
||||||
|
call(mock_pf_get_dev(), 0xCC20441A, ANY),
|
||||||
|
]
|
||||||
|
assert mock_pfctl.mock_calls == [
|
||||||
|
call('-s Interfaces -i lo -v'),
|
||||||
|
call('-f /dev/stdin', b'pass on lo\n'),
|
||||||
|
call('-s all'),
|
||||||
|
call('-a sshuttle6-1024 -f /dev/stdin',
|
||||||
|
b'table <forward_subnets> {'
|
||||||
|
b'!2404:6800:4004:80c::101f/128,2404:6800:4004:80c::/64'
|
||||||
|
b'}\n'
|
||||||
|
b'table <dns_servers> {2404:6800:4004:80c::33}\n'
|
||||||
|
b'rdr pass on lo0 inet6 proto tcp '
|
||||||
|
b'to <forward_subnets> -> ::1 port 1024\n'
|
||||||
|
b'rdr pass on lo0 inet6 proto udp '
|
||||||
|
b'to <dns_servers> port 53 -> ::1 port 1026\n'
|
||||||
|
b'pass out route-to lo0 inet6 proto tcp '
|
||||||
|
b'to <forward_subnets> keep state\n'
|
||||||
|
b'pass out route-to lo0 inet6 proto udp '
|
||||||
|
b'to <dns_servers> port 53 keep state\n'),
|
||||||
|
call('-E'),
|
||||||
|
]
|
||||||
|
mock_pf_get_dev.reset_mock()
|
||||||
|
mock_ioctl.reset_mock()
|
||||||
|
mock_pfctl.reset_mock()
|
||||||
|
|
||||||
with pytest.raises(Exception) as excinfo:
|
with pytest.raises(Exception) as excinfo:
|
||||||
method.setup_firewall(
|
method.setup_firewall(
|
||||||
@ -192,13 +243,15 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
|||||||
call(mock_pf_get_dev(), 0xCC20441A, ANY),
|
call(mock_pf_get_dev(), 0xCC20441A, ANY),
|
||||||
]
|
]
|
||||||
assert mock_pfctl.mock_calls == [
|
assert mock_pfctl.mock_calls == [
|
||||||
|
call('-s Interfaces -i lo -v'),
|
||||||
|
call('-f /dev/stdin', b'pass on lo\n'),
|
||||||
call('-s all'),
|
call('-s all'),
|
||||||
call('-a sshuttle -f /dev/stdin',
|
call('-a sshuttle-1025 -f /dev/stdin',
|
||||||
b'table <forward_subnets> {!1.2.3.66/32,1.2.3.0/24}\n'
|
b'table <forward_subnets> {!1.2.3.66/32,1.2.3.0/24}\n'
|
||||||
b'table <dns_servers> {1.2.3.33}\n'
|
b'table <dns_servers> {1.2.3.33}\n'
|
||||||
b'rdr pass on lo0 proto tcp '
|
b'rdr pass on lo0 inet proto tcp '
|
||||||
b'to <forward_subnets> -> 127.0.0.1 port 1025\n'
|
b'to <forward_subnets> -> 127.0.0.1 port 1025\n'
|
||||||
b'rdr pass on lo0 proto udp '
|
b'rdr pass on lo0 inet proto udp '
|
||||||
b'to <dns_servers> port 53 -> 127.0.0.1 port 1027\n'
|
b'to <dns_servers> port 53 -> 127.0.0.1 port 1027\n'
|
||||||
b'pass out route-to lo0 inet proto tcp '
|
b'pass out route-to lo0 inet proto tcp '
|
||||||
b'to <forward_subnets> keep state\n'
|
b'to <forward_subnets> keep state\n'
|
||||||
@ -213,7 +266,7 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
|||||||
method.restore_firewall(1025, 2, False)
|
method.restore_firewall(1025, 2, False)
|
||||||
assert mock_ioctl.mock_calls == []
|
assert mock_ioctl.mock_calls == []
|
||||||
assert mock_pfctl.mock_calls == [
|
assert mock_pfctl.mock_calls == [
|
||||||
call('-a sshuttle -F all'),
|
call('-a sshuttle-1025 -F all'),
|
||||||
call("-X abcdefg"),
|
call("-X abcdefg"),
|
||||||
]
|
]
|
||||||
mock_pf_get_dev.reset_mock()
|
mock_pf_get_dev.reset_mock()
|
||||||
@ -222,29 +275,44 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
|||||||
|
|
||||||
|
|
||||||
@patch('sshuttle.helpers.verbose', new=3)
|
@patch('sshuttle.helpers.verbose', new=3)
|
||||||
@patch('sshuttle.methods.pf.osdefs', OsDefs('notdarwin'))
|
@patch('sshuttle.methods.pf.pf', FreeBsd())
|
||||||
@patch('sshuttle.methods.pf.pfctl')
|
@patch('sshuttle.methods.pf.pfctl')
|
||||||
@patch('sshuttle.methods.pf.ioctl')
|
@patch('sshuttle.methods.pf.ioctl')
|
||||||
@patch('sshuttle.methods.pf.pf_get_dev')
|
@patch('sshuttle.methods.pf.pf_get_dev')
|
||||||
def test_setup_firewall_notdarwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
||||||
mock_pfctl.side_effect = pfctl
|
mock_pfctl.side_effect = pfctl
|
||||||
|
|
||||||
method = get_method('pf')
|
method = get_method('pf')
|
||||||
assert method.name == 'pf'
|
assert method.name == 'pf'
|
||||||
|
|
||||||
with pytest.raises(Exception) as excinfo:
|
method.setup_firewall(
|
||||||
method.setup_firewall(
|
1024, 1026,
|
||||||
1024, 1026,
|
[(10, u'2404:6800:4004:80c::33')],
|
||||||
[(10, u'2404:6800:4004:80c::33')],
|
10,
|
||||||
10,
|
[(10, 64, False, u'2404:6800:4004:80c::'),
|
||||||
[(10, 64, False, u'2404:6800:4004:80c::'),
|
(10, 128, True, u'2404:6800:4004:80c::101f')],
|
||||||
(10, 128, True, u'2404:6800:4004:80c::101f')],
|
False)
|
||||||
True)
|
|
||||||
assert str(excinfo.value) \
|
assert mock_pfctl.mock_calls == [
|
||||||
== 'Address family "AF_INET6" unsupported by pf method_name'
|
call('-s all'),
|
||||||
assert mock_pf_get_dev.mock_calls == []
|
call('-a sshuttle6-1024 -f /dev/stdin',
|
||||||
assert mock_ioctl.mock_calls == []
|
b'table <forward_subnets> {'
|
||||||
assert mock_pfctl.mock_calls == []
|
b'!2404:6800:4004:80c::101f/128,2404:6800:4004:80c::/64'
|
||||||
|
b'}\n'
|
||||||
|
b'table <dns_servers> {2404:6800:4004:80c::33}\n'
|
||||||
|
b'rdr pass on lo0 inet6 proto tcp '
|
||||||
|
b'to <forward_subnets> -> ::1 port 1024\n'
|
||||||
|
b'rdr pass on lo0 inet6 proto udp '
|
||||||
|
b'to <dns_servers> port 53 -> ::1 port 1026\n'
|
||||||
|
b'pass out route-to lo0 inet6 proto tcp '
|
||||||
|
b'to <forward_subnets> keep state\n'
|
||||||
|
b'pass out route-to lo0 inet6 proto udp '
|
||||||
|
b'to <dns_servers> port 53 keep state\n'),
|
||||||
|
call('-e'),
|
||||||
|
]
|
||||||
|
mock_pf_get_dev.reset_mock()
|
||||||
|
mock_ioctl.reset_mock()
|
||||||
|
mock_pfctl.reset_mock()
|
||||||
|
|
||||||
with pytest.raises(Exception) as excinfo:
|
with pytest.raises(Exception) as excinfo:
|
||||||
method.setup_firewall(
|
method.setup_firewall(
|
||||||
@ -274,12 +342,12 @@ def test_setup_firewall_notdarwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
|||||||
]
|
]
|
||||||
assert mock_pfctl.mock_calls == [
|
assert mock_pfctl.mock_calls == [
|
||||||
call('-s all'),
|
call('-s all'),
|
||||||
call('-a sshuttle -f /dev/stdin',
|
call('-a sshuttle-1025 -f /dev/stdin',
|
||||||
b'table <forward_subnets> {!1.2.3.66/32,1.2.3.0/24}\n'
|
b'table <forward_subnets> {!1.2.3.66/32,1.2.3.0/24}\n'
|
||||||
b'table <dns_servers> {1.2.3.33}\n'
|
b'table <dns_servers> {1.2.3.33}\n'
|
||||||
b'rdr pass on lo0 proto tcp '
|
b'rdr pass on lo0 inet proto tcp '
|
||||||
b'to <forward_subnets> -> 127.0.0.1 port 1025\n'
|
b'to <forward_subnets> -> 127.0.0.1 port 1025\n'
|
||||||
b'rdr pass on lo0 proto udp '
|
b'rdr pass on lo0 inet proto udp '
|
||||||
b'to <dns_servers> port 53 -> 127.0.0.1 port 1027\n'
|
b'to <dns_servers> port 53 -> 127.0.0.1 port 1027\n'
|
||||||
b'pass out route-to lo0 inet proto tcp '
|
b'pass out route-to lo0 inet proto tcp '
|
||||||
b'to <forward_subnets> keep state\n'
|
b'to <forward_subnets> keep state\n'
|
||||||
@ -294,7 +362,106 @@ def test_setup_firewall_notdarwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
|||||||
method.restore_firewall(1025, 2, False)
|
method.restore_firewall(1025, 2, False)
|
||||||
assert mock_ioctl.mock_calls == []
|
assert mock_ioctl.mock_calls == []
|
||||||
assert mock_pfctl.mock_calls == [
|
assert mock_pfctl.mock_calls == [
|
||||||
call('-a sshuttle -F all'),
|
call('-a sshuttle-1025 -F all'),
|
||||||
|
call("-d"),
|
||||||
|
]
|
||||||
|
mock_pf_get_dev.reset_mock()
|
||||||
|
mock_pfctl.reset_mock()
|
||||||
|
mock_ioctl.reset_mock()
|
||||||
|
|
||||||
|
|
||||||
|
@patch('sshuttle.helpers.verbose', new=3)
|
||||||
|
@patch('sshuttle.methods.pf.pf', OpenBsd())
|
||||||
|
@patch('sshuttle.methods.pf.pfctl')
|
||||||
|
@patch('sshuttle.methods.pf.ioctl')
|
||||||
|
@patch('sshuttle.methods.pf.pf_get_dev')
|
||||||
|
def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
||||||
|
mock_pfctl.side_effect = pfctl
|
||||||
|
|
||||||
|
method = get_method('pf')
|
||||||
|
assert method.name == 'pf'
|
||||||
|
|
||||||
|
method.setup_firewall(
|
||||||
|
1024, 1026,
|
||||||
|
[(10, u'2404:6800:4004:80c::33')],
|
||||||
|
10,
|
||||||
|
[(10, 64, False, u'2404:6800:4004:80c::'),
|
||||||
|
(10, 128, True, u'2404:6800:4004:80c::101f')],
|
||||||
|
False)
|
||||||
|
|
||||||
|
assert mock_ioctl.mock_calls == [
|
||||||
|
call(mock_pf_get_dev(), 0xcd48441a, ANY),
|
||||||
|
call(mock_pf_get_dev(), 0xcd48441a, ANY),
|
||||||
|
]
|
||||||
|
assert mock_pfctl.mock_calls == [
|
||||||
|
call('-s Interfaces -i lo -v'),
|
||||||
|
call('-f /dev/stdin', b'match on lo\n'),
|
||||||
|
call('-s all'),
|
||||||
|
call('-a sshuttle6-1024 -f /dev/stdin',
|
||||||
|
b'table <forward_subnets> {'
|
||||||
|
b'!2404:6800:4004:80c::101f/128,2404:6800:4004:80c::/64'
|
||||||
|
b'}\n'
|
||||||
|
b'table <dns_servers> {2404:6800:4004:80c::33}\n'
|
||||||
|
b'pass in on lo0 inet6 proto tcp to '
|
||||||
|
b'<forward_subnets> divert-to ::1 port 1024\n'
|
||||||
|
b'pass in on lo0 inet6 proto udp '
|
||||||
|
b'to <dns_servers> port 53 rdr-to ::1 port 1026\n'
|
||||||
|
b'pass out inet6 proto tcp to '
|
||||||
|
b'<forward_subnets> route-to lo0 keep state\n'
|
||||||
|
b'pass out inet6 proto udp to '
|
||||||
|
b'<dns_servers> port 53 route-to lo0 keep state\n'),
|
||||||
|
call('-e'),
|
||||||
|
]
|
||||||
|
mock_pf_get_dev.reset_mock()
|
||||||
|
mock_ioctl.reset_mock()
|
||||||
|
mock_pfctl.reset_mock()
|
||||||
|
|
||||||
|
with pytest.raises(Exception) as excinfo:
|
||||||
|
method.setup_firewall(
|
||||||
|
1025, 1027,
|
||||||
|
[(2, u'1.2.3.33')],
|
||||||
|
2,
|
||||||
|
[(2, 24, False, u'1.2.3.0'), (2, 32, True, u'1.2.3.66')],
|
||||||
|
True)
|
||||||
|
assert str(excinfo.value) == 'UDP not supported by pf method_name'
|
||||||
|
assert mock_pf_get_dev.mock_calls == []
|
||||||
|
assert mock_ioctl.mock_calls == []
|
||||||
|
assert mock_pfctl.mock_calls == []
|
||||||
|
|
||||||
|
method.setup_firewall(
|
||||||
|
1025, 1027,
|
||||||
|
[(2, u'1.2.3.33')],
|
||||||
|
2,
|
||||||
|
[(2, 24, False, u'1.2.3.0'), (2, 32, True, u'1.2.3.66')],
|
||||||
|
False)
|
||||||
|
assert mock_ioctl.mock_calls == [
|
||||||
|
call(mock_pf_get_dev(), 0xcd48441a, ANY),
|
||||||
|
call(mock_pf_get_dev(), 0xcd48441a, ANY),
|
||||||
|
]
|
||||||
|
assert mock_pfctl.mock_calls == [
|
||||||
|
call('-s Interfaces -i lo -v'),
|
||||||
|
call('-f /dev/stdin', b'match on lo\n'),
|
||||||
|
call('-s all'),
|
||||||
|
call('-a sshuttle-1025 -f /dev/stdin',
|
||||||
|
b'table <forward_subnets> {!1.2.3.66/32,1.2.3.0/24}\n'
|
||||||
|
b'table <dns_servers> {1.2.3.33}\n'
|
||||||
|
b'pass in on lo0 inet proto tcp to <forward_subnets> divert-to 127.0.0.1 port 1025\n'
|
||||||
|
b'pass in on lo0 inet proto udp to '
|
||||||
|
b'<dns_servers> port 53 rdr-to 127.0.0.1 port 1027\n'
|
||||||
|
b'pass out inet proto tcp to '
|
||||||
|
b'<forward_subnets> route-to lo0 keep state\n'
|
||||||
|
b'pass out inet proto udp to '
|
||||||
|
b'<dns_servers> port 53 route-to lo0 keep state\n'),
|
||||||
|
call('-e'),
|
||||||
|
]
|
||||||
|
mock_pf_get_dev.reset_mock()
|
||||||
|
mock_ioctl.reset_mock()
|
||||||
|
mock_pfctl.reset_mock()
|
||||||
|
|
||||||
|
method.restore_firewall(1025, 2, False)
|
||||||
|
assert mock_ioctl.mock_calls == []
|
||||||
|
assert mock_pfctl.mock_calls == [
|
||||||
|
call('-a sshuttle-1025 -F all'),
|
||||||
call("-d"),
|
call("-d"),
|
||||||
]
|
]
|
||||||
mock_pf_get_dev.reset_mock()
|
mock_pf_get_dev.reset_mock()
|
68
sshuttle/tests/server/test_server.py
Normal file
68
sshuttle/tests/server/test_server.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import os
|
||||||
|
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()
|
||||||
|
|
||||||
|
env = {
|
||||||
|
'PATH': os.environ['PATH'],
|
||||||
|
'LC_ALL': "C",
|
||||||
|
}
|
||||||
|
assert mock_popen.mock_calls == [
|
||||||
|
call(['netstat', '-rn'], stdout=-1, env=env),
|
||||||
|
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