Compare commits

...

221 Commits

Author SHA1 Message Date
abb48f1996 Update changes file 2020-08-24 08:00:36 +10:00
1c27a6cad0 Merge pull request #510 from sshuttle/dependabot/pip/attrs-20.1.0
Bump attrs from 19.3.0 to 20.1.0
2020-08-21 16:42:05 +10:00
8a2d5802c1 Bump attrs from 19.3.0 to 20.1.0
Bumps [attrs](https://github.com/python-attrs/attrs) from 19.3.0 to 20.1.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.3.0...20.1.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-08-21 06:37:11 +00:00
e7d4931b3d Merge pull request #507 from ddstreet/old_py
allow Mux() flush/fill to work with python < 3.5
2020-08-18 07:46:59 +10:00
1e364b2c0b Merge pull request #509 from sshuttle/dependabot/pip/pytest-cov-2.10.1
Bump pytest-cov from 2.10.0 to 2.10.1
2020-08-17 19:00:50 +10:00
8816dbfd23 Bump pytest-cov from 2.10.0 to 2.10.1
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 2.10.0 to 2.10.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.10.0...v2.10.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-08-17 08:18:23 +00:00
98d052d19e allow Mux() flush/fill to work with python < 3.5
Fixes: #503
2020-08-15 15:12:51 -04:00
be4b081a0d Merge pull request #506 from sshuttle/test_parse_hostport
Fix parse_hostport to always return string for host
2020-08-13 07:59:12 +10:00
9c5f1f5bbf Fix parse_hostport to always return string for host
This fixes #488 and provides an alternative solution for #489.
2020-08-13 07:53:38 +10:00
33d09ffcaf Merge pull request #501 from lnaundorf/patch-1
Add missing package in OpenWRT documentation
2020-08-12 07:56:10 +10:00
45f8cce2f8 Merge pull request #502 from joshuarli/ref/require-remote
fix: require -r/--remote
2020-08-12 07:36:37 +10:00
d4001c11f9 fix: workaround 2020-08-10 15:44:08 -07:00
450ad79b18 Revert "fix: require -r/--remote"
This reverts commit 5debf1f11a.
2020-08-10 15:31:20 -07:00
5debf1f11a fix: require -r/--remote 2020-08-10 15:12:24 -07:00
79181043bc Add missing package in OpenWRT documentation
The package 'iptables-mod-extra' also needs to be installed
2020-08-10 16:35:05 +02:00
c0a81353ab Fix doc about --listen option (#500)
* Can't use this option twice, separate by comma actually.

* Broke the line because it was too long.
2020-08-05 20:28:36 +10:00
5bdf36152a Merge pull request #498 from sshuttle/dependabot/pip/pytest-6.0.1
Bump pytest from 6.0.0 to 6.0.1
2020-08-01 18:07:00 +10:00
a9ee66d905 Bump pytest from 6.0.0 to 6.0.1
Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.0.0 to 6.0.1.
- [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/6.0.0...6.0.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-31 08:26:00 +00:00
094d3d9b97 Merge pull request #497 from sshuttle/dependabot/pip/pytest-6.0.0
Bump pytest from 5.4.3 to 6.0.0
2020-07-31 07:57:58 +10:00
19b677892e Bump pytest from 5.4.3 to 6.0.0
Bumps [pytest](https://github.com/pytest-dev/pytest) from 5.4.3 to 6.0.0.
- [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.3...6.0.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-29 08:13:07 +00:00
319c122861 Merge pull request #495 from AsciiWolf/patch-1
README: add Ubuntu
2020-07-25 08:55:57 +10:00
f4bd290919 README: add Ubuntu 2020-07-24 17:38:16 +02:00
f353701f24 Merge pull request #490 from erikselin/42-is-not-the-answer
Douglas Adams and Deep Thought was wrong, 42 is not the answer
2020-07-17 11:09:04 +10:00
3037a91e51 Increase IP4 ttl to 63 hops instead of 42 2020-07-16 20:51:27 -04:00
cdd1e2c538 Merge pull request #487 from sshuttle/brianmay-patch-2
Fix formatting in installation.rst
2020-07-17 07:08:39 +10:00
eb01c0b184 Fix formatting in installation.rst 2020-07-15 08:14:51 +10:00
c5dcc918db Add changelog entry for 1.0.3 2020-07-12 19:09:32 +10:00
329b9cd0a0 Merge pull request #483 from chrisburr/patch-1
Fix formatting typos in usage docs
2020-07-05 11:00:18 +10:00
5537a90338 Fix formatting typos in usage docs 2020-07-04 12:02:44 +02:00
636e0442e5 Merge pull request #476 from sshuttle/brianmay-patch-1
Add missing import
2020-06-30 07:36:57 +10:00
dc526747b1 Add missing import
Fixes #474.
2020-06-27 18:54:02 +10:00
73eb3b6479 Merge pull request #471 from wilsonehusin/deprecate-py2-setuptools
Restrict setuptools from executing on Python2
2020-06-18 15:27:29 +10:00
1b50d364c6 Ask setuptools to require Python 3.5 and above
python_requires will be evaluated by setuptools to ensure the package is compatible
with currently active Python interpreter.

Reference: https://packaging.python.org/guides/distributing-packages-using-setuptools/#python-requires

Closes #470

Signed-off-by: Wilson Husin <wilsonehusin@gmail.com>
2020-06-17 22:17:03 -07:00
8c91958ff3 Prepare to release 1.0.2 2020-06-18 07:45:08 +10:00
d2f751f0d3 leave use of default port to ssh command
to prevent overwriting ports configured in ~/.ssh/config
if no port is specified, don't set the port explicitly to 22
2020-06-17 08:04:35 +10:00
9d79bb82c5 Bump pytest-cov from 2.9.0 to 2.10.0
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 2.9.0 to 2.10.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.9.0...v2.10.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-06-15 19:14:10 +10:00
a53f026056 Merge pull request #461 from joshuarli/doc/update-py2-removal
doc: py2 removal clean sweep
2020-06-11 09:55:36 +10:00
ae4c7e3a7b remove outdated comment 2020-06-10 16:47:23 -07:00
61bbbca956 another python 3.5 change 2020-06-10 16:46:52 -07:00
e56f8f2349 server side is py3.5+ 2020-06-10 16:46:10 -07:00
0a36eac686 ref: replace usage of deprecated imp (#449)
* Use types instead of imp.new_module.

I can follow up with https://docs.python.org/3/library/importlib.html#importlib.util.module_from_spec if need be.

* use source loader from importlib

* Revert "use source loader from importlib"

This reverts commit 1f255704f7.

* use inspect.getsource, but alas

* placate linter

* use find_spec to resolve a module spec to a file path

* better function naming

* remove outdated comment
2020-06-11 06:57:46 +10:00
16b462880b Merge pull request #460 from alekseymykhailov/fix_connection
fix connection with @ sign in username
2020-06-11 06:15:19 +10:00
500aa65693 fix connection with @ sign in username 2020-06-10 08:20:28 -07:00
7d998f6d42 Bump flake8 from 3.8.2 to 3.8.3
Bumps [flake8](https://gitlab.com/pycqa/flake8) from 3.8.2 to 3.8.3.
- [Release notes](https://gitlab.com/pycqa/flake8/tags)
- [Commits](https://gitlab.com/pycqa/flake8/compare/3.8.2...3.8.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-06-09 18:48:45 +10:00
8c9dad1c6b Fix errors in long_documentation 2020-06-05 08:19:24 +10:00
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
752a953101 Release 0.78.5 2019-01-28 16:28:57 +13:00
61f4cd9de5 Update CHANGES.rst for new release 2019-01-28 11:36:14 +13:00
8e35f049e2 auto-nets: retrieve routes only if using auto-nets
There's a known issue that makes sshuttle crash if there are too
many routes on the remote host (that don't fit in 64KB). This patch
requests the routes only if auto-nets is specified on the command
line.
2019-01-28 08:53:51 +13:00
0e99adc5d1 Fix potential deadlock condition in nft_get_handle
This was susceptible to the same deadlock issue that ipt_chain_exists
had and was fixed in d43db80 where if the command returned a significant
amount of output, it wouldn't all be read in, resulting in the
subprocess hanging waiting for the output to be read.
2019-01-23 18:53:45 +13:00
04849df7e3 Use subprocess.check_output instead of run
subprocess.run only exists for python3, and this needs to also support
python 2.7
2019-01-23 18:53:45 +13:00
531a17c151 docs: document --ns-hosts --to-ns and update --dns
--ns-hosts is available since commit d2ee34d71c
("dns: Added --ns-hosts to tunnel only some requests")
(released as v0.72), but was never documented.

--to-ns is available since commit be559fc78b
("Fix case where there is no --dns.") after several
bugfixes, released as v0.78.4, but was never
documented.
2018-12-29 15:02:18 +11:00
d43db80dec Fix deadlock with iptables with large ruleset
When running sshuttle with a large list of routes it's failing to clean
them up at exit. It returns the following:

$ sshuttle -r user@host.example.com -s /tmp/aws-cidrs.txt
user@host.example.com's password:
client: Connected.
^CAnother app is currently holding the xtables lock; still -9s 0us time ahead to have a chance to grab the lock...
Another app is currently holding the xtables lock; still -19s 0us time ahead to have a chance to grab the lock...
Another app is currently holding the xtables lock; still -29s 0us time ahead to have a chance to grab the lock...

This continues indefinitely. Looking in ps reveals that there are 2
iptables processes running. Killing -9 the first one, allows sshuttle to
continue and clean up successfully.

The problem lies with the use of Popen here. The function currently
returns as soon as it finds a match without consuming everything from
stdout. This means that if there's more output from iptables than will
fit in the buffer it doesn't exit, and therefore doesn't release the
kernel xtables lock.
2018-12-09 18:03:54 +11:00
0b1a260436 Fix typo in docs 2018-12-03 14:34:42 +11:00
efc854c33e Document --version option 2018-11-29 08:02:58 +11:00
ca41026c89 Changes pf exclusion rules precedence
Before this change, in pf, exclusions used a pass out quick which gave
them higher precedence than any other rule independent of subnet width.
As reported in #265 this causes exclusion from one instance of sshuttle
to also take effect on other instances because quick aborts the
evaluation of rules across all anchors.

This commit changes the precedence of rules so quick can now be
dropped. The new order is defined by the following rule, from
subnet_weight:

"We need to go from smaller, more specific, port ranges, to larger,
less-specific, port ranges. At each level, we order by subnet
width, from most-specific subnets (largest swidth) to
least-specific. On ties, excludes come first."
2018-11-03 12:24:32 +11:00
b473b91633 Close stdin, stdout, and stderr when using syslog or forking to daemon (#283)
* Close stdin, stdout, and stderr when using syslog or forking to daemon

Fixes #139

* Ensure we close devnull after use
2018-11-01 09:27:50 +11:00
7a54d12f80 Fixes support for OpenBSD (6.1+) (#282)
* Fixes support for OpenBSD (6.1+)

As reported in #219, new versions of OpenBSD ship with a different
pfioc_rule struct. This commit adjusts the offset to match the new struct.

* Fixes tests for OpenBSD 6.1+
2018-10-23 07:31:29 +11:00
d4bbf3b68d Added coverage report to tests 2018-10-17 20:54:28 +11:00
41f5b3e9c1 replace path /dev/null by os.devnull 2018-10-17 20:53:06 +11:00
c780597de3 updated bandit config 2018-10-17 20:52:04 +11:00
d085a419b2 updated path 2018-10-17 20:52:04 +11:00
842768f9cf Moved sshuttle/tests into tests to.
Having the tests in a `tests` directory in root is the most common
approach. Also moved pytest's conftest.py into `tests` making the
fixture available for client and server tests.
2018-10-17 20:52:04 +11:00
97ed2030f3 Fix missing string formatting argument 2018-10-07 11:30:41 +11:00
6dc368bde8 Merge pull request #271 from usabilla/no-sudo-pythonpath
Add --no-sudo-pythonpath option
2018-09-22 17:57:33 +10:00
f528bb9846 Add --no-sudo-pythonpath option
This provides a way to avoid setting PYTHONPATH when invoking the
privileged part of sshuttle with sudo. This is useful if running
sshuttle as a PEX archive, as Telepresence does, as it enables
sshuttle's sudo access to be securely locked down.

PEX archives will extract themselves into the invoking user's home
directory, which means that the invoking user has full control over
the code in them. This makes restricting sudo access with
PYTHONPATH set completely pointless in this scenario -- an attacker
could put any code into ~/.pex and gain full root access anyway.

On the other hand, if sshuttle is a PEX archive, the privileged
invocation will simply extract itself into /root/.pex anyway, so
there is no need to set PYTHONPATH in this case.
2018-09-21 18:48:31 +02:00
561b648e4b works on ChromeOS with Crostini VM (#262)
* works on ChromeOS with Crostini VM

tested on ASUS C101PA on Dev channel, should also work on Intel machines and Beta channel

* crostini doc, and a note about xterms and VNC

tested on ASUS C101PA on Dev channel, should also work on Intel machines and Beta channel
2018-08-25 10:30:45 +10:00
0dba8a8beb Don't crash if we can't look up peername
Peername is only used for information display messages.

Fixes #259
2018-08-21 08:36:51 +10:00
7b6f082454 Doc Update
Remove reference to autossh per
https://github.com/sshuttle/sshuttle/issues/143
2018-06-29 07:38:55 +10:00
1ec17e1b1b Update README.rst 2018-06-22 16:02:11 +10:00
cecccc2efd Doc: Fix typo 2018-06-07 07:00:17 +10:00
db69ba6d8d Doc: Improve Systemd service recommendation (fixes #238) 2018-06-03 07:38:21 +10:00
2bb92cd6d4 Fix sudo/doas
Fixes #227
2018-05-13 20:35:18 +10:00
ae5bd28dcf Add doas support for client 2018-05-09 17:46:33 +10:00
55bd78fd43 Fix line length for CI. 2018-04-30 07:40:58 +10:00
1f5ed9c66e Fix concatening string to tuple. Allow for forwarding a single port. 2018-04-30 07:40:58 +10:00
6ec42adbf4 Prepare for 0.78.4 2018-04-02 14:52:22 +10:00
2200d824bf Improve formatting 2018-03-22 07:59:10 +11:00
9715a1d6f2 Preserve peer and port properly 2018-03-22 07:59:10 +11:00
8bfc03b256 Make --to-dns and --ns-host work well together 2018-03-22 07:59:10 +11:00
884bd6deb0 Remove test that fails under OSX
Fixes #213
2018-03-16 18:40:32 +11:00
a215f1b227 Remove Python 2.6 from automatic tests
Automatic python 2.6 testing is becoming harder, especially as pytest
3.4.2 is unavailable for Python 2.6.
2018-03-16 18:34:15 +11:00
11455d0bcd Various updates to tests 2018-03-16 18:27:50 +11:00
74acc10385 Add entries to .gitignore 2018-03-16 18:10:09 +11:00
084bf5f0f2 Specify pip requirements for tests 2018-03-16 18:10:09 +11:00
1940b524f1 Add nat-like method using nftables instead of iptables 2018-03-13 07:36:00 +11:00
d11f5b9d16 Use flake8 to find Python syntax errors or undefined names 2018-02-22 18:02:36 +11:00
93b969a049 Fix compatibility with the sudoers file
Starting sshuttle without having to type in one's password requires to
put the sudo-ed command in the `/etc/sudoers` file. However, sshuttle
sets an environment variable, which cannot be done as-is in the sudoers
file. This fix prepend the /usr/bin/env command, which allows one to
pass fixed environment variables to a sudo-ed command.

In practice, the sub-command:

```
sudo PYTHONPATH=/usr/lib/python3/dist-packages -- \
        /usr/bin/python3 /usr/bin/sshuttle --method auto --firewall
```

becomes

```
sudo /usr/bin/env PYTHONPATH=/usr/lib/python3/dist-packages \
        /usr/bin/python3 /usr/bin/sshuttle --method auto --firewall
```
2018-02-16 08:07:02 +11:00
f27b27b0e8 Stop using SO_REUSEADDR on sockets 2018-02-16 08:04:22 +11:00
fc08fb4086 Declare 'verbosity' as global variable to placate linters 2018-02-15 21:34:05 +11:00
e82d5a8e7c Adds 'cd sshuttle' after 'git' to README and docs 2018-02-15 07:37:15 +11:00
d9d61e6ab2 Documentation for loading options from configuration file 2018-01-30 17:08:30 +11:00
179bb107e1 Load options from a file
This small change will allow a file path to be passed as argument from which
the command line options will be loaded.

Extra command line options can be passed (in addition to those already in the
file) and existing ones can be overriden.

Example sshuttle.conf file:
192.168.0.0/16
--remote
user@example.com

Example sshuttle call:
sshuttle @/path/to/sshuttle.conf

Example sshuttle call with verbose flags added:
sshuttle @/path/to/sshuttle.conf -vvv

Example sshuttle call overriding the remote server:
sshuttle @/path/to/sshuttle.conf -r otheruser@test.example.com
2018-01-30 17:08:30 +11:00
9a176aa96f Update firewall.py 2018-01-01 09:35:41 +11:00
6b48301b86 move sdnotify after setting up firewall rules 2018-01-01 09:35:41 +11:00
be90cc8abd Fix tests on Macos
Swap hardcoded AF_INET(6) values for Python-provided values as they
differ between Darwin and Linux (30 vs 10 for AF_INET6 for instance).
2018-01-01 09:33:41 +11:00
512396e06b Add changelog entry about fixed license 2017-11-16 19:57:33 +11:00
7a71ae1380 Remove trailing whitespace 2017-11-16 18:06:33 +11:00
3a6f6cb795 Add changes entry for next release 2017-11-16 18:06:01 +11:00
81ab587698 Updating per @brianmay correspondence in https://github.com/sshuttle/sshuttle/issues/186 2017-11-16 18:05:39 +11:00
817284c2f8 Use more standard filename and format for bandit conifguration 2017-11-13 11:58:43 +11:00
71d65f3831 Fixes some style issues and minor bugs 2017-11-13 11:58:43 +11:00
9f238ebca8 Properly decode seed hosts argument in server.py
When I starting sshuttle with option `--seed-hosts example.com`, got the following error:

```
hostwatch: Starting hostwatch with Python version 3.5.2
hostwatch: Traceback (most recent call last):
--->   File "sshuttle.server", line 144, in start_hostwatch
--->   File "sshuttle.hostwatch", line 272, in hw_main
--->   File "sshuttle.hostwatch", line 234, in check_host
--->   File "sshuttle.hostwatch", line 32, in _is_ip
--->   File "/usr/lib/python3.5/re.py", line 163, in match
--->     return _compile(pattern, flags).match(string)
---> TypeError: cannot use a string pattern on a bytes-like object
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "assembler.py", line 37, in <module>
  File "sshuttle.server", line 393, in main
  File "sshuttle.ssnet", line 596, in runonce
  File "sshuttle.server", line 324, in hostwatch_ready
sshuttle.helpers.Fatal: hostwatch process died
```

It seems like the list of hosts is not properly decoded on the server side. This is an attempt to fix that.
2017-11-11 10:06:37 +11:00
9b315746d1 Using exec in the assembler is okay 2017-11-09 12:02:31 +11:00
6a488b3db9 Initial configuration for Bandit and Prospector
With this configuration it should be feasible to achieve a perfect score
without contortion.

Rules skiped for Bandit:
B101: assert_used
B104: hardcoded_bind_all_interfaces
B404: import_subprocess
B603: subprocess_without_shell_equals_true
B606: start_process_with_no_shell
B607: start_process_with_partial_path

Rules skiped for pylint:
- too-many-statements
- too-many-locals
- too-many-function-args
- too-many-arguments
- too-many-branches
- bare-except
- protected-access
- no-else-return
2017-11-09 12:02:31 +11:00
112931dd2c Changes methods that do not reference the instance to static methods 2017-11-08 16:17:06 +11:00
ad676029c7 Fix no value passed for argument auto_hosts in hw_main call 2017-11-08 16:17:06 +11:00
47030e846b Remove trailing whitespaces 2017-11-08 16:17:06 +11:00
416636fa9b Mock socket bind to avoid depending on local IPs being available in test box 2017-11-07 10:08:16 +11:00
4300a02343 Remove unused variable 'timeout' 2017-11-07 10:08:16 +11:00
4e8c5411b5 Also register por for dns proxy and for pairs in use by other procs 2017-11-07 10:08:16 +11:00
6cdc4da1e4 Fixes UDP and DNS proxies binding to the same socket address
As suggested by @colinmkeith the UDP and DNS proxies should listen on different
ports otherwise the DNS proxy can get traffic intended to the UDP proxy (or
vice-versa) and handle it incorrectly as reported in #178.

At first sight it seems that we had the code in place to try another port if
the one we are binding is already bound, however, with UDP and REUSEADDR the
OS will not refuse to bind two sockets to the same socket address, so both
the UDP proxy and DNS proxy were being bound to the same pair.
2017-11-07 10:08:16 +11:00
8add00866c turn off debugging 2017-10-23 06:58:21 +11:00
94ea0a3bed nested if should be and 2017-10-23 06:58:21 +11:00
9b7ce2811e Use versions of python3 greater than 3.5 when available (e.g. 3.6)
Some Linux distros, like Alpine, Arch, etc and some BSDs, like FreeBSD, are
now shipping with python3.6 as the default python3. Both the client and the
server are failing to run in this distros, because we are specifically looking
for python3.5.

These changes make the run shell script use python3 if the version is greater
than 3.5, otherwise falling back as usual.

On the server any version of python3 will do, use it before falling back to
python, as the server code can run with any version of python3.
2017-10-23 06:58:21 +11:00
7726dea27c Test double restore (ipv4, ipv6) disables only once; test kldload 2017-10-21 12:10:31 +11:00
3635cc17ad Load pf kernel module when enabling pf
When the pf module is not loaded our calls to pfctl will fail with
unhelpful messages.
This change spares the user the pain of decrypting those messages and manually
enabling pf. It also keeps track if pf was loaded by sshuttle and unloads on
exit if that was the case.

Also fixed the case where both ipv4 and ipv6 anchors were added by sshuttle
but the first call of disable would disable pf before the second call had the
chance of cleaning it's anchor.
2017-10-21 12:10:31 +11:00
ae13316e83 Just skip empty lines of routes data instead of stopping processing 2017-10-19 13:45:34 +11:00
e173eb6016 Skip empty lines on incoming routes data
If we receive no routes from server or if, for some reason, we receive
some empty lines, we should skip them instead of crashing.

Fixes on of the problems in #147.
2017-10-19 13:45:34 +11:00
29cd75b6f7 Make hostwatch find both fqdn and hostname
Currently hostwatch only adds hostnames even when FQDNs are available.
This commit changes found_host so that when the name is a FQDN, both the FQDN
and an hostname are added, e.g., given api.foo.com both api and api.foo.com
will be added.

Fixes #151 if merged.

N.B.: I rarely use hostwatch, it would probably be a good idea to get feedback
from people who actually use it before merging. Not too sure about this...
2017-10-17 07:12:06 +11:00
4c50be0bc7 Use getaddrinfo to obtain a correct sockaddr
While with AF_INET sockaddr is a 2-tuple composed by (address, port),
with AF_INET6 it is a 4-tuple with (address, port, flow info, scope id).

We were always passing a 2-tuple to socket.connect which would fail whenever
the address was, for instance, a link-local IPv6 address that needs a scope id.

With this change we now use getaddrinfo to correctly compute the full tuple.

Fixes #156.
2017-10-15 12:43:04 +11:00
max
2fa0cd06fb Route traffic by linux user 2017-09-17 15:33:34 +10:00
4d8b758d32 Add homebrew instructions
Per https://github.com/apenwarr/sshuttle/pull/45/files
2017-08-03 13:55:04 +10:00
4e8c2b9c68 Avoid port forwarding from loopback address
When doing port forwarding on lo0 avoid the special case where the
traffic on lo0 did not came from sshuttle pass out rule but from the lo0
address itself. Fixes #159.
2017-07-29 17:15:32 +10:00
be559fc78b Fix case where there is no --dns. 2017-07-18 17:15:03 +10:00
d2e97a60f7 Add new option for overriding destination DNS server. 2017-07-18 17:15:03 +10:00
cdbb379910 Talk to custom DNS server on pod, instead of the ones in /etc/resolv.conf 2017-07-18 17:15:03 +10:00
b65bb29023 Update changelog for 0.78.3 2017-07-09 09:12:04 +10:00
c093b4bd96 Get version for sphinx from sshuttle.version 2017-07-09 09:08:48 +10:00
e76d1e14bd Fix error in requirements.rst 2017-07-09 09:08:48 +10:00
6c6a39fefa Pin version in requirements.txt 2017-07-09 09:08:48 +10:00
714bd9f81b Update setup.cfg 2017-07-09 09:08:48 +10:00
c746d6f7db Update and reformat changelog 2017-07-09 09:08:48 +10:00
f9361d7014 Order first by port range and only then by swidth
This change makes the subnets with the most specific port ranges come
before subnets with larger, least specific, port ranges. Before this
change subnets with smaller swidth would always come first and only for
subnets with the same width would the size of the port range be
considered.

Example:
188.0.0.0/8 -x 0.0.0.0/0:443
Before: 188.0.0.0/8 would come first meaning that all ports would be
routed through the VPN for the subnet 188.0.0.0/8
After: 0.0.0.0/0:443 comes first, meaning that port 443 will be
excluded for all subnets, including 188.0.0.0/8. All other ports of
188.0.0.0/8 will be routed.
2017-05-08 16:56:42 +10:00
c4a41ada09 Adds support for tunneling specific port ranges (#144)
* Adds support for tunneling specific port ranges

This set of changes implements the ability of specifying a port or port
range for an IP or subnet to only tunnel those ports for that subnet.
Also supports excluding a port or port range for a given IP or subnet.

When, for a given subnet, there are intercepting ranges being added and
excluded, the most specific, i.e., smaller range, takes precedence. In
case of a tie the exclusion wins.

For different subnets, the most specific, i.e., largest swidth, takes
precedence independent of any eventual port ranges.

Examples:
Tunnels all traffic to the 188.0.0.0/8 subnet except those to port 443.
```
sshuttle -r <server> 188.0.0.0/8 -x 188.0.0.0/8:443
```

Only tunnels traffic to port 80 of the 188.0.0.0/8 subnet.
```
sshuttle -r <server> 188.0.0.0/8:80
```

Tunnels traffic to the 188.0.0.0/8 subnet and the port range that goes
from 80 to 89.
```
sshuttle -r <server> 188.0.0.0/8:80-89 -x 188.0.0.0/8:80-90
```

* Allow subnets to be specified with domain names

Simplifies the implementation of address parsing by using
socket.getaddrinfo(), which can handle domain resolution, IPv4 and IPv6
addresses. This was proposed and mostly implemented by @DavidBuchanan314
in #146.

Signed-off-by: David Buchanan <DavidBuchanan314@users.noreply.github.com>
Signed-off-by: João Vieira <vieira@yubo.be>

* Also use getaddrinfo for parsing listen addr:port

* Fixes tests for tunneling a port range

* Updates documentation to include port/port range

Adds some examples with subnet:port and subnet:port-port.
Also clarifies the versions of Python supported on the server while
maintaining the recommendation for Python 2.7, 3.5 or later.
Mentions support for pfSense.

* In Py2 only named arguments may follow *expression

Fixes issue in Python 2.7 where *expression may only be followed by
named arguments.

* Use right regex to extract ip4/6, mask and ports

* Tests for parse_subnetport
2017-05-07 13:18:13 +10:00
ef83a5c573 Work around non tabular headers in BSD netstat
netstat outputs some headers in BSD (that the Linux version does not)
that are not tabular and were breaking our 'split line into columns
and get nth column' logic. We now skip such headers.

Should fix #141.
2017-04-05 13:11:08 +10:00
af9ebd0f4b Fix UDP and DNS support on Python 2.7 with tproxy method
There was runtime failure on UDP or DNS processing, because "socket" was redefined to PyXAPI's socket_ext in tproxy.py, but still was plain Python's socket in client.py
Fixed https://github.com/sshuttle/sshuttle/issues/134 for me
2017-02-21 16:42:18 +11:00
9a9015a75e Fixed tests after adding support for iproute2 2017-02-11 09:07:50 +11:00
d7d24f956b Small refactoring of netstat/iproute parsing 2017-02-11 09:07:50 +11:00
809fad537f Add support for iproute2
`netstat` has been deprecated for some time and some distros might
start shipping without it in the near future. This commit adds support
for `ip route` and uses it when available.
2017-02-11 09:07:50 +11:00
abce18cfc2 Allow remote hosts with colons in the username 2017-02-11 09:02:28 +11:00
5e90491344 Re-introduce ipfw support for sshuttle on FreeBSD with support for --DNS option as well
Sponsored-by: rsync.net
2017-01-28 11:36:26 +11:00
e8ceccc3d5 Add support for PfSense
PfSense is based on FreeBSD and its pf is pretty close to the one
FreeBSD ships, however some structures have different fields and two
offsets had to be fixed.
2017-01-15 19:08:53 +11:00
e39c4afce0 Set started_by_sshuttle False after disabling pf
We set it to true when we enable pf, but do not set it back to False
after disabling. When using IPv4 and IPv6 we end up trying to disable
twice which procudes an error while undoing changes in FreeBSD 11.
2017-01-09 10:07:38 +11:00
0e52cce9d1 Fix punctuation and explain Type=notify
Added missing full stops and explain that Type=notify is needed in the
systemd service unit.
2016-10-30 10:58:03 +11:00
6d5d0d766f Tests and documentation for systemd integration
Some tests and documentation for the systemd notification feature.
Also fixes some corner case issues detected while writing the tests.
2016-10-30 10:58:03 +11:00
08fb3be7a0 Move pytest-runner to tests_require
As it is only required to run the tests move pytest-runner from
setup_requires to tests_require as suggested by @jonathanunderwood
on #115.
2016-10-29 12:04:22 +11:00
fee5868196 Fix warning: closed channel got=STOP_SENDING 2016-10-28 08:25:21 +11:00
fbbcc05d58 Support sdnotify for better systemd integration
These changes introduce support for sdnotify allowing sshuttle to notify
systemd when it finishes connecting to the server and installing
firewall rules, and is ready to tunnel requests.
2016-10-24 17:54:33 +11:00
15b394da86 Fix #117 to allow for no subnets via file (-s)
This should fix an issue introduced in #117 where when no subnets are
given via file (-s file) the variable is None instead of an empty list
and the concatenation with the subnets given as positional parameters
fails.
2016-10-13 17:52:58 +11:00
0ed5ef9a97 Fix argument splitting for multi-word arguments
By just splitting at spaces, multi-word arguments are torn apart even if
quoted. In case of custom ssh-cmd, this makes it practically impossible
to set certian options through `ssh -o`.
shlex splits arguments like a shell and e.g. respects quotes.
2016-10-04 18:19:59 +11:00
c0c3612e6d Allow subnets to be given only by file (-s)
This should fix #116. Handling this while still having the positional
arguments and -s both write to the same list turned out to be more
complicated than it's worth so each writes to their own variable and we
merge them at the end.
2016-09-27 08:12:39 +10:00
0033efca11 Merge pull request #113 from RichiH/patch-1
requirements.rst: Fix mistakes
2016-09-05 07:32:38 +10:00
ae6e25302f requirements.rst: Fix mistakes 2016-09-04 18:54:12 +02:00
ffd95fb776 Fix typo, space not required here 2016-09-01 18:38:13 +10:00
acb5aa5386 Update installation instructions
Closes #111.
2016-09-01 18:37:39 +10:00
4801ae6627 Support using run from different directory 2016-08-30 19:03:46 +10:00
f57ad356b9 Ensure we update sshuttle/version.py in run 2016-08-30 18:52:26 +10:00
a441a03e57 Don't print python version in run 2016-08-30 18:52:06 +10:00
d2fdb6c029 Add CWD to PYTHONPATH in run 2016-08-30 18:51:19 +10:00
56 changed files with 2856 additions and 1071 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

3
.gitignore vendored
View File

@ -1,5 +1,4 @@
/sshuttle/version.py
/docs/conf.py
/tmp/
/.cache/
/.eggs/
@ -14,3 +13,5 @@
/.do_built
/.do_built.dir
/.redo
/.pytest_cache/
/.python-version

24
.prospector.yml Normal file
View File

@ -0,0 +1,24 @@
strictness: medium
pylint:
disable:
- too-many-statements
- too-many-locals
- too-many-function-args
- too-many-arguments
- too-many-branches
- bare-except
- protected-access
- no-else-return
- unused-argument
- method-hidden
- arguments-differ
- wrong-import-position
- raising-bad-type
pep8:
options:
max-line-length: 79
mccabe:
run: false

View File

@ -1,13 +0,0 @@
language: python
python:
- 2.6
- 2.7
- 3.4
- 3.5
- pypy
install:
- travis_retry pip install -q pytest mock
script:
- PYTHONPATH=. py.test

View File

@ -1,13 +1,235 @@
Release 0.78.1 (6th August, 2016)
=================================
==========
Change log
==========
All notable changes to this project will be documented in this file. The format
is based on `Keep a Changelog`_ and this project
adheres to `Semantic Versioning`_.
.. _`Keep a Changelog`: http://keepachangelog.com/
.. _`Semantic Versioning`: http://semver.org/
1.0.3 - 2020-08-24
------------------
Fixed
~~~~~
* Allow Mux() flush/fill to work with python < 3.5
* Fix parse_hostport to always return string for host.
* Require -r/--remote parameter.
* Add missing package in OpenWRT documentation.
* Fix doc about --listen option.
* README: add Ubuntu.
* Increase IP4 ttl to 63 hops instead of 42.
* Fix formatting in installation.rst
1.0.3 - 2020-07-12
------------------
Fixed
~~~~~
* Ask setuptools to require Python 3.5 and above.
* Add missing import.
* Fix formatting typos in usage docs
1.0.2 - 2020-06-18
------------------
Fixed
~~~~~
* Leave use of default port to ssh command.
* Remove unwanted references to Python 2.7 in docs.
* Replace usage of deprecated imp.
* Fix connection with @ sign in username.
1.0.1 - 2020-06-05
------------------
Fixed
~~~~~
* Errors in python long_documentation.
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
-------------------
Added
~~~~~
* doas support as replacmeent for sudo on OpenBSD.
* Added ChromeOS section to documentation (#262)
* Add --no-sudo-pythonpath option
Fixed
~~~~~
* Fix forwarding to a single port.
* Various updates to documentation.
* Don't crash if we can't look up peername
* Fix missing string formatting argument
* Moved sshuttle/tests into tests.
* Updated bandit config.
* Replace path /dev/null by os.devnull.
* Added coverage report to tests.
* Fixes support for OpenBSD (6.1+) (#282).
* Close stdin, stdout, and stderr when using syslog or forking to daemon (#283).
* Changes pf exclusion rules precedence.
* Fix deadlock with iptables with large ruleset.
* docs: document --ns-hosts --to-ns and update --dns.
* Use subprocess.check_output instead of run.
* Fix potential deadlock condition in nft_get_handle.
* auto-nets: retrieve routes only if using auto-nets.
0.78.4 - 2018-04-02
-------------------
Added
~~~~~
* Add homebrew instructions.
* Route traffic by linux user.
* Add nat-like method using nftables instead of iptables.
Changed
~~~~~~~
* Talk to custom DNS server on pod, instead of the ones in /etc/resolv.conf.
* Add new option for overriding destination DNS server.
* Changed subnet parsing. Previously 10/8 become 10.0.0.0/8. Now it gets
parsed as 0.0.0.10/8.
* Make hostwatch find both fqdn and hostname.
* Use versions of python3 greater than 3.5 when available (e.g. 3.6).
Removed
~~~~~~~
* Remove Python 2.6 from automatic tests.
Fixed
~~~~~
* Fix case where there is no --dns.
* [pf] Avoid port forwarding from loopback address.
* Use getaddrinfo to obtain a correct sockaddr.
* Skip empty lines on incoming routes data.
* Just skip empty lines of routes data instead of stopping processing.
* [pf] Load pf kernel module when enabling pf.
* [pf] Test double restore (ipv4, ipv6) disables only once; test kldload.
* Fixes UDP and DNS proxies binding to the same socket address.
* Mock socket bind to avoid depending on local IPs being available in test box.
* Fix no value passed for argument auto_hosts in hw_main call.
* Fixed incorrect license information in setup.py.
* Preserve peer and port properly.
* Make --to-dns and --ns-host work well together.
* Remove test that fails under OSX.
* Specify pip requirements for tests.
* Use flake8 to find Python syntax errors or undefined names.
* Fix compatibility with the sudoers file.
* Stop using SO_REUSEADDR on sockets.
* Declare 'verbosity' as global variable to placate linters.
* Adds 'cd sshuttle' after 'git' to README and docs.
* Documentation for loading options from configuration file.
* Load options from a file.
* Fix firewall.py.
* Move sdnotify after setting up firewall rules.
* Fix tests on Macos.
0.78.3 - 2017-07-09
-------------------
The "I should have done a git pull" first release.
Fixed
~~~~~
* Order first by port range and only then by swidth
0.78.2 - 2017-07-09
-------------------
Added
~~~~~
* Adds support for tunneling specific port ranges (#144).
* Add support for iproute2.
* Allow remote hosts with colons in the username.
* Re-introduce ipfw support for sshuttle on FreeBSD with support for --DNS option as well.
* Add support for PfSense.
* Tests and documentation for systemd integration.
* Allow subnets to be given only by file (-s).
Fixed
~~~~~
* Work around non tabular headers in BSD netstat.
* Fix UDP and DNS support on Python 2.7 with tproxy method.
* Fixed tests after adding support for iproute2.
* Small refactoring of netstat/iproute parsing.
* Set started_by_sshuttle False after disabling pf.
* Fix punctuation and explain Type=notify.
* Move pytest-runner to tests_require.
* Fix warning: closed channel got=STOP_SENDING.
* Support sdnotify for better systemd integration.
* Fix #117 to allow for no subnets via file (-s).
* Fix argument splitting for multi-word arguments.
* requirements.rst: Fix mistakes.
* Fix typo, space not required here.
* Update installation instructions.
* Support using run from different directory.
* Ensure we update sshuttle/version.py in run.
* Don't print python version in run.
* Add CWD to PYTHONPATH in run.
0.78.1 - 2016-08-06
-------------------
* Fix readthedocs versioning.
* Don't crash on ENETUNREACH.
* Various bug fixes.
* Improvements to BSD and OSX support.
Release 0.78.0 (Apr 8, 2016)
============================
0.78.0 - 2016-04-08
-------------------
* Don't force IPv6 if IPv6 nameservers supplied. Fixes #74.
* Call /bin/sh as users shell may not be POSIX compliant. Fixes #77.
@ -17,22 +239,22 @@ Release 0.78.0 (Apr 8, 2016)
* Make server parts work with old versions of Python. Fixes #81.
Release 0.77.2 (Mar 7, 2016)
============================
0.77.2 - 2016-03-07
-------------------
* Accidentally switched LGPL2 license with GPL2 license in 0.77.1 - now fixed.
Release 0.77.1 (Mar 7, 2016)
============================
0.77.1 - 2016-03-07
-------------------
* Use semantic versioning. http://semver.org/
* Update GPL 2 license text.
* New release to fix PyPI.
Release 0.77 (Mar 3, 2016)
==========================
0.77 - 2016-03-03
-----------------
* Various bug fixes.
* Fix Documentation.
@ -40,8 +262,8 @@ Release 0.77 (Mar 3, 2016)
* Add support for OpenBSD.
Release 0.76 (Jan 17, 2016)
===========================
0.76 - 2016-01-17
-----------------
* Add option to disable IPv6 support.
* Update documentation.
@ -49,14 +271,14 @@ Release 0.76 (Jan 17, 2016)
* Use setuptools-scm for automatic versioning.
Release 0.75 (Jan 12, 2016)
===========================
0.75 - 2016-01-12
-----------------
* Revert change that broke sshuttle entry point.
Release 0.74 (Jan 10, 2016)
===========================
0.74 - 2016-01-10
-----------------
* Add CHANGES.rst file.
* Numerous bug fixes.

View File

@ -6,7 +6,6 @@ include LICENSE
include run
include tox.ini
exclude sshuttle/version.py
exclude docs/conf.py
recursive-include docs *.bat
recursive-include docs *.py
recursive-include docs *.rst

View File

@ -23,25 +23,85 @@ common case:
- You can't use openssh's PermitTunnel feature because
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
------------------
- Ubuntu 16.04 or later::
apt-get install sshuttle
- Debian stretch or later::
apt-get install sshuttle
- Arch Linux::
pacman -S sshuttle
- Fedora::
dnf install sshuttle
- NixOS::
nix-env -iA nixos.sshuttle
- From PyPI::
pip install sshuttle
sudo pip install sshuttle
- Clone::
git clone https://github.com/sshuttle/sshuttle.git
cd sshuttle
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.
- From PyPI::
virtualenv -p python3 /tmp/sshuttle
. /tmp/sshuttle/bin/activate
pip install sshuttle
- Clone::
virtualenv -p python3 /tmp/sshuttle
. /tmp/sshuttle/bin/activate
git clone https://github.com/sshuttle/sshuttle.git
cd sshuttle
./setup.py install
- Homebrew::
brew install sshuttle
- Nix::
nix-env -iA nixpkgs.sshuttle
Documentation
-------------
The documentation for the stable version is available at:
http://sshuttle.readthedocs.org/
https://sshuttle.readthedocs.org/
The documentation for the latest development version is available at:
http://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

9
bandit.yml Normal file
View File

@ -0,0 +1,9 @@
exclude_dirs:
- tests
skips:
- B101
- B104
- B404
- B603
- B606
- B607

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

@ -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("sshuttle/tests/client")

View File

@ -1,4 +1 @@
Changelog
---------
.. include:: ../CHANGES.rst

12
docs/chromeos.rst Normal file
View File

@ -0,0 +1,12 @@
Google ChromeOS
===============
Currently there is no built in support for running sshuttle directly on
Google ChromeOS/Chromebooks.
What we can really do is to create a Linux VM with Crostini. In the default
stretch/Debian 9 VM, you can then install sshuttle as on any Linux box and
it just works, as do xterms and ssvncviewer etc.
https://www.reddit.com/r/Crostini/wiki/getstarted/crostini-setup-guide

View File

@ -13,9 +13,10 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
# import sys
# import os
from setuptools_scm import get_version
import sys
import os
sys.path.insert(0, os.path.abspath('..'))
import sshuttle.version # NOQA
# 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
@ -54,10 +55,10 @@ copyright = '2016, Brian May'
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = get_version(root="..")
# The full version, including alpha/beta/rc tags.
release = version
release = sshuttle.version.version
# The short X.Y version.
version = '.'.join(release.split('.')[:2])
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.

View File

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

View File

@ -11,7 +11,7 @@ Description
-----------
:program:`sshuttle` allows you to create a VPN connection from your
machine to any remote server that you can connect to via
ssh, as long as that server has python 2.3 or higher.
ssh, as long as that server has python 3.5 or higher.
To work, you must have root access on the local machine,
but you can have a normal account on the server.
@ -28,22 +28,29 @@ Options
-------
.. program:: sshuttle
.. option:: subnets
.. option:: <subnets>
A list of subnets to route over the VPN, in the form
``a.b.c.d[/width]``. Valid examples are 1.2.3.4 (a
``a.b.c.d[/width][port[-port]]``. Valid examples are 1.2.3.4 (a
single IP address), 1.2.3.4/32 (equivalent to 1.2.3.4),
1.2.3.0/24 (a 24-bit subnet, ie. with a 255.255.255.0
netmask), and 0/0 ('just route everything through the
VPN').
VPN'). Any of the previous examples are also valid if you append
a port or a port range, so 1.2.3.4:8000 will only tunnel traffic
that has as the destination port 8000 of 1.2.3.4 and
1.2.3.0/24:8000-9000 will tunnel traffic going to any port between
8000 and 9000 (inclusive) for all IPs in the 1.2.3.0/24 subnet.
It is also possible to use a name in which case the first IP it resolves
to during startup will be routed over the VPN. Valid examples are
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
guess the appropriate method depending on what it can find in PATH. The
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
proxy port. By default :program:`sshuttle` finds an available
@ -54,9 +61,12 @@ Options
connections from other machines on your network (ie. to
run :program:`sshuttle` on a router) try enabling IP Forwarding in
your kernel, then using ``--listen 0.0.0.0:0``.
You can use any name resolving to an IP address of the machine running
:program:`sshuttle`, e.g. ``--listen localhost``.
For the tproxy method this can be an IPv6 address. Use this option twice if
required, to provide both IPv4 and IPv6 addresses.
For the tproxy and pf methods this can be an IPv6 address. Use this option
with comma separated values if required, to provide both IPv4 and IPv6
addresses, e.g. ``--listen 127.0.0.1:0,[::1]:0``.
.. option:: -H, --auto-hosts
@ -85,7 +95,30 @@ Options
.. option:: --dns
Capture local DNS requests and forward to the remote DNS
server.
server. All queries to any of the local system's DNS
servers (/etc/resolv.conf) will be intercepted and
resolved on the remote side of the tunnel instead, there
using the DNS specified via the :option:`--to-ns` option,
if specified.
.. option:: --ns-hosts=<server1[,server2[,server3[...]]]>
Capture local DNS requests to the specified server(s)
and forward to the remote DNS server. Contrary to the
:option:`--dns` option, this flag allows to specify the
DNS server(s) the queries to which to intercept,
instead of intercepting all DNS traffic on the local
machine. This can be useful when only certain DNS
requests should be resolved on the remote side of the
tunnel, e.g. in combination with dnsmasq.
.. option:: --to-ns=<server>
The DNS to forward requests to when remote DNS
resolution is enabled. If not given, sshuttle will
simply resolve using the system configured resolver on
the remote side (via /etc/resolv.conf on the remote
side).
.. option:: --python
@ -93,14 +126,14 @@ Options
The default is just ``python``, which means to use the
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
port number to use for connecting to the remote server.
For example, example.com, testuser@example.com,
testuser@example.com:2222, or example.com:2244.
.. option:: -x, --exclude=subnet
.. option:: -x <subnet>, --exclude=<subnet>
Explicitly exclude this subnet from forwarding. The
format of this option is the same as the ``<subnets>``
@ -109,7 +142,7 @@ Options
``0/0 -x 1.2.3.0/24`` to forward everything except the
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
line. Useful when you have lots of subnets to exclude.
@ -157,6 +190,13 @@ Options
control feature, maximizing bandwidth usage. Use at
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
Automatically fork into the background after connecting
@ -168,7 +208,7 @@ Options
:manpage:`syslog(3)` service instead of stderr. This is
implicit if you use :option:`--daemon`.
.. option:: --pidfile=pidfilename
.. option:: --pidfile=<pidfilename>
when using :option:`--daemon`, save :program:`sshuttle`'s pid to
*pidfilename*. The default is ``sshuttle.pid`` in the
@ -176,7 +216,7 @@ Options
.. option:: --disable-ipv6
If using the tproxy method, this will disable IPv6 support.
If using tproxy or pf methods, this will disable IPv6 support.
.. option:: --firewall
@ -195,6 +235,56 @@ Options
makes it a lot easier to debug and test the :option:`--auto-hosts`
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
Print program version.
Configuration File
------------------
All the options described above can optionally be specified in a configuration
file.
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.
`@/etc/sshuttle.conf`.
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
defined in the file. If a given option is defined both in the file and in
the command line, the value in the command line will take precedence.
Arguments read from a file must be one per line, as shown below::
value
--option1
value1
--option2
value2
Examples
--------
@ -244,6 +334,24 @@ and subnet guessing::
c : Keyboard interrupt: exiting.
c : SW#6:192.168.42.121:60554: deleting
Run :program:`sshuttle` with a `/etc/sshuttle.conf` configuration file::
$ sshuttle @/etc/sshuttle.conf
Use the options defined in `/etc/sshuttle.conf` but be more verbose::
$ sshuttle @/etc/sshuttle.conf -vvv
Override the remote server defined in `/etc/sshuttle.conf`::
$ sshuttle @/etc/sshuttle.conf -r otheruser@test.example.com
Example configuration file::
192.168.0.0/16
--remote
user@example.com
Discussion
----------

8
docs/openwrt.rst Normal file
View File

@ -0,0 +1,8 @@
OpenWRT
========
Run::
opkg install python3 python3-pip iptables-mod-extra 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

@ -4,7 +4,7 @@ Overview
As far as I know, sshuttle is the only program that solves the following
common case:
- Your client machine (or router) is Linux, FreeBSD, or MacOS.
- Your client machine (or router) is Linux, MacOS, FreeBSD, OpenBSD or pfSense.
- You have access to a remote network via ssh.

View File

@ -6,5 +6,7 @@ Contents:
.. toctree::
:maxdepth: 2
chromeos
tproxy
windows
openwrt

View File

@ -6,7 +6,7 @@ Client side Requirements
- sudo, or root access on your client machine.
(The server doesn't need admin access.)
- Python 2.7 or Python 3.5.
- Python 3.5 or greater.
Linux with NAT method
@ -26,28 +26,23 @@ Linux with TPROXY method
Supports:
* IPv4 TCP
* IPv4 UDP (requires ``recmsg`` - see below)
* IPv6 DNS (requires ``recmsg`` - see below)
* IPv4 UDP (requires ``recvmsg`` - see below)
* IPv6 DNS (requires ``recvmsg`` - see below)
* IPv6 TCP
* IPv6 UDP (requires ``recmsg`` - see below)
* IPv6 DNS (requires ``recmsg`` - 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 is in Python 3.5 and
later. Under Python 2 you might find it sufficient installing PyXAPI_ to get
the ``recvmsg()`` function. See :doc:`tproxy` for more information.
* IPv6 UDP (requires ``recvmsg`` - see below)
* IPv6 DNS (requires ``recvmsg`` - see below)
MacOS / FreeBSD / OpenBSD
~~~~~~~~~~~~~~~~~~~~~~~~~
MacOS / FreeBSD / OpenBSD / pfSense
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Method: pf
Supports:
* IPv4 TCP
* IPv4 DNS
* IPv6 TCP
* IPv6 DNS
Requires:
@ -62,12 +57,28 @@ cmd.exe with Administrator access. See :doc:`windows` for more information.
Server side Requirements
------------------------
Server requirements are more relaxed, however it is recommended that you use
Python 2.7 or Python 3.5.
- Python 3.5 or greater.
Additional Suggested Software
-----------------------------
- You may want to use autossh, available in various package management
systems
- If you are using systemd, sshuttle can notify it when the connection to
the remote end is established and the firewall rules are installed. For
this feature to work you must configure the process start-up type for the
sshuttle service unit to notify, as shown in the example below.
.. code-block:: ini
:emphasize-lines: 6
[Unit]
Description=sshuttle
After=network.target
[Service]
Type=notify
ExecStart=/usr/bin/sshuttle --dns --remote <user>@<server> <subnets...>
[Install]
WantedBy=multi-user.target

View File

@ -20,6 +20,11 @@ Forward all traffic::
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
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
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

7
requirements-tests.txt Normal file
View File

@ -0,0 +1,7 @@
-r requirements.txt
attrs==20.1.0
pytest==6.0.1
pytest-cov==2.10.1
mock==2.0.0
flake8==3.8.3
pyflakes==2.2.0

View File

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

23
run
View File

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

View File

@ -1,2 +1,17 @@
[aliases]
test=pytest
[bdist_wheel]
universal = 1
[upload]
sign=true
identity=0x1784577F811F6EAC
[flake8]
count=true
show-source=true
statistics=true
[tool:pytest]
addopts = --cov=sshuttle --cov-branch --cov-report=term-missing

View File

@ -2,65 +2,72 @@
# Copyright 2012-2014 Brian May
#
# This file is part of python-tldap.
# This file is part of sshuttle.
#
# python-tldap is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# sshuttle is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 2.1 of
# the License, or (at your option) any later version.
#
# python-tldap is distributed in the hope that it will be useful,
# sshuttle is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with python-tldap If not, see <http://www.gnu.org/licenses/>.
# You should have received a copy of the GNU Lesser General Public License
# along with sshuttle; If not, see <http://www.gnu.org/licenses/>.
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):
from setuptools_scm.version import guess_next_dev_version
version = guess_next_dev_version(version)
return version.lstrip("v")
setup(
name="sshuttle",
use_scm_version={
'write_to': "sshuttle/version.py",
'version_scheme': version_scheme,
},
setup_requires=['setuptools_scm', 'pytest-runner'],
setup_requires=['setuptools_scm'],
# version=version,
url='https://github.com/sshuttle/sshuttle',
author='Brian May',
author_email='brian@linuxpenguins.xyz',
description='Full-featured" VPN over an SSH tunnel',
packages=find_packages(),
license="GPL2+",
license="LGPL2.1+",
long_description=open('README.rst').read(),
long_description_content_type="text/x-rst",
classifiers=[
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Intended Audience :: End Users/Desktop",
"License :: OSI Approved :: "
"GNU General Public License v2 or later (GPLv2+)",
"GNU Lesser General Public License v2 or later (LGPLv2+)",
"Operating System :: OS Independent",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Topic :: System :: Networking",
],
scripts=['bin/sudoers-add'],
entry_points={
'console_scripts': [
'sshuttle = sshuttle.cmdline:main',
],
},
tests_require=['pytest', 'mock'],
python_requires='>=3.5',
tests_require=[
'pytest',
'pytest-cov',
'pytest-runner',
'mock',
'flake8',
],
keywords="ssh vpn",
)

View File

@ -1,7 +1,8 @@
import sys
import zlib
import imp
import types
verbosity = verbosity # noqa: F821 must be a previously defined global
z = zlib.decompressobj()
while 1:
name = sys.stdin.readline().strip()
@ -14,14 +15,14 @@ while 1:
% (name, nbytes))
content = z.decompress(sys.stdin.read(nbytes))
module = imp.new_module(name)
module = types.ModuleType(name)
parents = name.rsplit(".", 1)
if len(parents) == 2:
parent, parent_name = parents
setattr(sys.modules[parent], parent_name, module)
code = compile(content, name, "exec")
exec(code, module.__dict__)
exec(code, module.__dict__) # nosec
sys.modules[name] = module
else:
break
@ -29,9 +30,12 @@ while 1:
sys.stderr.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
import sshuttle.cmdline_options as options
from sshuttle.server import main
main(options.latency_control, options.auto_hosts)
import sshuttle.cmdline_options as options # noqa: E402
from sshuttle.server import main # noqa: E402
main(options.latency_control, options.auto_hosts, options.to_nameserver,
options.auto_nets)

View File

@ -1,22 +1,41 @@
import socket
import errno
import re
import signal
import time
import subprocess as ssubprocess
import sshuttle.helpers as helpers
import os
import sys
import platform
import sshuttle.helpers as helpers
import sshuttle.ssnet as ssnet
import sshuttle.ssh as ssh
import sshuttle.ssyslog as ssyslog
import sys
import platform
import sshuttle.sdnotify as sdnotify
from sshuttle.ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
from sshuttle.helpers import log, debug1, debug2, debug3, Fatal, islocal, \
resolvconf_nameservers
from sshuttle.methods import get_method, Features
try:
from pwd import getpwnam
except ImportError:
getpwnam = None
_extra_fd = os.open('/dev/null', os.O_RDONLY)
try:
# try getting recvmsg from python
import socket as pythonsocket
getattr(pythonsocket.socket, "recvmsg")
socket = pythonsocket
except AttributeError:
# try getting recvmsg from socket_ext library
try:
import socket_ext
getattr(socket_ext.socket, "recvmsg")
socket = socket_ext
except ImportError:
import socket
_extra_fd = os.open(os.devnull, os.O_RDONLY)
def got_signal(signum, frame):
@ -76,7 +95,7 @@ def daemonize():
# be deleted.
signal.signal(signal.SIGTERM, got_signal)
si = open('/dev/null', 'r+')
si = open(os.devnull, 'r+')
os.dup2(si.fileno(), 0)
os.dup2(si.fileno(), 1)
si.close()
@ -94,8 +113,8 @@ def daemon_cleanup():
class MultiListener:
def __init__(self, type=socket.SOCK_STREAM, proto=0):
self.type = type
def __init__(self, kind=socket.SOCK_STREAM, proto=0):
self.type = kind
self.proto = proto
self.v6 = None
self.v4 = None
@ -143,13 +162,11 @@ class MultiListener:
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)
else:
self.v6 = None
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)
else:
self.v4 = None
@ -168,7 +185,7 @@ class MultiListener:
class FirewallClient:
def __init__(self, method_name):
def __init__(self, method_name, sudo_pythonpath):
self.auto_nets = []
python_path = os.path.dirname(os.path.dirname(__file__))
argvbase = ([sys.executable, sys.argv[0]] +
@ -177,11 +194,15 @@ class FirewallClient:
['--firewall'])
if ssyslog._p:
argvbase += ['--syslog']
argv_tries = [
['sudo', '-p', '[local sudo] Password: ',
('PYTHONPATH=%s' % python_path), '--'] + argvbase,
argvbase
]
# Default to sudo unless on OpenBSD in which case use built in `doas`
if platform.platform().startswith('OpenBSD'):
elev_prefix = ['doas']
else:
elev_prefix = ['sudo', '-p', '[local sudo] Password: ']
if sudo_pythonpath:
elev_prefix += ['/usr/bin/env',
'PYTHONPATH=%s' % python_path]
argv_tries = [elev_prefix + argvbase, argvbase]
# we can't use stdin/stdout=subprocess.PIPE here, as we normally would,
# because stupid Linux 'su' requires that stdin be attached to a tty.
@ -200,17 +221,13 @@ class FirewallClient:
if argv[0] == 'su':
sys.stderr.write('[local su] ')
self.p = ssubprocess.Popen(argv, stdout=s1, preexec_fn=setup)
# No env: Talking to `FirewallClient.start`, which has no i18n.
e = None
break
except OSError as e:
except OSError:
pass
self.argv = argv
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')
if e:
log('Spawning firewall manager: %r\n' % self.argv)
@ -224,7 +241,8 @@ class FirewallClient:
self.method.set_firewall(self)
def setup(self, subnets_include, subnets_exclude, nslist,
redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4, udp):
redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4, udp,
user):
self.subnets_include = subnets_include
self.subnets_exclude = subnets_exclude
self.nslist = nslist
@ -233,6 +251,7 @@ class FirewallClient:
self.dnsport_v6 = dnsport_v6
self.dnsport_v4 = dnsport_v4
self.udp = udp
self.user = user
def check(self):
rv = self.p.poll()
@ -241,12 +260,15 @@ class FirewallClient:
def start(self):
self.pfile.write(b'ROUTES\n')
for (family, ip, width) in self.subnets_include + self.auto_nets:
self.pfile.write(b'%d,%d,0,%s\n'
% (family, width, ip.encode("ASCII")))
for (family, ip, width) in self.subnets_exclude:
self.pfile.write(b'%d,%d,1,%s\n'
% (family, width, ip.encode("ASCII")))
for (family, ip, width, fport, lport) \
in self.subnets_include + self.auto_nets:
self.pfile.write(b'%d,%d,0,%s,%d,%d\n' % (family, width,
ip.encode("ASCII"),
fport, lport))
for (family, ip, width, fport, lport) in self.subnets_exclude:
self.pfile.write(b'%d,%d,1,%s,%d,%d\n' % (family, width,
ip.encode("ASCII"),
fport, lport))
self.pfile.write(b'NSLIST\n')
for (family, ip) in self.nslist:
@ -261,8 +283,14 @@ class FirewallClient:
udp = 0
if self.udp:
udp = 1
if self.user is None:
user = b'-'
elif isinstance(self.user, str):
user = bytes(self.user, 'utf-8')
else:
user = b'%d' % self.user
self.pfile.write(b'GO %d\n' % udp)
self.pfile.write(b'GO %d %s\n' % (udp, user))
self.pfile.flush()
line = self.pfile.readline()
@ -271,8 +299,8 @@ class FirewallClient:
raise Fatal('%r expected STARTED, got %r' % (self.argv, line))
def sethostip(self, hostname, ip):
assert(not re.search(b'[^-\w]', hostname))
assert(not re.search(b'[^0-9.]', ip))
assert(not re.search(rb'[^-\w\.]', hostname))
assert(not re.search(rb'[^0-9.]', ip))
self.pfile.write(b'HOST %s,%s\n' % (hostname, ip))
self.pfile.flush()
@ -323,7 +351,7 @@ def onaccept_tcp(listener, method, mux, handlers):
sock, srcip = listener.accept()
sock.close()
finally:
_extra_fd = os.open('/dev/null', os.O_RDONLY)
_extra_fd = os.open(os.devnull, os.O_RDONLY)
return
else:
raise
@ -362,7 +390,7 @@ def onaccept_udp(listener, method, mux, handlers):
srcip, dstip, data = t
debug1('Accept UDP: %r -> %r.\n' % (srcip, dstip,))
if srcip in udp_by_src:
chan, timeout = udp_by_src[srcip]
chan, _ = udp_by_src[srcip]
else:
chan = mux.next_channel()
mux.channels[chan] = lambda cmd, data: udp_done(
@ -400,7 +428,8 @@ def ondns(listener, method, mux, handlers):
def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
python, latency_control,
dns_listener, seed_hosts, auto_hosts, auto_nets, daemon):
dns_listener, seed_hosts, auto_hosts, auto_nets, daemon,
to_nameserver):
debug1('Starting client with Python version %s\n'
% platform.python_version())
@ -419,13 +448,15 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
ssh_cmd, remotename, python,
stderr=ssyslog._p and ssyslog._p.stdin,
options=dict(latency_control=latency_control,
auto_hosts=auto_hosts))
auto_hosts=auto_hosts,
to_nameserver=to_nameserver,
auto_nets=auto_nets))
except socket.error as e:
if e.args[0] == errno.EPIPE:
raise Fatal("failed to establish ssh session (1)")
else:
raise
mux = Mux(serversock, serversock)
mux = Mux(serversock.makefile("rb"), serversock.makefile("wb"))
handlers.append(mux)
expected = b'SSHUTTLE0001'
@ -460,6 +491,8 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
def onroutes(routestr):
if auto_nets:
for line in routestr.strip().split(b'\n'):
if not line:
continue
(family, ip, width) = line.split(b',', 2)
family = int(family)
width = int(width)
@ -470,7 +503,7 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
debug2("Ignored auto net %d/%s/%d\n" % (family, ip, width))
else:
debug2("Adding auto net %d/%s/%d\n" % (family, ip, width))
fw.auto_nets.append((family, ip, width))
fw.auto_nets.append((family, ip, width, 0, 0))
# we definitely want to do this *after* starting ssh, or we might end
# up intercepting the ssh connection!
@ -480,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
# ignore its contents.
mux.got_routes = None
fw.start()
serverready()
mux.got_routes = onroutes
def serverready():
fw.start()
sdnotify.send(sdnotify.ready(), sdnotify.status('Connected'))
def onhostlist(hostlist):
debug2('got host list: %r\n' % hostlist)
for line in hostlist.strip().split():
@ -516,7 +554,13 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
def main(listenip_v6, listenip_v4,
ssh_cmd, remotename, python, latency_control, dns, nslist,
method_name, seed_hosts, auto_hosts, auto_nets,
subnets_include, subnets_exclude, daemon, pidfile):
subnets_include, subnets_exclude, daemon, to_nameserver, pidfile,
user, sudo_pythonpath):
if not remotename:
# XXX: We can't make it required at the argparse level,
# because sshuttle calls out to itself in FirewallClient.
raise Fatal("You must specify -r/--remote.")
if daemon:
try:
@ -526,11 +570,16 @@ def main(listenip_v6, listenip_v4,
return 5
debug1('Starting sshuttle proxy.\n')
fw = FirewallClient(method_name)
fw = FirewallClient(method_name, sudo_pythonpath)
# Get family specific subnet lists
if dns:
nslist += resolvconf_nameservers()
if to_nameserver is not None:
to_nameserver = "%s@%s" % tuple(to_nameserver[1:])
else:
# option doesn't make sense if we aren't proxying dns
to_nameserver = None
subnets = subnets_include + subnets_exclude # we don't care here
subnets_v6 = [i for i in subnets if i[0] == socket.AF_INET6]
@ -548,10 +597,24 @@ def main(listenip_v6, listenip_v4,
else:
listenip_v6 = None
if user is not None:
if getpwnam is None:
raise Fatal("Routing by user not available on this system.")
try:
user = getpwnam(user).pw_uid
except KeyError:
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.ipv4 = len(subnets_v4) > 0 or listenip_v4 is not None
else:
required.ipv6 = None
required.ipv4 = None
required.udp = avail.udp
required.dns = len(nslist) > 0
required.user = False if user is None else True
# if IPv6 not supported, ignore IPv6 DNS servers
if not required.ipv6:
@ -567,6 +630,7 @@ def main(listenip_v6, listenip_v4,
debug1("IPv6 enabled: %r\n" % required.ipv6)
debug1("UDP enabled: %r\n" % required.udp)
debug1("DNS enabled: %r\n" % required.dns)
debug1("User enabled: %r\n" % required.user)
# bind to required ports
if listenip_v4 == "auto":
@ -574,11 +638,11 @@ def main(listenip_v6, listenip_v4,
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))
subnets_exclude.append((socket.AF_INET, listenip_v4[0], 32, 0, 0))
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))
subnets_exclude.append((socket.AF_INET6, listenip_v6[0], 128, 0, 0))
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
@ -586,6 +650,9 @@ def main(listenip_v6, listenip_v4,
else:
# if at least one port missing, we have to search
ports = range(12300, 9000, -1)
# keep track of failed bindings and used ports to avoid trying to
# bind to the same socket address twice in different listeners
used_ports = []
# search for free ports and try to bind
last_e = None
@ -627,10 +694,12 @@ def main(listenip_v6, listenip_v4,
if udp_listener:
udp_listener.bind(lv6, lv4)
bound = True
used_ports.append(port)
break
except socket.error as e:
if e.errno == errno.EADDRINUSE:
last_e = e
used_ports.append(port)
else:
raise e
@ -650,6 +719,9 @@ def main(listenip_v6, listenip_v4,
ports = range(12300, 9000, -1)
for port in ports:
debug2(' %d' % port)
if port in used_ports:
continue
dns_listener = MultiListener(socket.SOCK_DGRAM)
if listenip_v6:
@ -669,10 +741,12 @@ def main(listenip_v6, listenip_v4,
try:
dns_listener.bind(lv6, lv4)
bound = True
used_ports.append(port)
break
except socket.error as e:
if e.errno == errno.EADDRINUSE:
last_e = e
used_ports.append(port)
else:
raise e
debug2('\n')
@ -688,22 +762,22 @@ def main(listenip_v6, listenip_v4,
# Last minute sanity checks.
# These should never fail.
# If these do fail, something is broken above.
if len(subnets_v6) > 0:
if subnets_v6:
assert required.ipv6
if redirectport_v6 == 0:
raise Fatal("IPv6 subnets defined but not listening")
if len(nslist_v6) > 0:
if nslist_v6:
assert required.dns
assert required.ipv6
if dnsport_v6 == 0:
raise Fatal("IPv6 ns servers defined but not listening")
if len(subnets_v4) > 0:
if subnets_v4:
if redirectport_v4 == 0:
raise Fatal("IPv4 subnets defined but not listening")
if len(nslist_v4) > 0:
if nslist_v4:
if dnsport_v4 == 0:
raise Fatal("IPv4 ns servers defined but not listening")
@ -717,19 +791,21 @@ def main(listenip_v6, listenip_v4,
# start the firewall
fw.setup(subnets_include, subnets_exclude, nslist,
redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4,
required.udp)
required.udp, user)
# start the client process
try:
return _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
python, latency_control, dns_listener,
seed_hosts, auto_hosts, auto_nets, daemon)
seed_hosts, auto_hosts, auto_nets, daemon, to_nameserver)
finally:
try:
if daemon:
# it's not our child anymore; can't waitpid
fw.p.returncode = 0
fw.done()
sdnotify.send(sdnotify.stop())
finally:
if daemon:
daemon_cleanup()

View File

@ -1,32 +1,53 @@
import re
import socket
import platform
import sshuttle.helpers as helpers
import sshuttle.client as client
import sshuttle.firewall as firewall
import sshuttle.hostwatch as hostwatch
import sshuttle.ssyslog as ssyslog
from sshuttle.options import parser, parse_ipport6, parse_ipport4
from sshuttle.options import parser, parse_ipport
from sshuttle.helpers import family_ip_tuple, log, Fatal
from sshuttle.sudoers import sudoers
def main():
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:
opt.syslog = 1
if opt.wrap:
import sshuttle.ssnet as ssnet
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
try:
if opt.firewall:
if opt.subnets:
if opt.subnets or opt.subnets_file:
parser.error('exactly zero arguments expected')
return firewall.main(opt.method, opt.syslog)
elif opt.hostwatch:
return hostwatch.hw_main(opt.subnets)
return hostwatch.hw_main(opt.subnets, opt.auto_hosts)
else:
includes = opt.subnets
includes = opt.subnets + opt.subnets_file
excludes = opt.exclude
if not includes and not opt.auto_nets:
parser.error('at least one subnet, subnet file, '
@ -44,12 +65,13 @@ def main():
if opt.listen:
ipport_v6 = None
ipport_v4 = None
list = opt.listen.split(",")
for ip in list:
if '[' in ip and ']' in ip:
ipport_v6 = parse_ipport6(ip)
lst = opt.listen.split(",")
for ip in lst:
family, ip, port = parse_ipport(ip)
if family == socket.AF_INET6:
ipport_v6 = (ip, port)
else:
ipport_v4 = parse_ipport4(ip)
ipport_v4 = (ip, port)
else:
# parse_ipport4('127.0.0.1:0')
ipport_v4 = "auto"
@ -57,6 +79,8 @@ def main():
ipport_v6 = "auto" if not opt.disable_ipv6 else None
if opt.syslog:
ssyslog.start_syslog()
ssyslog.close_stdin()
ssyslog.stdout_to_syslog()
ssyslog.stderr_to_syslog()
return_code = client.main(ipport_v6, ipport_v4,
opt.ssh_cmd,
@ -71,12 +95,16 @@ def main():
opt.auto_nets,
includes,
excludes,
opt.daemon, opt.pidfile)
opt.daemon,
opt.to_ns,
opt.pidfile,
opt.user,
opt.sudo_pythonpath)
if return_code == 0:
log('Normal exit code, exiting...')
else:
log('Abnormal exit code detected, failing...' % return_code)
log('Abnormal exit code %d detected, failing...' % return_code)
return return_code
except Fatal as e:

View File

@ -1,11 +1,12 @@
import errno
import socket
import signal
import sshuttle.ssyslog as ssyslog
import sys
import os
import platform
import traceback
import sshuttle.ssyslog as ssyslog
from sshuttle.helpers import debug1, debug2, Fatal
from sshuttle.methods import get_auto_method, get_method
@ -74,6 +75,16 @@ def setup_daemon():
return sys.stdin, sys.stdout
# Note that we're sorting in a very particular order:
# we need to go from smaller, more specific, port ranges, to larger,
# less-specific, port ranges. At each level, we order by subnet
# width, from most-specific subnets (largest swidth) to
# least-specific. On ties, excludes come first.
# s:(inet, subnet width, exclude flag, subnet, first port, last port)
def subnet_weight(s):
return (-s[-1] + (s[-2] or -65535), s[1], s[2])
# This is some voodoo for setting up the kernel's transparent
# proxying stuff. If subnets is empty, we just delete our sshuttle rules;
# otherwise we delete it, then make them from scratch.
@ -119,10 +130,17 @@ def main(method_name, syslog):
elif line.startswith("NSLIST\n"):
break
try:
(family, width, exclude, ip) = line.strip().split(',', 3)
except:
(family, width, exclude, ip, fport, lport) = \
line.strip().split(',', 5)
except BaseException:
raise Fatal('firewall: expected route or NSLIST but got %r' % line)
subnets.append((int(family), int(width), bool(int(exclude)), ip))
subnets.append((
int(family),
int(width),
bool(int(exclude)),
ip,
int(fport),
int(lport)))
debug2('firewall manager: Got subnets: %r\n' % subnets)
nslist = []
@ -136,7 +154,7 @@ def main(method_name, syslog):
break
try:
(family, ip) = line.strip().split(',', 1)
except:
except BaseException:
raise Fatal('firewall: expected nslist or PORTS but got %r' % line)
nslist.append((int(family), ip))
debug2('firewall manager: Got partial nslist: %r\n' % nslist)
@ -147,7 +165,7 @@ def main(method_name, syslog):
_, _, ports = line.partition(" ")
ports = ports.split(",")
if len(ports) != 4:
raise Fatal('firewall: expected 4 ports but got %n' % len(ports))
raise Fatal('firewall: expected 4 ports but got %d' % len(ports))
port_v6 = int(ports[0])
port_v4 = int(ports[1])
dnsport_v6 = int(ports[2])
@ -171,9 +189,12 @@ def main(method_name, syslog):
elif not line.startswith("GO "):
raise Fatal('firewall: expected GO but got %r' % line)
_, _, udp = line.partition(" ")
_, _, args = line.partition(" ")
udp, user = args.strip().split(" ", 1)
udp = bool(int(udp))
debug2('firewall manager: Got udp: %r\n' % udp)
if user == '-':
user = None
debug2('firewall manager: Got udp: %r, user: %r\n' % (udp, user))
subnets_v6 = [i for i in subnets if i[0] == socket.AF_INET6]
nslist_v6 = [i for i in nslist if i[0] == socket.AF_INET6]
@ -183,17 +204,19 @@ def main(method_name, syslog):
try:
debug1('firewall manager: setting up.\n')
if len(subnets_v6) > 0 or len(nslist_v6) > 0:
if subnets_v6 or nslist_v6:
debug2('firewall manager: setting up IPv6.\n')
method.setup_firewall(
port_v6, dnsport_v6, nslist_v6,
socket.AF_INET6, subnets_v6, udp)
socket.AF_INET6, subnets_v6, udp,
user)
if len(subnets_v4) > 0 or len(nslist_v4) > 0:
if subnets_v4 or nslist_v4:
debug2('firewall manager: setting up IPv4.\n')
method.setup_firewall(
port_v4, dnsport_v4, nslist_v4,
socket.AF_INET, subnets_v4, udp)
socket.AF_INET, subnets_v4, udp,
user)
stdout.write('STARTED\n')
@ -222,43 +245,43 @@ def main(method_name, syslog):
finally:
try:
debug1('firewall manager: undoing changes.\n')
except:
pass
except BaseException:
debug2('An error occurred, ignoring it.')
try:
if len(subnets_v6) > 0 or len(nslist_v6) > 0:
if subnets_v6 or nslist_v6:
debug2('firewall manager: undoing IPv6 changes.\n')
method.restore_firewall(port_v6, socket.AF_INET6, udp)
except:
method.restore_firewall(port_v6, socket.AF_INET6, udp, user)
except BaseException:
try:
debug1("firewall manager: "
"Error trying to undo IPv6 firewall.\n")
for line in traceback.format_exc().splitlines():
debug1("---> %s\n" % line)
except:
pass
except BaseException:
debug2('An error occurred, ignoring it.')
try:
if len(subnets_v4) > 0 or len(nslist_v4) > 0:
if subnets_v4 or nslist_v4:
debug2('firewall manager: undoing IPv4 changes.\n')
method.restore_firewall(port_v4, socket.AF_INET, udp)
except:
method.restore_firewall(port_v4, socket.AF_INET, udp, user)
except BaseException:
try:
debug1("firewall manager: "
"Error trying to undo IPv4 firewall.\n")
for line in traceback.format_exc().splitlines():
debug1("firewall manager: ---> %s\n" % line)
except:
pass
except BaseException:
debug2('An error occurred, ignoring it.')
try:
debug2('firewall manager: undoing /etc/hosts changes.\n')
restore_etc_hosts(port_v6 or port_v4)
except:
except BaseException:
try:
debug1("firewall manager: "
"Error trying to undo /etc/hosts changes.\n")
for line in traceback.format_exc().splitlines():
debug1("firewall manager: ---> %s\n" % line)
except:
pass
except BaseException:
debug2('An error occurred, ignoring it.')

View File

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

View File

@ -21,7 +21,7 @@ _smb_ok = True
hostnames = {}
queue = {}
try:
null = open('/dev/null', 'wb')
null = open(os.devnull, 'wb')
except IOError:
_, e = sys.exc_info()[:2]
log('warning: %s\n' % e)
@ -44,7 +44,7 @@ def write_host_cache():
finally:
try:
os.unlink(tmpname)
except:
except BaseException:
pass
@ -61,23 +61,27 @@ def read_host_cache():
words = line.strip().split(',')
if len(words) == 2:
(name, ip) = words
name = re.sub(r'[^-\w]', '-', name).strip()
name = re.sub(r'[^-\w\.]', '-', name).strip()
ip = re.sub(r'[^0-9.]', '', ip).strip()
if name and ip:
found_host(name, ip)
def found_host(hostname, ip):
hostname = re.sub(r'\..*', '', hostname)
hostname = re.sub(r'[^-\w]', '_', hostname)
def found_host(name, ip):
hostname = re.sub(r'\..*', '', name)
hostname = re.sub(r'[^-\w\.]', '_', hostname)
if (ip.startswith('127.') or ip.startswith('255.') or
hostname == 'localhost'):
return
oldip = hostnames.get(hostname)
if hostname != name:
found_host(hostname, ip)
oldip = hostnames.get(name)
if oldip != ip:
hostnames[hostname] = ip
debug1('Found: %s: %s\n' % (hostname, ip))
sys.stdout.write('%s,%s\n' % (hostname, ip))
hostnames[name] = ip
debug1('Found: %s: %s\n' % (name, ip))
sys.stdout.write('%s,%s\n' % (name, ip))
write_host_cache()
@ -104,7 +108,7 @@ def _check_revdns(ip):
debug3('< %s\n' % r[0])
check_host(r[0])
found_host(r[0], ip)
except socket.herror:
except (socket.herror, UnicodeError):
pass
@ -115,15 +119,20 @@ def _check_dns(hostname):
debug3('< %s\n' % ip)
check_host(ip)
found_host(hostname, ip)
except socket.gaierror:
except (socket.gaierror, UnicodeError):
pass
def _check_netstat():
debug2(' > netstat\n')
env = {
'PATH': os.environ['PATH'],
'LC_ALL': "C",
}
argv = ['netstat', '-n']
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")
p.wait()
except OSError:
@ -141,10 +150,15 @@ def _check_smb(hostname):
global _smb_ok
if not _smb_ok:
return
argv = ['smbclient', '-U', '%', '-L', hostname]
debug2(' > smb: %s\n' % hostname)
env = {
'PATH': os.environ['PATH'],
'LC_ALL': "C",
}
argv = ['smbclient', '-U', '%', '-L', hostname]
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()
p.wait()
except OSError:
@ -199,10 +213,15 @@ def _check_nmb(hostname, is_workgroup, is_master):
global _nmb_ok
if not _nmb_ok:
return
argv = ['nmblookup'] + ['-M'] * 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:
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null)
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null,
env=env)
lines = p.stdout.readlines()
rv = p.wait()
except OSError:
@ -247,7 +266,7 @@ def _enqueue(op, *args):
def _stdin_still_ok(timeout):
r, w, x = select.select([sys.stdin.fileno()], [], [], timeout)
r, _, _ = select.select([sys.stdin.fileno()], [], [], timeout)
if r:
b = os.read(sys.stdin.fileno(), 4096)
if not b:

View File

@ -23,13 +23,13 @@ def ipt_chain_exists(family, table, name):
'PATH': os.environ['PATH'],
'LC_ALL': "C",
}
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=env)
for line in p.stdout:
if line.startswith(b'Chain %s ' % name.encode("ASCII")):
try:
output = ssubprocess.check_output(argv, env=env)
for line in output.decode('ASCII').split('\n'):
if line.startswith('Chain %s ' % name):
return True
rv = p.wait()
if rv:
raise Fatal('%r returned %d' % (argv, rv))
except ssubprocess.CalledProcessError as e:
raise Fatal('%r returned %d' % (argv, e.returncode))
def ipt(family, table, *args):
@ -49,6 +49,21 @@ def ipt(family, table, *args):
raise Fatal('%r returned %d' % (argv, rv))
def nft(family, table, action, *args):
if family in (socket.AF_INET, socket.AF_INET6):
argv = ['nft', action, 'inet', table] + list(args)
else:
raise Exception('Unsupported family "%s"' % family_to_string(family))
debug1('>> %s\n' % ' '.join(argv))
env = {
'PATH': os.environ['PATH'],
'LC_ALL': "C",
}
rv = ssubprocess.call(argv, env=env)
if rv:
raise Fatal('%r returned %d' % (argv, rv))
_no_ttl_module = False
@ -56,10 +71,10 @@ def ipt_ttl(family, *args):
global _no_ttl_module
if not _no_ttl_module:
# we avoid infinite loops by generating server-side connections
# with ttl 42. This makes the client side not recapture those
# with ttl 63. This makes the client side not recapture those
# connections, in case client == server.
try:
argsplus = list(args) + ['-m', 'ttl', '!', '--ttl', '42']
argsplus = list(args) + ['-m', 'ttl', '!', '--ttl', '63']
ipt(family, *argsplus)
except Fatal:
ipt(family, *args)

View File

@ -35,17 +35,21 @@ class BaseMethod(object):
def set_firewall(self, firewall):
self.firewall = firewall
def get_supported_features(self):
@staticmethod
def get_supported_features():
result = Features()
result.ipv6 = False
result.udp = False
result.dns = True
result.user = False
return result
def get_tcp_dstip(self, sock):
@staticmethod
def get_tcp_dstip(sock):
return original_dst(sock)
def recv_udp(self, udp_listener, bufsize):
@staticmethod
def recv_udp(udp_listener, bufsize):
debug3('Accept UDP using recvfrom.\n')
data, srcip = udp_listener.recvfrom(bufsize)
return (srcip, None, data)
@ -64,19 +68,21 @@ class BaseMethod(object):
def assert_features(self, features):
avail = self.get_supported_features()
for key in ["udp", "dns", "ipv6"]:
for key in ["udp", "dns", "ipv6", "user"]:
if getattr(features, key) and not getattr(avail, key):
raise Fatal(
"Feature %s not supported with method %s.\n" %
(key, self.name))
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp):
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
user):
raise NotImplementedError()
def restore_firewall(self, port, family, udp):
def restore_firewall(self, port, family, udp, user):
raise NotImplementedError()
def firewall_command(self, line):
@staticmethod
def firewall_command(line):
return False
@ -96,10 +102,14 @@ def get_method(method_name):
def get_auto_method():
if _program_exists('iptables'):
method_name = "nat"
elif _program_exists('nft'):
method_name = "nft"
elif _program_exists('pfctl'):
method_name = "pf"
elif _program_exists('ipfw'):
method_name = "ipfw"
else:
raise Fatal(
"can't find either iptables or pfctl; check your PATH")
"can't find either iptables, nft or pfctl; check your PATH")
return get_method(method_name)

263
sshuttle/methods/ipfw.py Normal file
View File

@ -0,0 +1,263 @@
import os
import subprocess as ssubprocess
from sshuttle.methods import BaseMethod
from sshuttle.helpers import log, debug1, debug3, \
Fatal, family_to_string
recvmsg = None
try:
# try getting recvmsg from python
import socket as pythonsocket
getattr(pythonsocket.socket, "recvmsg")
socket = pythonsocket
recvmsg = "python"
except AttributeError:
# try getting recvmsg from socket_ext library
try:
import socket_ext
getattr(socket_ext.socket, "recvmsg")
socket = socket_ext
recvmsg = "socket_ext"
except ImportError:
import socket
IP_BINDANY = 24
IP_RECVDSTADDR = 7
SOL_IPV6 = 41
IPV6_RECVDSTADDR = 74
if recvmsg == "python":
def recv_udp(listener, bufsize):
debug3('Accept UDP python using recvmsg.\n')
data, ancdata, _, srcip = listener.recvmsg(4096,
socket.CMSG_SPACE(4))
dstip = None
for cmsg_level, cmsg_type, cmsg_data in ancdata:
if cmsg_level == socket.SOL_IP and cmsg_type == IP_RECVDSTADDR:
port = 53
ip = socket.inet_ntop(socket.AF_INET, cmsg_data[0:4])
dstip = (ip, port)
break
return (srcip, dstip, data)
elif recvmsg == "socket_ext":
def recv_udp(listener, bufsize):
debug3('Accept UDP using socket_ext recvmsg.\n')
srcip, data, adata, _ = listener.recvmsg((bufsize,),
socket.CMSG_SPACE(4))
dstip = None
for a in adata:
if a.cmsg_level == socket.SOL_IP and a.cmsg_type == IP_RECVDSTADDR:
port = 53
ip = socket.inet_ntop(socket.AF_INET, a.cmsg_data[0:4])
dstip = (ip, port)
break
return (srcip, dstip, data[0])
else:
def recv_udp(listener, bufsize):
debug3('Accept UDP using recvfrom.\n')
data, srcip = listener.recvfrom(bufsize)
return (srcip, None, data)
def ipfw_rule_exists(n):
argv = ['ipfw', 'list']
env = {
'PATH': os.environ['PATH'],
'LC_ALL': "C",
}
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=env)
found = False
for line in p.stdout:
if line.startswith(b'%05d ' % n):
if not ('ipttl 63' in line or 'check-state' in line):
log('non-sshuttle ipfw rule: %r\n' % line.strip())
raise Fatal('non-sshuttle ipfw rule #%d already exists!' % n)
found = True
rv = p.wait()
if rv:
raise Fatal('%r returned %d' % (argv, rv))
return found
_oldctls = {}
def _fill_oldctls(prefix):
argv = ['sysctl', prefix]
env = {
'PATH': os.environ['PATH'],
'LC_ALL': "C",
}
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=env)
for line in p.stdout:
line = line.decode()
assert(line[-1] == '\n')
(k, v) = line[:-1].split(': ', 1)
_oldctls[k] = v.strip()
rv = p.wait()
if rv:
raise Fatal('%r returned %d' % (argv, rv))
if not line:
raise Fatal('%r returned no data' % (argv,))
def _sysctl_set(name, val):
argv = ['sysctl', '-w', '%s=%s' % (name, val)]
debug1('>> %s\n' % ' '.join(argv))
return ssubprocess.call(argv, stdout=open(os.devnull, 'w'))
# No env: No output. (Or error that won't be parsed.)
_changedctls = []
def sysctl_set(name, val, permanent=False):
PREFIX = 'net.inet.ip'
assert(name.startswith(PREFIX + '.'))
val = str(val)
if not _oldctls:
_fill_oldctls(PREFIX)
if not (name in _oldctls):
debug1('>> No such sysctl: %r\n' % name)
return False
oldval = _oldctls[name]
if val != oldval:
rv = _sysctl_set(name, val)
if rv == 0 and permanent:
debug1('>> ...saving permanently in /etc/sysctl.conf\n')
f = open('/etc/sysctl.conf', 'a')
f.write('\n'
'# Added by sshuttle\n'
'%s=%s\n' % (name, val))
f.close()
else:
_changedctls.append(name)
return True
def ipfw(*args):
argv = ['ipfw', '-q'] + list(args)
debug1('>> %s\n' % ' '.join(argv))
rv = ssubprocess.call(argv)
# No env: No output. (Or error that won't be parsed.)
if rv:
raise Fatal('%r returned %d' % (argv, rv))
def ipfw_noexit(*args):
argv = ['ipfw', '-q'] + list(args)
debug1('>> %s\n' % ' '.join(argv))
ssubprocess.call(argv)
# No env: No output. (Or error that won't be parsed.)
class Method(BaseMethod):
def get_supported_features(self):
result = super(Method, self).get_supported_features()
result.ipv6 = False
result.udp = False # NOTE: Almost there, kernel patch needed
result.dns = True
return result
def get_tcp_dstip(self, sock):
return sock.getsockname()
def recv_udp(self, udp_listener, bufsize):
srcip, dstip, data = recv_udp(udp_listener, bufsize)
if not dstip:
debug1(
"-- ignored UDP from %r: "
"couldn't determine destination IP address\n" % (srcip,))
return None
return srcip, dstip, data
def send_udp(self, sock, srcip, dstip, data):
if not srcip:
debug1(
"-- ignored UDP to %r: "
"couldn't determine source IP address\n" % (dstip,))
return
# debug3('Sending SRC: %r DST: %r\n' % (srcip, dstip))
sender = socket.socket(sock.family, socket.SOCK_DGRAM)
sender.setsockopt(socket.SOL_IP, IP_BINDANY, 1)
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
sender.setsockopt(socket.SOL_IP, socket.IP_TTL, 63)
sender.bind(srcip)
sender.sendto(data, dstip)
sender.close()
def setup_udp_listener(self, udp_listener):
if udp_listener.v4 is not None:
udp_listener.v4.setsockopt(socket.SOL_IP, IP_RECVDSTADDR, 1)
# if udp_listener.v6 is not None:
# udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVDSTADDR, 1)
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
user):
# IPv6 not supported
if family not in [socket.AF_INET]:
raise Exception(
'Address family "%s" unsupported by ipfw method_name'
% family_to_string(family))
# XXX: Any risk from this?
ipfw_noexit('delete', '1')
while _changedctls:
name = _changedctls.pop()
oldval = _oldctls[name]
_sysctl_set(name, oldval)
if subnets or dnsport:
sysctl_set('net.inet.ip.fw.enable', 1)
ipfw('add', '1', 'check-state', 'ip',
'from', 'any', 'to', 'any')
ipfw('add', '1', 'skipto', '2',
'tcp',
'from', 'any', 'to', 'table(125)')
ipfw('add', '1', 'fwd', '127.0.0.1,%d' % port,
'tcp',
'from', 'any', 'to', 'table(126)',
'not', 'ipttl', '63', 'keep-state', 'setup')
ipfw_noexit('table', '124', 'flush')
dnscount = 0
for _, ip in [i for i in nslist if i[0] == family]:
ipfw('table', '124', 'add', '%s' % (ip))
dnscount += 1
if dnscount > 0:
ipfw('add', '1', 'fwd', '127.0.0.1,%d' % dnsport,
'udp',
'from', 'any', 'to', 'table(124)',
'not', 'ipttl', '63')
ipfw('add', '1', 'allow',
'udp',
'from', 'any', 'to', 'any',
'ipttl', '63')
if subnets:
# create new subnet entries
for _, swidth, sexclude, snet in sorted(subnets,
key=lambda s: s[1],
reverse=True):
if sexclude:
ipfw('table', '125', 'add', '%s/%s' % (snet, swidth))
else:
ipfw('table', '126', 'add', '%s/%s' % (snet, swidth))
def restore_firewall(self, port, family, udp, user):
if family not in [socket.AF_INET]:
raise Exception(
'Address family "%s" unsupported by tproxy method'
% family_to_string(family))
ipfw_noexit('delete', '1')
ipfw_noexit('table', '124', 'flush')
ipfw_noexit('table', '125', 'flush')
ipfw_noexit('table', '126', 'flush')

View File

@ -1,4 +1,5 @@
import socket
from sshuttle.firewall import subnet_weight
from sshuttle.helpers import family_to_string
from sshuttle.linux import ipt, ipt_ttl, ipt_chain_exists, nonfatal
from sshuttle.methods import BaseMethod
@ -11,7 +12,8 @@ class Method(BaseMethod):
# the multiple copies shouldn't have overlapping subnets, or only the most-
# recently-started one will win (because we use "-I OUTPUT 1" instead of
# "-A OUTPUT").
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp):
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
user):
# only ipv4 supported with NAT
if family != socket.AF_INET:
raise Exception(
@ -28,41 +30,62 @@ class Method(BaseMethod):
def _ipt_ttl(*args):
return ipt_ttl(family, table, *args)
def _ipm(*args):
return ipt(family, "mangle", *args)
chain = 'sshuttle-%s' % port
# basic cleanup/setup of chains
self.restore_firewall(port, family, udp)
self.restore_firewall(port, family, udp, user)
_ipt('-N', chain)
_ipt('-F', chain)
_ipt('-I', 'OUTPUT', '1', '-j', chain)
_ipt('-I', 'PREROUTING', '1', '-j', chain)
if user is not None:
_ipm('-I', 'OUTPUT', '1', '-m', 'owner', '--uid-owner', str(user),
'-j', 'MARK', '--set-mark', str(port))
args = '-m', 'mark', '--mark', str(port), '-j', chain
else:
args = '-j', chain
_ipt('-I', 'OUTPUT', '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.
for _, swidth, sexclude, snet, fport, lport \
in sorted(subnets, key=subnet_weight, reverse=True):
tcp_ports = ('-p', 'tcp')
if fport:
tcp_ports = tcp_ports + ('--dport', '%d:%d' % (fport, lport))
# create new subnet entries. Note that we're sorting in a very
# particular order: we need to go from most-specific (largest
# swidth) to least-specific, and at any given level of specificity,
# we want excludes to come first. That's why the columns are in
# such a non- intuitive order.
for f, swidth, sexclude, snet \
in sorted(subnets, key=lambda s: s[1], reverse=True):
if sexclude:
_ipt('-A', chain, '-j', 'RETURN',
'--dest', '%s/%s' % (snet, swidth),
'-p', 'tcp')
*tcp_ports)
else:
_ipt_ttl('-A', chain, '-j', 'REDIRECT',
'--dest', '%s/%s' % (snet, swidth),
'-p', 'tcp',
'--to-ports', str(port))
*(tcp_ports + ('--to-ports', str(port))))
for f, ip in [i for i in nslist if i[0] == family]:
for _, ip in [i for i in nslist if i[0] == family]:
_ipt_ttl('-A', chain, '-j', 'REDIRECT',
'--dest', '%s/32' % ip,
'-p', 'udp',
'--dport', '53',
'--to-ports', str(dnsport))
def restore_firewall(self, port, family, udp):
def restore_firewall(self, port, family, udp, user):
# only ipv4 supported with NAT
if family != socket.AF_INET:
raise Exception(
@ -79,11 +102,25 @@ class Method(BaseMethod):
def _ipt_ttl(*args):
return ipt_ttl(family, table, *args)
def _ipm(*args):
return ipt(family, "mangle", *args)
chain = 'sshuttle-%s' % port
# basic cleanup/setup of chains
if ipt_chain_exists(family, table, chain):
nonfatal(_ipt, '-D', 'OUTPUT', '-j', chain)
nonfatal(_ipt, '-D', 'PREROUTING', '-j', chain)
if user is not None:
nonfatal(_ipm, '-D', 'OUTPUT', '-m', 'owner', '--uid-owner',
str(user), '-j', 'MARK', '--set-mark', str(port))
args = '-m', 'mark', '--mark', str(port), '-j', chain
else:
args = '-j', chain
nonfatal(_ipt, '-D', 'OUTPUT', *args)
nonfatal(_ipt, '-D', 'PREROUTING', *args)
nonfatal(_ipt, '-F', chain)
_ipt('-X', chain)
def get_supported_features(self):
result = super(Method, self).get_supported_features()
result.user = True
return result

76
sshuttle/methods/nft.py Normal file
View File

@ -0,0 +1,76 @@
import socket
from sshuttle.firewall import subnet_weight
from sshuttle.linux import nft, nonfatal
from sshuttle.methods import BaseMethod
class Method(BaseMethod):
# We name the chain based on the transproxy port number so that it's
# possible to run multiple copies of sshuttle at the same time. Of course,
# the multiple copies shouldn't have overlapping subnets, or only the most-
# recently-started one will win (because we use "-I OUTPUT 1" instead of
# "-A OUTPUT").
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
user):
if udp:
raise Exception("UDP not supported by nft")
table = 'sshuttle-%s' % port
def _nft(action, *args):
return nft(family, table, action, *args)
chain = table
# basic cleanup/setup of chains
_nft('add table', '')
_nft('add chain', 'prerouting',
'{ type nat hook prerouting priority -100; policy accept; }')
_nft('add chain', 'output',
'{ type nat hook output priority -100; policy accept; }')
_nft('add chain', chain)
_nft('flush chain', chain)
_nft('add rule', 'output jump %s' % chain)
_nft('add rule', 'prerouting jump %s' % chain)
# create new subnet entries.
for _, swidth, sexclude, snet, fport, lport \
in sorted(subnets, key=subnet_weight, reverse=True):
tcp_ports = ('ip', 'protocol', 'tcp')
if fport and fport != lport:
tcp_ports = \
tcp_ports + \
('tcp', 'dport', '{ %d-%d }' % (fport, lport))
elif fport and fport == lport:
tcp_ports = tcp_ports + ('tcp', 'dport', '%d' % (fport))
if sexclude:
_nft('add rule', chain, *(tcp_ports + (
'ip daddr %s/%s' % (snet, swidth), 'return')))
else:
_nft('add rule', chain, *(tcp_ports + (
'ip daddr %s/%s' % (snet, swidth), 'ip ttl != 63',
('redirect to :' + str(port)))))
for _, ip in [i for i in nslist if i[0] == family]:
if family == socket.AF_INET:
_nft('add rule', chain, 'ip protocol udp ip daddr %s' % ip,
'udp dport { 53 }', 'ip ttl != 63',
('redirect to :' + str(dnsport)))
elif family == socket.AF_INET6:
_nft('add rule', chain, 'ip6 protocol udp ip6 daddr %s' % ip,
'udp dport { 53 }', 'ip ttl != 63',
('redirect to :' + str(dnsport)))
def restore_firewall(self, port, family, udp, user):
if udp:
raise Exception("UDP not supported by nft method_name")
table = 'sshuttle-%s' % port
def _nft(action, *args):
return nft(family, table, action, *args)
# basic cleanup/setup of chains
nonfatal(_nft, 'delete table', '')

View File

@ -1,17 +1,25 @@
import os
import sys
import platform
import re
import socket
import errno
import struct
import subprocess as ssubprocess
import shlex
from fcntl import ioctl
from ctypes import c_char, c_uint8, c_uint16, c_uint32, Union, Structure, \
sizeof, addressof, memmove
from sshuttle.firewall import subnet_weight
from sshuttle.helpers import debug1, debug2, debug3, Fatal, family_to_string
from sshuttle.methods import BaseMethod
_pf_context = {'started_by_sshuttle': False, 'Xtoken': []}
_pf_context = {
'started_by_sshuttle': 0,
'loaded_by_sshuttle': True,
'Xtoken': []
}
_pf_fd = None
@ -57,12 +65,14 @@ class Generic(object):
def enable(self):
if b'INFO:\nStatus: Disabled' in self.status:
pfctl('-e')
_pf_context['started_by_sshuttle'] = True
_pf_context['started_by_sshuttle'] += 1
def disable(self, anchor):
@staticmethod
def disable(anchor):
pfctl('-a %s -F all' % anchor)
if _pf_context['started_by_sshuttle']:
if _pf_context['started_by_sshuttle'] == 1:
pfctl('-d')
_pf_context['started_by_sshuttle'] -= 1
def query_nat(self, family, proto, src_ip, src_port, dst_ip, dst_port):
[proto, family, src_port, dst_port] = [
@ -90,11 +100,13 @@ class Generic(object):
port = socket.ntohs(self._get_natlook_port(pnl.rdxport))
return (ip, port)
def _add_natlook_ports(self, pnl, src_port, dst_port):
@staticmethod
def _add_natlook_ports(pnl, src_port, dst_port):
pnl.sxport = socket.htons(src_port)
pnl.dxport = socket.htons(dst_port)
def _get_natlook_port(self, xport):
@staticmethod
def _get_natlook_port(xport):
return xport
def add_anchors(self, anchor, status=None):
@ -104,39 +116,44 @@ class Generic(object):
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):
def _add_anchor_rule(self, kind, 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
struct.pack('I', kind), 4) # rule.action = kind
memmove(addressof(pr) + self.ACTION_OFFSET, struct.pack(
'I', self.PF_CHANGE_GET_TICKET), 4) # action = PF_CHANGE_GET_TICKET
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
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):
@staticmethod
def _inet_version(family):
return b'inet' if family == socket.AF_INET else b'inet6'
def _lo_addr(self, family):
@staticmethod
def _lo_addr(family):
return b'127.0.0.1' if family == socket.AF_INET else b'::1'
def add_rules(self, anchor, rules):
@staticmethod
def add_rules(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):
@staticmethod
def has_skip_loopback():
return b'skip' in pfctl('-s Interfaces -i lo -v')[0]
class FreeBsd(Generic):
RULE_ACTION_OFFSET = 2968
@ -161,8 +178,19 @@ class FreeBsd(Generic):
freebsd.pfioc_natlook = pfioc_natlook
return freebsd
def __init__(self):
super(FreeBsd, self).__init__()
def enable(self):
returncode = ssubprocess.call(['kldload', 'pf'])
# No env: No output.
super(FreeBsd, self).enable()
if returncode == 0:
_pf_context['loaded_by_sshuttle'] = True
def disable(self, anchor):
super(FreeBsd, self).disable(anchor)
if _pf_context['loaded_by_sshuttle'] and \
_pf_context['started_by_sshuttle'] == 0:
ssubprocess.call(['kldunload', 'pf'])
# No env: No output.
def add_anchors(self, anchor):
status = pfctl('-s all')[0]
@ -170,32 +198,34 @@ class FreeBsd(Generic):
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()
def _add_anchor_rule(self, kind, name, pr=None):
pr = pr or 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)
super(FreeBsd, self)._add_anchor_rule(kind, 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)
]
tables = []
translating_rules = [
b'rdr pass on lo0 %s proto tcp to <forward_subnets> '
b'-> %s port %r' % (inet_version, lo_addr, port)
b'rdr pass on lo0 %s proto tcp from ! %s to %s '
b'-> %s port %r' % (inet_version, lo_addr, subnet, lo_addr, port)
for exclude, subnet in includes if not exclude
]
filtering_rules = [
b'pass out route-to lo0 %s proto tcp '
b'to <forward_subnets> keep state' % inet_version
b'to %s keep state' % (inet_version, subnet)
if not exclude else
b'pass out %s proto tcp to %s' % (inet_version, subnet)
for exclude, subnet in includes
]
if len(nslist) > 0:
if nslist:
tables.append(
b'table <dns_servers> {%s}' %
b','.join([ns[1].encode("ASCII") for ns in nslist]))
@ -235,7 +265,7 @@ class OpenBsd(Generic):
("proto_variant", c_uint8),
("direction", c_uint8)]
self.pfioc_rule = c_char * 3400
self.pfioc_rule = c_char * 3424
self.pfioc_natlook = pfioc_natlook
super(OpenBsd, self).__init__()
@ -251,19 +281,21 @@ class OpenBsd(Generic):
inet_version = self._inet_version(family)
lo_addr = self._lo_addr(family)
tables = [
b'table <forward_subnets> {%s}' % b','.join(includes)
]
tables = []
translating_rules = [
b'pass in on lo0 %s proto tcp to <forward_subnets> '
b'divert-to %s port %r' % (inet_version, lo_addr, port)
b'pass in on lo0 %s proto tcp to %s '
b'divert-to %s port %r' % (inet_version, subnet, lo_addr, port)
for exclude, subnet in includes if not exclude
]
filtering_rules = [
b'pass out %s proto tcp to <forward_subnets> '
b'route-to lo0 keep state' % inet_version
b'pass out %s proto tcp to %s '
b'route-to lo0 keep state' % (inet_version, subnet)
if not exclude else
b'pass out %s proto tcp to %s' % (inet_version, subnet)
for exclude, subnet in includes
]
if len(nslist) > 0:
if nslist:
tables.append(
b'table <dns_servers> {%s}' %
b','.join([ns[1].encode("ASCII") for ns in nslist]))
@ -333,16 +365,26 @@ class Darwin(FreeBsd):
return xport.port
class PfSense(FreeBsd):
RULE_ACTION_OFFSET = 3040
def __init__(self):
self.pfioc_rule = c_char * 3112
super(PfSense, self).__init__()
if sys.platform == 'darwin':
pf = Darwin()
elif sys.platform.startswith('openbsd'):
pf = OpenBsd()
elif platform.version().endswith('pfSense'):
pf = PfSense()
else:
pf = FreeBsd()
def pfctl(args, stdin=None):
argv = ['pfctl'] + list(args.split(" "))
argv = ['pfctl'] + shlex.split(args)
debug1('>> %s\n' % ' '.join(argv))
env = {
@ -382,7 +424,13 @@ class Method(BaseMethod):
def get_tcp_dstip(self, sock):
pfile = self.firewall.pfile
try:
peer = sock.getpeername()
except socket.error:
_, e = sys.exc_info()[:2]
if e.args[0] == errno.EINVAL:
return sock.getsockname()
proxy = sock.getsockname()
argv = (sock.family, socket.IPPROTO_TCP,
@ -399,11 +447,8 @@ class Method(BaseMethod):
return sock.getsockname()
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp):
tables = []
translating_rules = []
filtering_rules = []
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
user):
if family not in [socket.AF_INET, socket.AF_INET6]:
raise Exception(
'Address family "%s" unsupported by pf method_name'
@ -411,24 +456,24 @@ class Method(BaseMethod):
if udp:
raise Exception("UDP not supported by pf method_name")
if len(subnets) > 0:
if subnets:
includes = []
# If a given subnet is both included and excluded, list the
# exclusion first; the table will ignore the second, opposite
# definition
for f, swidth, sexclude, snet in sorted(
subnets, key=lambda s: (s[1], s[2]), reverse=True):
includes.append(b"%s%s/%d" %
(b"!" if sexclude else b"",
for _, swidth, sexclude, snet, fport, lport \
in sorted(subnets, key=subnet_weight):
includes.append((sexclude, b"%s/%d%s" % (
snet.encode("ASCII"),
swidth))
swidth,
b" port %d:%d" % (fport, lport) if fport else b"")))
anchor = pf_get_anchor(family, port)
pf.add_anchors(anchor)
pf.add_rules(anchor, includes, port, dnsport, nslist, family)
pf.enable()
def restore_firewall(self, port, family, udp):
def restore_firewall(self, port, family, udp, user):
if family not in [socket.AF_INET, socket.AF_INET6]:
raise Exception(
'Address family "%s" unsupported by pf method_name'

View File

@ -1,4 +1,5 @@
import struct
from sshuttle.firewall import subnet_weight
from sshuttle.helpers import family_to_string
from sshuttle.linux import ipt, ipt_ttl, ipt_chain_exists
from sshuttle.methods import BaseMethod
@ -32,7 +33,7 @@ IPV6_RECVORIGDSTADDR = IPV6_ORIGDSTADDR
if recvmsg == "python":
def recv_udp(listener, bufsize):
debug3('Accept UDP python using recvmsg.\n')
data, ancdata, msg_flags, srcip = listener.recvmsg(
data, ancdata, _, srcip = listener.recvmsg(
4096, socket.CMSG_SPACE(24))
dstip = None
family = None
@ -63,7 +64,7 @@ if recvmsg == "python":
elif recvmsg == "socket_ext":
def recv_udp(listener, bufsize):
debug3('Accept UDP using socket_ext recvmsg.\n')
srcip, data, adata, flags = listener.recvmsg(
srcip, data, adata, _ = listener.recvmsg(
(bufsize,), socket.CMSG_SPACE(24))
dstip = None
family = None
@ -149,7 +150,8 @@ class Method(BaseMethod):
if udp_listener.v6 is not None:
udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1)
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp):
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
user):
if family not in [socket.AF_INET, socket.AF_INET6]:
raise Exception(
'Address family "%s" unsupported by tproxy method'
@ -163,12 +165,16 @@ class Method(BaseMethod):
def _ipt_ttl(*args):
return ipt_ttl(family, table, *args)
def _ipt_proto_ports(proto, fport, lport):
return proto + ('--dport', '%d:%d' % (fport, lport)) \
if fport else proto
mark_chain = 'sshuttle-m-%s' % port
tproxy_chain = 'sshuttle-t-%s' % port
divert_chain = 'sshuttle-d-%s' % port
# basic cleanup/setup of chains
self.restore_firewall(port, family, udp)
self.restore_firewall(port, family, udp, user)
_ipt('-N', mark_chain)
_ipt('-F', mark_chain)
@ -187,7 +193,7 @@ class Method(BaseMethod):
_ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain,
'-m', 'udp', '-p', 'udp')
for f, ip in [i for i in nslist if i[0] == family]:
for _, ip in [i for i in nslist if i[0] == family]:
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
'--dest', '%s/32' % ip,
'-m', 'udp', '-p', 'udp', '--dport', '53')
@ -197,44 +203,56 @@ class Method(BaseMethod):
'-m', 'udp', '-p', 'udp', '--dport', '53',
'--on-port', str(dnsport))
for f, swidth, sexclude, snet \
in sorted(subnets, key=lambda s: s[1], reverse=True):
for _, swidth, sexclude, snet, fport, lport \
in sorted(subnets, key=subnet_weight, reverse=True):
tcp_ports = ('-p', 'tcp')
tcp_ports = _ipt_proto_ports(tcp_ports, fport, lport)
if sexclude:
_ipt('-A', mark_chain, '-j', 'RETURN',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'tcp', '-p', 'tcp')
'-m', 'tcp',
*tcp_ports)
_ipt('-A', tproxy_chain, '-j', 'RETURN',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'tcp', '-p', 'tcp')
'-m', 'tcp',
*tcp_ports)
else:
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'tcp', '-p', 'tcp')
'-m', 'tcp',
*tcp_ports)
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'tcp', '-p', 'tcp',
'--on-port', str(port))
'-m', 'tcp',
*(tcp_ports + ('--on-port', str(port))))
if udp:
udp_ports = ('-p', 'udp')
udp_ports = _ipt_proto_ports(udp_ports, fport, lport)
if sexclude:
_ipt('-A', mark_chain, '-j', 'RETURN',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'udp', '-p', 'udp')
'-m', 'udp',
*udp_ports)
_ipt('-A', tproxy_chain, '-j', 'RETURN',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'udp', '-p', 'udp')
'-m', 'udp',
*udp_ports)
else:
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'udp', '-p', 'udp')
'-m', 'udp',
*udp_ports)
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'udp', '-p', 'udp',
'--on-port', str(port))
'-m', 'udp',
*(udp_ports + ('--on-port', str(port))))
def restore_firewall(self, port, family, udp):
def restore_firewall(self, port, family, udp, user):
if family not in [socket.AF_INET, socket.AF_INET6]:
raise Exception(
'Address family "%s" unsupported by tproxy method'

View File

@ -1,44 +1,12 @@
import re
import socket
from argparse import ArgumentParser, Action, ArgumentTypeError as Fatal
from sshuttle import __version__
# 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):
def parse_subnetport_file(s):
try:
handle = open(s, 'r')
except OSError:
@ -46,57 +14,76 @@ def parse_subnet_file(s):
raw_config_lines = handle.readlines()
subnets = []
for line_no, line in enumerate(raw_config_lines):
for _, line in enumerate(raw_config_lines):
line = line.strip()
if len(line) == 0:
if not line:
continue
if line[0] == '#':
continue
subnets.append(parse_subnet(line))
subnets.append(parse_subnetport(line))
return subnets
# 1.2.3.4/5 or just 1.2.3.4
# 1:2::3/64 or just 1:2::3
def parse_subnet(subnet_str):
if ':' in subnet_str:
return parse_subnet6(subnet_str)
# 1.2.3.4/5:678, 1.2.3.4:567, 1.2.3.4/16 or just 1.2.3.4
# [1:2::3/64]:456, [1:2::3]:456, 1:2::3/64 or just 1:2::3
# example.com:123 or just example.com
def parse_subnetport(s):
if s.count(':') > 1:
rx = r'(?:\[?([\w\:]+)(?:/(\d+))?]?)(?::(\d+)(?:-(\d+))?)?$'
else:
return parse_subnet4(subnet_str)
rx = r'([\w\.\-]+)(?:/(\d+))?(?::(\d+)(?:-(\d+))?)?$'
m = re.match(rx, s)
if not m:
raise Fatal('%r is not a valid address/mask:port format' % s)
addr, width, fport, lport = m.groups()
try:
addrinfo = socket.getaddrinfo(addr, 0, 0, socket.SOCK_STREAM)
except socket.gaierror:
raise Fatal('Unable to resolve address: %s' % addr)
family, _, _, _, addr = min(addrinfo)
max_width = 32 if family == socket.AF_INET else 128
width = int(width or max_width)
if not 0 <= width <= max_width:
raise Fatal('width %d is not between 0 and %d' % (width, max_width))
return (family, addr[0], width, int(fport or 0), int(lport or fport or 0))
# 1.2.3.4:567 or just 1.2.3.4 or just 567
def parse_ipport4(s):
# [1:2::3]:456 or [1:2::3] or just [::]:567
# example.com:123 or just example.com
def parse_ipport(s):
s = str(s)
m = re.match(r'(?:(\d+)\.(\d+)\.(\d+)\.(\d+))?(?::)?(?:(\d+))?$', s)
if s.isdigit():
rx = r'()(\d+)$'
elif ']' in s:
rx = r'(?:\[([^]]+)])(?::(\d+))?$'
else:
rx = r'([\w\.\-]+)(?::(\d+))?$'
m = re.match(rx, 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)
ip, port = m.groups()
ip = ip or '0.0.0.0'
port = int(port or 0)
try:
addrinfo = socket.getaddrinfo(ip, port, 0, socket.SOCK_STREAM)
except socket.gaierror:
raise Fatal('%r is not a valid IP:port format' % s)
family, _, _, _, addr = min(addrinfo)
return (family,) + addr[:2]
# [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 []
def parse_list(lst):
return re.split(r'[\s,]+', lst.strip()) if lst else []
class Concat(Action):
@ -106,19 +93,20 @@ class Concat(Action):
super(Concat, self).__init__(option_strings, dest, **kwargs)
def __call__(self, parser, namespace, values, option_string=None):
curr_value = getattr(namespace, self.dest, [])
curr_value = getattr(namespace, self.dest, None) or []
setattr(namespace, self.dest, curr_value + values)
parser = ArgumentParser(
prog="sshuttle",
usage="%(prog)s [-l [ip:]port] [-r [user@]sshserver[:port]] <subnets...>"
usage="%(prog)s [-l [ip:]port] [-r [user@]sshserver[:port]] <subnets...>",
fromfile_prefix_chars="@"
)
parser.add_argument(
"subnets",
metavar="IP/MASK [IP/MASK...]",
metavar="IP/MASK[:PORT[-PORT]]...",
nargs="*",
type=parse_subnet,
type=parse_subnetport,
help="""
capture and forward traffic to these subnets (whitespace separated)
"""
@ -134,7 +122,8 @@ parser.add_argument(
"-H", "--auto-hosts",
action="store_true",
help="""
continuously scan for remote hostnames and update local /etc/hosts as they are found
continuously scan for remote hostnames and update local /etc/hosts as
they are found
"""
)
parser.add_argument(
@ -160,9 +149,19 @@ parser.add_argument(
capture and forward DNS requests made to the following servers
"""
)
parser.add_argument(
"--to-ns",
metavar="IP[:PORT]",
type=parse_ipport,
help="""
the DNS server to forward requests to; defaults to servers in
/etc/resolv.conf on remote side if not given.
"""
)
parser.add_argument(
"--method",
choices=["auto", "nat", "tproxy", "pf"],
choices=["auto", "nat", "nft", "tproxy", "pf", "ipfw"],
metavar="TYPE",
default="auto",
help="""
@ -178,17 +177,17 @@ parser.add_argument(
)
parser.add_argument(
"-r", "--remote",
metavar="[USERNAME@]ADDR[:PORT]",
metavar="[USERNAME[:PASSWORD]@]ADDR[:PORT]",
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(
"-x", "--exclude",
metavar="IP/MASK",
metavar="IP/MASK[:PORT[-PORT]]",
action="append",
default=[],
type=parse_subnet,
type=parse_subnetport,
help="""
exclude this subnet (can be used more than once)
"""
@ -198,7 +197,7 @@ parser.add_argument(
metavar="PATH",
action=Concat,
dest="exclude",
type=parse_subnet_file,
type=parse_subnetport_file,
help="""
exclude the subnets in a file (whitespace separated)
"""
@ -232,7 +231,8 @@ parser.add_argument(
metavar="HOSTNAME[,HOSTNAME]",
default=[],
help="""
comma-separated list of hostnames for initial scan (may be used with or without --auto-hosts)
comma-separated list of hostnames for initial scan (may be used with
or without --auto-hosts)
"""
)
parser.add_argument(
@ -243,6 +243,16 @@ parser.add_argument(
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(
"--wrap",
metavar="NUM",
@ -269,8 +279,9 @@ parser.add_argument(
"-s", "--subnets",
metavar="PATH",
action=Concat,
dest="subnets",
type=parse_subnet_file,
dest="subnets_file",
default=[],
type=parse_subnetport_file,
help="""
file where the subnets are stored, instead of on the command line
"""
@ -290,6 +301,12 @@ parser.add_argument(
pidfile name (only if using --daemon) [%(default)s]
"""
)
parser.add_argument(
"--user",
help="""
apply all the rules only to this linux user
"""
)
parser.add_argument(
"--firewall",
action="store_true",
@ -304,3 +321,42 @@ parser.add_argument(
(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(
"--no-sudo-pythonpath",
action="store_false",
dest="sudo_pythonpath",
help="""
do not set PYTHONPATH when invoking sudo
"""
)

46
sshuttle/sdnotify.py Normal file
View File

@ -0,0 +1,46 @@
import socket
import os
from sshuttle.helpers import debug1
def _notify(message):
addr = os.environ.get("NOTIFY_SOCKET", None)
if not addr or len(addr) == 1 or addr[0] not in ('/', '@'):
return False
addr = '\0' + addr[1:] if addr[0] == '@' else addr
try:
sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
except (OSError, IOError) as e:
debug1("Error creating socket to notify systemd: %s\n" % e)
return False
if not message:
return False
assert isinstance(message, bytes)
try:
return (sock.sendto(message, addr) > 0)
except (OSError, IOError) as e:
debug1("Error notifying systemd: %s\n" % e)
return False
def send(*messages):
return _notify(b'\n'.join(messages))
def ready():
return b"READY=1"
def stop():
return b"STOPPING=1"
def status(message):
return b"STATUS=%s" % message.encode('utf8')

View File

@ -6,6 +6,7 @@ import time
import sys
import os
import platform
from shutil import which
import sshuttle.ssnet as ssnet
import sshuttle.helpers as helpers
@ -20,7 +21,7 @@ def _ipmatch(ipstr):
# FIXME: IPv4 only
if ipstr == 'default':
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:
g = m.groups()
ips = g[0]
@ -60,9 +61,27 @@ def _shl(n, bits):
return n * int(2 ** bits)
def _list_routes():
def _route_netstat(line):
cols = line.split(None)
if len(cols) < 3:
return None, None
ipw = _ipmatch(cols[0])
maskw = _ipmatch(cols[2]) # linux only
mask = _maskbits(maskw) # returns 32 if maskw is null
return ipw, mask
def _route_iproute(line):
ipm = line.split(None, 1)[0]
if '/' not in ipm:
return None, None
ip, mask = ipm.split('/')
ipw = _ipmatch(ip)
return ipw, int(mask)
def _list_routes(argv, extract_route):
# FIXME: IPv4 only
argv = ['netstat', '-rn']
env = {
'PATH': os.environ['PATH'],
'LC_ALL': "C",
@ -70,12 +89,11 @@ def _list_routes():
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=env)
routes = []
for line in p.stdout:
cols = re.split(r'\s+', line.decode("ASCII"))
ipw = _ipmatch(cols[0])
if not line.strip():
continue
ipw, mask = extract_route(line.decode("ASCII"))
if not ipw:
continue # some lines won't be parseable; never mind
maskw = _ipmatch(cols[2]) # linux only
mask = _maskbits(maskw) # returns 32 if maskw is null
continue
width = min(ipw[1], mask)
ip = ipw[0] & _shl(_shl(1, width) - 1, 32 - width)
routes.append(
@ -84,11 +102,20 @@ def _list_routes():
if rv != 0:
log('WARNING: %r returned %d\n' % (argv, rv))
log('WARNING: That prevents --auto-nets from working.\n')
return routes
def list_routes():
for (family, ip, width) in _list_routes():
if which('ip'):
routes = _list_routes(['ip', 'route'], _route_iproute)
elif which('netstat'):
routes = _list_routes(['netstat', '-rn'], _route_netstat)
else:
log('WARNING: Neither ip nor netstat were found on the server.\n')
routes = []
for (family, ip, width) in routes:
if not ip.startswith('0.') and not ip.startswith('127.'):
yield (family, ip, width)
@ -129,7 +156,7 @@ class Hostwatch:
class DnsProxy(Handler):
def __init__(self, mux, chan, request):
def __init__(self, mux, chan, request, to_nameserver):
Handler.__init__(self, [])
self.timeout = time.time() + 30
self.mux = mux
@ -137,22 +164,43 @@ class DnsProxy(Handler):
self.tries = 0
self.request = request
self.peers = {}
self.to_ns_peer = None
self.to_ns_port = None
if to_nameserver is None:
self.to_nameserver = None
else:
self.to_ns_peer, self.to_ns_port = to_nameserver.split("@")
self.to_nameserver = self._addrinfo(self.to_ns_peer,
self.to_ns_port)
self.try_send()
@staticmethod
def _addrinfo(peer, port):
if int(port) == 0:
port = 53
family, _, _, _, sockaddr = socket.getaddrinfo(peer, port)[0]
return (family, sockaddr)
def try_send(self):
if self.tries >= 3:
return
self.tries += 1
family, peer = resolvconf_random_nameserver()
if self.to_nameserver is None:
_, peer = resolvconf_random_nameserver()
port = 53
else:
peer = self.to_ns_peer
port = int(self.to_ns_port)
family, sockaddr = self._addrinfo(peer, port)
sock = socket.socket(family, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
sock.connect((peer, 53))
sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 63)
sock.connect(sockaddr)
self.peers[sock] = peer
debug2('DNS: sending to %r (try %d)\n' % (peer, self.tries))
debug2('DNS: sending to %r:%d (try %d)\n' % (peer, port, self.tries))
try:
sock.send(self.request)
self.socks.append(sock)
@ -204,7 +252,7 @@ class UdpProxy(Handler):
self.chan = chan
self.sock = sock
if family == socket.AF_INET:
self.sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
self.sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 63)
def send(self, dstip, data):
debug2('UDP: sending to %r port %d\n' % dstip)
@ -227,7 +275,7 @@ class UdpProxy(Handler):
self.mux.send(self.chan, ssnet.CMD_UDP_DATA, hdr + data)
def main(latency_control, auto_hosts):
def main(latency_control, auto_hosts, to_nameserver, auto_nets):
debug1('Starting server with Python version %s\n'
% platform.python_version())
@ -237,21 +285,23 @@ def main(latency_control, auto_hosts):
helpers.logprefix = 'server: '
debug1('latency control setting = %r\n' % latency_control)
routes = list(list_routes())
debug1('available routes:\n')
for r in routes:
debug1(' %d/%s/%d\n' % r)
# synchronization header
sys.stdout.write('\0\0SSHUTTLE0001')
sys.stdout.flush()
handlers = []
mux = Mux(socket.fromfd(sys.stdin.fileno(),
socket.AF_INET, socket.SOCK_STREAM),
socket.fromfd(sys.stdout.fileno(),
socket.AF_INET, socket.SOCK_STREAM))
mux = Mux(sys.stdin, sys.stdout)
handlers.append(mux)
debug1('auto-nets:' + str(auto_nets) + '\n')
if auto_nets:
routes = list(list_routes())
debug1('available routes:\n')
for r in routes:
debug1(' %d/%s/%d\n' % r)
else:
routes = []
routepkt = ''
for r in routes:
routepkt += '%d,%s,%d\n' % r
@ -278,7 +328,7 @@ def main(latency_control, auto_hosts):
def got_host_req(data):
if not hw.pid:
(hw.pid, hw.sock) = start_hostwatch(
data.strip().split(), auto_hosts)
data.decode("ASCII").strip().split(), auto_hosts)
handlers.append(Handler(socks=[hw.sock],
callback=hostwatch_ready))
mux.got_host_req = got_host_req
@ -301,7 +351,7 @@ def main(latency_control, auto_hosts):
def dns_req(channel, data):
debug2('Incoming DNS request channel=%d.\n' % channel)
h = DnsProxy(mux, channel, data)
h = DnsProxy(mux, channel, data, to_nameserver)
handlers.append(h)
dnshandlers[channel] = h
mux.got_dns_req = dns_req

View File

@ -3,88 +3,94 @@ import os
import re
import socket
import zlib
import imp
import importlib
import importlib.util
import subprocess as ssubprocess
import shlex
from shlex import quote
import ipaddress
from urllib.parse import urlparse
import sshuttle.helpers as helpers
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):
tokens = name.split(".")
f = None
token = tokens[0]
token_name = [token]
token_str = ".".join(token_name)
try:
f, pathname, description = imp.find_module(token_str)
for token in tokens[1:]:
module = imp.load_module(token_str, f, pathname, description)
if f is not None:
f.close()
token_name.append(token)
token_str = ".".join(token_name)
f, pathname, description = imp.find_module(
token, module.__path__)
if f is not None:
contents = f.read()
else:
contents = ""
finally:
if f is not None:
f.close()
return contents.encode("UTF8")
def get_module_source(name):
spec = importlib.util.find_spec(name)
with open(spec.origin, "rt") as f:
return f.read().encode("utf-8")
def empackage(z, name, data=None):
if not data:
data = readfile(name)
data = get_module_source(name)
content = z.compress(data)
content += z.flush(zlib.Z_SYNC_FLUSH)
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)
"""
# leave use of default port to ssh command to prevent overwriting
# ports configured in ~/.ssh/config when no port is given
port = None
username = None
password = None
host = rhostport
if "@" in host:
# split username (and possible password) from the host[:port]
username, host = host.rsplit("@", 1)
# Fix #410 bad username error detect
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 = str(ipaddress.ip_address(host))
except ValueError:
# if that fails parse as URL to get the port
parsed = urlparse('//{}'.format(host))
try:
host = str(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):
portl = []
if (rhostport or '').count(':') > 1:
if rhostport.count(']') or rhostport.count('['):
result = rhostport.split(']')
rhost = result[0].strip('[')
if len(result) > 1:
result[1] = result[1].strip(':')
if result[1] is not '':
portl = ['-p', str(int(result[1]))]
# can't disambiguate IPv6 colons and a port number. pass the hostname
# through.
username, password, port, host = parse_hostport(rhostport)
if username:
rhost = "{}@{}".format(username, host)
else:
rhost = rhostport
else: # IPv4
l = (rhostport or '').split(':', 1)
rhost = l[0]
if len(l) > 1:
portl = ['-p', str(int(l[1]))]
if rhost == '-':
rhost = None
rhost = host
z = zlib.compressobj(1)
content = readfile('sshuttle.assembler')
content = get_module_source('sshuttle.assembler')
optdata = ''.join("%s=%r\n" % (k, v) for (k, v) in list(options.items()))
optdata = optdata.encode("UTF8")
content2 = (empackage(z, 'sshuttle') +
@ -109,15 +115,27 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
argv = [sys.executable, '-c', pyscript]
else:
if ssh_cmd:
sshl = ssh_cmd.split(' ')
sshl = shlex.split(ssh_cmd)
else:
sshl = ['ssh']
if port is not None:
portl = ["-p", str(port)]
else:
portl = []
if python:
pycmd = "'%s' -c '%s'" % (python, pyscript)
else:
pycmd = ("P=python3.5; $P -V 2>/dev/null || P=python; "
"exec \"$P\" -c %s") % quote(pyscript)
pycmd = ("exec /bin/sh -c %s" % quote(pycmd))
pycmd = ("P=python3; $P -V 2>%s || P=python; "
"exec \"$P\" -c %s") % (os.devnull, quote(pyscript))
pycmd = ("/bin/sh -c {}".format(quote(pycmd)))
if password is not None:
os.environ['SSHPASS'] = str(password)
argv = (["sshpass", "-e"] + sshl +
portl +
[rhost, '--', pycmd])
else:
argv = (sshl +
portl +
[rhost, '--', pycmd])

View File

@ -4,19 +4,19 @@ import socket
import errno
import select
import os
from sshuttle.helpers import b, binary_type, log, debug1, debug2, debug3, Fatal
import fcntl
from sshuttle.helpers import b, log, debug1, debug2, debug3, Fatal
MAX_CHANNEL = 65535
LATENCY_BUFFER_SIZE = 32768
# these don't exist in the socket module in python 2.3!
SHUT_RD = 0
SHUT_WR = 1
SHUT_RDWR = 2
HDR_LEN = 8
CMD_EXIT = 0x4200
CMD_PING = 0x4201
CMD_PONG = 0x4202
@ -55,17 +55,18 @@ cmd_to_name = {
NET_ERRS = [errno.ECONNREFUSED, errno.ETIMEDOUT,
errno.EHOSTUNREACH, errno.ENETUNREACH,
errno.EHOSTDOWN, errno.ENETDOWN,
errno.ENETUNREACH]
errno.ENETUNREACH, errno.ECONNABORTED,
errno.ECONNRESET]
def _add(l, elem):
if elem not in l:
l.append(elem)
def _add(socks, elem):
if elem not in socks:
socks.append(elem)
def _fds(l):
def _fds(socks):
out = []
for i in l:
for i in socks:
try:
out.append(i.fileno())
except AttributeError:
@ -93,8 +94,12 @@ def _try_peername(sock):
return '%s:%s' % (pn[0], pn[1])
except socket.error:
_, 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
except AttributeError:
pass
return 'unknown'
@ -200,7 +205,8 @@ class SockWrapper:
_, e = sys.exc_info()[:2]
self.seterr('nowrite: %s' % e)
def too_full(self):
@staticmethod
def too_full():
return False # fullness is determined by the socket's select() state
def uwrite(self, buf):
@ -270,7 +276,7 @@ class Handler:
def callback(self, sock):
log('--no callback defined-- %r\n' % self)
(r, w, x) = select.select(self.socks, [], [], 0)
(r, _, _) = select.select(self.socks, [], [], 0)
for s in r:
v = s.recv(4096)
if not v:
@ -331,10 +337,10 @@ class Proxy(Handler):
class Mux(Handler):
def __init__(self, rsock, wsock):
Handler.__init__(self, [rsock, wsock])
self.rsock = rsock
self.wsock = wsock
def __init__(self, rfile, wfile):
Handler.__init__(self, [rfile, wfile])
self.rfile = rfile
self.wfile = wfile
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_host_req = self.got_host_list = None
@ -349,7 +355,7 @@ class Mux(Handler):
def next_channel(self):
# channel 0 is special, so we never allocate it
for timeout in range(1024):
for _ in range(1024):
self.chani += 1
if self.chani > MAX_CHANNEL:
self.chani = 1
@ -363,7 +369,7 @@ class Mux(Handler):
return total
def check_fullness(self):
if self.fullness > 32768:
if self.fullness > LATENCY_BUFFER_SIZE:
if not self.too_full:
self.send(0, CMD_PING, b('rttest'))
self.too_full = True
@ -374,7 +380,7 @@ class Mux(Handler):
# log('outbuf: %d %r\n' % (self.amount_queued(), ob))
def send(self, channel, cmd, data):
assert isinstance(data, binary_type)
assert isinstance(data, bytes)
assert len(data) <= 65535
p = struct.pack('!ccHHH', b('S'), b('S'), channel, cmd, len(data)) \
+ data
@ -431,9 +437,15 @@ class Mux(Handler):
callback(cmd, data)
def flush(self):
self.wsock.setblocking(False)
try:
os.set_blocking(self.wfile.fileno(), False)
except AttributeError:
# python < 3.5
flags = fcntl.fcntl(self.wfile.fileno(), fcntl.F_GETFL)
flags |= os.O_NONBLOCK
flags = fcntl.fcntl(self.wfile.fileno(), fcntl.F_SETFL, flags)
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])))
if wrote:
self.outbuf[0] = self.outbuf[0][wrote:]
@ -441,9 +453,15 @@ class Mux(Handler):
self.outbuf[0:1] = []
def fill(self):
self.rsock.setblocking(False)
try:
read = _nb_clean(os.read, self.rsock.fileno(), 32768)
os.set_blocking(self.rfile.fileno(), False)
except AttributeError:
# python < 3.5
flags = fcntl.fcntl(self.rfile.fileno(), fcntl.F_GETFL)
flags |= os.O_NONBLOCK
flags = fcntl.fcntl(self.rfile.fileno(), fcntl.F_SETFL, flags)
try:
read = _nb_clean(os.read, self.rfile.fileno(), LATENCY_BUFFER_SIZE)
except OSError:
_, e = sys.exc_info()[:2]
raise Fatal('other end: %r' % e)
@ -473,22 +491,22 @@ class Mux(Handler):
break
def pre_select(self, r, w, x):
_add(r, self.rsock)
_add(r, self.rfile)
if self.outbuf:
_add(w, self.wsock)
_add(w, self.wfile)
def callback(self, sock):
(r, w, x) = select.select([self.rsock], [self.wsock], [], 0)
if self.rsock in r:
(r, w, _) = select.select([self.rfile], [self.wfile], [], 0)
if self.rfile in r:
self.handle()
if self.outbuf and self.wsock in w:
if self.outbuf and self.wfile in w:
self.flush()
class MuxWrapper(SockWrapper):
def __init__(self, mux, channel):
SockWrapper.__init__(self, mux.rsock, mux.wsock)
SockWrapper.__init__(self, mux.rfile, mux.wfile)
self.mux = mux
self.channel = channel
self.mux.channels[channel] = self.got_packet
@ -503,17 +521,25 @@ class MuxWrapper(SockWrapper):
return 'SW%r:Mux#%d' % (self.peername, self.channel)
def noread(self):
if not self.shut_read:
self.mux.send(self.channel, CMD_TCP_STOP_SENDING, b(''))
self.setnoread()
def setnoread(self):
if not self.shut_read:
debug2('%r: done reading\n' % self)
self.shut_read = True
self.mux.send(self.channel, CMD_TCP_STOP_SENDING, b(''))
self.maybe_close()
def nowrite(self):
if not self.shut_write:
self.mux.send(self.channel, CMD_TCP_EOF, b(''))
self.setnowrite()
def setnowrite(self):
if not self.shut_write:
debug2('%r: done writing\n' % self)
self.shut_write = True
self.mux.send(self.channel, CMD_TCP_EOF, b(''))
self.maybe_close()
def maybe_close(self):
@ -542,9 +568,11 @@ class MuxWrapper(SockWrapper):
def got_packet(self, cmd, data):
if cmd == CMD_TCP_EOF:
self.noread()
# Remote side already knows the status - set flag but don't notify
self.setnoread()
elif cmd == CMD_TCP_STOP_SENDING:
self.nowrite()
# Remote side already knows the status - set flag but don't notify
self.setnowrite()
elif cmd == CMD_TCP_DATA:
self.buf.append(data)
else:
@ -555,7 +583,7 @@ class MuxWrapper(SockWrapper):
def connect_dst(family, ip, port):
debug2('Connecting to %s:%d\n' % (ip, port))
outsock = socket.socket(family)
outsock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
outsock.setsockopt(socket.SOL_IP, socket.IP_TTL, 63)
return SockWrapper(outsock, outsock,
connect_to=(ip, port),
peername='%s:%d' % (ip, port))

View File

@ -8,12 +8,24 @@ _p = None
def start_syslog():
global _p
_p = ssubprocess.Popen(['logger',
'-p', 'daemon.notice',
'-t', 'sshuttle'], stdin=ssubprocess.PIPE)
with open(os.devnull, 'w') as devnull:
_p = ssubprocess.Popen(
['logger', '-p', 'daemon.notice', '-t', 'sshuttle'],
stdin=ssubprocess.PIPE,
stdout=devnull,
stderr=devnull
)
def close_stdin():
sys.stdin.close()
def stdout_to_syslog():
sys.stdout.flush()
os.dup2(_p.stdin.fileno(), sys.stdout.fileno())
def stderr_to_syslog():
sys.stdout.flush()
sys.stderr.flush()
os.dup2(_p.stdin.fileno(), 2)
os.dup2(_p.stdin.fileno(), sys.stderr.fileno())

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,283 +0,0 @@
from mock import Mock, patch, call
from sshuttle.methods import get_method
@patch("sshuttle.methods.tproxy.recvmsg")
def test_get_supported_features_recvmsg(mock_recvmsg):
method = get_method('tproxy')
features = method.get_supported_features()
assert features.ipv6
assert features.udp
assert features.dns
@patch("sshuttle.methods.tproxy.recvmsg", None)
def test_get_supported_features_norecvmsg():
method = get_method('tproxy')
features = method.get_supported_features()
assert features.ipv6
assert not features.udp
assert not features.dns
def test_get_tcp_dstip():
sock = Mock()
sock.getsockname.return_value = ('127.0.0.1', 1024)
method = get_method('tproxy')
assert method.get_tcp_dstip(sock) == ('127.0.0.1', 1024)
assert sock.mock_calls == [call.getsockname()]
@patch("sshuttle.methods.tproxy.recv_udp")
def test_recv_udp(mock_recv_udp):
mock_recv_udp.return_value = ("127.0.0.1", "127.0.0.2", "11111")
sock = Mock()
method = get_method('tproxy')
result = method.recv_udp(sock, 1024)
assert sock.mock_calls == []
assert mock_recv_udp.mock_calls == [call(sock, 1024)]
assert result == ("127.0.0.1", "127.0.0.2", "11111")
@patch("sshuttle.methods.socket.socket")
def test_send_udp(mock_socket):
sock = Mock()
method = get_method('tproxy')
method.send_udp(sock, "127.0.0.2", "127.0.0.1", "2222222")
assert sock.mock_calls == []
assert mock_socket.mock_calls == [
call(sock.family, 2),
call().setsockopt(1, 2, 1),
call().setsockopt(0, 19, 1),
call().bind('127.0.0.2'),
call().sendto("2222222", '127.0.0.1'),
call().close()
]
def test_setup_tcp_listener():
listener = Mock()
method = get_method('tproxy')
method.setup_tcp_listener(listener)
assert listener.mock_calls == [
call.setsockopt(0, 19, 1)
]
def test_setup_udp_listener():
listener = Mock()
method = get_method('tproxy')
method.setup_udp_listener(listener)
assert listener.mock_calls == [
call.setsockopt(0, 19, 1),
call.v4.setsockopt(0, 20, 1),
call.v6.setsockopt(41, 74, 1)
]
def test_assert_features():
method = get_method('tproxy')
features = method.get_supported_features()
method.assert_features(features)
def test_firewall_command():
method = get_method('tproxy')
assert not method.firewall_command("somthing")
@patch('sshuttle.methods.tproxy.ipt')
@patch('sshuttle.methods.tproxy.ipt_ttl')
@patch('sshuttle.methods.tproxy.ipt_chain_exists')
def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
mock_ipt_chain_exists.return_value = True
method = get_method('tproxy')
assert method.name == 'tproxy'
# IPV6
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')],
True)
assert mock_ipt_chain_exists.mock_calls == [
call(10, 'mangle', 'sshuttle-m-1024'),
call(10, 'mangle', 'sshuttle-t-1024'),
call(10, 'mangle', 'sshuttle-d-1024')
]
assert mock_ipt_ttl.mock_calls == []
assert mock_ipt.mock_calls == [
call(10, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1024'),
call(10, 'mangle', '-F', 'sshuttle-m-1024'),
call(10, 'mangle', '-X', 'sshuttle-m-1024'),
call(10, 'mangle', '-D', 'PREROUTING', '-j', 'sshuttle-t-1024'),
call(10, 'mangle', '-F', 'sshuttle-t-1024'),
call(10, 'mangle', '-X', 'sshuttle-t-1024'),
call(10, 'mangle', '-F', 'sshuttle-d-1024'),
call(10, 'mangle', '-X', 'sshuttle-d-1024'),
call(10, 'mangle', '-N', 'sshuttle-m-1024'),
call(10, 'mangle', '-F', 'sshuttle-m-1024'),
call(10, 'mangle', '-N', 'sshuttle-d-1024'),
call(10, 'mangle', '-F', 'sshuttle-d-1024'),
call(10, 'mangle', '-N', 'sshuttle-t-1024'),
call(10, 'mangle', '-F', 'sshuttle-t-1024'),
call(10, 'mangle', '-I', 'OUTPUT', '1', '-j', 'sshuttle-m-1024'),
call(10, 'mangle', '-I', 'PREROUTING', '1', '-j', 'sshuttle-t-1024'),
call(10, 'mangle', '-A', 'sshuttle-d-1024', '-j', 'MARK',
'--set-mark', '1'),
call(10, 'mangle', '-A', 'sshuttle-d-1024', '-j', 'ACCEPT'),
call(10, 'mangle', '-A', 'sshuttle-t-1024', '-m', 'socket',
'-j', 'sshuttle-d-1024', '-m', 'tcp', '-p', 'tcp'),
call(10, 'mangle', '-A', 'sshuttle-t-1024', '-m', 'socket',
'-j', 'sshuttle-d-1024', '-m', 'udp', '-p', 'udp'),
call(10, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
'--set-mark', '1', '--dest', u'2404:6800:4004:80c::33/32',
'-m', 'udp', '-p', 'udp', '--dport', '53'),
call(10, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1',
'--dest', u'2404:6800:4004:80c::33/32',
'-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1026'),
call(10, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'RETURN',
'--dest', u'2404:6800:4004:80c::101f/128',
'-m', 'tcp', '-p', 'tcp'),
call(10, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'RETURN',
'--dest', u'2404:6800:4004:80c::101f/128',
'-m', 'tcp', '-p', 'tcp'),
call(10, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'RETURN',
'--dest', u'2404:6800:4004:80c::101f/128',
'-m', 'udp', '-p', 'udp'),
call(10, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'RETURN',
'--dest', u'2404:6800:4004:80c::101f/128',
'-m', 'udp', '-p', 'udp'),
call(10, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
'--set-mark', '1', '--dest', u'2404:6800:4004:80c::/64',
'-m', 'tcp', '-p', 'tcp'),
call(10, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1', '--dest', u'2404:6800:4004:80c::/64',
'-m', 'tcp', '-p', 'tcp', '--on-port', '1024'),
call(10, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
'--set-mark', '1', '--dest', u'2404:6800:4004:80c::/64',
'-m', 'udp', '-p', 'udp'),
call(10, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1', '--dest', u'2404:6800:4004:80c::/64',
'-m', 'udp', '-p', 'udp', '--on-port', '1024')
]
mock_ipt_chain_exists.reset_mock()
mock_ipt_ttl.reset_mock()
mock_ipt.reset_mock()
method.restore_firewall(1025, 10, True)
assert mock_ipt_chain_exists.mock_calls == [
call(10, 'mangle', 'sshuttle-m-1025'),
call(10, 'mangle', 'sshuttle-t-1025'),
call(10, 'mangle', 'sshuttle-d-1025')
]
assert mock_ipt_ttl.mock_calls == []
assert mock_ipt.mock_calls == [
call(10, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'),
call(10, 'mangle', '-F', 'sshuttle-m-1025'),
call(10, 'mangle', '-X', 'sshuttle-m-1025'),
call(10, 'mangle', '-D', 'PREROUTING', '-j', 'sshuttle-t-1025'),
call(10, 'mangle', '-F', 'sshuttle-t-1025'),
call(10, 'mangle', '-X', 'sshuttle-t-1025'),
call(10, 'mangle', '-F', 'sshuttle-d-1025'),
call(10, 'mangle', '-X', 'sshuttle-d-1025')
]
mock_ipt_chain_exists.reset_mock()
mock_ipt_ttl.reset_mock()
mock_ipt.reset_mock()
# IPV4
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 mock_ipt_chain_exists.mock_calls == [
call(2, 'mangle', 'sshuttle-m-1025'),
call(2, 'mangle', 'sshuttle-t-1025'),
call(2, 'mangle', 'sshuttle-d-1025')
]
assert mock_ipt_ttl.mock_calls == []
assert mock_ipt.mock_calls == [
call(2, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'),
call(2, 'mangle', '-F', 'sshuttle-m-1025'),
call(2, 'mangle', '-X', 'sshuttle-m-1025'),
call(2, 'mangle', '-D', 'PREROUTING', '-j', 'sshuttle-t-1025'),
call(2, 'mangle', '-F', 'sshuttle-t-1025'),
call(2, 'mangle', '-X', 'sshuttle-t-1025'),
call(2, 'mangle', '-F', 'sshuttle-d-1025'),
call(2, 'mangle', '-X', 'sshuttle-d-1025'),
call(2, 'mangle', '-N', 'sshuttle-m-1025'),
call(2, 'mangle', '-F', 'sshuttle-m-1025'),
call(2, 'mangle', '-N', 'sshuttle-d-1025'),
call(2, 'mangle', '-F', 'sshuttle-d-1025'),
call(2, 'mangle', '-N', 'sshuttle-t-1025'),
call(2, 'mangle', '-F', 'sshuttle-t-1025'),
call(2, 'mangle', '-I', 'OUTPUT', '1', '-j', 'sshuttle-m-1025'),
call(2, 'mangle', '-I', 'PREROUTING', '1', '-j', 'sshuttle-t-1025'),
call(2, 'mangle', '-A', 'sshuttle-d-1025',
'-j', 'MARK', '--set-mark', '1'),
call(2, 'mangle', '-A', 'sshuttle-d-1025', '-j', 'ACCEPT'),
call(2, 'mangle', '-A', 'sshuttle-t-1025', '-m', 'socket',
'-j', 'sshuttle-d-1025', '-m', 'tcp', '-p', 'tcp'),
call(2, 'mangle', '-A', 'sshuttle-t-1025', '-m', 'socket',
'-j', 'sshuttle-d-1025', '-m', 'udp', '-p', 'udp'),
call(2, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK',
'--set-mark', '1', '--dest', u'1.2.3.33/32',
'-m', 'udp', '-p', 'udp', '--dport', '53'),
call(2, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1', '--dest', u'1.2.3.33/32',
'-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1027'),
call(2, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'RETURN',
'--dest', u'1.2.3.66/32', '-m', 'tcp', '-p', 'tcp'),
call(2, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'RETURN',
'--dest', u'1.2.3.66/32', '-m', 'tcp', '-p', 'tcp'),
call(2, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'RETURN',
'--dest', u'1.2.3.66/32', '-m', 'udp', '-p', 'udp'),
call(2, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'RETURN',
'--dest', u'1.2.3.66/32', '-m', 'udp', '-p', 'udp'),
call(2, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK',
'--set-mark', '1', '--dest', u'1.2.3.0/24',
'-m', 'tcp', '-p', 'tcp'),
call(2, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1', '--dest', u'1.2.3.0/24',
'-m', 'tcp', '-p', 'tcp', '--on-port', '1025'),
call(2, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK',
'--set-mark', '1', '--dest', u'1.2.3.0/24',
'-m', 'udp', '-p', 'udp'),
call(2, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1', '--dest', u'1.2.3.0/24',
'-m', 'udp', '-p', 'udp', '--on-port', '1025')
]
mock_ipt_chain_exists.reset_mock()
mock_ipt_ttl.reset_mock()
mock_ipt.reset_mock()
method.restore_firewall(1025, 2, True)
assert mock_ipt_chain_exists.mock_calls == [
call(2, 'mangle', 'sshuttle-m-1025'),
call(2, 'mangle', 'sshuttle-t-1025'),
call(2, 'mangle', 'sshuttle-d-1025')
]
assert mock_ipt_ttl.mock_calls == []
assert mock_ipt.mock_calls == [
call(2, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'),
call(2, 'mangle', '-F', 'sshuttle-m-1025'),
call(2, 'mangle', '-X', 'sshuttle-m-1025'),
call(2, 'mangle', '-D', 'PREROUTING', '-j', 'sshuttle-t-1025'),
call(2, 'mangle', '-F', 'sshuttle-t-1025'),
call(2, 'mangle', '-X', 'sshuttle-t-1025'),
call(2, 'mangle', '-F', 'sshuttle-d-1025'),
call(2, 'mangle', '-X', 'sshuttle-d-1025')
]
mock_ipt_chain_exists.reset_mock()
mock_ipt_ttl.reset_mock()
mock_ipt.reset_mock()

View File

@ -1,22 +1,23 @@
from mock import Mock, patch, call
import io
from socket import AF_INET, AF_INET6
from mock import Mock, patch, call
import sshuttle.firewall
def setup_daemon():
stdin = io.StringIO(u"""ROUTES
2,24,0,1.2.3.0
2,32,1,1.2.3.66
10,64,0,2404:6800:4004:80c::
10,128,1,2404:6800:4004:80c::101f
{inet},24,0,1.2.3.0,8000,9000
{inet},32,1,1.2.3.66,8080,8080
{inet6},64,0,2404:6800:4004:80c::,0,0
{inet6},128,1,2404:6800:4004:80c::101f,80,80
NSLIST
2,1.2.3.33
10,2404:6800:4004:80c::33
{inet},1.2.3.33
{inet6},2404:6800:4004:80c::33
PORTS 1024,1025,1026,1027
GO 1
GO 1 -
HOST 1.2.3.3,existing
""")
""".format(inet=AF_INET, inet6=AF_INET6))
stdout = Mock()
return stdin, stdout
@ -58,6 +59,37 @@ def test_rewrite_etc_hosts(tmpdir):
assert orig_hosts.computehash() == new_hosts.computehash()
def test_subnet_weight():
subnets = [
(AF_INET, 16, 0, '192.168.0.0', 0, 0),
(AF_INET, 24, 0, '192.168.69.0', 0, 0),
(AF_INET, 32, 0, '192.168.69.70', 0, 0),
(AF_INET, 32, 1, '192.168.69.70', 0, 0),
(AF_INET, 32, 1, '192.168.69.70', 80, 80),
(AF_INET, 0, 1, '0.0.0.0', 0, 0),
(AF_INET, 0, 1, '0.0.0.0', 8000, 9000),
(AF_INET, 0, 1, '0.0.0.0', 8000, 8500),
(AF_INET, 0, 1, '0.0.0.0', 8000, 8000),
(AF_INET, 0, 1, '0.0.0.0', 400, 450)
]
subnets_sorted = [
(AF_INET, 32, 1, '192.168.69.70', 80, 80),
(AF_INET, 0, 1, '0.0.0.0', 8000, 8000),
(AF_INET, 0, 1, '0.0.0.0', 400, 450),
(AF_INET, 0, 1, '0.0.0.0', 8000, 8500),
(AF_INET, 0, 1, '0.0.0.0', 8000, 9000),
(AF_INET, 32, 1, '192.168.69.70', 0, 0),
(AF_INET, 32, 0, '192.168.69.70', 0, 0),
(AF_INET, 24, 0, '192.168.69.0', 0, 0),
(AF_INET, 16, 0, '192.168.0.0', 0, 0),
(AF_INET, 0, 1, '0.0.0.0', 0, 0)
]
assert subnets_sorted == sorted(subnets,
key=sshuttle.firewall.subnet_weight,
reverse=True)
@patch('sshuttle.firewall.rewrite_etc_hosts')
@patch('sshuttle.firewall.setup_daemon')
@patch('sshuttle.firewall.get_method')
@ -86,17 +118,20 @@ def test_main(mock_get_method, mock_setup_daemon, mock_rewrite_etc_hosts):
call('not_auto'),
call().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')],
True),
[(AF_INET6, u'2404:6800:4004:80c::33')],
AF_INET6,
[(AF_INET6, 64, False, u'2404:6800:4004:80c::', 0, 0),
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 80, 80)],
True,
None),
call().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),
call().restore_firewall(1024, 10, True),
call().restore_firewall(1025, 2, True),
[(AF_INET, u'1.2.3.33')],
AF_INET,
[(AF_INET, 24, False, u'1.2.3.0', 8000, 9000),
(AF_INET, 32, True, u'1.2.3.66', 8080, 8080)],
True,
None),
call().restore_firewall(1024, AF_INET6, True, None),
call().restore_firewall(1025, AF_INET, True, None),
]

View File

@ -1,8 +1,9 @@
from mock import patch, call
import sys
import io
import socket
from socket import AF_INET, AF_INET6
import errno
from mock import patch, call
import sshuttle.helpers
@ -132,10 +133,12 @@ nameserver 2404:6800:4004:80c::4
ns = sshuttle.helpers.resolvconf_nameservers()
assert ns == [
(2, u'192.168.1.1'), (2, u'192.168.2.1'),
(2, u'192.168.3.1'), (2, u'192.168.4.1'),
(10, u'2404:6800:4004:80c::1'), (10, u'2404:6800:4004:80c::2'),
(10, u'2404:6800:4004:80c::3'), (10, u'2404:6800:4004:80c::4')
(AF_INET, u'192.168.1.1'), (AF_INET, u'192.168.2.1'),
(AF_INET, u'192.168.3.1'), (AF_INET, u'192.168.4.1'),
(AF_INET6, u'2404:6800:4004:80c::1'),
(AF_INET6, u'2404:6800:4004:80c::2'),
(AF_INET6, u'2404:6800:4004:80c::3'),
(AF_INET6, u'2404:6800:4004:80c::4')
]
@ -155,37 +158,39 @@ nameserver 2404:6800:4004:80c::4
""")
ns = sshuttle.helpers.resolvconf_random_nameserver()
assert ns in [
(2, u'192.168.1.1'), (2, u'192.168.2.1'),
(2, u'192.168.3.1'), (2, u'192.168.4.1'),
(10, u'2404:6800:4004:80c::1'), (10, u'2404:6800:4004:80c::2'),
(10, u'2404:6800:4004:80c::3'), (10, u'2404:6800:4004:80c::4')
(AF_INET, u'192.168.1.1'), (AF_INET, u'192.168.2.1'),
(AF_INET, u'192.168.3.1'), (AF_INET, u'192.168.4.1'),
(AF_INET6, u'2404:6800:4004:80c::1'),
(AF_INET6, u'2404:6800:4004:80c::2'),
(AF_INET6, u'2404:6800:4004:80c::3'),
(AF_INET6, u'2404:6800:4004:80c::4')
]
def test_islocal():
assert sshuttle.helpers.islocal("127.0.0.1", socket.AF_INET)
assert not sshuttle.helpers.islocal("192.0.2.1", socket.AF_INET)
assert sshuttle.helpers.islocal("::1", socket.AF_INET6)
assert not sshuttle.helpers.islocal("2001:db8::1", socket.AF_INET6)
@patch('sshuttle.helpers.socket.socket.bind')
def test_islocal(mock_bind):
bind_error = socket.error(errno.EADDRNOTAVAIL)
mock_bind.side_effect = [None, bind_error, None, bind_error]
assert sshuttle.helpers.islocal("127.0.0.1", AF_INET)
assert not sshuttle.helpers.islocal("192.0.2.1", AF_INET)
assert sshuttle.helpers.islocal("::1", AF_INET6)
assert not sshuttle.helpers.islocal("2001:db8::1", AF_INET6)
def test_family_ip_tuple():
assert sshuttle.helpers.family_ip_tuple("127.0.0.1") \
== (socket.AF_INET, "127.0.0.1")
== (AF_INET, "127.0.0.1")
assert sshuttle.helpers.family_ip_tuple("192.168.2.6") \
== (socket.AF_INET, "192.168.2.6")
== (AF_INET, "192.168.2.6")
assert sshuttle.helpers.family_ip_tuple("::1") \
== (socket.AF_INET6, "::1")
== (AF_INET6, "::1")
assert sshuttle.helpers.family_ip_tuple("2404:6800:4004:80c::1") \
== (socket.AF_INET6, "2404:6800:4004:80c::1")
== (AF_INET6, "2404:6800:4004:80c::1")
def test_family_to_string():
assert sshuttle.helpers.family_to_string(socket.AF_INET) == "AF_INET"
assert sshuttle.helpers.family_to_string(socket.AF_INET6) == "AF_INET6"
if sys.version_info < (3, 0):
expected = "1"
assert sshuttle.helpers.family_to_string(socket.AF_UNIX) == "1"
else:
assert sshuttle.helpers.family_to_string(AF_INET) == "AF_INET"
assert sshuttle.helpers.family_to_string(AF_INET6) == "AF_INET6"
expected = 'AddressFamily.AF_UNIX'
assert sshuttle.helpers.family_to_string(socket.AF_UNIX) == expected

View File

@ -1,8 +1,9 @@
import pytest
from mock import Mock, patch, call
import socket
from socket import AF_INET, AF_INET6
import struct
import pytest
from mock import Mock, patch, call
from sshuttle.helpers import Fatal
from sshuttle.methods import get_method
@ -18,7 +19,7 @@ def test_get_supported_features():
def test_get_tcp_dstip():
sock = Mock()
sock.getsockopt.return_value = struct.pack(
'!HHBBBB', socket.ntohs(socket.AF_INET), 1024, 127, 0, 0, 1)
'!HHBBBB', socket.ntohs(AF_INET), 1024, 127, 0, 0, 1)
method = get_method('nat')
assert method.get_tcp_dstip(sock) == ('127.0.0.1', 1024)
assert sock.mock_calls == [call.getsockopt(0, 80, 16)]
@ -84,11 +85,12 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
with pytest.raises(Exception) as excinfo:
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')],
True)
[(AF_INET6, u'2404:6800:4004:80c::33')],
AF_INET6,
[(AF_INET6, 64, False, u'2404:6800:4004:80c::', 0, 0),
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 80, 80)],
True,
None)
assert str(excinfo.value) \
== 'Address family "AF_INET6" unsupported by nat method_name'
assert mock_ipt_chain_exists.mock_calls == []
@ -98,10 +100,12 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
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)
[(AF_INET, u'1.2.3.33')],
AF_INET,
[(AF_INET, 24, False, u'1.2.3.0', 8000, 9000),
(AF_INET, 32, True, u'1.2.3.66', 8080, 8080)],
True,
None)
assert str(excinfo.value) == 'UDP not supported by nat method_name'
assert mock_ipt_chain_exists.mock_calls == []
assert mock_ipt_ttl.mock_calls == []
@ -109,46 +113,55 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
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)
[(AF_INET, u'1.2.3.33')],
AF_INET,
[(AF_INET, 24, False, u'1.2.3.0', 8000, 9000),
(AF_INET, 32, True, u'1.2.3.66', 8080, 8080)],
False,
None)
assert mock_ipt_chain_exists.mock_calls == [
call(2, 'nat', 'sshuttle-1025')
call(AF_INET, 'nat', 'sshuttle-1025')
]
assert mock_ipt_ttl.mock_calls == [
call(2, 'nat', '-A', 'sshuttle-1025', '-j', 'REDIRECT',
'--dest', u'1.2.3.0/24', '-p', 'tcp', '--to-ports', '1025'),
call(2, 'nat', '-A', 'sshuttle-1025', '-j', 'REDIRECT',
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'REDIRECT',
'--dest', u'1.2.3.0/24', '-p', 'tcp', '--dport', '8000:9000',
'--to-ports', '1025'),
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'REDIRECT',
'--dest', u'1.2.3.33/32', '-p', 'udp',
'--dport', '53', '--to-ports', '1027')
]
assert mock_ipt.mock_calls == [
call(2, 'nat', '-D', 'OUTPUT', '-j', 'sshuttle-1025'),
call(2, 'nat', '-D', 'PREROUTING', '-j', 'sshuttle-1025'),
call(2, 'nat', '-F', 'sshuttle-1025'),
call(2, 'nat', '-X', 'sshuttle-1025'),
call(2, 'nat', '-N', 'sshuttle-1025'),
call(2, 'nat', '-F', 'sshuttle-1025'),
call(2, 'nat', '-I', 'OUTPUT', '1', '-j', 'sshuttle-1025'),
call(2, 'nat', '-I', 'PREROUTING', '1', '-j', 'sshuttle-1025'),
call(2, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN',
'--dest', u'1.2.3.66/32', '-p', 'tcp')
call(AF_INET, 'nat', '-D', 'OUTPUT', '-j', 'sshuttle-1025'),
call(AF_INET, 'nat', '-D', 'PREROUTING', '-j', 'sshuttle-1025'),
call(AF_INET, 'nat', '-F', 'sshuttle-1025'),
call(AF_INET, 'nat', '-X', 'sshuttle-1025'),
call(AF_INET, 'nat', '-N', 'sshuttle-1025'),
call(AF_INET, 'nat', '-F', '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', '-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',
'--dest', u'1.2.3.66/32', '-p', 'tcp', '--dport', '8080:8080')
]
mock_ipt_chain_exists.reset_mock()
mock_ipt_ttl.reset_mock()
mock_ipt.reset_mock()
method.restore_firewall(1025, 2, False)
method.restore_firewall(1025, AF_INET, False, None)
assert mock_ipt_chain_exists.mock_calls == [
call(2, 'nat', 'sshuttle-1025')
call(AF_INET, 'nat', 'sshuttle-1025')
]
assert mock_ipt_ttl.mock_calls == []
assert mock_ipt.mock_calls == [
call(2, 'nat', '-D', 'OUTPUT', '-j', 'sshuttle-1025'),
call(2, 'nat', '-D', 'PREROUTING', '-j', 'sshuttle-1025'),
call(2, 'nat', '-F', 'sshuttle-1025'),
call(2, 'nat', '-X', 'sshuttle-1025')
call(AF_INET, 'nat', '-D', 'OUTPUT', '-j', 'sshuttle-1025'),
call(AF_INET, 'nat', '-D', 'PREROUTING', '-j', 'sshuttle-1025'),
call(AF_INET, 'nat', '-F', 'sshuttle-1025'),
call(AF_INET, 'nat', '-X', 'sshuttle-1025')
]
mock_ipt_chain_exists.reset_mock()
mock_ipt_ttl.reset_mock()

View File

@ -1,7 +1,8 @@
import socket
from socket import AF_INET, AF_INET6
import pytest
from mock import Mock, patch, call, ANY
import socket
from sshuttle.methods import get_method
from sshuttle.helpers import Fatal
from sshuttle.methods.pf import FreeBsd, Darwin, OpenBsd
@ -20,7 +21,7 @@ def test_get_tcp_dstip():
sock = Mock()
sock.getpeername.return_value = ("127.0.0.1", 1024)
sock.getsockname.return_value = ("127.0.0.2", 1025)
sock.family = socket.AF_INET
sock.family = AF_INET
firewall = Mock()
firewall.pfile.readline.return_value = \
@ -94,7 +95,7 @@ def test_firewall_command_darwin(mock_pf_get_dev, mock_ioctl, mock_stdout):
assert not method.firewall_command("somthing")
command = "QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % (
socket.AF_INET, socket.IPPROTO_TCP,
AF_INET, socket.IPPROTO_TCP,
"127.0.0.1", 1025, "127.0.0.2", 1024)
assert method.firewall_command(command)
@ -117,7 +118,7 @@ def test_firewall_command_freebsd(mock_pf_get_dev, mock_ioctl, mock_stdout):
assert not method.firewall_command("somthing")
command = "QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % (
socket.AF_INET, socket.IPPROTO_TCP,
AF_INET, socket.IPPROTO_TCP,
"127.0.0.1", 1025, "127.0.0.2", 1024)
assert method.firewall_command(command)
@ -140,7 +141,7 @@ def test_firewall_command_openbsd(mock_pf_get_dev, mock_ioctl, mock_stdout):
assert not method.firewall_command("somthing")
command = "QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % (
socket.AF_INET, socket.IPPROTO_TCP,
AF_INET, socket.IPPROTO_TCP,
"127.0.0.1", 1025, "127.0.0.2", 1024)
assert method.firewall_command(command)
@ -180,11 +181,12 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
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)
[(AF_INET6, u'2404:6800:4004:80c::33')],
AF_INET6,
[(AF_INET6, 64, False, u'2404:6800:4004:80c::', 8000, 9000),
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
False,
None)
assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xC4704433, ANY),
call(mock_pf_get_dev(), 0xCC20441A, ANY),
@ -198,16 +200,15 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
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 tcp from ! ::1 to '
b'2404:6800:4004:80c::/64 port 8000:9000 -> ::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 tcp to '
b'2404:6800:4004:80c::/64 port 8000:9000 keep state\n'
b'pass out inet6 proto tcp to '
b'2404:6800:4004:80c::101f/128 port 8080:8080\n'
b'pass out route-to lo0 inet6 proto udp '
b'to <dns_servers> port 53 keep state\n'),
call('-E'),
@ -219,10 +220,12 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
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)
[(AF_INET, u'1.2.3.33')],
AF_INET,
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
True,
None)
assert str(excinfo.value) == 'UDP not supported by pf method_name'
assert mock_pf_get_dev.mock_calls == []
assert mock_ioctl.mock_calls == []
@ -230,10 +233,12 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
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)
[(AF_INET, u'1.2.3.33')],
AF_INET,
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
False,
None)
assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xC4704433, ANY),
call(mock_pf_get_dev(), 0xCC20441A, ANY),
@ -247,14 +252,13 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
call('-f /dev/stdin', b'pass 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'rdr pass on lo0 inet proto tcp '
b'to <forward_subnets> -> 127.0.0.1 port 1025\n'
b'rdr pass on lo0 inet proto tcp from ! 127.0.0.1 to 1.2.3.0/24 '
b'-> 127.0.0.1 port 1025\n'
b'rdr pass on lo0 inet proto udp '
b'to <dns_servers> port 53 -> 127.0.0.1 port 1027\n'
b'pass out route-to lo0 inet proto tcp '
b'to <forward_subnets> keep state\n'
b'pass out route-to lo0 inet proto tcp to 1.2.3.0/24 keep state\n'
b'pass out inet proto tcp to 1.2.3.66/32 port 80:80\n'
b'pass out route-to lo0 inet proto udp '
b'to <dns_servers> port 53 keep state\n'),
call('-E'),
@ -263,7 +267,7 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
mock_ioctl.reset_mock()
mock_pfctl.reset_mock()
method.restore_firewall(1025, 2, False)
method.restore_firewall(1025, AF_INET, False, None)
assert mock_ioctl.mock_calls == []
assert mock_pfctl.mock_calls == [
call('-a sshuttle-1025 -F all'),
@ -276,10 +280,12 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
@patch('sshuttle.helpers.verbose', new=3)
@patch('sshuttle.methods.pf.pf', FreeBsd())
@patch('subprocess.call')
@patch('sshuttle.methods.pf.pfctl')
@patch('sshuttle.methods.pf.ioctl')
@patch('sshuttle.methods.pf.pf_get_dev')
def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl,
mock_subprocess_call):
mock_pfctl.side_effect = pfctl
method = get_method('pf')
@ -287,29 +293,30 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
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)
[(AF_INET6, u'2404:6800:4004:80c::33')],
AF_INET6,
[(AF_INET6, 64, False, u'2404:6800:4004:80c::', 8000, 9000),
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
False,
None)
assert mock_pfctl.mock_calls == [
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 tcp from ! ::1 to '
b'2404:6800:4004:80c::/64 port 8000:9000 -> ::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 tcp to '
b'2404:6800:4004:80c::/64 port 8000:9000 keep state\n'
b'pass out inet6 proto tcp to '
b'2404:6800:4004:80c::101f/128 port 8080:8080\n'
b'pass out route-to lo0 inet6 proto udp '
b'to <dns_servers> port 53 keep state\n'),
call('-e'),
]
assert call(['kldload', 'pf']) in mock_subprocess_call.mock_calls
mock_pf_get_dev.reset_mock()
mock_ioctl.reset_mock()
mock_pfctl.reset_mock()
@ -317,10 +324,12 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
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)
[(AF_INET, u'1.2.3.33')],
AF_INET,
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
True,
None)
assert str(excinfo.value) == 'UDP not supported by pf method_name'
assert mock_pf_get_dev.mock_calls == []
assert mock_ioctl.mock_calls == []
@ -328,10 +337,12 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
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)
[(AF_INET, u'1.2.3.33')],
AF_INET,
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
False,
None)
assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xC4704433, ANY),
call(mock_pf_get_dev(), 0xCBE0441A, ANY),
@ -343,14 +354,13 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
assert mock_pfctl.mock_calls == [
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'rdr pass on lo0 inet proto tcp '
b'to <forward_subnets> -> 127.0.0.1 port 1025\n'
b'rdr pass on lo0 inet proto tcp from ! 127.0.0.1 '
b'to 1.2.3.0/24 -> 127.0.0.1 port 1025\n'
b'rdr pass on lo0 inet proto udp '
b'to <dns_servers> port 53 -> 127.0.0.1 port 1027\n'
b'pass out route-to lo0 inet proto tcp '
b'to <forward_subnets> keep state\n'
b'pass out route-to lo0 inet proto tcp to 1.2.3.0/24 keep state\n'
b'pass out inet proto tcp to 1.2.3.66/32 port 80:80\n'
b'pass out route-to lo0 inet proto udp '
b'to <dns_servers> port 53 keep state\n'),
call('-e'),
@ -359,10 +369,12 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
mock_ioctl.reset_mock()
mock_pfctl.reset_mock()
method.restore_firewall(1025, 2, False)
method.restore_firewall(1025, AF_INET, False, None)
method.restore_firewall(1024, AF_INET6, False, None)
assert mock_ioctl.mock_calls == []
assert mock_pfctl.mock_calls == [
call('-a sshuttle-1025 -F all'),
call('-a sshuttle6-1024 -F all'),
call("-d"),
]
mock_pf_get_dev.reset_mock()
@ -383,31 +395,31 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
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)
[(AF_INET6, u'2404:6800:4004:80c::33')],
AF_INET6,
[(AF_INET6, 64, False, u'2404:6800:4004:80c::', 8000, 9000),
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
False,
None)
assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xcd48441a, ANY),
call(mock_pf_get_dev(), 0xcd48441a, ANY),
call(mock_pf_get_dev(), 0xcd60441a, ANY),
call(mock_pf_get_dev(), 0xcd60441a, 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 tcp to 2404:6800:4004:80c::/64 '
b'port 8000:9000 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 2404:6800:4004:80c::/64 '
b'port 8000:9000 route-to lo0 keep state\n'
b'pass out inet6 proto tcp to '
b'<forward_subnets> route-to lo0 keep state\n'
b'2404:6800:4004:80c::101f/128 port 8080:8080\n'
b'pass out inet6 proto udp to '
b'<dns_servers> port 53 route-to lo0 keep state\n'),
call('-e'),
@ -419,10 +431,12 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
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)
[(AF_INET, u'1.2.3.33')],
AF_INET,
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
True,
None)
assert str(excinfo.value) == 'UDP not supported by pf method_name'
assert mock_pf_get_dev.mock_calls == []
assert mock_ioctl.mock_calls == []
@ -430,26 +444,28 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
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)
[(AF_INET, u'1.2.3.33')],
AF_INET,
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
False,
None)
assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xcd48441a, ANY),
call(mock_pf_get_dev(), 0xcd48441a, ANY),
call(mock_pf_get_dev(), 0xcd60441a, ANY),
call(mock_pf_get_dev(), 0xcd60441a, 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 tcp to 1.2.3.0/24 divert-to '
b'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 tcp to 1.2.3.0/24 route-to lo0 keep state\n'
b'pass out inet proto tcp to 1.2.3.66/32 port 80:80\n'
b'pass out inet proto udp to '
b'<dns_servers> port 53 route-to lo0 keep state\n'),
call('-e'),
@ -458,11 +474,13 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
mock_ioctl.reset_mock()
mock_pfctl.reset_mock()
method.restore_firewall(1025, 2, False)
method.restore_firewall(1025, AF_INET, False, None)
method.restore_firewall(1024, AF_INET6, False, None)
assert mock_ioctl.mock_calls == []
assert mock_pfctl.mock_calls == [
call('-a sshuttle-1025 -F all'),
call("-d"),
call('-a sshuttle6-1024 -F all'),
call('-d'),
]
mock_pf_get_dev.reset_mock()
mock_pfctl.reset_mock()

View File

@ -0,0 +1,297 @@
import socket
from socket import AF_INET, AF_INET6
from mock import Mock, patch, call
from sshuttle.methods import get_method
@patch("sshuttle.methods.tproxy.recvmsg")
def test_get_supported_features_recvmsg(mock_recvmsg):
method = get_method('tproxy')
features = method.get_supported_features()
assert features.ipv6
assert features.udp
assert features.dns
@patch("sshuttle.methods.tproxy.recvmsg", None)
def test_get_supported_features_norecvmsg():
method = get_method('tproxy')
features = method.get_supported_features()
assert features.ipv6
assert not features.udp
assert not features.dns
def test_get_tcp_dstip():
sock = Mock()
sock.getsockname.return_value = ('127.0.0.1', 1024)
method = get_method('tproxy')
assert method.get_tcp_dstip(sock) == ('127.0.0.1', 1024)
assert sock.mock_calls == [call.getsockname()]
@patch("sshuttle.methods.tproxy.recv_udp")
def test_recv_udp(mock_recv_udp):
mock_recv_udp.return_value = ("127.0.0.1", "127.0.0.2", "11111")
sock = Mock()
method = get_method('tproxy')
result = method.recv_udp(sock, 1024)
assert sock.mock_calls == []
assert mock_recv_udp.mock_calls == [call(sock, 1024)]
assert result == ("127.0.0.1", "127.0.0.2", "11111")
@patch("sshuttle.methods.socket.socket")
def test_send_udp(mock_socket):
sock = Mock()
method = get_method('tproxy')
method.send_udp(sock, "127.0.0.2", "127.0.0.1", "2222222")
assert sock.mock_calls == []
assert mock_socket.mock_calls == [
call(sock.family, 2),
call().setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1),
call().setsockopt(0, 19, 1),
call().bind('127.0.0.2'),
call().sendto("2222222", '127.0.0.1'),
call().close()
]
def test_setup_tcp_listener():
listener = Mock()
method = get_method('tproxy')
method.setup_tcp_listener(listener)
assert listener.mock_calls == [
call.setsockopt(0, 19, 1)
]
def test_setup_udp_listener():
listener = Mock()
method = get_method('tproxy')
method.setup_udp_listener(listener)
assert listener.mock_calls == [
call.setsockopt(0, 19, 1),
call.v4.setsockopt(0, 20, 1),
call.v6.setsockopt(41, 74, 1)
]
def test_assert_features():
method = get_method('tproxy')
features = method.get_supported_features()
method.assert_features(features)
def test_firewall_command():
method = get_method('tproxy')
assert not method.firewall_command("somthing")
@patch('sshuttle.methods.tproxy.ipt')
@patch('sshuttle.methods.tproxy.ipt_ttl')
@patch('sshuttle.methods.tproxy.ipt_chain_exists')
def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
mock_ipt_chain_exists.return_value = True
method = get_method('tproxy')
assert method.name == 'tproxy'
# IPV6
method.setup_firewall(
1024, 1026,
[(AF_INET6, u'2404:6800:4004:80c::33')],
AF_INET6,
[(AF_INET6, 64, False, u'2404:6800:4004:80c::', 8000, 9000),
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
True,
None)
assert mock_ipt_chain_exists.mock_calls == [
call(AF_INET6, 'mangle', 'sshuttle-m-1024'),
call(AF_INET6, 'mangle', 'sshuttle-t-1024'),
call(AF_INET6, 'mangle', 'sshuttle-d-1024')
]
assert mock_ipt_ttl.mock_calls == []
assert mock_ipt.mock_calls == [
call(AF_INET6, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1024'),
call(AF_INET6, 'mangle', '-F', 'sshuttle-m-1024'),
call(AF_INET6, 'mangle', '-X', 'sshuttle-m-1024'),
call(AF_INET6, 'mangle', '-D', 'PREROUTING', '-j', 'sshuttle-t-1024'),
call(AF_INET6, 'mangle', '-F', 'sshuttle-t-1024'),
call(AF_INET6, 'mangle', '-X', 'sshuttle-t-1024'),
call(AF_INET6, 'mangle', '-F', 'sshuttle-d-1024'),
call(AF_INET6, 'mangle', '-X', 'sshuttle-d-1024'),
call(AF_INET6, 'mangle', '-N', 'sshuttle-m-1024'),
call(AF_INET6, 'mangle', '-F', 'sshuttle-m-1024'),
call(AF_INET6, 'mangle', '-N', 'sshuttle-d-1024'),
call(AF_INET6, 'mangle', '-F', 'sshuttle-d-1024'),
call(AF_INET6, 'mangle', '-N', 'sshuttle-t-1024'),
call(AF_INET6, 'mangle', '-F', 'sshuttle-t-1024'),
call(AF_INET6, 'mangle', '-I', 'OUTPUT', '1', '-j', 'sshuttle-m-1024'),
call(AF_INET6, 'mangle', '-I', 'PREROUTING', '1', '-j',
'sshuttle-t-1024'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-d-1024', '-j', 'MARK',
'--set-mark', '1'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-d-1024', '-j', 'ACCEPT'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-m', 'socket',
'-j', 'sshuttle-d-1024', '-m', 'tcp', '-p', 'tcp'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-m', 'socket',
'-j', 'sshuttle-d-1024', '-m', 'udp', '-p', 'udp'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
'--set-mark', '1', '--dest', u'2404:6800:4004:80c::33/32',
'-m', 'udp', '-p', 'udp', '--dport', '53'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1',
'--dest', u'2404:6800:4004:80c::33/32',
'-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1026'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'RETURN',
'--dest', u'2404:6800:4004:80c::101f/128',
'-m', 'tcp', '-p', 'tcp', '--dport', '8080:8080'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'RETURN',
'--dest', u'2404:6800:4004:80c::101f/128',
'-m', 'tcp', '-p', 'tcp', '--dport', '8080:8080'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'RETURN',
'--dest', u'2404:6800:4004:80c::101f/128',
'-m', 'udp', '-p', 'udp', '--dport', '8080:8080'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'RETURN',
'--dest', u'2404:6800:4004:80c::101f/128',
'-m', 'udp', '-p', 'udp', '--dport', '8080:8080'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
'--set-mark', '1', '--dest', u'2404:6800:4004:80c::/64',
'-m', 'tcp', '-p', 'tcp', '--dport', '8000:9000'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1', '--dest', u'2404:6800:4004:80c::/64',
'-m', 'tcp', '-p', 'tcp', '--dport', '8000:9000',
'--on-port', '1024'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
'--set-mark', '1', '--dest', u'2404:6800:4004:80c::/64',
'-m', 'udp', '-p', 'udp', '--dport', '8000:9000'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1', '--dest', u'2404:6800:4004:80c::/64',
'-m', 'udp', '-p', 'udp', '--dport', '8000:9000',
'--on-port', '1024')
]
mock_ipt_chain_exists.reset_mock()
mock_ipt_ttl.reset_mock()
mock_ipt.reset_mock()
method.restore_firewall(1025, AF_INET6, True, None)
assert mock_ipt_chain_exists.mock_calls == [
call(AF_INET6, 'mangle', 'sshuttle-m-1025'),
call(AF_INET6, 'mangle', 'sshuttle-t-1025'),
call(AF_INET6, 'mangle', 'sshuttle-d-1025')
]
assert mock_ipt_ttl.mock_calls == []
assert mock_ipt.mock_calls == [
call(AF_INET6, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'),
call(AF_INET6, 'mangle', '-F', 'sshuttle-m-1025'),
call(AF_INET6, 'mangle', '-X', 'sshuttle-m-1025'),
call(AF_INET6, 'mangle', '-D', 'PREROUTING', '-j', 'sshuttle-t-1025'),
call(AF_INET6, 'mangle', '-F', 'sshuttle-t-1025'),
call(AF_INET6, 'mangle', '-X', 'sshuttle-t-1025'),
call(AF_INET6, 'mangle', '-F', 'sshuttle-d-1025'),
call(AF_INET6, 'mangle', '-X', 'sshuttle-d-1025')
]
mock_ipt_chain_exists.reset_mock()
mock_ipt_ttl.reset_mock()
mock_ipt.reset_mock()
# IPV4
method.setup_firewall(
1025, 1027,
[(AF_INET, u'1.2.3.33')],
AF_INET,
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
True,
None)
assert mock_ipt_chain_exists.mock_calls == [
call(AF_INET, 'mangle', 'sshuttle-m-1025'),
call(AF_INET, 'mangle', 'sshuttle-t-1025'),
call(AF_INET, 'mangle', 'sshuttle-d-1025')
]
assert mock_ipt_ttl.mock_calls == []
assert mock_ipt.mock_calls == [
call(AF_INET, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'),
call(AF_INET, 'mangle', '-F', 'sshuttle-m-1025'),
call(AF_INET, 'mangle', '-X', 'sshuttle-m-1025'),
call(AF_INET, 'mangle', '-D', 'PREROUTING', '-j', 'sshuttle-t-1025'),
call(AF_INET, 'mangle', '-F', 'sshuttle-t-1025'),
call(AF_INET, 'mangle', '-X', 'sshuttle-t-1025'),
call(AF_INET, 'mangle', '-F', 'sshuttle-d-1025'),
call(AF_INET, 'mangle', '-X', 'sshuttle-d-1025'),
call(AF_INET, 'mangle', '-N', 'sshuttle-m-1025'),
call(AF_INET, 'mangle', '-F', 'sshuttle-m-1025'),
call(AF_INET, 'mangle', '-N', 'sshuttle-d-1025'),
call(AF_INET, 'mangle', '-F', 'sshuttle-d-1025'),
call(AF_INET, 'mangle', '-N', 'sshuttle-t-1025'),
call(AF_INET, 'mangle', '-F', 'sshuttle-t-1025'),
call(AF_INET, 'mangle', '-I', 'OUTPUT', '1', '-j', 'sshuttle-m-1025'),
call(AF_INET, 'mangle', '-I', 'PREROUTING', '1', '-j',
'sshuttle-t-1025'),
call(AF_INET, 'mangle', '-A', 'sshuttle-d-1025',
'-j', 'MARK', '--set-mark', '1'),
call(AF_INET, 'mangle', '-A', 'sshuttle-d-1025', '-j', 'ACCEPT'),
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-m', 'socket',
'-j', 'sshuttle-d-1025', '-m', 'tcp', '-p', 'tcp'),
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-m', 'socket',
'-j', 'sshuttle-d-1025', '-m', 'udp', '-p', 'udp'),
call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK',
'--set-mark', '1', '--dest', u'1.2.3.33/32',
'-m', 'udp', '-p', 'udp', '--dport', '53'),
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1', '--dest', u'1.2.3.33/32',
'-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1027'),
call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'RETURN',
'--dest', u'1.2.3.66/32', '-m', 'tcp', '-p', 'tcp',
'--dport', '80:80'),
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'RETURN',
'--dest', u'1.2.3.66/32', '-m', 'tcp', '-p', 'tcp',
'--dport', '80:80'),
call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'RETURN',
'--dest', u'1.2.3.66/32', '-m', 'udp', '-p', 'udp',
'--dport', '80:80'),
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'RETURN',
'--dest', u'1.2.3.66/32', '-m', 'udp', '-p', 'udp',
'--dport', '80:80'),
call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK',
'--set-mark', '1', '--dest', u'1.2.3.0/24',
'-m', 'tcp', '-p', 'tcp'),
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1', '--dest', u'1.2.3.0/24',
'-m', 'tcp', '-p', 'tcp', '--on-port', '1025'),
call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK',
'--set-mark', '1', '--dest', u'1.2.3.0/24',
'-m', 'udp', '-p', 'udp'),
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1', '--dest', u'1.2.3.0/24',
'-m', 'udp', '-p', 'udp', '--on-port', '1025')
]
mock_ipt_chain_exists.reset_mock()
mock_ipt_ttl.reset_mock()
mock_ipt.reset_mock()
method.restore_firewall(1025, AF_INET, True, None)
assert mock_ipt_chain_exists.mock_calls == [
call(AF_INET, 'mangle', 'sshuttle-m-1025'),
call(AF_INET, 'mangle', 'sshuttle-t-1025'),
call(AF_INET, 'mangle', 'sshuttle-d-1025')
]
assert mock_ipt_ttl.mock_calls == []
assert mock_ipt.mock_calls == [
call(AF_INET, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'),
call(AF_INET, 'mangle', '-F', 'sshuttle-m-1025'),
call(AF_INET, 'mangle', '-X', 'sshuttle-m-1025'),
call(AF_INET, 'mangle', '-D', 'PREROUTING', '-j', 'sshuttle-t-1025'),
call(AF_INET, 'mangle', '-F', 'sshuttle-t-1025'),
call(AF_INET, 'mangle', '-X', 'sshuttle-t-1025'),
call(AF_INET, 'mangle', '-F', 'sshuttle-d-1025'),
call(AF_INET, 'mangle', '-X', 'sshuttle-d-1025')
]
mock_ipt_chain_exists.reset_mock()
mock_ipt_ttl.reset_mock()
mock_ipt.reset_mock()

View File

@ -0,0 +1,101 @@
import socket
from argparse import ArgumentTypeError as Fatal
import pytest
import sshuttle.options
_ip4_reprs = {
'0.0.0.0': '0.0.0.0',
'255.255.255.255': '255.255.255.255',
'10.0': '10.0.0.0',
'184.172.10.74': '184.172.10.74',
'3098282570': '184.172.10.74',
'0xb8.0xac.0x0a.0x4a': '184.172.10.74',
'0270.0254.0012.0112': '184.172.10.74',
'localhost': '127.0.0.1'
}
_ip4_swidths = (1, 8, 22, 27, 32)
_ip6_reprs = {
'::': '::',
'::1': '::1',
'fc00::': 'fc00::',
'2a01:7e00:e000:188::1': '2a01:7e00:e000:188::1'
}
_ip6_swidths = (48, 64, 96, 115, 128)
def test_parse_subnetport_ip4():
for ip_repr, ip in _ip4_reprs.items():
assert sshuttle.options.parse_subnetport(ip_repr) \
== (socket.AF_INET, ip, 32, 0, 0)
with pytest.raises(Fatal) as excinfo:
sshuttle.options.parse_subnetport('10.256.0.0')
assert str(excinfo.value) == 'Unable to resolve address: 10.256.0.0'
def test_parse_subnetport_ip4_with_mask():
for ip_repr, ip in _ip4_reprs.items():
for swidth in _ip4_swidths:
assert sshuttle.options.parse_subnetport(
'/'.join((ip_repr, str(swidth)))
) == (socket.AF_INET, ip, swidth, 0, 0)
assert sshuttle.options.parse_subnetport('0/0') \
== (socket.AF_INET, '0.0.0.0', 0, 0, 0)
with pytest.raises(Fatal) as excinfo:
sshuttle.options.parse_subnetport('10.0.0.0/33')
assert str(excinfo.value) == 'width 33 is not between 0 and 32'
def test_parse_subnetport_ip4_with_port():
for ip_repr, ip in _ip4_reprs.items():
assert sshuttle.options.parse_subnetport(':'.join((ip_repr, '80'))) \
== (socket.AF_INET, ip, 32, 80, 80)
assert sshuttle.options.parse_subnetport(':'.join((ip_repr, '80-90')))\
== (socket.AF_INET, ip, 32, 80, 90)
def test_parse_subnetport_ip4_with_mask_and_port():
for ip_repr, ip in _ip4_reprs.items():
assert sshuttle.options.parse_subnetport(ip_repr + '/32:80') \
== (socket.AF_INET, ip, 32, 80, 80)
assert sshuttle.options.parse_subnetport(ip_repr + '/16:80-90') \
== (socket.AF_INET, ip, 16, 80, 90)
def test_parse_subnetport_ip6():
for ip_repr, ip in _ip6_reprs.items():
assert sshuttle.options.parse_subnetport(ip_repr) \
== (socket.AF_INET6, ip, 128, 0, 0)
def test_parse_subnetport_ip6_with_mask():
for ip_repr, ip in _ip6_reprs.items():
for swidth in _ip4_swidths + _ip6_swidths:
assert sshuttle.options.parse_subnetport(
'/'.join((ip_repr, str(swidth)))
) == (socket.AF_INET6, ip, swidth, 0, 0)
assert sshuttle.options.parse_subnetport('::/0') \
== (socket.AF_INET6, '::', 0, 0, 0)
with pytest.raises(Fatal) as excinfo:
sshuttle.options.parse_subnetport('fc00::/129')
assert str(excinfo.value) == 'width 129 is not between 0 and 128'
def test_parse_subnetport_ip6_with_port():
for ip_repr, ip in _ip6_reprs.items():
assert sshuttle.options.parse_subnetport('[' + ip_repr + ']:80') \
== (socket.AF_INET6, ip, 128, 80, 80)
assert sshuttle.options.parse_subnetport('[' + ip_repr + ']:80-90') \
== (socket.AF_INET6, ip, 128, 80, 90)
def test_parse_subnetport_ip6_with_mask_and_port():
for ip_repr, ip in _ip6_reprs.items():
assert sshuttle.options.parse_subnetport('[' + ip_repr + '/128]:80') \
== (socket.AF_INET6, ip, 128, 80, 80)
assert sshuttle.options.parse_subnetport('[' + ip_repr + '/16]:80-90')\
== (socket.AF_INET6, ip, 16, 80, 90)

View File

@ -0,0 +1,65 @@
import socket
from mock import Mock, patch, call
import sshuttle.sdnotify
@patch('sshuttle.sdnotify.os.environ.get')
def test_notify_invalid_socket_path(mock_get):
mock_get.return_value = 'invalid_path'
assert not sshuttle.sdnotify.send(sshuttle.sdnotify.ready())
@patch('sshuttle.sdnotify.os.environ.get')
def test_notify_socket_not_there(mock_get):
mock_get.return_value = '/run/valid_nonexistent_path'
assert not sshuttle.sdnotify.send(sshuttle.sdnotify.ready())
@patch('sshuttle.sdnotify.os.environ.get')
def test_notify_no_message(mock_get):
mock_get.return_value = '/run/valid_path'
assert not sshuttle.sdnotify.send()
@patch('sshuttle.sdnotify.socket.socket')
@patch('sshuttle.sdnotify.os.environ.get')
def test_notify_socket_error(mock_get, mock_socket):
mock_get.return_value = '/run/valid_path'
mock_socket.side_effect = socket.error('test error')
assert not sshuttle.sdnotify.send(sshuttle.sdnotify.ready())
@patch('sshuttle.sdnotify.socket.socket')
@patch('sshuttle.sdnotify.os.environ.get')
def test_notify_sendto_error(mock_get, mock_socket):
message = sshuttle.sdnotify.ready()
socket_path = '/run/valid_path'
sock = Mock()
sock.sendto.side_effect = socket.error('test error')
mock_get.return_value = '/run/valid_path'
mock_socket.return_value = sock
assert not sshuttle.sdnotify.send(message)
assert sock.sendto.mock_calls == [
call(message, socket_path),
]
@patch('sshuttle.sdnotify.socket.socket')
@patch('sshuttle.sdnotify.os.environ.get')
def test_notify(mock_get, mock_socket):
messages = [sshuttle.sdnotify.ready(), sshuttle.sdnotify.status('Running')]
socket_path = '/run/valid_path'
sock = Mock()
sock.sendto.return_value = 1
mock_get.return_value = '/run/valid_path'
mock_socket.return_value = sock
assert sshuttle.sdnotify.send(*messages)
assert sock.sendto.mock_calls == [
call(b'\n'.join(messages), socket_path),
]

View File

@ -1,8 +1,9 @@
import os
import io
import socket
from mock import patch, Mock
import sshuttle.server
from mock import patch, Mock, call
def test__ipmatch():
@ -21,36 +22,9 @@ def test__maskbits():
sshuttle.server._maskbits(netmask)
@patch('sshuttle.server.which', side_effect=lambda x: x == 'netstat')
@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):
def test_listroutes_netstat(mock_popen, mock_which):
mock_pobj = Mock()
mock_pobj.stdout = io.BytesIO(b"""
Kernel IP routing table
@ -66,3 +40,21 @@ Destination Gateway Genmask Flags MSS Window irtt Iface
assert list(routes) == [
(socket.AF_INET, '192.168.1.0', 24)
]
@patch('sshuttle.server.which', side_effect=lambda x: x == 'ip')
@patch('sshuttle.server.ssubprocess.Popen')
def test_listroutes_iproute(mock_popen, mock_which):
mock_pobj = Mock()
mock_pobj.stdout = io.BytesIO(b"""
default via 192.168.1.1 dev wlan0 proto static
192.168.1.0/24 dev wlan0 proto kernel scope link src 192.168.1.1
""")
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)
]

View File

@ -0,0 +1,20 @@
from sshuttle.ssh import parse_hostport
def test_host_only():
assert parse_hostport("host") == (None, None, None, "host")
assert parse_hostport("1.2.3.4") == (None, None, None, "1.2.3.4")
assert parse_hostport("2001::1") == (None, None, None, "2001::1")
assert parse_hostport("[2001::1]") == (None, None, None, "2001::1")
def test_host_and_port():
assert parse_hostport("host:22") == (None, None, 22, "host")
assert parse_hostport("1.2.3.4:22") == (None, None, 22, "1.2.3.4")
assert parse_hostport("[2001::1]:22") == (None, None, 22, "2001::1")
def test_username_and_host():
assert parse_hostport("user@host") == ("user", None, None, "host")
assert parse_hostport("user:@host") == ("user", None, None, "host")
assert parse_hostport("user:pass@host") == ("user", "pass", None, "host")

24
tox.ini
View File

@ -1,20 +1,22 @@
[tox]
downloadcache = {toxworkdir}/cache/
envlist =
py26,
py27,
py34,
py35,
py36,
py37,
py38,
[testenv]
basepython =
py26: python2.6
py27: python2.7
py34: python3.4
py35: python3.5
py36: python3.6
py37: python3.7
py38: python3.8
commands =
py.test
deps =
pip install -e .
# actual flake8 test
flake8 sshuttle tests
# flake8 complexity warnings
flake8 sshuttle tests --exit-zero --max-complexity=10
pytest
mock
setuptools>=17.1
deps =
-rrequirements-tests.txt