Compare commits

...

62 Commits

Author SHA1 Message Date
7fde5b6fa6 Release version 1.0.0 2020-06-05 08:13:23 +10:00
734168531f Update changelog 2020-06-05 08:05:41 +10:00
d058d9bc93 Bump pytest from 5.4.2 to 5.4.3
Bumps [pytest](https://github.com/pytest-dev/pytest) from 5.4.2 to 5.4.3.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/5.4.2...5.4.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-06-03 19:44:52 +10:00
1db3281c16 shutil.which is 3.3+ 2020-06-03 14:30:37 +10:00
bef54e778d remove ImportError based branching 2020-06-03 14:30:37 +10:00
9bcca27965 reduce 2020-06-03 14:30:37 +10:00
d0f0aa9f17 remove version_info based branching 2020-06-03 14:30:37 +10:00
ec2018d664 Bump setuptools-scm from 4.1.1 to 4.1.2
Bumps [setuptools-scm](https://github.com/pypa/setuptools_scm) from 4.1.1 to 4.1.2.
- [Release notes](https://github.com/pypa/setuptools_scm/releases)
- [Changelog](https://github.com/pypa/setuptools_scm/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pypa/setuptools_scm/compare/v4.1.1...v4.1.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-06-01 19:30:15 +10:00
c9bc389992 Remove legacy travis file 2020-05-29 07:45:49 +10:00
9f27c1943b Updated supported Python versions
* Drop 2.7
* Add 3.7 and 3.8
2020-05-29 07:44:51 +10:00
6c21addde9 Fix Python 3.8 file operations
Under Python 3.8 we can not wrap a File in a Sock.

Note this currently requires Python >= 3.5
2020-05-29 07:44:51 +10:00
4b320180c4 Bump setuptools-scm from 4.1.0 to 4.1.1
Bumps [setuptools-scm](https://github.com/pypa/setuptools_scm) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/pypa/setuptools_scm/releases)
- [Changelog](https://github.com/pypa/setuptools_scm/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pypa/setuptools_scm/compare/v4.1.0...v4.1.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-27 20:53:12 +10:00
994ce07466 Bump setuptools-scm from 4.0.0 to 4.1.0
Bumps [setuptools-scm](https://github.com/pypa/setuptools_scm) from 4.0.0 to 4.1.0.
- [Release notes](https://github.com/pypa/setuptools_scm/releases)
- [Changelog](https://github.com/pypa/setuptools_scm/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pypa/setuptools_scm/compare/v4.0.0...v4.1.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-26 19:38:26 +10:00
34197c492c Bump setuptools-scm from 3.5.0 to 4.0.0
Bumps [setuptools-scm](https://github.com/pypa/setuptools_scm) from 3.5.0 to 4.0.0.
- [Release notes](https://github.com/pypa/setuptools_scm/releases)
- [Changelog](https://github.com/pypa/setuptools_scm/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pypa/setuptools_scm/compare/v3.5.0...v4.0.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-26 07:26:46 +10:00
75eaac7e06 Bump pytest-cov from 2.8.1 to 2.9.0
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 2.8.1 to 2.9.0.
- [Release notes](https://github.com/pytest-dev/pytest-cov/releases)
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v2.8.1...v2.9.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-26 07:26:15 +10:00
b0c87b01b7 Bump flake8 from 3.8.1 to 3.8.2
Bumps [flake8](https://gitlab.com/pycqa/flake8) from 3.8.1 to 3.8.2.
- [Release notes](https://gitlab.com/pycqa/flake8/tags)
- [Commits](https://gitlab.com/pycqa/flake8/compare/3.8.1...3.8.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-26 07:25:59 +10:00
cf32a5cfa8 Bump flake8 from 3.6.0 to 3.8.1
Bumps [flake8](https://gitlab.com/pycqa/flake8) from 3.6.0 to 3.8.1.
- [Release notes](https://gitlab.com/pycqa/flake8/tags)
- [Commits](https://gitlab.com/pycqa/flake8/compare/3.6.0...3.8.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-22 08:10:45 +10:00
f674aacdc8 Bump setuptools-scm from 1.15.6 to 3.5.0
Bumps [setuptools-scm](https://github.com/pypa/setuptools_scm) from 1.15.6 to 3.5.0.
- [Release notes](https://github.com/pypa/setuptools_scm/releases)
- [Changelog](https://github.com/pypa/setuptools_scm/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pypa/setuptools_scm/compare/v1.15.6...v3.5.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-21 08:17:31 +10:00
432f86f253 Bump attrs from 19.1.0 to 19.3.0
Bumps [attrs](https://github.com/python-attrs/attrs) from 19.1.0 to 19.3.0.
- [Release notes](https://github.com/python-attrs/attrs/releases)
- [Changelog](https://github.com/python-attrs/attrs/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/python-attrs/attrs/compare/19.1.0...19.3.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-21 08:15:06 +10:00
b63e58f494 Create github workflow 2020-05-21 08:12:26 +10:00
88ce5c0bca Update flake8 2020-05-21 08:12:26 +10:00
50a4c36635 Bump pytest from 3.4.2 to 5.4.2
Bumps [pytest](https://github.com/pytest-dev/pytest) from 3.4.2 to 5.4.2.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/3.4.2...5.4.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-21 07:58:44 +10:00
25461c70a3 Bump pytest-cov from 2.6.0 to 2.8.1
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 2.6.0 to 2.8.1.
- [Release notes](https://github.com/pytest-dev/pytest-cov/releases)
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v2.6.0...v2.8.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-21 07:55:53 +10:00
365d30de14 Add 'My VPN broke and need a solution fast' to the docs. 2020-05-15 07:23:43 +10:00
6e19496fb7 remove debug message for getpeername failure 2020-05-10 14:20:38 +10:00
534ad8dfed fix crash triggered by port scans closing socket 2020-05-10 14:20:38 +10:00
535eb62928 sshuttle as service link 2020-05-10 14:19:36 +10:00
966fd0c523 Fix parsing of hostnames to allow ssh aliases defined in ssh configs) (#418)
* Fix parsing of hostnames to allow ssh aliases defined in ssh configs)

* nicer formatting, pep8 applied

* Properly parse IPv6 addresses with port specification

* Now also handles hostnames with port specified and IPv6 addresses without port  properly

* Updated parameter description for the remotehost specification

* Make the urlparse import backwards compatible to python2

Co-authored-by: Tobi <tobi-git@grimm.gr>
2020-04-25 09:40:39 +10:00
580462156e # Fix 410 Issue Correcte syntax write for connect server (#411) 2020-03-28 17:44:28 +11:00
9e78abd2c2 Add password in prompt cmd (#401)
* Add auto password prompt

Add auto password with sshpass
use user:password@host or user:password:port@host

* Update ssh.py

* Fix for IPv4 only

* Delete print sorry bad commit

* ipv4 fix

* Fix IPv4 args

* Fix for ipv6

* Fix ipv6 no password

* Add function parse_hostport

* Fix minor bug detect port

* Fix minor bug password detect

* Clear Code

* bad write "=" replace with "=="

* Rewrite code for more understand logical and fix minor bug

* add default define port

* delete old variable unused

* replace "==" per "is" try fix code reviews

* reback

* try define password with len

* Fix consistence variable password PR

* simplify function split ipv4 or ipv6

* clear code
2020-03-24 16:19:21 +11:00
e3201969b5 systemd integration doc adjustment
* the default NotifyAccess (=main) works now, no need to suggest =all
2020-03-24 16:15:41 +11:00
9b10df21b6 Arrange systemd notification to clarify the meaning
* this spot in the lifecycle is precisely when we know that the
   remote server is running AND that the local firewall-control
   daemon is started.
2020-03-24 16:15:41 +11:00
accb4ecc97 Run all systemd notifications from main process 2020-03-24 16:15:41 +11:00
ee36cc0d00 Trap UnicodeError to handle cases where hostnames returned by DNS are invalid. 2020-03-24 16:13:33 +11:00
f0c13eafe9 Fix formatting error in CHANGES.rst 2020-02-09 19:45:13 +11:00
36d34b18da Fix various errors in documentation 2020-02-09 19:45:13 +11:00
3edeb726b8 Simplify nftables based method 2020-02-07 07:53:47 +11:00
6e9c58b4b4 Fixed, removed unused imports 2020-02-04 07:41:29 +11:00
13db89916a Added nft_chain_exists() and fixed nft to use that 2020-02-04 07:41:29 +11:00
84076f29fa Handle when default chains already exists (#392) 2020-02-04 07:41:29 +11:00
ad31ac4e18 Link Directly to TCP Over TCP Explanation
See Below was confusing because it linked to the entire documentation section.
This provides a direct link to the section explaining why TCP over TCP is a bad idea.
2019-12-13 15:56:57 +11:00
69d3f7dc64 Auto sudoers file (#269)
* added sudoers options to command line arguments

* added sudoers options to command line arguments

* template for sudoers file

* Added option for GUI sudo

* added support for GUI sudo

* script for auto adding sudo file

* sudoers auto add works and validates

* small change

* Clean up for CI

* removed code that belongs in another PR

* added path for package bins

* added sudoers bin

* added sudoers-add to setup file

* fixed issue with sudoers bash script

* auto sudoers now works

* added --sudoers-no-modify option

* bin now works with ./run

* removed debug print

* Updated sudoers-add script

* Fixed error passing sudoers config to script

* more dynamic building of sudoers file

* added option to specify sudoers.d file name

* fixed indent issue

* fixed indent issue

* indent issue

* clean up

* formating

* docs

* fix for flags

* Update usage.rst

* removed shell=true

* cleared CI errors

* cleared CI errors

* removed random

* cleared linter issue

* cleared linter issue

* cleared linter issue

* updated sudoers-add script

* safer temp file

* moved bin directory

* moved bin directory

* removed print

* fixed spacing issue

* sudoers commands must only containe upper case latters
2019-12-13 08:15:31 +11:00
6ad4473c87 Make hostwatch locale-independent (#379)
* Make hostwatch locale-independent

See #377: hostwatch used to call netstat and parse the result,
without setting the locale.
The problem is converting the binary output to a unicode string,
as the locale may be utf-8, latin-1, or literally anything.
Setting the locale to C avoids this issue, as netstat's source
strings to not use non-ASCII characters.

* Break line, check all other invocations
2019-11-09 11:27:57 +11:00
23516ebd71 Add option for latency control buffer size
This commit resolves #297, allowing the buffers used in the latency control to be changed with a command line option ‘--latency-buffer-size’.

We do this by changing a module variable in ssnet.py (similar to the MAX_CHANNEL variable) which seems to be the simplest code change without extensive hacking.

Documentation is also updated.
2019-11-08 08:01:52 +11:00
c69b9d6f4b Fix broken string substitution from a765aa32
The changes in a765aa32 removed a more complex pieced of code for parsing which sudo command to use. The %(eb)s no longer refers to any variable and is directly printed to the command line.

%(eb)s is now replaced with ‘sudo’.
2019-10-27 14:47:55 +11:00
2d92090625 README: add FreeBSD 2019-10-24 07:25:51 +11:00
502b36e990 Add tproxy udp port mark filter that was missed in #144, fixes #367. 2019-10-13 11:45:04 +11:00
fe1df00be7 readme: add Nix 2019-10-03 11:12:29 +10:00
a32689d030 Lock version of attrs
Latest version of attrs breaks pytest, see:
https://stackoverflow.com/questions/58189683/typeerror-attrib-got-an-unexpected-keyword-argument-convert
2019-10-03 11:08:39 +10:00
a7193f508a Fix capturing of local DNS servers
Regression was introduced in #337 that is skipping all local traffic,
including DNS. This change makes UDP port 53 (DNS) LOCAL traffic to be
treated as special case.

Fixes #357
2019-09-22 10:37:49 +10:00
7ebff92637 docs: openwrt 2019-09-22 10:34:27 +10:00
138d2877c6 Fix crashing on ECONNABORTED
In certain cases socket.connect fails with ECONNABORTED, which is
treated as "unknown" error causing sshuttle to crash.

Fixes #356
2019-09-22 10:32:37 +10:00
21ef365c59 The size of pf_rule grew in OpenBSD 6.4 2019-09-22 10:29:28 +10:00
a765aa3235 Use prompt for sudo, not needed for doas 2019-09-22 10:28:19 +10:00
71f2248b07 Fix Arch linux installation instructions
`pacman -Sy` does a partial upgrade, which is specifically documented as being unsupported.
2019-07-25 07:42:26 +10:00
935393b261 update readme to correct flag for arch linux.
Correct the install flag for arch linux installation example.
2019-06-21 07:34:28 +10:00
3e2ad68796 Fix tests for existing PR-312 (#337)
* use addrtype match to return the LOCAL trafik

* Add assertion for the new LOCAL firewall rule added in PR 312.

* Fix linter complaints
2019-06-08 12:12:21 +10:00
635cf8605e Add install instructions for Fedora 2019-06-08 10:34:53 +10:00
cb917d7e6c Add install instructions for Arch Linux 2019-04-04 12:31:10 +11:00
4372c6c117 Hyphen in hostname fix 2019-02-14 14:14:56 +11:00
4e945ca4de assembler import fix (#319)
* assembler import fix.
* Added noqa to import statements.
2019-02-14 12:11:11 +11:00
3bfb975ed9 Fix/pep8 (#277)
* re-organized imports according to pep8
* fixed all remaining pep8 issues
* moved common config into setup.cfg, additionally test `tests`
* removed --select=X -- the errors selected where by default not in
  flake8's --ignore list so effectively had no effect
* update .travis.yml to reflect changes in tox.ini
* make travis just use tox in order to avoid code duplaction
* replace py.test with pytest
* fixed .travis.yml
* try different pypy toxenv
* hopefully fixed testenv for pypy
* added pypy basepython, removed unused python2.6
* install dev package before testing (fixes missing coverage)
* fixed empty exception pass blocks with noqa
* Added dummy log message on empty try-except-pass blocks to make dodacy happy :(
* Replaced Exception with BaseException
2019-02-11 09:59:13 +11:00
44 changed files with 766 additions and 318 deletions

35
.github/workflows/pythonpackage.yml vendored Normal file
View File

@ -0,0 +1,35 @@
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
name: Python package
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.5, 3.6, 3.7, 3.8]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements-tests.txt
- name: Lint with flake8
run: |
flake8 sshuttle tests --count --show-source --statistics
- name: Test with pytest
run: |
PYTHONPATH=$PWD pytest

View File

@ -1,19 +0,0 @@
language: python
python:
- 2.7
- 3.4
- 3.5
- 3.6
- pypy
install:
- travis_retry pip install -q -r requirements-tests.txt
before_script:
# stop the build if there are Python syntax errors or undefined names.
- flake8 sshuttle --count --select=E901,E999,F821,F822,F823 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide.
- flake8 sshuttle --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
script:
- PYTHONPATH=. py.test

View File

@ -9,11 +9,54 @@ adheres to `Semantic Versioning`_.
.. _`Semantic Versioning`: http://semver.org/ .. _`Semantic Versioning`: http://semver.org/
1.0.0 - 2020-06-05
------------------
Added
~~~~~
* Python 3.8 support.
* sshpass support.
* Auto sudoers file (#269).
* option for latency control buffer size.
* Docs: FreeBSD'.
* Docs: Nix'.
* Docs: openwrt'.
* Docs: install instructions for Fedora'.
* Docs: install instructions for Arch Linux'.
* Docs: 'My VPN broke and need a solution fast'.
Removed
~~~~~~~
* Python 2.6 support.
* Python 2.7 support.
Fixed
~~~~~
* Remove debug message for getpeername failure.
* Fix crash triggered by port scans closing socket.
* Added "Running as a service" to docs.
* Systemd integration.
* Trap UnicodeError to handle cases where hostnames returned by DNS are invalid.
* Formatting error in CHANGES.rst
* Various errors in documentation.
* Nftables based method.
* Make hostwatch locale-independent (#379).
* Add tproxy udp port mark filter that was missed in #144, fixes #367.
* Capturing of local DNS servers.
* Crashing on ECONNABORTED.
* Size of pf_rule, which grew in OpenBSD 6.4.
* Use prompt for sudo, not needed for doas.
* Arch linux installation instructions.
* tests for existing PR-312 (#337).
* Hyphen in hostname.
* Assembler import (#319).
0.78.5 - 2019-01-28 0.78.5 - 2019-01-28
------------------- -------------------
Added Added
----- ~~~~~
* doas support as replacmeent for sudo on OpenBSD. * doas support as replacmeent for sudo on OpenBSD.
* Added ChromeOS section to documentation (#262) * Added ChromeOS section to documentation (#262)
* Add --no-sudo-pythonpath option * Add --no-sudo-pythonpath option

View File

@ -23,8 +23,9 @@ common case:
- You can't use openssh's PermitTunnel feature because - You can't use openssh's PermitTunnel feature because
it's disabled by default on openssh servers; plus it does it's disabled by default on openssh servers; plus it does
TCP-over-TCP, which has terrible performance (see below). TCP-over-TCP, which has `terrible performance`_.
.. _terrible performance: https://sshuttle.readthedocs.io/en/stable/how-it-works.html
Obtaining sshuttle Obtaining sshuttle
------------------ ------------------
@ -33,6 +34,18 @@ Obtaining sshuttle
apt-get install sshuttle apt-get install sshuttle
- Arch Linux::
pacman -S sshuttle
- Fedora::
dnf install sshuttle
- NixOS::
nix-env -iA nixos.sshuttle
- From PyPI:: - From PyPI::
sudo pip install sshuttle sudo pip install sshuttle
@ -43,6 +56,13 @@ Obtaining sshuttle
cd sshuttle cd sshuttle
sudo ./setup.py install sudo ./setup.py install
- FreeBSD::
# ports
cd /usr/ports/net/py-sshuttle && make install clean
# pkg
pkg install py36-sshuttle
It is also possible to install into a virtualenv as a non-root user. It is also possible to install into a virtualenv as a non-root user.
- From PyPI:: - From PyPI::
@ -63,6 +83,10 @@ It is also possible to install into a virtualenv as a non-root user.
brew install sshuttle brew install sshuttle
- Nix::
nix-env -iA nixpkgs.sshuttle
Documentation Documentation
------------- -------------
@ -71,3 +95,9 @@ https://sshuttle.readthedocs.org/
The documentation for the latest development version is available at: The documentation for the latest development version is available at:
https://sshuttle.readthedocs.org/en/latest/ https://sshuttle.readthedocs.org/en/latest/
Running as a service
-------------
Sshuttle can also be run as a service and configured using a config management system:
https://medium.com/@mike.reider/using-sshuttle-as-a-service-bec2684a65fe

76
bin/sudoers-add Executable file
View File

@ -0,0 +1,76 @@
#!/usr/bin/env bash
# William Mantly <wmantly@gmail.com>
# MIT License
# https://github.com/wmantly/sudoers-add
NEWLINE=$'\n'
CONTENT=""
ME="$(basename "$(test -L "$0" && readlink "$0" || echo "$0")")"
if [ "$1" == "--help" ] || [ "$1" == "-h" ]; then
echo "Usage: $ME [file_path] [sudoers-file-name]"
echo "Usage: [content] | $ME sudoers-file-name"
echo "This will take a sudoers config validate it and add it to /etc/sudoers.d/{sudoers-file-name}"
echo "The config can come from a file, first usage example or piped in second example."
exit 0
fi
if [ "$1" == "" ]; then
(>&2 echo "This command take at lest one argument. See $ME --help")
exit 1
fi
if [ "$2" == "" ]; then
FILE_NAME=$1
shift
else
FILE_NAME=$2
fi
if [[ $EUID -ne 0 ]]; then
echo "This script must be run as root"
exit 1
fi
while read -r line
do
CONTENT+="${line}${NEWLINE}"
done < "${1:-/dev/stdin}"
if [ "$CONTENT" == "" ]; then
(>&2 echo "No config content specified. See $ME --help")
exit 1
fi
if [ "$FILE_NAME" == "" ]; then
(>&2 echo "No sudoers file name specified. See $ME --help")
exit 1
fi
# Make a temp file to hold the sudoers config
umask 077
TEMP_FILE=$(mktemp)
echo "$CONTENT" > "$TEMP_FILE"
# Make sure the content is valid
visudo_STDOUT=$(visudo -c -f "$TEMP_FILE" 2>&1)
visudo_code=$?
# The temp file is no longer needed
rm "$TEMP_FILE"
if [ $visudo_code -eq 0 ]; then
echo "$CONTENT" > "/etc/sudoers.d/$FILE_NAME"
chmod 0440 "/etc/sudoers.d/$FILE_NAME"
echo "The sudoers file /etc/sudoers.d/$FILE_NAME has been successfully created!"
exit 0
else
echo "Invalid sudoers config!"
echo "$visudo_STDOUT"
exit 1
fi

View File

@ -5,8 +5,18 @@ Installation
pip install sshuttle pip install sshuttle
- Debain package manager::
sudo apt install sshuttle
- Clone:: - Clone::
git clone https://github.com/sshuttle/sshuttle.git git clone https://github.com/sshuttle/sshuttle.git
cd sshuttle cd sshuttle
./setup.py install ./setup.py install
Optionally after installation
-----------------------------
- Add to sudoers file
sshuttle --sudoers

View File

@ -28,7 +28,7 @@ Options
------- -------
.. program:: sshuttle .. program:: sshuttle
.. option:: subnets .. option:: <subnets>
A list of subnets to route over the VPN, in the form A list of subnets to route over the VPN, in the form
``a.b.c.d[/width][port[-port]]``. Valid examples are 1.2.3.4 (a ``a.b.c.d[/width][port[-port]]``. Valid examples are 1.2.3.4 (a
@ -44,13 +44,13 @@ Options
to during startup will be routed over the VPN. Valid examples are to during startup will be routed over the VPN. Valid examples are
example.com, example.com:8000 and example.com:8000-9000. example.com, example.com:8000 and example.com:8000-9000.
.. option:: --method [auto|nat|tproxy|pf] .. option:: --method <auto|nat|nft|tproxy|pf>
Which firewall method should sshuttle use? For auto, sshuttle attempts to Which firewall method should sshuttle use? For auto, sshuttle attempts to
guess the appropriate method depending on what it can find in PATH. The guess the appropriate method depending on what it can find in PATH. The
default value is auto. default value is auto.
.. option:: -l, --listen=[ip:]port .. option:: -l <[ip:]port>, --listen=<[ip:]port>
Use this ip address and port number as the transparent Use this ip address and port number as the transparent
proxy port. By default :program:`sshuttle` finds an available proxy port. By default :program:`sshuttle` finds an available
@ -97,10 +97,10 @@ Options
server. All queries to any of the local system's DNS server. All queries to any of the local system's DNS
servers (/etc/resolv.conf) will be intercepted and servers (/etc/resolv.conf) will be intercepted and
resolved on the remote side of the tunnel instead, there resolved on the remote side of the tunnel instead, there
using the DNS specified via the :option:`--to-ns=` option, using the DNS specified via the :option:`--to-ns` option,
if specified. if specified.
.. option:: --ns-hosts=server1[,server2[,server3[...]]] .. option:: --ns-hosts=<server1[,server2[,server3[...]]]>
Capture local DNS requests to the specified server(s) Capture local DNS requests to the specified server(s)
and forward to the remote DNS server. Contrary to the and forward to the remote DNS server. Contrary to the
@ -111,7 +111,7 @@ Options
requests should be resolved on the remote side of the requests should be resolved on the remote side of the
tunnel, e.g. in combination with dnsmasq. tunnel, e.g. in combination with dnsmasq.
.. option:: --to-ns=server .. option:: --to-ns=<server>
The DNS to forward requests to when remote DNS The DNS to forward requests to when remote DNS
resolution is enabled. If not given, sshuttle will resolution is enabled. If not given, sshuttle will
@ -125,14 +125,14 @@ Options
The default is just ``python``, which means to use the The default is just ``python``, which means to use the
default python interpreter on the remote system's PATH. default python interpreter on the remote system's PATH.
.. option:: -r, --remote=[username@]sshserver[:port] .. option:: -r <[username@]sshserver[:port]>, --remote=<[username@]sshserver[:port]>
The remote hostname and optional username and ssh The remote hostname and optional username and ssh
port number to use for connecting to the remote server. port number to use for connecting to the remote server.
For example, example.com, testuser@example.com, For example, example.com, testuser@example.com,
testuser@example.com:2222, or example.com:2244. testuser@example.com:2222, or example.com:2244.
.. option:: -x, --exclude=subnet .. option:: -x <subnet>, --exclude=<subnet>
Explicitly exclude this subnet from forwarding. The Explicitly exclude this subnet from forwarding. The
format of this option is the same as the ``<subnets>`` format of this option is the same as the ``<subnets>``
@ -141,7 +141,7 @@ Options
``0/0 -x 1.2.3.0/24`` to forward everything except the ``0/0 -x 1.2.3.0/24`` to forward everything except the
local subnet over the VPN, for example. local subnet over the VPN, for example.
.. option:: -X, --exclude-from=file .. option:: -X <file>, --exclude-from=<file>
Exclude the subnets specified in a file, one subnet per Exclude the subnets specified in a file, one subnet per
line. Useful when you have lots of subnets to exclude. line. Useful when you have lots of subnets to exclude.
@ -189,6 +189,13 @@ Options
control feature, maximizing bandwidth usage. Use at control feature, maximizing bandwidth usage. Use at
your own risk. your own risk.
.. option:: --latency-buffer-size
Set the size of the buffer used in latency control. The
default is ``32768``. Changing this option allows a compromise
to be made between latency and bandwidth without completely
disabling latency control (with :option:`--no-latency-control`).
.. option:: -D, --daemon .. option:: -D, --daemon
Automatically fork into the background after connecting Automatically fork into the background after connecting
@ -200,7 +207,7 @@ Options
:manpage:`syslog(3)` service instead of stderr. This is :manpage:`syslog(3)` service instead of stderr. This is
implicit if you use :option:`--daemon`. implicit if you use :option:`--daemon`.
.. option:: --pidfile=pidfilename .. option:: --pidfile=<pidfilename>
when using :option:`--daemon`, save :program:`sshuttle`'s pid to when using :option:`--daemon`, save :program:`sshuttle`'s pid to
*pidfilename*. The default is ``sshuttle.pid`` in the *pidfilename*. The default is ``sshuttle.pid`` in the
@ -227,6 +234,29 @@ Options
makes it a lot easier to debug and test the :option:`--auto-hosts` makes it a lot easier to debug and test the :option:`--auto-hosts`
feature. feature.
.. option:: --sudoers
sshuttle will auto generate the proper sudoers.d config file and add it.
Once this is completed, sshuttle will exit and tell the user if
it succeed or not. Do not call this options with sudo, it may generate a
incorrect config file.
.. option:: --sudoers-no-modify
sshuttle will auto generate the proper sudoers.d config and print it to
stdout. The option will not modify the system at all.
.. option:: --sudoers-user
Set the user name or group with %group_name for passwordless operation.
Default is the current user.set ALL for all users. Only works with
--sudoers or --sudoers-no-modify option.
.. option:: --sudoers-filename
Set the file name for the sudoers.d file to be added. Default is
"sshuttle_auto". Only works with --sudoers.
.. option:: --version .. option:: --version
Print program version. Print program version.
@ -239,7 +269,7 @@ file.
To run :program:`sshuttle` with options defined in, e.g., `/etc/sshuttle.conf` To run :program:`sshuttle` with options defined in, e.g., `/etc/sshuttle.conf`
just pass the path to the file preceded by the `@` character, e.g. just pass the path to the file preceded by the `@` character, e.g.
:option:`@/etc/sshuttle.conf`. `@/etc/sshuttle.conf`.
When running :program:`sshuttle` with options defined in a configuration file, When running :program:`sshuttle` with options defined in a configuration file,
options can still be passed via the command line in addition to what is options can still be passed via the command line in addition to what is

8
docs/openwrt.rst Normal file
View File

@ -0,0 +1,8 @@
OpenWRT
========
Run::
opkg install python3 python3-pip iptables-mod-nat-extra iptables-mod-ipopt
python3 /usr/bin/pip3 install sshuttle
sshuttle -l 0.0.0.0 -r <IP> -x 192.168.1.1 0/0

View File

@ -9,3 +9,4 @@ Contents:
chromeos chromeos
tproxy tproxy
windows windows
openwrt

View File

@ -6,7 +6,7 @@ Client side Requirements
- sudo, or root access on your client machine. - sudo, or root access on your client machine.
(The server doesn't need admin access.) (The server doesn't need admin access.)
- Python 2.7 or Python 3.5. - Python 3.5 or greater.
Linux with NAT method Linux with NAT method
@ -32,14 +32,6 @@ Supports:
* IPv6 UDP (requires ``recvmsg`` - see below) * IPv6 UDP (requires ``recvmsg`` - see below)
* IPv6 DNS (requires ``recvmsg`` - see below) * IPv6 DNS (requires ``recvmsg`` - see below)
.. _PyXAPI: http://www.pps.univ-paris-diderot.fr/~ylg/PyXAPI/
Full UDP or DNS support with the TPROXY method requires the ``recvmsg()``
syscall. This is not available in Python 2, however it is in Python 3.5 and
later. Under Python 2 you might find it sufficient to install PyXAPI_ in
order to get the ``recvmsg()`` function. See :doc:`tproxy` for more
information.
MacOS / FreeBSD / OpenBSD / pfSense MacOS / FreeBSD / OpenBSD / pfSense
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -87,7 +79,6 @@ Additional Suggested Software
[Service] [Service]
Type=notify Type=notify
NotifyAccess=all
ExecStart=/usr/bin/sshuttle --dns --remote <user>@<server> <subnets...> ExecStart=/usr/bin/sshuttle --dns --remote <user>@<server> <subnets...>
[Install] [Install]

View File

@ -20,6 +20,11 @@ Forward all traffic::
sshuttle -r username@sshserver 0/0 sshuttle -r username@sshserver 0/0
- For 'My VPN broke and need a temporary solution FAST to access local IPv4 addresses':
sshuttle --dns -NHr username@sshserver 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
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::
@ -60,3 +65,46 @@ 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.
Sudoers File
------------
sshuttle can auto-generate the proper sudoers.d file using the current user
for Linux and OSX. Doing this will allow sshuttle to run without asking for
the local sudo password and to give users who do not have sudo access
ability to run sshuttle.
sshuttle --sudoers
DO NOT run this command with sudo, it will ask for your sudo password when
it is needed.
A costume user or group can be set with the :
option:`sshuttle --sudoers --sudoers-username {user_descriptor}` option. Valid
values for this vary based on how your system is configured. Values such as
usernames, groups pre-pended with `%` and sudoers user aliases will work. See
the sudoers manual for more information on valid user specif actions.
The options must be used with `--sudoers`
sshuttle --sudoers --sudoers-user mike
sshuttle --sudoers --sudoers-user %sudo
The name of the file to be added to sudoers.d can be configured as well. This
is mostly not necessary but can be useful for giving more than one user
access to sshuttle. The default is `sshuttle_auto`
sshuttle --sudoer --sudoers-filename sshuttle_auto_mike
sshuttle --sudoer --sudoers-filename sshuttle_auto_tommy
You can also see what configuration will be added to your system without
modifying anything. This can be helpfull is the auto feature does not work, or
you want more control. This option also works with `--sudoers-username`.
`--sudoers-filename` has no effect with this option.
sshuttle --sudoers-no-modify
This will simply sprint the generated configuration to STDOUT. Example
08:40 PM william$ sshuttle --sudoers-no-modify
Cmnd_Alias SSHUTTLE304 = /usr/bin/env PYTHONPATH=/usr/local/lib/python2.7/dist-packages/sshuttle-0.78.5.dev30+gba5e6b5.d20180909-py2.7.egg /usr/bin/python /usr/local/bin/sshuttle --method auto --firewall
william ALL=NOPASSWD: SSHUTTLE304

View File

@ -1,5 +1,7 @@
-r requirements.txt -r requirements.txt
pytest==3.4.2 attrs==19.3.0
pytest-cov==2.6.0 pytest==5.4.3
pytest-cov==2.9.0
mock==2.0.0 mock==2.0.0
flake8==3.5.0 flake8==3.8.2
pyflakes==2.2.0

View File

@ -1 +1 @@
setuptools-scm==1.15.6 setuptools-scm==4.1.2

3
run
View File

@ -1,13 +1,12 @@
#!/usr/bin/env sh #!/usr/bin/env sh
set -e set -e
export PYTHONPATH="$(dirname $0):$PYTHONPATH" export PYTHONPATH="$(dirname $0):$PYTHONPATH"
export PATH="$(dirname $0)/bin:$PATH"
python_best_version() { python_best_version() {
if [ -x "$(command -v python3)" ] && if [ -x "$(command -v python3)" ] &&
python3 -c "import sys; sys.exit(not sys.version_info > (3, 5))"; then python3 -c "import sys; sys.exit(not sys.version_info > (3, 5))"; then
exec python3 "$@" exec python3 "$@"
elif [ -x "$(command -v python2.7)" ]; then
exec python2.7 "$@"
else else
exec python "$@" exec python "$@"
fi fi

View File

@ -8,5 +8,10 @@ universal = 1
sign=true sign=true
identity=0x1784577F811F6EAC identity=0x1784577F811F6EAC
[flake8]
count=true
show-source=true
statistics=true
[tool:pytest] [tool:pytest]
addopts = --cov=sshuttle --cov-branch --cov-report=term-missing addopts = --cov=sshuttle --cov-branch --cov-report=term-missing

View File

@ -48,10 +48,13 @@ setup(
"License :: OSI Approved :: " "License :: OSI Approved :: "
"GNU Lesser General Public License v2 or later (LGPLv2+)", "GNU Lesser General Public License v2 or later (LGPLv2+)",
"Operating System :: OS Independent", "Operating System :: OS Independent",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Topic :: System :: Networking", "Topic :: System :: Networking",
], ],
scripts=['bin/sudoers-add'],
entry_points={ entry_points={
'console_scripts': [ 'console_scripts': [
'sshuttle = sshuttle.cmdline:main', 'sshuttle = sshuttle.cmdline:main',

View File

@ -30,10 +30,12 @@ while 1:
sys.stderr.flush() sys.stderr.flush()
sys.stdout.flush() sys.stdout.flush()
import sshuttle.helpers # import can only happen once the code has been transferred to
# the server. 'noqa: E402' excludes these lines from QA checks.
import sshuttle.helpers # noqa: E402
sshuttle.helpers.verbose = verbosity sshuttle.helpers.verbose = verbosity
import sshuttle.cmdline_options as options import sshuttle.cmdline_options as options # noqa: E402
from sshuttle.server import main from sshuttle.server import main # noqa: E402
main(options.latency_control, options.auto_hosts, options.to_nameserver, main(options.latency_control, options.auto_hosts, options.to_nameserver,
options.auto_nets) options.auto_nets)

View File

@ -3,13 +3,15 @@ import re
import signal import signal
import time import time
import subprocess as ssubprocess import subprocess as ssubprocess
import sshuttle.helpers as helpers
import os import os
import sys
import platform
import sshuttle.helpers as helpers
import sshuttle.ssnet as ssnet import sshuttle.ssnet as ssnet
import sshuttle.ssh as ssh import sshuttle.ssh as ssh
import sshuttle.ssyslog as ssyslog import sshuttle.ssyslog as ssyslog
import sys import sshuttle.sdnotify as sdnotify
import platform
from sshuttle.ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper from sshuttle.ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
from sshuttle.helpers import log, debug1, debug2, debug3, Fatal, islocal, \ from sshuttle.helpers import log, debug1, debug2, debug3, Fatal, islocal, \
resolvconf_nameservers resolvconf_nameservers
@ -184,12 +186,6 @@ class MultiListener:
class FirewallClient: class FirewallClient:
def __init__(self, method_name, sudo_pythonpath): def __init__(self, method_name, sudo_pythonpath):
# Default to sudo unless on OpenBSD in which case use built in `doas`
elevbin = 'sudo'
if platform.platform().startswith('OpenBSD'):
elevbin = 'doas'
self.auto_nets = [] self.auto_nets = []
python_path = os.path.dirname(os.path.dirname(__file__)) python_path = os.path.dirname(os.path.dirname(__file__))
argvbase = ([sys.executable, sys.argv[0]] + argvbase = ([sys.executable, sys.argv[0]] +
@ -198,9 +194,11 @@ class FirewallClient:
['--firewall']) ['--firewall'])
if ssyslog._p: if ssyslog._p:
argvbase += ['--syslog'] argvbase += ['--syslog']
elev_prefix = [part % {'eb': elevbin} # Default to sudo unless on OpenBSD in which case use built in `doas`
for part in ['%(eb)s', '-p', if platform.platform().startswith('OpenBSD'):
'[local %(eb)s] Password: ']] elev_prefix = ['doas']
else:
elev_prefix = ['sudo', '-p', '[local sudo] Password: ']
if sudo_pythonpath: if sudo_pythonpath:
elev_prefix += ['/usr/bin/env', elev_prefix += ['/usr/bin/env',
'PYTHONPATH=%s' % python_path] 'PYTHONPATH=%s' % python_path]
@ -223,17 +221,13 @@ class FirewallClient:
if argv[0] == 'su': if argv[0] == 'su':
sys.stderr.write('[local su] ') sys.stderr.write('[local su] ')
self.p = ssubprocess.Popen(argv, stdout=s1, preexec_fn=setup) self.p = ssubprocess.Popen(argv, stdout=s1, preexec_fn=setup)
# No env: Talking to `FirewallClient.start`, which has no i18n.
e = None e = None
break break
except OSError as e: except OSError:
pass pass
self.argv = argv self.argv = argv
s1.close() s1.close()
if sys.version_info < (3, 0):
# python 2.7
self.pfile = s2.makefile('wb+')
else:
# python 3.5
self.pfile = s2.makefile('rwb') self.pfile = s2.makefile('rwb')
if e: if e:
log('Spawning firewall manager: %r\n' % self.argv) log('Spawning firewall manager: %r\n' % self.argv)
@ -268,11 +262,13 @@ class FirewallClient:
self.pfile.write(b'ROUTES\n') self.pfile.write(b'ROUTES\n')
for (family, ip, width, fport, lport) \ for (family, ip, width, fport, lport) \
in self.subnets_include + self.auto_nets: in self.subnets_include + self.auto_nets:
self.pfile.write(b'%d,%d,0,%s,%d,%d\n' self.pfile.write(b'%d,%d,0,%s,%d,%d\n' % (family, width,
% (family, width, ip.encode("ASCII"), fport, lport)) ip.encode("ASCII"),
fport, lport))
for (family, ip, width, fport, lport) in self.subnets_exclude: for (family, ip, width, fport, lport) in self.subnets_exclude:
self.pfile.write(b'%d,%d,1,%s,%d,%d\n' self.pfile.write(b'%d,%d,1,%s,%d,%d\n' % (family, width,
% (family, width, ip.encode("ASCII"), fport, lport)) ip.encode("ASCII"),
fport, lport))
self.pfile.write(b'NSLIST\n') self.pfile.write(b'NSLIST\n')
for (family, ip) in self.nslist: for (family, ip) in self.nslist:
@ -303,8 +299,8 @@ class FirewallClient:
raise Fatal('%r expected STARTED, got %r' % (self.argv, line)) raise Fatal('%r expected STARTED, got %r' % (self.argv, line))
def sethostip(self, hostname, ip): def sethostip(self, hostname, ip):
assert(not re.search(b'[^-\w\.]', hostname)) assert(not re.search(rb'[^-\w\.]', hostname))
assert(not re.search(b'[^0-9.]', ip)) assert(not re.search(rb'[^0-9.]', ip))
self.pfile.write(b'HOST %s,%s\n' % (hostname, ip)) self.pfile.write(b'HOST %s,%s\n' % (hostname, ip))
self.pfile.flush() self.pfile.flush()
@ -460,7 +456,7 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
raise Fatal("failed to establish ssh session (1)") raise Fatal("failed to establish ssh session (1)")
else: else:
raise raise
mux = Mux(serversock, serversock) mux = Mux(serversock.makefile("rb"), serversock.makefile("wb"))
handlers.append(mux) handlers.append(mux)
expected = b'SSHUTTLE0001' expected = b'SSHUTTLE0001'
@ -495,7 +491,8 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
def onroutes(routestr): def onroutes(routestr):
if auto_nets: if auto_nets:
for line in routestr.strip().split(b'\n'): for line in routestr.strip().split(b'\n'):
if not line: continue if not line:
continue
(family, ip, width) = line.split(b',', 2) (family, ip, width) = line.split(b',', 2)
family = int(family) family = int(family)
width = int(width) width = int(width)
@ -516,9 +513,14 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
# set --auto-nets, we might as well wait for the message first, then # set --auto-nets, we might as well wait for the message first, then
# ignore its contents. # ignore its contents.
mux.got_routes = None mux.got_routes = None
fw.start() serverready()
mux.got_routes = onroutes mux.got_routes = onroutes
def serverready():
fw.start()
sdnotify.send(sdnotify.ready(), sdnotify.status('Connected'))
def onhostlist(hostlist): def onhostlist(hostlist):
debug2('got host list: %r\n' % hostlist) debug2('got host list: %r\n' % hostlist)
for line in hostlist.strip().split(): for line in hostlist.strip().split():
@ -598,8 +600,13 @@ def main(listenip_v6, listenip_v4,
except KeyError: except KeyError:
raise Fatal("User %s does not exist." % user) raise Fatal("User %s does not exist." % user)
if fw.method.name != 'nat':
required.ipv6 = len(subnets_v6) > 0 or listenip_v6 is not None required.ipv6 = len(subnets_v6) > 0 or listenip_v6 is not None
required.ipv4 = len(subnets_v4) > 0 or listenip_v4 is not None required.ipv4 = len(subnets_v4) > 0 or listenip_v4 is not None
else:
required.ipv6 = None
required.ipv4 = None
required.udp = avail.udp required.udp = avail.udp
required.dns = len(nslist) > 0 required.dns = len(nslist) > 0
required.user = False if user is None else True required.user = False if user is None else True
@ -707,7 +714,8 @@ def main(listenip_v6, listenip_v4,
ports = range(12300, 9000, -1) ports = range(12300, 9000, -1)
for port in ports: for port in ports:
debug2(' %d' % port) debug2(' %d' % port)
if port in used_ports: continue if port in used_ports:
continue
dns_listener = MultiListener(socket.SOCK_DGRAM) dns_listener = MultiListener(socket.SOCK_DGRAM)
@ -791,6 +799,8 @@ def main(listenip_v6, listenip_v4,
# it's not our child anymore; can't waitpid # it's not our child anymore; can't waitpid
fw.p.returncode = 0 fw.p.returncode = 0
fw.done() fw.done()
sdnotify.send(sdnotify.stop())
finally: finally:
if daemon: if daemon:
daemon_cleanup() daemon_cleanup()

View File

@ -1,5 +1,6 @@
import re import re
import socket import socket
import platform
import sshuttle.helpers as helpers import sshuttle.helpers as helpers
import sshuttle.client as client import sshuttle.client as client
import sshuttle.firewall as firewall import sshuttle.firewall as firewall
@ -7,16 +8,35 @@ import sshuttle.hostwatch as hostwatch
import sshuttle.ssyslog as ssyslog import sshuttle.ssyslog as ssyslog
from sshuttle.options import parser, parse_ipport from sshuttle.options import parser, parse_ipport
from sshuttle.helpers import family_ip_tuple, log, Fatal from sshuttle.helpers import family_ip_tuple, log, Fatal
from sshuttle.sudoers import sudoers
def main(): def main():
opt = parser.parse_args() opt = parser.parse_args()
if opt.sudoers or opt.sudoers_no_modify:
if platform.platform().startswith('OpenBSD'):
log('Automatic sudoers does not work on BSD')
exit(1)
if not opt.sudoers_filename:
log('--sudoers-file must be set or omited.')
exit(1)
sudoers(
user_name=opt.sudoers_user,
no_modify=opt.sudoers_no_modify,
file_name=opt.sudoers_filename
)
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 = opt.wrap ssnet.MAX_CHANNEL = opt.wrap
if opt.latency_buffer_size:
import sshuttle.ssnet as ssnet
ssnet.LATENCY_BUFFER_SIZE = opt.latency_buffer_size
helpers.verbose = opt.verbose helpers.verbose = opt.verbose
try: try:

View File

@ -1,12 +1,12 @@
import errno import errno
import socket import socket
import signal import signal
import sshuttle.ssyslog as ssyslog
import sshuttle.sdnotify as sdnotify
import sys import sys
import os import os
import platform import platform
import traceback import traceback
import sshuttle.ssyslog as ssyslog
from sshuttle.helpers import debug1, debug2, Fatal from sshuttle.helpers import debug1, debug2, Fatal
from sshuttle.methods import get_auto_method, get_method from sshuttle.methods import get_auto_method, get_method
@ -132,7 +132,7 @@ def main(method_name, syslog):
try: try:
(family, width, exclude, ip, fport, lport) = \ (family, width, exclude, ip, fport, lport) = \
line.strip().split(',', 5) line.strip().split(',', 5)
except: except BaseException:
raise Fatal('firewall: expected route or NSLIST but got %r' % line) raise Fatal('firewall: expected route or NSLIST but got %r' % line)
subnets.append(( subnets.append((
int(family), int(family),
@ -154,7 +154,7 @@ def main(method_name, syslog):
break break
try: try:
(family, ip) = line.strip().split(',', 1) (family, ip) = line.strip().split(',', 1)
except: except BaseException:
raise Fatal('firewall: expected nslist or PORTS but got %r' % line) raise Fatal('firewall: expected nslist or PORTS but got %r' % line)
nslist.append((int(family), ip)) nslist.append((int(family), ip))
debug2('firewall manager: Got partial nslist: %r\n' % nslist) debug2('firewall manager: Got partial nslist: %r\n' % nslist)
@ -219,8 +219,6 @@ def main(method_name, syslog):
user) user)
stdout.write('STARTED\n') stdout.write('STARTED\n')
sdnotify.send(sdnotify.ready(),
sdnotify.status('Connected'))
try: try:
stdout.flush() stdout.flush()
@ -246,45 +244,44 @@ def main(method_name, syslog):
break break
finally: finally:
try: try:
sdnotify.send(sdnotify.stop())
debug1('firewall manager: undoing changes.\n') debug1('firewall manager: undoing changes.\n')
except: except BaseException:
pass debug2('An error occurred, ignoring it.')
try: try:
if subnets_v6 or nslist_v6: if subnets_v6 or nslist_v6:
debug2('firewall manager: undoing IPv6 changes.\n') debug2('firewall manager: undoing IPv6 changes.\n')
method.restore_firewall(port_v6, socket.AF_INET6, udp, user) method.restore_firewall(port_v6, socket.AF_INET6, udp, user)
except: except BaseException:
try: try:
debug1("firewall manager: " debug1("firewall manager: "
"Error trying to undo IPv6 firewall.\n") "Error trying to undo IPv6 firewall.\n")
for line in traceback.format_exc().splitlines(): for line in traceback.format_exc().splitlines():
debug1("---> %s\n" % line) debug1("---> %s\n" % line)
except: except BaseException:
pass debug2('An error occurred, ignoring it.')
try: try:
if subnets_v4 or nslist_v4: if subnets_v4 or nslist_v4:
debug2('firewall manager: undoing IPv4 changes.\n') debug2('firewall manager: undoing IPv4 changes.\n')
method.restore_firewall(port_v4, socket.AF_INET, udp, user) method.restore_firewall(port_v4, socket.AF_INET, udp, user)
except: except BaseException:
try: try:
debug1("firewall manager: " debug1("firewall manager: "
"Error trying to undo IPv4 firewall.\n") "Error trying to undo IPv4 firewall.\n")
for line in traceback.format_exc().splitlines(): for line in traceback.format_exc().splitlines():
debug1("firewall manager: ---> %s\n" % line) debug1("firewall manager: ---> %s\n" % line)
except: except BaseException:
pass debug2('An error occurred, ignoring it.')
try: try:
debug2('firewall manager: undoing /etc/hosts changes.\n') debug2('firewall manager: undoing /etc/hosts changes.\n')
restore_etc_hosts(port_v6 or port_v4) restore_etc_hosts(port_v6 or port_v4)
except: except BaseException:
try: try:
debug1("firewall manager: " debug1("firewall manager: "
"Error trying to undo /etc/hosts changes.\n") "Error trying to undo /etc/hosts changes.\n")
for line in traceback.format_exc().splitlines(): for line in traceback.format_exc().splitlines():
debug1("firewall manager: ---> %s\n" % line) debug1("firewall manager: ---> %s\n" % line)
except: except BaseException:
pass debug2('An error occurred, ignoring it.')

View File

@ -5,16 +5,9 @@ import errno
logprefix = '' logprefix = ''
verbose = 0 verbose = 0
if sys.version_info[0] == 3:
binary_type = bytes
def b(s): def b(s):
return s.encode("ASCII") return s.encode("ASCII")
else:
binary_type = str
def b(s):
return s
def log(s): def log(s):
@ -56,22 +49,22 @@ class Fatal(Exception):
def resolvconf_nameservers(): def resolvconf_nameservers():
l = [] lines = []
for line in open('/etc/resolv.conf'): for line in open('/etc/resolv.conf'):
words = line.lower().split() words = line.lower().split()
if len(words) >= 2 and words[0] == 'nameserver': if len(words) >= 2 and words[0] == 'nameserver':
l.append(family_ip_tuple(words[1])) lines.append(family_ip_tuple(words[1]))
return l return lines
def resolvconf_random_nameserver(): def resolvconf_random_nameserver():
l = resolvconf_nameservers() lines = resolvconf_nameservers()
if l: if lines:
if len(l) > 1: if len(lines) > 1:
# don't import this unless we really need it # don't import this unless we really need it
import random import random
random.shuffle(l) random.shuffle(lines)
return l[0] return lines[0]
else: else:
return (socket.AF_INET, '127.0.0.1') return (socket.AF_INET, '127.0.0.1')

View File

@ -44,7 +44,7 @@ def write_host_cache():
finally: finally:
try: try:
os.unlink(tmpname) os.unlink(tmpname)
except: except BaseException:
pass pass
@ -108,7 +108,7 @@ def _check_revdns(ip):
debug3('< %s\n' % r[0]) debug3('< %s\n' % r[0])
check_host(r[0]) check_host(r[0])
found_host(r[0], ip) found_host(r[0], ip)
except socket.herror: except (socket.herror, UnicodeError):
pass pass
@ -119,15 +119,20 @@ def _check_dns(hostname):
debug3('< %s\n' % ip) debug3('< %s\n' % ip)
check_host(ip) check_host(ip)
found_host(hostname, ip) found_host(hostname, ip)
except socket.gaierror: except (socket.gaierror, UnicodeError):
pass pass
def _check_netstat(): def _check_netstat():
debug2(' > netstat\n') debug2(' > netstat\n')
env = {
'PATH': os.environ['PATH'],
'LC_ALL': "C",
}
argv = ['netstat', '-n'] argv = ['netstat', '-n']
try: try:
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null) p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null,
env=env)
content = p.stdout.read().decode("ASCII") content = p.stdout.read().decode("ASCII")
p.wait() p.wait()
except OSError: except OSError:
@ -145,10 +150,15 @@ def _check_smb(hostname):
global _smb_ok global _smb_ok
if not _smb_ok: if not _smb_ok:
return return
argv = ['smbclient', '-U', '%', '-L', hostname]
debug2(' > smb: %s\n' % hostname) debug2(' > smb: %s\n' % hostname)
env = {
'PATH': os.environ['PATH'],
'LC_ALL': "C",
}
argv = ['smbclient', '-U', '%', '-L', hostname]
try: try:
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null) p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null,
env=env)
lines = p.stdout.readlines() lines = p.stdout.readlines()
p.wait() p.wait()
except OSError: except OSError:
@ -203,10 +213,15 @@ def _check_nmb(hostname, is_workgroup, is_master):
global _nmb_ok global _nmb_ok
if not _nmb_ok: if not _nmb_ok:
return return
argv = ['nmblookup'] + ['-M'] * is_master + ['--', hostname]
debug2(' > n%d%d: %s\n' % (is_workgroup, is_master, hostname)) debug2(' > n%d%d: %s\n' % (is_workgroup, is_master, hostname))
env = {
'PATH': os.environ['PATH'],
'LC_ALL': "C",
}
argv = ['nmblookup'] + ['-M'] * is_master + ['--', hostname]
try: try:
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null) p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null,
env=env)
lines = p.stdout.readlines() lines = p.stdout.readlines()
rv = p.wait() rv = p.wait()
except OSError: except OSError:

View File

@ -1,4 +1,3 @@
import re
import os import os
import socket import socket
import subprocess as ssubprocess import subprocess as ssubprocess
@ -51,10 +50,8 @@ def ipt(family, table, *args):
def nft(family, table, action, *args): def nft(family, table, action, *args):
if family == socket.AF_INET: if family in (socket.AF_INET, socket.AF_INET6):
argv = ['nft', action, 'ip', table] + list(args) argv = ['nft', action, 'inet', table] + list(args)
elif family == socket.AF_INET6:
argv = ['nft', action, 'ip6', table] + list(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))
@ -67,22 +64,6 @@ def nft(family, table, action, *args):
raise Fatal('%r returned %d' % (argv, rv)) raise Fatal('%r returned %d' % (argv, rv))
def nft_get_handle(expression, chain):
cmd = 'nft'
argv = [cmd, 'list', expression, '-a']
env = {
'PATH': os.environ['PATH'],
'LC_ALL': "C",
}
try:
output = ssubprocess.check_output(argv, env=env)
for line in output.decode('utf-8').split('\n'):
if ('jump %s' % chain) in line:
return re.sub('.*# ', '', line)
except ssubprocess.CalledProcessError as e:
raise Fatal('%r returned %d' % (argv, e.returncode))
_no_ttl_module = False _no_ttl_module = False

View File

@ -29,8 +29,8 @@ IPV6_RECVDSTADDR = 74
if recvmsg == "python": if recvmsg == "python":
def recv_udp(listener, bufsize): def recv_udp(listener, bufsize):
debug3('Accept UDP python using recvmsg.\n') debug3('Accept UDP python using recvmsg.\n')
data, ancdata, _, srcip = \ data, ancdata, _, srcip = listener.recvmsg(4096,
listener.recvmsg(4096, socket.CMSG_SPACE(4)) socket.CMSG_SPACE(4))
dstip = None dstip = None
for cmsg_level, cmsg_type, cmsg_data in ancdata: for cmsg_level, cmsg_type, cmsg_data in ancdata:
if cmsg_level == socket.SOL_IP and cmsg_type == IP_RECVDSTADDR: if cmsg_level == socket.SOL_IP and cmsg_type == IP_RECVDSTADDR:
@ -42,8 +42,8 @@ if recvmsg == "python":
elif recvmsg == "socket_ext": elif recvmsg == "socket_ext":
def recv_udp(listener, bufsize): def recv_udp(listener, bufsize):
debug3('Accept UDP using socket_ext recvmsg.\n') debug3('Accept UDP using socket_ext recvmsg.\n')
srcip, data, adata, _ = \ srcip, data, adata, _ = listener.recvmsg((bufsize,),
listener.recvmsg((bufsize,), socket.CMSG_SPACE(4)) socket.CMSG_SPACE(4))
dstip = None dstip = None
for a in adata: for a in adata:
if a.cmsg_level == socket.SOL_IP and a.cmsg_type == IP_RECVDSTADDR: if a.cmsg_level == socket.SOL_IP and a.cmsg_type == IP_RECVDSTADDR:
@ -106,6 +106,7 @@ def _sysctl_set(name, val):
argv = ['sysctl', '-w', '%s=%s' % (name, val)] argv = ['sysctl', '-w', '%s=%s' % (name, val)]
debug1('>> %s\n' % ' '.join(argv)) debug1('>> %s\n' % ' '.join(argv))
return ssubprocess.call(argv, stdout=open(os.devnull, 'w')) return ssubprocess.call(argv, stdout=open(os.devnull, 'w'))
# No env: No output. (Or error that won't be parsed.)
_changedctls = [] _changedctls = []
@ -134,10 +135,12 @@ def sysctl_set(name, val, permanent=False):
_changedctls.append(name) _changedctls.append(name)
return True return True
def ipfw(*args): def ipfw(*args):
argv = ['ipfw', '-q'] + list(args) argv = ['ipfw', '-q'] + list(args)
debug1('>> %s\n' % ' '.join(argv)) debug1('>> %s\n' % ' '.join(argv))
rv = ssubprocess.call(argv) rv = ssubprocess.call(argv)
# No env: No output. (Or error that won't be parsed.)
if rv: if rv:
raise Fatal('%r returned %d' % (argv, rv)) raise Fatal('%r returned %d' % (argv, rv))
@ -146,6 +149,8 @@ def ipfw_noexit(*args):
argv = ['ipfw', '-q'] + list(args) argv = ['ipfw', '-q'] + list(args)
debug1('>> %s\n' % ' '.join(argv)) debug1('>> %s\n' % ' '.join(argv))
ssubprocess.call(argv) ssubprocess.call(argv)
# No env: No output. (Or error that won't be parsed.)
class Method(BaseMethod): class Method(BaseMethod):
@ -238,8 +243,9 @@ class Method(BaseMethod):
if subnets: if subnets:
# create new subnet entries # create new subnet entries
for _, swidth, sexclude, snet \ for _, swidth, sexclude, snet in sorted(subnets,
in sorted(subnets, key=lambda s: s[1], reverse=True): key=lambda s: s[1],
reverse=True):
if sexclude: if sexclude:
ipfw('table', '125', 'add', '%s/%s' % (snet, swidth)) ipfw('table', '125', 'add', '%s/%s' % (snet, swidth))
else: else:

View File

@ -50,6 +50,18 @@ class Method(BaseMethod):
_ipt('-I', 'OUTPUT', '1', *args) _ipt('-I', 'OUTPUT', '1', *args)
_ipt('-I', 'PREROUTING', '1', *args) _ipt('-I', 'PREROUTING', '1', *args)
# Firstly we always skip all LOCAL addtrype address, i.e. avoid
# tunnelling the traffic designated to all local TCP/IP addresses.
_ipt('-A', chain, '-j', 'RETURN',
'-m', 'addrtype',
'--dst-type', 'LOCAL',
'!', '-p', 'udp')
# Skip LOCAL traffic if it's not DNS.
_ipt('-A', chain, '-j', 'RETURN',
'-m', 'addrtype',
'--dst-type', 'LOCAL',
'-p', 'udp', '!', '--dport', '53')
# create new subnet entries. # create new subnet entries.
for _, swidth, sexclude, snet, fport, lport \ for _, swidth, sexclude, snet, fport, lport \
in sorted(subnets, key=subnet_weight, reverse=True): in sorted(subnets, key=subnet_weight, reverse=True):

View File

@ -1,6 +1,6 @@
import socket import socket
from sshuttle.firewall import subnet_weight from sshuttle.firewall import subnet_weight
from sshuttle.linux import nft, nft_get_handle, nonfatal from sshuttle.linux import nft, nonfatal
from sshuttle.methods import BaseMethod from sshuttle.methods import BaseMethod
@ -16,19 +16,17 @@ class Method(BaseMethod):
if udp: if udp:
raise Exception("UDP not supported by nft") raise Exception("UDP not supported by nft")
table = "nat" table = 'sshuttle-%s' % port
def _nft(action, *args): def _nft(action, *args):
return nft(family, table, action, *args) return nft(family, table, action, *args)
chain = 'sshuttle-%s' % port chain = table
# basic cleanup/setup of chains # basic cleanup/setup of chains
_nft('add table', '') _nft('add table', '')
_nft('add chain', 'prerouting', _nft('add chain', 'prerouting',
'{ type nat hook prerouting priority -100; policy accept; }') '{ type nat hook prerouting priority -100; policy accept; }')
_nft('add chain', 'postrouting',
'{ type nat hook postrouting priority 100; policy accept; }')
_nft('add chain', 'output', _nft('add chain', 'output',
'{ type nat hook output priority -100; policy accept; }') '{ type nat hook output priority -100; policy accept; }')
_nft('add chain', chain) _nft('add chain', chain)
@ -69,16 +67,10 @@ class Method(BaseMethod):
if udp: if udp:
raise Exception("UDP not supported by nft method_name") raise Exception("UDP not supported by nft method_name")
table = "nat" table = 'sshuttle-%s' % port
def _nft(action, *args): def _nft(action, *args):
return nft(family, table, action, *args) return nft(family, table, action, *args)
chain = 'sshuttle-%s' % port
# basic cleanup/setup of chains # basic cleanup/setup of chains
handle = nft_get_handle('chain ip nat output', chain) nonfatal(_nft, 'delete table', '')
nonfatal(_nft, 'delete rule', 'output', handle)
handle = nft_get_handle('chain ip nat prerouting', chain)
nonfatal(_nft, 'delete rule', 'prerouting', handle)
nonfatal(_nft, 'delete chain', chain)

View File

@ -3,6 +3,7 @@ import sys
import platform import platform
import re import re
import socket import socket
import errno
import struct import struct
import subprocess as ssubprocess import subprocess as ssubprocess
import shlex import shlex
@ -124,12 +125,14 @@ class Generic(object):
memmove(addressof(pr) + self.RULE_ACTION_OFFSET, memmove(addressof(pr) + self.RULE_ACTION_OFFSET,
struct.pack('I', kind), 4) # rule.action = kind struct.pack('I', kind), 4) # rule.action = kind
memmove(addressof(pr) + self.ACTION_OFFSET, struct.pack( memmove(addressof(pr) + self.ACTION_OFFSET,
'I', self.PF_CHANGE_GET_TICKET), 4) # action = PF_CHANGE_GET_TICKET struct.pack('I', self.PF_CHANGE_GET_TICKET),
4) # action = PF_CHANGE_GET_TICKET
ioctl(pf_get_dev(), pf.DIOCCHANGERULE, pr) ioctl(pf_get_dev(), pf.DIOCCHANGERULE, pr)
memmove(addressof(pr) + self.ACTION_OFFSET, struct.pack( memmove(addressof(pr) + self.ACTION_OFFSET,
'I', self.PF_CHANGE_ADD_TAIL), 4) # action = PF_CHANGE_ADD_TAIL struct.pack('I', self.PF_CHANGE_ADD_TAIL),
4) # action = PF_CHANGE_ADD_TAIL
ioctl(pf_get_dev(), pf.DIOCCHANGERULE, pr) ioctl(pf_get_dev(), pf.DIOCCHANGERULE, pr)
@staticmethod @staticmethod
@ -151,7 +154,6 @@ class Generic(object):
return b'skip' in pfctl('-s Interfaces -i lo -v')[0] return b'skip' in pfctl('-s Interfaces -i lo -v')[0]
class FreeBsd(Generic): class FreeBsd(Generic):
RULE_ACTION_OFFSET = 2968 RULE_ACTION_OFFSET = 2968
@ -178,6 +180,7 @@ class FreeBsd(Generic):
def enable(self): def enable(self):
returncode = ssubprocess.call(['kldload', 'pf']) returncode = ssubprocess.call(['kldload', 'pf'])
# No env: No output.
super(FreeBsd, self).enable() super(FreeBsd, self).enable()
if returncode == 0: if returncode == 0:
_pf_context['loaded_by_sshuttle'] = True _pf_context['loaded_by_sshuttle'] = True
@ -187,6 +190,7 @@ class FreeBsd(Generic):
if _pf_context['loaded_by_sshuttle'] and \ if _pf_context['loaded_by_sshuttle'] and \
_pf_context['started_by_sshuttle'] == 0: _pf_context['started_by_sshuttle'] == 0:
ssubprocess.call(['kldunload', 'pf']) ssubprocess.call(['kldunload', 'pf'])
# No env: No output.
def add_anchors(self, anchor): def add_anchors(self, anchor):
status = pfctl('-s all')[0] status = pfctl('-s all')[0]
@ -261,7 +265,7 @@ class OpenBsd(Generic):
("proto_variant", c_uint8), ("proto_variant", c_uint8),
("direction", c_uint8)] ("direction", c_uint8)]
self.pfioc_rule = c_char * 3416 self.pfioc_rule = c_char * 3424
self.pfioc_natlook = pfioc_natlook self.pfioc_natlook = pfioc_natlook
super(OpenBsd, self).__init__() super(OpenBsd, self).__init__()
@ -420,7 +424,13 @@ class Method(BaseMethod):
def get_tcp_dstip(self, sock): def get_tcp_dstip(self, sock):
pfile = self.firewall.pfile pfile = self.firewall.pfile
try:
peer = sock.getpeername() peer = sock.getpeername()
except socket.error:
_, e = sys.exc_info()[:2]
if e.args[0] == errno.EINVAL:
return sock.getsockname()
proxy = sock.getsockname() proxy = sock.getsockname()
argv = (sock.family, socket.IPPROTO_TCP, argv = (sock.family, socket.IPPROTO_TCP,

View File

@ -169,7 +169,6 @@ class Method(BaseMethod):
return proto + ('--dport', '%d:%d' % (fport, lport)) \ return proto + ('--dport', '%d:%d' % (fport, lport)) \
if fport else proto if fport else proto
mark_chain = 'sshuttle-m-%s' % port mark_chain = 'sshuttle-m-%s' % port
tproxy_chain = 'sshuttle-t-%s' % port tproxy_chain = 'sshuttle-t-%s' % port
divert_chain = 'sshuttle-d-%s' % port divert_chain = 'sshuttle-d-%s' % port
@ -245,7 +244,8 @@ class Method(BaseMethod):
else: else:
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1', _ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
'--dest', '%s/%s' % (snet, swidth), '--dest', '%s/%s' % (snet, swidth),
'-m', 'udp', '-p', 'udp') '-m', 'udp',
*udp_ports)
_ipt('-A', tproxy_chain, '-j', 'TPROXY', _ipt('-A', tproxy_chain, '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1', '--tproxy-mark', '0x1/0x1',
'--dest', '%s/%s' % (snet, swidth), '--dest', '%s/%s' % (snet, swidth),

View File

@ -1,6 +1,7 @@
import re import re
import socket import socket
from argparse import ArgumentParser, Action, ArgumentTypeError as Fatal from argparse import ArgumentParser, Action, ArgumentTypeError as Fatal
from sshuttle import __version__ from sshuttle import __version__
@ -31,7 +32,7 @@ def parse_subnetport(s):
if s.count(':') > 1: if s.count(':') > 1:
rx = r'(?:\[?([\w\:]+)(?:/(\d+))?]?)(?::(\d+)(?:-(\d+))?)?$' rx = r'(?:\[?([\w\:]+)(?:/(\d+))?]?)(?::(\d+)(?:-(\d+))?)?$'
else: else:
rx = r'([\w\.]+)(?:/(\d+))?(?::(\d+)(?:-(\d+))?)?$' rx = r'([\w\.\-]+)(?:/(\d+))?(?::(\d+)(?:-(\d+))?)?$'
m = re.match(rx, s) m = re.match(rx, s)
if not m: if not m:
@ -62,7 +63,7 @@ def parse_ipport(s):
elif ']' in s: elif ']' in s:
rx = r'(?:\[([^]]+)])(?::(\d+))?$' rx = r'(?:\[([^]]+)])(?::(\d+))?$'
else: else:
rx = r'([\w\.]+)(?::(\d+))?$' rx = r'([\w\.\-]+)(?::(\d+))?$'
m = re.match(rx, s) m = re.match(rx, s)
if not m: if not m:
@ -176,9 +177,9 @@ parser.add_argument(
) )
parser.add_argument( parser.add_argument(
"-r", "--remote", "-r", "--remote",
metavar="[USERNAME@]ADDR[:PORT]", metavar="[USERNAME[:PASSWORD]@]ADDR[:PORT]",
help=""" help="""
ssh hostname (and optional username) of remote %(prog)s server ssh hostname (and optional username and password) of remote %(prog)s server
""" """
) )
parser.add_argument( parser.add_argument(
@ -242,6 +243,16 @@ parser.add_argument(
sacrifice latency to improve bandwidth benchmarks sacrifice latency to improve bandwidth benchmarks
""" """
) )
parser.add_argument(
"--latency-buffer-size",
metavar="SIZE",
type=int,
default=32768,
dest="latency_buffer_size",
help="""
size of latency control buffer
"""
)
parser.add_argument( parser.add_argument(
"--wrap", "--wrap",
metavar="NUM", metavar="NUM",
@ -310,6 +321,37 @@ parser.add_argument(
(internal use only) (internal use only)
""" """
) )
parser.add_argument(
"--sudoers",
action="store_true",
help="""
Add sshuttle to the sudoers for this user
"""
)
parser.add_argument(
"--sudoers-no-modify",
action="store_true",
help="""
Prints the sudoers config to STDOUT and DOES NOT modify anything.
"""
)
parser.add_argument(
"--sudoers-user",
default="",
help="""
Set the user name or group with %%group_name for passwordless operation.
Default is the current user.set ALL for all users. Only works with
--sudoers or --sudoers-no-modify option.
"""
)
parser.add_argument(
"--sudoers-filename",
default="sshuttle_auto",
help="""
Set the file name for the sudoers.d file to be added. Default is
"sshuttle_auto". Only works with --sudoers or --sudoers-no-modify option.
"""
)
parser.add_argument( parser.add_argument(
"--no-sudo-pythonpath", "--no-sudo-pythonpath",
action="store_false", action="store_false",

View File

@ -1,7 +1,9 @@
import socket import socket
import os import os
from sshuttle.helpers import debug1 from sshuttle.helpers import debug1
def _notify(message): def _notify(message):
addr = os.environ.get("NOTIFY_SOCKET", None) addr = os.environ.get("NOTIFY_SOCKET", None)
@ -27,14 +29,18 @@ def _notify(message):
debug1("Error notifying systemd: %s\n" % e) debug1("Error notifying systemd: %s\n" % e)
return False return False
def send(*messages): def send(*messages):
return _notify(b'\n'.join(messages)) return _notify(b'\n'.join(messages))
def ready(): def ready():
return b"READY=1" return b"READY=1"
def stop(): def stop():
return b"STOPPING=1" return b"STOPPING=1"
def status(message): def status(message):
return b"STATUS=%s" % message.encode('utf8') return b"STATUS=%s" % message.encode('utf8')

View File

@ -6,6 +6,7 @@ import time
import sys import sys
import os import os
import platform import platform
from shutil import which
import sshuttle.ssnet as ssnet import sshuttle.ssnet as ssnet
import sshuttle.helpers as helpers import sshuttle.helpers as helpers
@ -15,17 +16,12 @@ from sshuttle.ssnet import Handler, Proxy, Mux, MuxWrapper
from sshuttle.helpers import b, log, debug1, debug2, debug3, Fatal, \ from sshuttle.helpers import b, log, debug1, debug2, debug3, Fatal, \
resolvconf_random_nameserver resolvconf_random_nameserver
try:
from shutil import which
except ImportError:
from distutils.spawn import find_executable as which
def _ipmatch(ipstr): def _ipmatch(ipstr):
# FIXME: IPv4 only # FIXME: IPv4 only
if ipstr == 'default': if ipstr == 'default':
ipstr = '0.0.0.0/0' ipstr = '0.0.0.0/0'
m = re.match('^(\d+(\.\d+(\.\d+(\.\d+)?)?)?)(?:/(\d+))?$', ipstr) m = re.match(r'^(\d+(\.\d+(\.\d+(\.\d+)?)?)?)(?:/(\d+))?$', ipstr)
if m: if m:
g = m.groups() g = m.groups()
ips = g[0] ips = g[0]
@ -289,16 +285,12 @@ def main(latency_control, auto_hosts, to_nameserver, auto_nets):
helpers.logprefix = 'server: ' helpers.logprefix = 'server: '
debug1('latency control setting = %r\n' % latency_control) debug1('latency control setting = %r\n' % latency_control)
# synchronization header # synchronization header
sys.stdout.write('\0\0SSHUTTLE0001') sys.stdout.write('\0\0SSHUTTLE0001')
sys.stdout.flush() sys.stdout.flush()
handlers = [] handlers = []
mux = Mux(socket.fromfd(sys.stdin.fileno(), mux = Mux(sys.stdin, sys.stdout)
socket.AF_INET, socket.SOCK_STREAM),
socket.fromfd(sys.stdout.fileno(),
socket.AF_INET, socket.SOCK_STREAM))
handlers.append(mux) handlers.append(mux)
debug1('auto-nets:' + str(auto_nets) + '\n') debug1('auto-nets:' + str(auto_nets) + '\n')

View File

@ -6,16 +6,13 @@ import zlib
import imp import imp
import subprocess as ssubprocess import subprocess as ssubprocess
import shlex import shlex
from shlex import quote
import ipaddress
from urllib.parse import urlparse
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(".")
@ -60,29 +57,64 @@ def empackage(z, name, data=None):
return b'%s\n%d\n%s' % (name.encode("ASCII"), len(content), content) return b'%s\n%d\n%s' % (name.encode("ASCII"), len(content), content)
def parse_hostport(rhostport):
"""
parses the given rhostport variable, looking like this:
[username[:password]@]host[:port]
if only host is given, can be a hostname, IPv4/v6 address or a ssh alias
from ~/.ssh/config
and returns a tuple (username, password, port, host)
"""
# default port for SSH is TCP port 22
port = 22
username = None
password = None
host = rhostport
if "@" in host:
# split username (and possible password) from the host[:port]
username, host = host.split("@")
# Fix #410 bad username error detect
# username cannot contain an @ sign in this scenario
if ":" in username:
# this will even allow for the username to be empty
username, password = username.split(":")
if ":" in host:
# IPv6 address and/or got a port specified
# If it is an IPv6 adress with port specification,
# then it will look like: [::1]:22
try:
# try to parse host as an IP adress,
# if that works it is an IPv6 address
host = ipaddress.ip_address(host)
except ValueError:
# if that fails parse as URL to get the port
parsed = urlparse('//{}'.format(host))
try:
host = ipaddress.ip_address(parsed.hostname)
except ValueError:
# else if both fails, we have a hostname with port
host = parsed.hostname
port = parsed.port
if password is None or len(password) == 0:
password = None
return username, password, port, host
def connect(ssh_cmd, rhostport, python, stderr, options): def connect(ssh_cmd, rhostport, python, stderr, options):
portl = [] username, password, port, host = parse_hostport(rhostport)
if username:
if re.sub(r'.*@', '', rhostport or '').count(':') > 1: rhost = "{}@{}".format(username, host)
if rhostport.count(']') or rhostport.count('['):
result = rhostport.split(']')
rhost = result[0].strip('[')
if len(result) > 1:
result[1] = result[1].strip(':')
if result[1] != '':
portl = ['-p', str(int(result[1]))]
# can't disambiguate IPv6 colons and a port number. pass the hostname
# through.
else: else:
rhost = rhostport rhost = host
else: # IPv4
l = (rhostport or '').rsplit(':', 1)
rhost = l[0]
if len(l) > 1:
portl = ['-p', str(int(l[1]))]
if rhost == '-':
rhost = None
z = zlib.compressobj(1) z = zlib.compressobj(1)
content = readfile('sshuttle.assembler') content = readfile('sshuttle.assembler')
@ -118,9 +150,17 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
else: else:
pycmd = ("P=python3; $P -V 2>%s || P=python; " pycmd = ("P=python3; $P -V 2>%s || P=python; "
"exec \"$P\" -c %s") % (os.devnull, quote(pyscript)) "exec \"$P\" -c %s") % (os.devnull, quote(pyscript))
pycmd = ("exec /bin/sh -c %s" % quote(pycmd)) pycmd = ("/bin/sh -c {}".format(quote(pycmd)))
if password is not None:
os.environ['SSHPASS'] = str(password)
argv = (["sshpass", "-e"] + sshl +
["-p", str(port)] +
[rhost, '--', pycmd])
else:
argv = (sshl + argv = (sshl +
portl + ["-p", str(port)] +
[rhost, '--', pycmd]) [rhost, '--', pycmd])
(s1, s2) = socket.socketpair() (s1, s2) = socket.socketpair()

View File

@ -4,9 +4,11 @@ import socket
import errno import errno
import select import select
import os import os
from sshuttle.helpers import b, binary_type, log, debug1, debug2, debug3, Fatal
from sshuttle.helpers import b, log, debug1, debug2, debug3, Fatal
MAX_CHANNEL = 65535 MAX_CHANNEL = 65535
LATENCY_BUFFER_SIZE = 32768
# these don't exist in the socket module in python 2.3! # these don't exist in the socket module in python 2.3!
SHUT_RD = 0 SHUT_RD = 0
@ -55,17 +57,18 @@ 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] errno.ENETUNREACH, errno.ECONNABORTED,
errno.ECONNRESET]
def _add(l, elem): def _add(socks, elem):
if elem not in l: if elem not in socks:
l.append(elem) socks.append(elem)
def _fds(l): def _fds(socks):
out = [] out = []
for i in l: for i in socks:
try: try:
out.append(i.fileno()) out.append(i.fileno())
except AttributeError: except AttributeError:
@ -93,7 +96,9 @@ def _try_peername(sock):
return '%s:%s' % (pn[0], pn[1]) return '%s:%s' % (pn[0], pn[1])
except socket.error: except socket.error:
_, e = sys.exc_info()[:2] _, e = sys.exc_info()[:2]
if e.args[0] not in (errno.ENOTCONN, errno.ENOTSOCK): if e.args[0] == errno.EINVAL:
pass
elif e.args[0] not in (errno.ENOTCONN, errno.ENOTSOCK):
raise raise
except AttributeError: except AttributeError:
pass pass
@ -334,10 +339,10 @@ class Proxy(Handler):
class Mux(Handler): class Mux(Handler):
def __init__(self, rsock, wsock): def __init__(self, rfile, wfile):
Handler.__init__(self, [rsock, wsock]) Handler.__init__(self, [rfile, wfile])
self.rsock = rsock self.rfile = rfile
self.wsock = wsock self.wfile = wfile
self.new_channel = self.got_dns_req = self.got_routes = None self.new_channel = self.got_dns_req = self.got_routes = None
self.got_udp_open = self.got_udp_data = self.got_udp_close = None self.got_udp_open = self.got_udp_data = self.got_udp_close = None
self.got_host_req = self.got_host_list = None self.got_host_req = self.got_host_list = None
@ -366,7 +371,7 @@ class Mux(Handler):
return total return total
def check_fullness(self): def check_fullness(self):
if self.fullness > 32768: if self.fullness > LATENCY_BUFFER_SIZE:
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
@ -377,7 +382,7 @@ 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, binary_type) assert isinstance(data, bytes)
assert len(data) <= 65535 assert len(data) <= 65535
p = struct.pack('!ccHHH', b('S'), b('S'), channel, cmd, len(data)) \ p = struct.pack('!ccHHH', b('S'), b('S'), channel, cmd, len(data)) \
+ data + data
@ -434,9 +439,9 @@ class Mux(Handler):
callback(cmd, data) callback(cmd, data)
def flush(self): def flush(self):
self.wsock.setblocking(False) os.set_blocking(self.wfile.fileno(), False)
if self.outbuf and self.outbuf[0]: if self.outbuf and self.outbuf[0]:
wrote = _nb_clean(os.write, self.wsock.fileno(), self.outbuf[0]) wrote = _nb_clean(os.write, self.wfile.fileno(), self.outbuf[0])
debug2('mux wrote: %r/%d\n' % (wrote, len(self.outbuf[0]))) debug2('mux wrote: %r/%d\n' % (wrote, len(self.outbuf[0])))
if wrote: if wrote:
self.outbuf[0] = self.outbuf[0][wrote:] self.outbuf[0] = self.outbuf[0][wrote:]
@ -444,9 +449,9 @@ class Mux(Handler):
self.outbuf[0:1] = [] self.outbuf[0:1] = []
def fill(self): def fill(self):
self.rsock.setblocking(False) os.set_blocking(self.rfile.fileno(), False)
try: try:
read = _nb_clean(os.read, self.rsock.fileno(), 32768) read = _nb_clean(os.read, self.rfile.fileno(), LATENCY_BUFFER_SIZE)
except OSError: except OSError:
_, e = sys.exc_info()[:2] _, e = sys.exc_info()[:2]
raise Fatal('other end: %r' % e) raise Fatal('other end: %r' % e)
@ -476,22 +481,22 @@ class Mux(Handler):
break break
def pre_select(self, r, w, x): def pre_select(self, r, w, x):
_add(r, self.rsock) _add(r, self.rfile)
if self.outbuf: if self.outbuf:
_add(w, self.wsock) _add(w, self.wfile)
def callback(self, sock): def callback(self, sock):
(r, w, _) = select.select([self.rsock], [self.wsock], [], 0) (r, w, _) = select.select([self.rfile], [self.wfile], [], 0)
if self.rsock in r: if self.rfile in r:
self.handle() self.handle()
if self.outbuf and self.wsock in w: if self.outbuf and self.wfile in w:
self.flush() self.flush()
class MuxWrapper(SockWrapper): class MuxWrapper(SockWrapper):
def __init__(self, mux, channel): def __init__(self, mux, channel):
SockWrapper.__init__(self, mux.rsock, mux.wsock) SockWrapper.__init__(self, mux.rfile, mux.wfile)
self.mux = mux self.mux = mux
self.channel = channel self.channel = channel
self.mux.channels[channel] = self.got_packet self.mux.channels[channel] = self.got_packet

64
sshuttle/sudoers.py Normal file
View File

@ -0,0 +1,64 @@
import os
import sys
import getpass
from uuid import uuid4
from subprocess import Popen, PIPE
from sshuttle.helpers import log, debug1
from distutils import spawn
path_to_sshuttle = sys.argv[0]
path_to_dist_packages = os.path.dirname(os.path.abspath(__file__))[:-9]
# randomize command alias to avoid collisions
command_alias = 'SSHUTTLE%(num)s' % {'num': uuid4().hex[-3:].upper()}
# Template for the sudoers file
template = '''
Cmnd_Alias %(ca)s = /usr/bin/env PYTHONPATH=%(dist_packages)s %(py)s %(path)s *
%(user_name)s ALL=NOPASSWD: %(ca)s
'''
def build_config(user_name):
content = template % {
'ca': command_alias,
'dist_packages': path_to_dist_packages,
'py': sys.executable,
'path': path_to_sshuttle,
'user_name': user_name,
}
return content
def save_config(content, file_name):
process = Popen([
'/usr/bin/sudo',
spawn.find_executable('sudoers-add'),
file_name,
], stdout=PIPE, stdin=PIPE)
process.stdin.write(content.encode())
streamdata = process.communicate()[0]
returncode = process.returncode
if returncode:
log('Failed updating sudoers file.\n')
debug1(streamdata)
exit(returncode)
else:
log('Success, sudoers file update.\n')
exit(0)
def sudoers(user_name=None, no_modify=None, file_name=None):
user_name = user_name or getpass.getuser()
content = build_config(user_name)
if no_modify:
sys.stdout.write(content)
exit(0)
else:
save_config(content, file_name)

View File

@ -1,7 +1,7 @@
from mock import Mock, patch, call
import io import io
from socket import AF_INET, AF_INET6 from socket import AF_INET, AF_INET6
from mock import Mock, patch, call
import sshuttle.firewall import sshuttle.firewall
@ -85,8 +85,9 @@ def test_subnet_weight():
(AF_INET, 0, 1, '0.0.0.0', 0, 0) (AF_INET, 0, 1, '0.0.0.0', 0, 0)
] ]
assert subnets_sorted == \ assert subnets_sorted == sorted(subnets,
sorted(subnets, key=sshuttle.firewall.subnet_weight, reverse=True) key=sshuttle.firewall.subnet_weight,
reverse=True)
@patch('sshuttle.firewall.rewrite_etc_hosts') @patch('sshuttle.firewall.rewrite_etc_hosts')

View File

@ -1,10 +1,9 @@
from mock import patch, call
import sys
import io import io
import socket import socket
from socket import AF_INET, AF_INET6 from socket import AF_INET, AF_INET6
import errno import errno
from mock import patch, call
import sshuttle.helpers import sshuttle.helpers
@ -193,9 +192,5 @@ def test_family_ip_tuple():
def test_family_to_string(): def test_family_to_string():
assert sshuttle.helpers.family_to_string(AF_INET) == "AF_INET" assert sshuttle.helpers.family_to_string(AF_INET) == "AF_INET"
assert sshuttle.helpers.family_to_string(AF_INET6) == "AF_INET6" assert sshuttle.helpers.family_to_string(AF_INET6) == "AF_INET6"
if sys.version_info < (3, 0):
expected = "1"
assert sshuttle.helpers.family_to_string(socket.AF_UNIX) == "1"
else:
expected = 'AddressFamily.AF_UNIX' expected = 'AddressFamily.AF_UNIX'
assert sshuttle.helpers.family_to_string(socket.AF_UNIX) == expected assert sshuttle.helpers.family_to_string(socket.AF_UNIX) == expected

View File

@ -1,9 +1,9 @@
import pytest
from mock import Mock, patch, call
import socket import socket
from socket import AF_INET, AF_INET6 from socket import AF_INET, AF_INET6
import struct import struct
import pytest
from mock import Mock, patch, call
from sshuttle.helpers import Fatal from sshuttle.helpers import Fatal
from sshuttle.methods import get_method from sshuttle.methods import get_method
@ -139,6 +139,12 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
call(AF_INET, 'nat', '-F', 'sshuttle-1025'), call(AF_INET, 'nat', '-F', 'sshuttle-1025'),
call(AF_INET, 'nat', '-I', 'OUTPUT', '1', '-j', 'sshuttle-1025'), call(AF_INET, 'nat', '-I', 'OUTPUT', '1', '-j', 'sshuttle-1025'),
call(AF_INET, 'nat', '-I', 'PREROUTING', '1', '-j', 'sshuttle-1025'), call(AF_INET, 'nat', '-I', 'PREROUTING', '1', '-j', 'sshuttle-1025'),
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN',
'-m', 'addrtype', '--dst-type', 'LOCAL',
'!', '-p', 'udp'),
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN',
'-m', 'addrtype', '--dst-type', 'LOCAL',
'-p', 'udp', '!', '--dport', '53'),
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN', call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN',
'--dest', u'1.2.3.66/32', '-p', 'tcp', '--dport', '8080:8080') '--dest', u'1.2.3.66/32', '-p', 'tcp', '--dport', '8080:8080')
] ]

View File

@ -1,8 +1,8 @@
import pytest
from mock import Mock, patch, call, ANY
import socket import socket
from socket import AF_INET, AF_INET6 from socket import AF_INET, AF_INET6
import pytest
from mock import Mock, patch, call, ANY
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 FreeBsd, Darwin, OpenBsd from sshuttle.methods.pf import FreeBsd, Darwin, OpenBsd
@ -403,8 +403,8 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
None) None)
assert mock_ioctl.mock_calls == [ assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xcd58441a, ANY), call(mock_pf_get_dev(), 0xcd60441a, ANY),
call(mock_pf_get_dev(), 0xcd58441a, ANY), call(mock_pf_get_dev(), 0xcd60441a, ANY),
] ]
assert mock_pfctl.mock_calls == [ assert mock_pfctl.mock_calls == [
call('-s Interfaces -i lo -v'), call('-s Interfaces -i lo -v'),
@ -451,8 +451,8 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
False, False,
None) None)
assert mock_ioctl.mock_calls == [ assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xcd58441a, ANY), call(mock_pf_get_dev(), 0xcd60441a, ANY),
call(mock_pf_get_dev(), 0xcd58441a, ANY), call(mock_pf_get_dev(), 0xcd60441a, ANY),
] ]
assert mock_pfctl.mock_calls == [ assert mock_pfctl.mock_calls == [
call('-s Interfaces -i lo -v'), call('-s Interfaces -i lo -v'),

View File

@ -168,7 +168,7 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
'--on-port', '1024'), '--on-port', '1024'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK', call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
'--set-mark', '1', '--dest', u'2404:6800:4004:80c::/64', '--set-mark', '1', '--dest', u'2404:6800:4004:80c::/64',
'-m', 'udp', '-p', 'udp'), '-m', 'udp', '-p', 'udp', '--dport', '8000:9000'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY', call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1', '--dest', u'2404:6800:4004:80c::/64', '--tproxy-mark', '0x1/0x1', '--dest', u'2404:6800:4004:80c::/64',
'-m', 'udp', '-p', 'udp', '--dport', '8000:9000', '-m', 'udp', '-p', 'udp', '--dport', '8000:9000',

View File

@ -1,8 +1,10 @@
import socket import socket
import pytest
import sshuttle.options
from argparse import ArgumentTypeError as Fatal from argparse import ArgumentTypeError as Fatal
import pytest
import sshuttle.options
_ip4_reprs = { _ip4_reprs = {
'0.0.0.0': '0.0.0.0', '0.0.0.0': '0.0.0.0',
'255.255.255.255': '255.255.255.255', '255.255.255.255': '255.255.255.255',
@ -25,6 +27,7 @@ _ip6_reprs = {
_ip6_swidths = (48, 64, 96, 115, 128) _ip6_swidths = (48, 64, 96, 115, 128)
def test_parse_subnetport_ip4(): def test_parse_subnetport_ip4():
for ip_repr, ip in _ip4_reprs.items(): for ip_repr, ip in _ip4_reprs.items():
assert sshuttle.options.parse_subnetport(ip_repr) \ assert sshuttle.options.parse_subnetport(ip_repr) \

View File

@ -1,6 +1,7 @@
from mock import Mock, patch, call
import socket import socket
from mock import Mock, patch, call
import sshuttle.sdnotify import sshuttle.sdnotify

View File

@ -1,10 +0,0 @@
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("client")

View File

@ -1,8 +1,10 @@
import io import io
import socket import socket
import sshuttle.server
from mock import patch, Mock from mock import patch, Mock
import sshuttle.server
def test__ipmatch(): def test__ipmatch():
assert sshuttle.server._ipmatch("1.2.3.4") is not None assert sshuttle.server._ipmatch("1.2.3.4") is not None

19
tox.ini
View File

@ -1,21 +1,22 @@
[tox] [tox]
downloadcache = {toxworkdir}/cache/ downloadcache = {toxworkdir}/cache/
envlist = envlist =
py27,
py34,
py35, py35,
py36, py36,
py37,
py38,
[testenv] [testenv]
basepython = basepython =
py26: python2.6
py27: python2.7
py34: python3.4
py35: python3.5
py36: python3.6 py36: python3.6
py37: python3.7
py38: python3.8
commands = commands =
flake8 sshuttle --count --select=E901,E999,F821,F822,F823 --show-source --statistics pip install -e .
flake8 sshuttle --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics # actual flake8 test
py.test flake8 sshuttle tests
# flake8 complexity warnings
flake8 sshuttle tests --exit-zero --max-complexity=10
pytest
deps = deps =
-rrequirements-tests.txt -rrequirements-tests.txt