Compare commits

...

469 Commits

Author SHA1 Message Date
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
2c20a1fd5a New release 2016-08-06 18:58:00 +10:00
915f72de35 Add changes for next release 2016-08-06 18:52:26 +10:00
1ffc3f52a1 Merge pull request #108 from vieira/pf-ipv6
IPv6 support for OSX and BSDs
2016-07-29 07:57:35 +10:00
8520ea2787 Use == instead of is to compare with AF_INET 2016-07-27 23:18:25 +00:00
6a394deaf2 Fixes missing comma from tuple in pf tests 2016-07-27 23:06:36 +00:00
83d5c59a57 Tests for IPv6 on pf 2016-07-27 22:17:02 +00:00
1cfd9eb9d7 Be more specific and consistent in some pf rules 2016-07-27 22:15:47 +00:00
f8d58fa4f0 IPv6 support for BSD and OSX
Adds IPv6 support for OpenBSD and OSX.
2016-07-24 22:04:29 +00:00
d2d5a37541 AF_INET6 is different between BSDs and Linux
AF_INET is the same constant on Linux and BSD but AF_INET6
is different. As the client and server can be running on
different platforms we can not just set the socket family
to what comes in the wire.
2016-07-24 22:02:17 +00:00
e9be2deea0 Exclude the IP where sshuttle is really listening
We were always excluding 127.0.0.1/8 but sshuttle might be listening on
other IP, e.g., ::1 for IPv6 or any other defined with -l
2016-07-24 21:58:20 +00:00
22b1b54bfd Add pytest-runner support 2016-07-10 11:26:32 +10:00
a43c668dde Fixes type mismatch between str and bytes
Should fix issue #104.
2016-07-09 22:49:12 +00:00
e0dfb95596 Fix OpenBSD pf test failure 2016-06-17 17:18:43 +08:00
5d28ce8272 Merge pull request #1 from vieira/patch-1
Add <forward_subnets> to divert rule in OpenBSD
2016-06-17 08:25:59 +08:00
f876c5db5e Add <forward_subnets> to divert rule in OpenBSD
Fixes bug where all traffic routed to loopback would end up being diverted to the same port.
2016-06-16 22:34:19 +01:00
2e1beefc9a Hack pf to enable multiple instances in Mac OS X 10.10 and above 2016-06-16 12:31:02 +08:00
5a20783baa tweak docs to match @vieira's changes 2016-05-02 21:40:53 -07:00
495b3c39ea Seed hosts without auto hosts
A possible implementation for the change requested in #94, so that seed
hosts can be used without auto hosts. In this scenario only the
specified hosts (or ips) will be looked up (or rev looked up).
2016-05-03 00:18:32 +00:00
f3cbc5018a Fix PEP8 issues 2016-04-30 18:08:46 +10:00
e73e797f33 Update files list 2016-04-30 18:05:47 +10:00
1d64879613 Fix tests 2016-04-23 13:19:06 +10:00
8fad282bfd Ensure locale is set to C for external commands
Otherwise the output can vary and confuse our attempts to parse it.

Fixes: 93
2016-04-23 12:53:45 +10:00
1dda9dd621 Add ENETUNREACH to NET_ERRS
We shouldn't come up with a fatal error because of a ENETUNREACH when
trying to contact the DNS server. Although this error shouldn't happen
either.

Fixes #89.
2016-04-20 15:18:59 +10:00
74e308a29f Don't mix tab and spaces in shell script
Sometime ago I was in python mode and incorrectly indented a line of the
shell script with spaces instead of tabs. Shame on me. This should bring
things back to their natural order.
2016-04-20 15:17:07 +10:00
516ff7bc4a Correctly obtains the python executable to use
Previously the sshuttle shell script would pass the python to use as the
first argument of the command. The new run script no longer does this.
Instead we can obtain the python being used via sys.executable.
Fixes #88.
2016-04-20 15:15:44 +10:00
89c5b57019 Attempt readthedocs workaround
readthedocs alters docs/conf.py which in turn means python_scm detects a
version and incorrectly adjusts the version number. Here we try to work
around this problem.

We do this by renaming the docs/conf.py file and copying it back to
docs/conf.py when setup.py is invoked. This way, hopefully, scm won't
see the changes to docs/conf.py

References:
http://stackoverflow.com/questions/35811267/readthedocs-and-setuptools-scm-version-wrong/36386177
https://github.com/pypa/setuptools_scm/issues/84
2016-04-18 11:44:05 +10:00
a8b288338b Release version 0.78.0 2016-04-08 12:01:37 +10:00
6fd02933dd Revert "Test for RTD"
This reverts commit 7ea2d973c7.
2016-04-08 12:00:45 +10:00
7ea2d973c7 Test for RTD 2016-04-06 11:28:41 +10:00
8ec5d1a5ac Update changes
In preparation for new release.
2016-04-05 21:14:50 +10:00
4241381d82 Backward compatibility with Python 2.4 (server)
It is often the case that the user has no administrative control over
the server that is being used. As such it is important to support as
many versions as possible, at least on the remote server end. These
fixes will allow sshuttle to be used with servers that have only
python 2.4 or python 2.6 installed while hopefully not breaking the
compatibility with 2.7 and 3.5.
2016-04-03 13:14:02 +10:00
6e15e69029 Support multiple subnet files (multiple -s options)
When passing multiple subnet files, e.g., by using -s/--subnets
multiple times or by using it together with subnets passed as positional
arguments append the content from all sources instead of only using the
subnets from the last source. This makes the behaviour of -s/--subnets
consistent with -x/--exclude.
2016-03-31 11:46:12 +11:00
8fa45885cc Remove --server option
As @brianmay observed in #82 this option is no longer used and can be
dropped.
2016-03-28 22:01:54 +00:00
b8160c4a37 Fix pep8 issues 2016-03-22 13:19:32 +11:00
05bacf6fd6 Use argparse for command line options
Fixes the kind of problems reported on #75 but does break the command
line "API" (hopefully).
2016-03-22 13:12:59 +11:00
dea3f21943 Write more server tests 2016-03-16 18:24:43 +11:00
d522d1e1bd Split client/server tests
This allows disabling all client tests using a conftest.py file, if for
example #56 gets merged and the server supports more python versions
then the server.

The server side tests are very incomplete.
2016-03-16 17:40:48 +11:00
3541e4bdfe Fix shell quoting
Due to nested shells, we need to have multiple layers of quoting. Yuck.

Closes #80
2016-03-16 16:38:22 +11:00
efdb9b8f94 If 3.5 not available, try to fallback to 2.7
In situations where 2.7 is available and some unsupported 3.x is the
system's default we should probably fallback to 2.7 instead of the
default (that might be e.g. 3.4). This might fix #78.
2016-03-16 16:16:53 +11:00
7875d1b97a Explicitly call /bin/sh for compatibility with non POSIX shells.
The fish shell doesn’t support ‘||’ and requires a ‘—python python’
workaround.  This change explicitly calls /bin/sh for the remote shell
commands.
2016-03-08 15:30:59 -08:00
2b0d0065c7 Don't force IPv6 if IPv6 name servers
Just because we may have found IPv6 DNS servers from /etc/resolv.conf
doesn't mean we should force IPv6 support.

Instead we should disable the IPv6 DNS servers if IPv6 is disabled.

Note: this will also result in any IPv6 servers specified on the command
line being silently ignored too.

Specifying an IPv6 subnet will still require IPv6 support.

Closes #74
2016-03-08 18:49:47 +11:00
9e3f02c199 Fix LGPL2 license. 2016-03-07 10:03:22 +11:00
8bdefcd10d Release 0.77.1 2016-03-07 09:46:01 +11:00
29b6e8301f Update GPL2 license text
Closes #73.
2016-03-06 17:27:02 +11:00
083293ea0d Version 0.77 2016-03-03 10:38:33 +11:00
2c07985924 Prepare documentation for release 2016-03-03 10:35:45 +11:00
756025b1bc Add date and version to docs 2016-03-03 10:30:20 +11:00
cedc8dc146 Add support for OpenBSD 2016-03-02 18:50:37 +11:00
e8047ce3a9 Fixed Python 3 issue 2016-03-02 18:38:43 +11:00
fae4cb1dbf Override the skip on lo that ends up in the chain
In some cases (see #43) it seems that some network configurations may
end up setting a skip on lo. As sshuttle adds rules that rely on
filtering/translating packets on lo, this causes problem. This fix
overrides the skip and makes the rules be applied again.
Should fix at least some of the problems reported on #43.
2016-03-02 18:36:14 +11:00
7d8309ef05 Refactor OS specific portions of PF
This will make it easier to support other platforms/versions in the
future, e.g., OpenBSD.
2016-03-02 18:04:43 +11:00
b7d37e44fb Remove legacy file 2016-03-02 12:47:45 +11:00
4a954c547a fix byte/string bug introduced in 1c46f25e
This is the error message that this commit fixes:
TypeError: sequence item 142: expected a bytes-like object, str found

Complete what 1c46f25e started, more or less.
2016-01-31 16:26:21 -05:00
4fcf7c73da Fix regression: ensure we do bind
Closes: #68
2016-01-31 19:15:02 +11:00
ba8e948c0d Don't allocate socket until we need it
Wew were trying to allocate an IPv6 socket even though we weren't using
IPv6, causing failures on systems without IPv6 support available.

This change means a number of methods on MultiListener, e.g. setsockopt,
should not be called until after the bind call.

Closes #68
2016-01-30 11:28:59 +11:00
e06f0240cb Make sure we use Python 3.5 2016-01-30 11:27:37 +11:00
517fc2c930 Remove references to number of years
Closes: #65
2016-01-21 08:42:34 +11:00
11533869a8 Fix description of excludes in Windows setup
And some subtle grammar.

Closes: #66
2016-01-21 08:38:38 +11:00
0392a779a2 Update usage documentation 2016-01-20 21:19:44 +11:00
ee26157faa Add Windows documentation
Copied from https://coderwall.com/p/adfxgw/sshuttle-on-windows

Closes #64
2016-01-20 20:55:10 +11:00
0bdfb883aa Don't distribute sshuttle/version.py
It is autogenerated.
2016-01-18 09:00:00 +11:00
ff9756f290 Release version 0.76 2016-01-17 18:38:43 +11:00
59865269ac Update documentation
Closes #60.
2016-01-17 18:34:10 +11:00
28017303f2 Add link to documentation 2016-01-17 18:20:42 +11:00
c5af6fef8c Remove table.
Suspect it is causing sphinx to crash on readthedocs. See
https://github.com/sphinx-doc/sphinx/issues/1871
2016-01-17 18:07:52 +11:00
6835183b37 Attempt work around of sphinx bug 2016-01-17 17:58:48 +11:00
242c266e7d Move recvmsg to requirements 2016-01-17 17:58:36 +11:00
7408ab3c53 Remove coverage
Not required as we are not documenting the source code.
2016-01-17 17:24:13 +11:00
ea10ff1305 Fix broken link. 2016-01-17 16:43:25 +11:00
d9939b8460 Add changelog to documentation 2016-01-17 16:37:47 +11:00
80f363842d Add requirements.txt for readthedocs 2016-01-17 16:32:53 +11:00
262affe94f Use Sphinx for documentation
See #60
2016-01-17 16:19:13 +11:00
d80b590a71 Fix joining of seed hosts to be compatible with python 3.5
this should also be backwards compatible with python 2
2016-01-17 12:05:23 +11:00
7f0b5c698b Fix installation from wheel
Fix the following error. Looks like we have to have a function to call
for the entrypoint.

$ pip install dist/sshuttle-0.76.dev8_ngf59508f-py2-none-any.whl
Processing ./dist/sshuttle-0.76.dev8_ngf59508f-py2-none-any.whl
Installing collected packages: sshuttle
Exception:
Traceback (most recent call last):
  File "/tmp/ddd/local/lib/python2.7/site-packages/pip/basecommand.py", line 211, in main
    status = self.run(options, args)
  File "/tmp/ddd/local/lib/python2.7/site-packages/pip/commands/install.py", line 311, in run
    root=options.root_path,
  File "/tmp/ddd/local/lib/python2.7/site-packages/pip/req/req_set.py", line 646, in install
    **kwargs
  File "/tmp/ddd/local/lib/python2.7/site-packages/pip/req/req_install.py", line 803, in install
    self.move_wheel_files(self.source_dir, root=root)
  File "/tmp/ddd/local/lib/python2.7/site-packages/pip/req/req_install.py", line 998, in move_wheel_files
    isolated=self.isolated,
  File "/tmp/ddd/local/lib/python2.7/site-packages/pip/wheel.py", line 479, in move_wheel_files
    maker.make_multiple(['%s = %s' % kv for kv in console.items()])
  File "/tmp/ddd/local/lib/python2.7/site-packages/pip/_vendor/distlib/scripts.py", line 364, in make_multiple
    filenames.extend(self.make(specification, options))
  File "/tmp/ddd/local/lib/python2.7/site-packages/pip/_vendor/distlib/scripts.py", line 353, in make
    self._make_script(entry, filenames, options=options)
  File "/tmp/ddd/local/lib/python2.7/site-packages/pip/_vendor/distlib/scripts.py", line 244, in _make_script
    script = self._get_script_text(entry).encode('utf-8')
  File "/tmp/ddd/local/lib/python2.7/site-packages/pip/wheel.py", line 396, in _get_script_text
    "import_name": entry.suffix.split(".")[0],
AttributeError: 'NoneType' object has no attribute 'split'
2016-01-17 10:21:21 +11:00
f59508f41b Be explicit
These files were included, however just to make sure.
2016-01-13 20:51:09 +11:00
329867a090 Move pyXAPI requirement 2016-01-13 19:34:20 +11:00
1e82571b3d Experimental: Use setuptools-scm
Closes: #58
2016-01-13 19:00:08 +11:00
aab973f12e Small grammar changes 2016-01-13 19:00:08 +11:00
d982b36521 Remove verbose debugging from example usage 2016-01-13 19:00:08 +11:00
a90877b49b Use correct rst formatting for inline monospace 2016-01-13 19:00:08 +11:00
fc5545ccde Remove a print from tproxy used for debug
Closes: #61
2016-01-13 19:00:08 +11:00
629c386dc5 Add the option to disable sshuttle ipv6 support
Using --disable-ipv6 will now force sshuttle not to capture
ipv6 traffic, even if the client supports ipv6.
2016-01-12 13:07:07 +11:00
45f572f7a8 Version 0.75 2016-01-12 12:29:08 +11:00
e7fe040e10 Revert "fixes the sshuttle entry-point in setup.py"
See #57 for details.

This reverts commit b4b283b214.
2016-01-12 12:25:04 +11:00
2e237b8fbe Remember to increment version. 2016-01-10 10:05:50 +11:00
098916a8de Version 0.74 2016-01-10 10:02:14 +11:00
d3624332dc Fix documentation.
Should work even with different python versions on client and server.
2016-01-10 10:01:47 +11:00
b4b283b214 fixes the sshuttle entry-point in setup.py
This fixes the following error:

    "import_name": entry.suffix.split(".")[0],
    AttributeError: 'NoneType' object has no attribute 'split'

See
https://pythonhosted.org/setuptools/setuptools.html#automatic-script-creation
2016-01-09 20:04:58 +00:00
1c46f25e13 Fixed str being used as bytes in hostwatch
This should solve the TypeError reported in #53 and some others I found
while testing the fix.

Closes: #53
2016-01-07 14:16:03 +11:00
11838d65c2 Adds support for FreeBSD PF
The PF firewall that is included in the FreeBSD base system does not
have exactly the same data structures as the OSX version. This commit
fixes the offsets and some field types that are also different. Tested
with FreeBSD 10.2 and OSX 10.11.2.
2016-01-05 18:00:57 +11:00
e433c599e4 IPv6 routes must be added manually 2015-12-15 14:26:39 +11:00
ba60d22478 Add another test. 2015-12-15 14:23:42 +11:00
3db38c992a Replace numbered points with dot points. 2015-12-15 14:23:19 +11:00
1e81bf3dfc Mirror setup/restore logic 2015-12-15 13:39:00 +11:00
7362ba9f52 If listenip_v6 we should declare ipv6 required 2015-12-15 13:31:03 +11:00
b207d1d0d6 Fixes for --auto-nets 2015-12-15 13:30:34 +11:00
56e3b22820 Add FIXME comment. 2015-12-15 13:29:04 +11:00
02fa49627f Fix server side Python3 issues.
Closes: #49.
2015-12-15 12:51:29 +11:00
ce5187100c Add to TPROXY documentation 2015-12-15 11:48:34 +11:00
bdc7d3a97c Fix UDP Python 3.5 issues.
Closes: #48
2015-12-15 11:41:48 +11:00
90654b4fb9 Simplify selection of features 2015-12-15 11:40:55 +11:00
6b4e36c528 Declare DNS support as feature 2015-12-14 21:00:31 +11:00
eed917f062 Don't declare udp feature without recvmsg 2015-12-14 20:59:26 +11:00
74f2d9ca7e Ensure Fatal errors are really Fatal 2015-12-14 20:51:49 +11:00
1e04eb1616 Updates to TPROXY docs. 2015-12-14 20:27:47 +11:00
117afc7a68 Fixed dictionary changed size during iteration
The removal loop should probably be outside the iteration loop.
2015-12-14 16:46:11 +11:00
c61984088b Test PF on non-darwin. 2015-12-14 09:28:43 +11:00
e63e121354 Print PF rules used.
Also support multiline debug output better.
2015-12-14 09:21:15 +11:00
2b235331d0 Split setup_firewall method.
* setup_firewall sets the firewall up.
* restore_firewall restores the firewall to initial state.
2015-12-13 11:56:18 +11:00
2eeea9536a Fixed str being used as bytes in daemonize 2015-12-09 16:32:39 +11:00
9a77d03edf Respect --syslog as soon as possible
When executing with the option --syslog start redirecting to
syslog immediately after the command line options are validated.
This way when using with some init daemon, e.g., upstart all the
relevant information (connection failures, etc) can be retrieved from
the log instead of being lost to stdout or stderr.
2015-12-09 14:46:11 +11:00
4fdd715bc1 Don't change object while iterating
Closes: #40
2015-12-09 10:29:40 +11:00
bea723c598 Add tox.ini file. 2015-12-07 13:17:09 +11:00
1ae4fce6b3 Fix logging with pf method and Python 3.5 2015-12-07 13:16:47 +11:00
118171af7f Fix get_tcp_dstip with MacOSX/Python3.5 2015-12-07 07:14:26 +11:00
3367124e6b Fix more brokenness. 2015-12-06 11:45:49 +11:00
aaa6062329 Remove IPFW support.
This is no longer used by modern MacOSX and not getting tested.

It also required a do_wait() function which was a complication for
sshuttle as a whole.

Can get resurrected if required.
2015-12-06 11:33:52 +11:00
da4ce19121 Fix MacOSX tests. 2015-12-06 11:24:38 +11:00
12d4b304c3 Fix another MacOSX/Python3.5 issue. 2015-12-06 11:24:11 +11:00
bd97506f7d Fixup firewall tests. 2015-12-06 11:02:31 +11:00
53c07f7d90 hostmap shouldn't be global. 2015-12-06 11:00:12 +11:00
7e0c1534df Be more explicit 2015-12-06 10:58:51 +11:00
a3fbf860ff Fix more MacOSX/Python3.5 issues. 2015-12-05 20:21:36 +11:00
7a9e36d211 Fix MacOSX/Python3.5 issues.
Closes: #36.
2015-12-05 16:41:33 +11:00
65e81d51c6 Try Python3.5 by default.
Python 3.0, 3.1, 3.2, and 3.4 not supported however.
2015-12-05 14:41:22 +11:00
43084eb49a Fix typo. 2015-12-05 14:40:33 +11:00
bbb4d31c3f Add accidentally removed line. 2015-12-05 14:39:07 +11:00
f7682d4c33 Make firewall messages consistent 2015-12-05 14:26:20 +11:00
d07a775d50 Don't fail if can't revert errors
We will log the errors, however no point in failing; not only can this
hide errors that occured setting up the firewall, but is pointless as we
can't actually handle these errors in a good way anyway.
2015-12-05 14:14:01 +11:00
50a6e87237 Don't use Xtoken if not set 2015-12-05 14:12:57 +11:00
ed0a92e714 Remove reference to obsolete global 2015-12-05 14:12:24 +11:00
36a1d7ead9 Python 3.5 fix. 2015-12-01 10:29:24 +11:00
43d6ad6a51 Print Python version used for the various stages. 2015-12-01 10:03:24 +11:00
5ab76a6ba9 Merge pull request #33 from felixonmars/master
Fix bug reported by @matiwinnetou in #31
2015-12-01 09:47:41 +11:00
61f9ae6fb4 Fix bug reported by @matiwinnetou in #31 2015-11-30 23:45:24 +08:00
191df92824 Ensure tempfiles are chmod 600 2015-11-28 16:13:56 +11:00
6dfbc467c0 Ensure verbose is never None.
None >= 1 not valid under Python3.

Fixes #31.
2015-11-28 16:03:01 +11:00
c06c972039 Prefer Python3 by default. 2015-11-28 16:02:47 +11:00
da62fe5b80 Merge pull request #30 from felixonmars/master
Add tests_require into setup.py
2015-11-27 20:01:30 +11:00
698351cf44 Add tests_require into setup.py
pytest and mock are needed for running tests.
2015-11-27 12:52:03 +08:00
13457c773b Improve summary line. 2015-11-27 14:28:52 +11:00
780997e8a7 New release 0.73. 2015-11-27 14:22:09 +11:00
d41579c265 Add comment about IPFW support. 2015-11-27 14:18:16 +11:00
974f9aee81 Remove legacy Debian packaging.
This needs to be redone; will do so at a later stage.
2015-11-27 14:13:45 +11:00
4252e81fb0 Update documentation. 2015-11-27 14:13:18 +11:00
7e10fc0756 Add to debugging messages. 2015-11-25 13:06:43 +11:00
2c2ee12e58 Formatting change. 2015-11-25 12:59:48 +11:00
256ed7d244 Fix reversed debug messages. 2015-11-25 12:59:17 +11:00
151634cd8c Fix typo setting up UDP. 2015-11-25 12:58:39 +11:00
c0748c2388 Support IPV6 DNS servers.
Closes #28.
2015-11-24 12:23:17 +11:00
71d46d77bf Add sock paramater to Handler callbacks
As Handler objects can have multiple sockets, we need to know which one
was involved in the incoming event.
2015-11-24 12:19:31 +11:00
c1083e983f Pass correct method back from firewall.
Don't pass auto back.
2015-11-24 12:08:12 +11:00
9944b97629 Remove legacy MACOSX files.
Broken and not been maintained in some time. See #21.
2015-11-24 07:17:19 +11:00
eaad54f68b Add FIXME comment. 2015-11-18 20:08:15 +11:00
6ebf76a5d8 Avoid hardcoding packed address lengths. 2015-11-18 20:07:41 +11:00
51eb7862c4 Fix tests under PyPy. 2015-11-18 20:07:03 +11:00
75b6865a1d Tests for pf method. 2015-11-17 20:52:31 +11:00
e3a1c56e54 Add more methods tests.
Fix bug in tproxy recv_udp() method.
2015-11-17 17:55:30 +11:00
99050aacb3 Fix for Python3.5. 2015-11-17 13:14:28 +11:00
021e6f57af Add more tests. 2015-11-17 13:08:12 +11:00
9cc6d63684 Fix firewall tests. 2015-11-17 12:46:35 +11:00
43566ebda6 Remove unused import. 2015-11-17 10:58:44 +11:00
537899c1df Remove unused function. 2015-11-17 10:58:29 +11:00
641a193d3d Use readline instead of next. 2015-11-17 09:39:53 +11:00
71d17e449e Disable Python 2.6 tests
importlib is Python 2.7 only.
2015-11-17 09:33:46 +11:00
9d443e4155 Don't use nested.
Is Python 2.x only. Not supported under Python 3.x.
2015-11-17 09:32:40 +11:00
cf0aaa7134 Fix PYTHONPATH for tests. 2015-11-17 09:28:58 +11:00
54de23aae3 Add firewall tests. 2015-11-17 09:19:20 +11:00
ac723694bf Restructure code
Pull out firewall methods code into seperate files.

Fix problems starting with method=='auto'; we were making decisions
based on the method, before the method had been finalized by the
firewall.

Only very basic testing so far. What could go wrong?
2015-11-16 18:55:56 +11:00
bcd3205db1 Fix passing latency_control to server. 2015-11-16 11:32:17 +11:00
a651d748cd Remove unused code. 2015-11-16 09:23:24 +11:00
fe48c7c026 Fix PEP8 issues. 2015-11-16 09:10:02 +11:00
4bd6ec8f01 Remove broken su fallback.
Was broken by passing environment variable PYTHONPATH to process. Will
fix this if there is a use case for it.
2015-11-16 09:09:02 +11:00
ba1cf58a6c Add Python 3.5 support. 2015-11-16 09:09:01 +11:00
dd8e68b6dc More formatting fixes. 2015-11-15 17:17:16 +11:00
1f2117917f Fix up formatting. 2015-11-15 17:10:04 +11:00
e6f2395dac Fixup PEP8 issues. 2015-11-15 16:49:20 +11:00
d4f10b232a Restructure code
* Make compatible with setuptools.
* Load modules via ssh into separate modules, not the one name space.
2015-11-15 16:45:26 +11:00
41b8ad4c97 Merge pull request #25 from vieira/ns-hosts
Import resolvconf_nameservers, fix wrong types
2015-11-11 13:09:59 +11:00
a82224c141 Import resolvconf_nameservers, fix wrong types
Add resolvconf_nameservers to the list of functions imported from
helpers.
Fixed an instance where the method client.main was being called with
ns_hosts (string obtained from optional argument --ns-hosts) instead of
nslist (list of tuples that was already being passed to other methods).
Should fix issue #24.
2015-11-08 01:27:10 +00:00
0fb714893a Merge pull request #23 from vieira/ns-hosts
dns: Added --ns-hosts to tunnel only some requests
2015-10-29 13:13:59 +11:00
28be71ef9a Removed commented out code 2015-10-27 17:53:35 +00:00
d2ee34d71c dns: Added --ns-hosts to tunnel only some requests
By default, the --dns flag configures the firewall to only intercept
queries made to the nameservers defined in resolvconf. This flag enables
the user to explicitly specify the nameservers which queries will be
redirected. This can be useful when the local nameserver forwards
queries to some domains to a nameserver on the remote site of the
tunnel.
2015-10-27 17:28:52 +00:00
3cf5002b62 Merge pull request #19 from naclander/patch-1
Remove no-latency-control assertion
2015-09-16 13:08:23 +10:00
f71704f54d Remove no-latency-control assertion
Remove an assertion that would fail when --no-latency-control is set.
2015-09-15 19:30:34 -07:00
ad83059da8 Merge pull request #17 from elasticdog/master
Update ui-macos sources.list with new icon names
2015-09-08 12:00:50 +10:00
d211fc28ee Update ui-macos sources.list with new icon names
I looks like building the app UI for OS X has been broken since
9eced8d049
due to the sources.list.do file still referencing the old .png images.

Without this fix the build will stop at:

    do            chicken-tiny.png
    do: Users/elasticdog/sshuttle/src/ui-macos/chicken-tiny.png: no .do file
    do:         Sshuttle VPN.app: got exit code 1
    do:       Sshuttle VPN.app.zip: got exit code 1
    do:     dist: got exit code 1
    do:   ui-macos/all: got exit code 1
    do: all: got exit code 1
2015-09-07 18:22:30 -07:00
f4dac68dc0 Merge pull request #16 from prutschman/localhost_fix
Don't redirect excluded subsets of included subnets
2015-09-04 17:30:26 +10:00
3a73520310 Don't redirect excluded subsets of included subnets 2015-09-03 21:25:23 -07:00
e127aab776 Merge pull request #14 from reactormonk/patch-1
Switched the ./sshuttle to src/sshuttle
2015-07-26 11:15:56 +10:00
5f90ee1f04 Switched the ./sshuttle to src/sshuttle 2015-07-25 17:38:42 +02:00
d70b5f2b89 Merge pull request #13 from shaiay/master
Fixed issue #12
2015-07-23 08:08:09 +10:00
3f2de26f67 Fixed issue #12
family should be an integer. fixed parsing routes
2015-07-22 22:52:25 +03:00
53d5260f8f Merge pull request #11 from douglas/master
Fix the excludes rule for OS X Yosemite and OS X El Captain
2015-06-19 17:34:14 +10:00
f870ceba00 Fix the excludes rule for OS X Yosemite and OS X El Captain
Without this fix, the rdr rule is executed sending the packages that
should be excluded to the ssh tunnel.

What I did was make sure that the packages that are going to the
excluded subnets are processed first and only after that, the remaining
packages will be sent to the ssh tunnel.

Thanks Warr1024 on #openssh channel in freenode for telling me about
the quick keyword and the rest of guys in the channel who tried to help.
2015-06-18 18:09:18 -03:00
a38963301e Merge pull request #10 from jbd/patch-1
Check for fileno attribute in _tty_width function
2015-05-13 11:01:05 +10:00
jbd
bbd54e150d Check for fileno attribute in _tty_width function
When using Options parser within a unittest.TextTestRunner with buffering enabled (buffer=True), it fails with: 
AttributeError: StringIO instance has no attribute 'fileno'

This change will prevent this kind of error.
2015-05-12 16:43:38 +02:00
00f20657e3 Merge pull request #9 from scommab/patch-1
Make firewall.py use the right params for islocal
2015-05-03 14:42:23 +10:00
84b30be904 Make firewall.py use the right params for islocal 2015-05-01 23:36:08 -07:00
5825dddb02 Merge pull request #8 from nanoant/patch/osx-improvements
Patch/osx improvements
2015-04-21 09:49:18 +10:00
9eced8d049 OSX: New Retina compatible menu & app icon
This icons are using scale independent PDF template images which make menu item
icon look great on both Yosemite light & dark theme. Also adding new flatter
and higher resolution app icon.
2015-04-20 20:32:02 +02:00
fecb53413d OSX: Remove status item on application quit
This ensures application is not leaving empty status item.
2015-04-20 20:23:20 +02:00
1b1ed4d495 OSX: Improve app startup time
Importing everything (*) from AppKit takes a while, since we got 3 scripts
doing that, startup could take up to few seconds. This change makes script
import only what they need, improving startup time to fraction of second.
2015-04-20 18:53:06 +02:00
b19272a67a Merge pull request #7 from xtaran/master
Fix the most blatant issues of the generated .deb
2015-04-20 10:39:55 +10:00
bc2a0b7fbc Fix path to main.py in .deb 2015-04-18 14:46:55 +02:00
6a96ace497 autossh is not required but nice to have 2015-04-18 14:40:00 +02:00
163aab2ca1 Fix typo in long description of .deb 2015-04-18 14:31:30 +02:00
964977220e Change .deb section from utils to net 2015-04-18 14:30:35 +02:00
db67834164 Add missing dependency on iptables in the .deb 2015-04-18 14:30:30 +02:00
1bc2f84d16 Use a less confusing version for .deb packages built from git 2015-04-18 14:30:30 +02:00
a229fc020c Properly separate short and long description in .deb 2015-04-18 14:30:30 +02:00
d6e7a9b6ad Update homepage header of .deb 2015-04-18 14:30:30 +02:00
e6ca7148fa Fix formatting of versioned dependency in the .deb 2015-04-18 14:30:21 +02:00
95529a5137 Don't include MacOS X stuff in .deb 2015-04-18 14:30:21 +02:00
93c4af6fc8 There's no need to have hard dependency on a init system in the .deb
Otherwise the package is only installable on current Ubuntu releases
and neither on future Ubuntu releases (which will use systemd) nor on
Debian and other Debian derivatives (where the administrator can
decided which init system is used).
2015-04-18 14:30:16 +02:00
2ca9aaa450 The .deb is and needs to be architecture-independent 2015-04-18 14:14:33 +02:00
2cfc39fac8 Fix UDP channel expiration. 2015-04-12 09:59:49 +10:00
29819ea0af Merge pull request #6 from lkorth/patch-1
Fix clone url in README
2015-04-11 10:15:20 +10:00
e43a40565b Fix clone url in README 2015-04-09 09:22:31 -07:00
57d1cb1e11 Merge pull request #5 from seanzxx/yosemite_support_sudo_fix
fix sudo issue in yosemite
2015-03-23 14:11:16 +11:00
6e32d1445a add -e/-d support 2015-03-21 22:43:12 -07:00
bdad253ef5 fix mistake 2015-03-21 15:36:42 -07:00
49c55f6825 use -E/-X to enable/disable pf on yosemite 2015-03-21 15:28:17 -07:00
1874aaceb4 refine firewall initlization 2015-03-21 00:00:15 -07:00
4c31bc02a4 add anchor rule directly 2015-03-20 18:21:00 -07:00
84047089a9 fix sudo issue 2015-03-19 02:43:11 -07:00
8be9270fdb Merge pull request #4 from seanzxx/yosemite_support
Yosemite support
2015-03-19 09:55:39 +11:00
10dc229125 fix bootstrapping issue when pf started before 2015-03-18 09:25:41 -07:00
cd77ad5e7b refine error message 2015-03-15 22:53:08 -07:00
c13cb9b8ca optimize the ctypes import 2015-03-15 22:45:32 -07:00
0fe48a4682 initial support for pf in yosemite 2015-03-15 22:34:40 -07:00
6121a6dca3 sshuttle.md: fix whitespace issues. 2014-12-16 14:06:13 +11:00
c576682caf sshuttle.md: document Internet Sharing incompatibility 2014-12-16 14:04:25 +11:00
343905784b Added --exclude-from feature.
(Slightly modified by apenwarr)
2014-10-06 13:04:33 +11:00
91d705c24f Document missing --dns option in sshuttle manpage 2014-10-06 13:01:31 +11:00
e5251969b0 firewall.py: catch SIGINT and SIGTERM too.
There were still a few conditions under some OSes that would cause
firewall.py to terminate without cleaning up the firewall settings.  'pkill
sshuttle' was one of them.  Ignore a couple more signals to further ensure a
correct cleanup.

(This only affects sshuttle --firewall, which is a subprocess of the main
sshuttle process.  The firewall is supposed to exit automatically whenever
the client exits, and so far that part seems to work reliably.)
2014-10-06 13:00:57 +11:00
b8e150fc4d Use python-config to compile with latest Python version.
For OS X systems without Python 2.5, runpython.c does not compile.
Use python-config to get the paths for the latest version.
2014-10-03 14:58:26 -07:00
36378efe5e Revert Debian package specific change.
This is required so sshuttle can be run from git repository.

The way the Debian package is created is non-standard, and probably
needs redoing anyway.
2014-09-23 11:14:56 +10:00
cba8b261c6 Use the new arguments from redo v0.10.
(apenwarr: also updates to the matching, latest minimal/do)
2014-09-23 10:14:59 +10:00
39425a03c5 firewall: catch SIGHUP and SIGPIPE.
Not sure if this will fix anything, but it might stop the problem reported
on some MacOS versions where the firewall doesn't get cleaned up correctly.
2014-09-23 10:14:27 +10:00
5a39341d50 ui-macos/main.py: fix wait() to avoid deadlock.
If the subprocess was trying to write to its stdout/stderr, its process
would never actually finish because it was blocked waiting for us to read
it, but we were blocked on waitpid().  Instead, use waitpid(WNOHANG) and
continually read from the subprocess (which should be a blocking operation)
until it exits.
2014-09-23 10:11:13 +10:00
3eef3635ac ipfw: don't use 'log' parameter.
I guess we were causing the kernel to syslog on every single packet on
MacOS.  Oops.
2014-09-23 10:09:16 +10:00
f1c79c7e92 PEP8 fixes. 2014-09-16 10:24:16 +10:00
5529a04cc9 Fix whitespace. 2014-09-15 14:46:45 +10:00
035c5ad7a6 Fix: Use sock for consistency. 2014-09-15 14:44:07 +10:00
c013386ecb If IPv4 bind but IPv6 succeeds don't error. 2014-09-15 14:32:59 +10:00
a33f6199c4 Remove broken IPv6 code. 2014-09-15 14:23:09 +10:00
0f2c249e4d Remove dodgy code. 2014-09-15 14:14:52 +10:00
192e5b36e8 Added some Ubuntu notes 2014-09-15 14:14:52 +10:00
4036b7dfcf Added some requirements 2014-09-15 14:14:52 +10:00
8ec6daf02a Added a shell script to make a .deb package 2014-09-15 14:14:52 +10:00
e2507f86d5 Added a control file for the Debian package 2014-09-15 14:14:52 +10:00
e4fe62de3c Added a sample prefixes file 2014-09-15 14:14:52 +10:00
734f32d112 Sample tunnel configuration 2014-09-15 14:14:52 +10:00
a34e106b55 Changed the sshuttle binary to point to install 2014-09-15 14:14:52 +10:00
e6e80f1f04 Changed the file to be more "canonical" 2014-09-15 14:14:52 +10:00
32865bd2dd Added the PyXAPI requirement to the readme 2014-09-15 14:14:52 +10:00
2f11f50bc2 Adding more robust exit codes 2014-09-15 14:14:52 +10:00
a95491765d Added -s to accept subnets from a config file 2014-09-15 14:14:52 +10:00
d8754dc3a0 First version; still has debugging 2014-09-15 14:14:52 +10:00
3956a5df94 Moved docs out of the src directory 2014-09-15 14:14:52 +10:00
7442eb61e9 Mass relocation of files to their own subdirectory 2014-09-15 14:14:51 +10:00
6107abf10f Fixed a bug where lack of IPv6 destination = fatal
There was a problem where trying to bind .v4 and .v6 listeners would set them
to None if there was nothing to bind (if, say, you weren't binding an IPv6
listener).  Later, the code then would try to call a member function of the
listener.  The member function would not do anything if there was no listener,
but trying to dereference None yielded the broken behavior.
2014-09-15 14:14:51 +10:00
5e8ad544ee TProxy UDP support, including DNS. 2011-08-26 09:53:59 +10:00
20254bab57 TProxy IPv6 support. 2011-07-11 11:20:52 +10:00
f41c6b62e5 TProxy support as well as NAT support. 2011-07-11 11:20:51 +10:00
9a7412c08f More changes to simplify the upcomming IPv6 patch. 2011-07-11 11:20:50 +10:00
c6200eecdc Choose which method to use for intercepting traffic. 2011-07-11 11:16:51 +10:00
55f86a8b3f Rewrite binding code. DNS port may now be different from TCP port. 2011-07-11 11:16:50 +10:00
e7caae8126 Make it clear ports are for IPv4. 2011-07-11 11:16:49 +10:00
4db9b372c2 Make iptables functions work with any table, not just nat. 2011-07-11 11:16:48 +10:00
061e6a0933 Keep track of address family address belongs too. 2011-06-16 14:51:34 +10:00
50849b86b0 This hack is IPv4 specific, ensure it doesn't get used for other
address families.
2011-06-16 14:51:34 +10:00
6b7cf770f5 Improve debugging. 2011-06-16 14:42:15 +10:00
b26e1ee652 Introduce independent_listener, will be used for both IPv4 and IPv6
connections.
2011-06-06 11:14:28 +10:00
6500067905 When DNS response received, MUX channel no longer required. Delete it. 2011-06-06 11:12:23 +10:00
50c2b86f15 Rename onaccept to onaccept_tcp as it is tcp specific. 2011-06-06 10:54:57 +10:00
97dca42291 Rename dnslistener to dns_listener for consistency with tcp_listener. 2011-06-06 10:44:38 +10:00
6e53b07002 Rename listener to tcp_listener, as it is TCP specific. 2011-06-06 10:43:39 +10:00
08bd1dca46 Rename TCP specific commands to clarify they are TCP specific. 2011-06-06 10:39:50 +10:00
94566b5efc Split expiration into handling into another function. 2011-06-06 10:23:04 +10:00
111 changed files with 7872 additions and 7269 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

11
.gitignore vendored
View File

@ -1,6 +1,17 @@
/sshuttle/version.py
/tmp/
/.cache/
/.eggs/
/.tox/
/build/
/dist/
/sshuttle.egg-info/
/docs/_build/
*.pyc
*~
*.8
/.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

261
CHANGES.rst Normal file
View File

@ -0,0 +1,261 @@
==========
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.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.
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.
* Use argparse for command line processing. Fixes #75.
* Remove useless --server option.
* Support multiple -s (subnet) options. Fixes #86.
* Make server parts work with old versions of Python. Fixes #81.
0.77.2 - 2016-03-07
-------------------
* Accidentally switched LGPL2 license with GPL2 license in 0.77.1 - now fixed.
0.77.1 - 2016-03-07
-------------------
* Use semantic versioning. http://semver.org/
* Update GPL 2 license text.
* New release to fix PyPI.
0.77 - 2016-03-03
-----------------
* Various bug fixes.
* Fix Documentation.
* Add fix for MacOS X issue.
* Add support for OpenBSD.
0.76 - 2016-01-17
-----------------
* Add option to disable IPv6 support.
* Update documentation.
* Move documentation, including man page, to Sphinx.
* Use setuptools-scm for automatic versioning.
0.75 - 2016-01-12
-----------------
* Revert change that broke sshuttle entry point.
0.74 - 2016-01-10
-----------------
* Add CHANGES.rst file.
* Numerous bug fixes.
* Python 3.5 fixes.
* PF fixes, especially for BSD.

20
LICENSE
View File

@ -1,15 +1,15 @@
GNU LIBRARY GENERAL PUBLIC LICENSE
Version 2, June 1991
GNU LIBRARY GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1991 Free Software Foundation, Inc.
675 Mass Ave, Cambridge, MA 02139, USA
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
[This is the first released version of the library GPL. It is
numbered 2 because it goes with version 2 of the ordinary GPL.]
Preamble
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
@ -99,7 +99,7 @@ works together with the library.
Note that it is possible for a library to be covered by the ordinary
General Public License rather than by this special one.
GNU LIBRARY GENERAL PUBLIC LICENSE
GNU LIBRARY GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library which
@ -411,7 +411,7 @@ decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
@ -434,9 +434,9 @@ FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
END OF TERMS AND CONDITIONS
END OF TERMS AND CONDITIONS
Appendix: How to Apply These Terms to Your New Libraries
How to Apply These Terms to Your New Libraries
If you develop a new library, and you want it to be of the greatest
possible use to the public, we recommend making it free software that
@ -463,8 +463,8 @@ convey the exclusion of warranty; and each file should have at least the
Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the Free
Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Also add information on how to contact you by electronic and paper mail.

14
MANIFEST.in Normal file
View File

@ -0,0 +1,14 @@
include *.txt
include *.rst
include *.py
include MANIFEST.in
include LICENSE
include run
include tox.ini
exclude sshuttle/version.py
recursive-include docs *.bat
recursive-include docs *.py
recursive-include docs *.rst
recursive-include docs Makefile
recursive-include sshuttle *.py
recursive-exclude docs/_build *

View File

@ -1,10 +0,0 @@
all:
Makefile:
@
%: FORCE
+./do $@
.PHONY: FORCE

189
README.md
View File

@ -1,189 +0,0 @@
WARNING:
On MacOS 10.6 (at least up to 10.6.6), your network will
stop responding about 10 minutes after the first time you
start sshuttle, because of a MacOS kernel bug relating to
arp and the net.inet.ip.scopedroute sysctl. To fix it,
just switch your wireless off and on. Sshuttle makes the
kernel setting it changes permanent, so this won't happen
again, even after a reboot.
sshuttle: where transparent proxy meets VPN meets ssh
=====================================================
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.
- You have access to a remote network via ssh.
- You don't necessarily have admin access on the remote network.
- The remote network has no VPN, or only stupid/complex VPN
protocols (IPsec, PPTP, etc). Or maybe you <i>are</i> the
admin and you just got frustrated with the awful state of
VPN tools.
- You don't want to create an ssh port forward for every
single host/port on the remote network.
- You hate openssh's port forwarding because it's randomly
slow and/or stupid.
- 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).
Prerequisites
-------------
- sudo, su, or logged in as root on your client machine.
(The server doesn't need admin access.)
- If you use Linux on your client machine:
iptables installed on the client, including at
least the iptables DNAT, REDIRECT, and ttl modules.
These are installed by default on most Linux distributions.
(The server doesn't need iptables and doesn't need to be
Linux.)
- If you use MacOS or BSD on your client machine:
Your kernel needs to be compiled with IPFIREWALL_FORWARD
(MacOS has this by default) and you need to have ipfw
available. (The server doesn't need to be MacOS or BSD.)
This is how you use it:
-----------------------
- <tt>git clone git://github.com/apenwarr/sshuttle</tt>
on your client machine. You'll need root or sudo
access, and python needs to be installed.
- The most basic use of sshuttle looks like:
<tt>./sshuttle -r username@sshserver 0.0.0.0/0 -vv</tt>
- There is a shortcut for 0.0.0.0/0 for those that value
their wrists
<tt>./sshuttle -r username@sshserver 0/0 -vv</tt>
- If you would also like your DNS queries to be proxied
through the DNS server of the server you are connect to:
<tt>./sshuttle --dns -vvr username@sshserver 0/0</tt>
The above is probably what you want to use to prevent
local network attacks such as Firesheep and friends.
(You may be prompted for one or more passwords; first, the
local password to become root using either sudo or su, and
then the remote ssh password. Or you might have sudo and ssh set
up to not require passwords, in which case you won't be
prompted at all.)
That's it! Now your local machine can access the remote network as if you
were right there. And if your "client" machine is a router, everyone on
your local network can make connections to your remote network.
You don't need to install sshuttle on the remote server;
the remote server just needs to have python available.
sshuttle will automatically upload and run its source code
to the remote python interpreter.
This creates a transparent proxy server on your local machine for all IP
addresses that match 0.0.0.0/0. (You can use more specific IP addresses if
you want; use any number of IP addresses or subnets to change which
addresses get proxied. Using 0.0.0.0/0 proxies <i>everything</i>, which is
interesting if you don't trust the people on your local network.)
Any TCP session you initiate to one of the proxied IP addresses will be
captured by sshuttle and sent over an ssh session to the remote copy of
sshuttle, which will then regenerate the connection on that end, and funnel
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.
Theory of Operation
-------------------
sshuttle is not exactly a VPN, and not exactly port forwarding. It's kind
of both, and kind of neither.
It's like a VPN, since it can forward every port on an entire network, not
just ports you specify. Conveniently, it lets you use the "real" IP
addresses of each host rather than faking port numbers on localhost.
On the other hand, the way it *works* is more like ssh port forwarding than
a VPN. Normally, a VPN forwards your data one packet at a time, and
doesn't care about individual connections; ie. it's "stateless" with respect
to the traffic. sshuttle is the opposite of stateless; it tracks every
single connection.
You could compare sshuttle to something like the old <a
href="http://en.wikipedia.org/wiki/Slirp">Slirp</a> program, which was a
userspace TCP/IP implementation that did something similar. But it
operated on a packet-by-packet basis on the client side, reassembling the
packets on the server side. That worked okay back in the "real live serial
port" days, because serial ports had predictable latency and buffering.
But you can't safely just forward TCP packets over a TCP session (like ssh),
because TCP's performance depends fundamentally on packet loss; it
<i>must</i> experience packet loss in order to know when to slow down! At
the same time, the outer TCP session (ssh, in this case) is a reliable
transport, which means that what you forward through the tunnel <i>never</i>
experiences packet loss. The ssh session itself experiences packet loss, of
course, but TCP fixes it up and ssh (and thus you) never know the
difference. But neither does your inner TCP session, and extremely screwy
performance ensues.
sshuttle assembles the TCP stream locally, multiplexes it statefully over
an ssh session, and disassembles it back into packets at the other end. So
it never ends up doing TCP-over-TCP. It's just data-over-TCP, which is
safe.
Useless Trivia
--------------
Back in 1998 (12 years ago! Yikes!), I released the first version of <a
href="http://alumnit.ca/wiki/?TunnelVisionReadMe">Tunnel Vision</a>, a
semi-intelligent VPN client for Linux. Unfortunately, I made two big mistakes:
I implemented the key exchange myself (oops), and I ended up doing
TCP-over-TCP (double oops). The resulting program worked okay - and people
used it for years - but the performance was always a bit funny. And nobody
ever found any security flaws in my key exchange, either, but that doesn't
mean anything. :)
The same year, dcoombs and I also released Fast Forward, a proxy server
supporting transparent proxying. Among other things, we used it for
automatically splitting traffic across more than one Internet connection (a
tool we called "Double Vision").
I was still in university at the time. A couple years after that, one of my
professors was working with some graduate students on the technology that
would eventually become <a href="http://www.slipstream.com/">Slipstream
Internet Acceleration</a>. He asked me to do a contract for him to build an
initial prototype of a transparent proxy server for mobile networks. The
idea was similar to sshuttle: if you reassemble and then disassemble the TCP
packets, you can reduce latency and improve performance vs. just forwarding
the packets over a plain VPN or mobile network. (It's unlikely that any of
my code has persisted in the Slipstream product today, but the concept is
still pretty cool. I'm still horrified that people use plain TCP on
complex mobile networks with crazily variable latency, for which it was
never really intended.)
That project I did for Slipstream was what first gave me the idea to merge
the concepts of Fast Forward, Double Vision, and Tunnel Vision into a single
program that was the best of all worlds. And here we are, at last, 10 years
later. You're welcome.
--
Avery Pennarun <apenwarr@gmail.com>
Mailing list:
Subscribe by sending a message to <sshuttle+subscribe@googlegroups.com>
List archives are at: http://groups.google.com/group/sshuttle

103
README.rst Normal file
View File

@ -0,0 +1,103 @@
sshuttle: where transparent proxy meets VPN meets ssh
=====================================================
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.
- You have access to a remote network via ssh.
- You don't necessarily have admin access on the remote network.
- The remote network has no VPN, or only stupid/complex VPN
protocols (IPsec, PPTP, etc). Or maybe you *are* the
admin and you just got frustrated with the awful state of
VPN tools.
- You don't want to create an ssh port forward for every
single host/port on the remote network.
- You hate openssh's port forwarding because it's randomly
slow and/or stupid.
- 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`_.
.. _terrible performance: https://sshuttle.readthedocs.io/en/stable/how-it-works.html
Obtaining 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::
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:
https://sshuttle.readthedocs.org/
The documentation for the latest development version is available at:
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

11
all.do
View File

@ -1,11 +0,0 @@
exec >&2
UI=
[ "$(uname)" = "Darwin" ] && UI=ui-macos/all
redo-ifchange sshuttle.8 $UI
echo
echo "What now?"
[ -z "$UI" ] || echo "- Try the MacOS GUI: open ui-macos/Sshuttle*.app"
echo "- Run sshuttle: ./sshuttle --dns -r HOSTNAME 0/0"
echo "- Read the README: less README.md"
echo "- Read the man page: less sshuttle.md"

View File

@ -1,26 +0,0 @@
import sys, zlib
z = zlib.decompressobj()
mainmod = sys.modules[__name__]
while 1:
name = sys.stdin.readline().strip()
if name:
nbytes = int(sys.stdin.readline())
if verbosity >= 2:
sys.stderr.write('server: assembling %r (%d bytes)\n'
% (name, nbytes))
content = z.decompress(sys.stdin.read(nbytes))
exec compile(content, name, "exec")
# FIXME: this crushes everything into a single module namespace,
# then makes each of the module names point at this one. Gross.
assert(name.endswith('.py'))
modname = name[:-3]
mainmod.__dict__[modname] = mainmod
else:
break
verbose = verbosity
sys.stderr.flush()
sys.stdout.flush()
main()

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,2 +0,0 @@
redo ui-macos/clean
rm -f *~ */*~ .*~ */.*~ *.8 *.tmp */*.tmp *.pyc */*.pyc

400
client.py
View File

@ -1,400 +0,0 @@
import struct, socket, select, errno, re, signal, time
import compat.ssubprocess as ssubprocess
import helpers, ssnet, ssh, ssyslog
from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
from helpers import *
_extra_fd = os.open('/dev/null', os.O_RDONLY)
def got_signal(signum, frame):
log('exiting on signal %d\n' % signum)
sys.exit(1)
_pidname = None
def check_daemon(pidfile):
global _pidname
_pidname = os.path.abspath(pidfile)
try:
oldpid = open(_pidname).read(1024)
except IOError, e:
if e.errno == errno.ENOENT:
return # no pidfile, ok
else:
raise Fatal("can't read %s: %s" % (_pidname, e))
if not oldpid:
os.unlink(_pidname)
return # invalid pidfile, ok
oldpid = int(oldpid.strip() or 0)
if oldpid <= 0:
os.unlink(_pidname)
return # invalid pidfile, ok
try:
os.kill(oldpid, 0)
except OSError, e:
if e.errno == errno.ESRCH:
os.unlink(_pidname)
return # outdated pidfile, ok
elif e.errno == errno.EPERM:
pass
else:
raise
raise Fatal("%s: sshuttle is already running (pid=%d)"
% (_pidname, oldpid))
def daemonize():
if os.fork():
os._exit(0)
os.setsid()
if os.fork():
os._exit(0)
outfd = os.open(_pidname, os.O_WRONLY|os.O_CREAT|os.O_EXCL, 0666)
try:
os.write(outfd, '%d\n' % os.getpid())
finally:
os.close(outfd)
os.chdir("/")
# Normal exit when killed, or try/finally won't work and the pidfile won't
# be deleted.
signal.signal(signal.SIGTERM, got_signal)
si = open('/dev/null', 'r+')
os.dup2(si.fileno(), 0)
os.dup2(si.fileno(), 1)
si.close()
ssyslog.stderr_to_syslog()
def daemon_cleanup():
try:
os.unlink(_pidname)
except OSError, e:
if e.errno == errno.ENOENT:
pass
else:
raise
def original_dst(sock):
try:
SO_ORIGINAL_DST = 80
SOCKADDR_MIN = 16
sockaddr_in = sock.getsockopt(socket.SOL_IP,
SO_ORIGINAL_DST, SOCKADDR_MIN)
(proto, port, a,b,c,d) = struct.unpack('!HHBBBB', sockaddr_in[:8])
assert(socket.htons(proto) == socket.AF_INET)
ip = '%d.%d.%d.%d' % (a,b,c,d)
return (ip,port)
except socket.error, e:
if e.args[0] == errno.ENOPROTOOPT:
return sock.getsockname()
raise
class FirewallClient:
def __init__(self, port, subnets_include, subnets_exclude, dnsport):
self.port = port
self.auto_nets = []
self.subnets_include = subnets_include
self.subnets_exclude = subnets_exclude
self.dnsport = dnsport
argvbase = ([sys.argv[1], sys.argv[0], sys.argv[1]] +
['-v'] * (helpers.verbose or 0) +
['--firewall', str(port), str(dnsport)])
if ssyslog._p:
argvbase += ['--syslog']
argv_tries = [
['sudo', '-p', '[local sudo] Password: '] + argvbase,
['su', '-c', ' '.join(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.
# Instead, attach a *bidirectional* socket to its stdout, and use
# that for talking in both directions.
(s1,s2) = socket.socketpair()
def setup():
# run in the child process
s2.close()
e = None
if os.getuid() == 0:
argv_tries = argv_tries[-1:] # last entry only
for argv in argv_tries:
try:
if argv[0] == 'su':
sys.stderr.write('[local su] ')
self.p = ssubprocess.Popen(argv, stdout=s1, preexec_fn=setup)
e = None
break
except OSError, e:
pass
self.argv = argv
s1.close()
self.pfile = s2.makefile('wb+')
if e:
log('Spawning firewall manager: %r\n' % self.argv)
raise Fatal(e)
line = self.pfile.readline()
self.check()
if line != 'READY\n':
raise Fatal('%r expected READY, got %r' % (self.argv, line))
def check(self):
rv = self.p.poll()
if rv:
raise Fatal('%r returned %d' % (self.argv, rv))
def start(self):
self.pfile.write('ROUTES\n')
for (ip,width) in self.subnets_include+self.auto_nets:
self.pfile.write('%d,0,%s\n' % (width, ip))
for (ip,width) in self.subnets_exclude:
self.pfile.write('%d,1,%s\n' % (width, ip))
self.pfile.write('GO\n')
self.pfile.flush()
line = self.pfile.readline()
self.check()
if line != 'STARTED\n':
raise Fatal('%r expected STARTED, got %r' % (self.argv, line))
def sethostip(self, hostname, ip):
assert(not re.search(r'[^-\w]', hostname))
assert(not re.search(r'[^0-9.]', ip))
self.pfile.write('HOST %s,%s\n' % (hostname, ip))
self.pfile.flush()
def done(self):
self.pfile.close()
rv = self.p.wait()
if rv:
raise Fatal('cleanup: %r returned %d' % (self.argv, rv))
def onaccept(listener, mux, handlers):
global _extra_fd
try:
sock,srcip = listener.accept()
except socket.error, e:
if e.args[0] in [errno.EMFILE, errno.ENFILE]:
debug1('Rejected incoming connection: too many open files!\n')
# free up an fd so we can eat the connection
os.close(_extra_fd)
try:
sock,srcip = listener.accept()
sock.close()
finally:
_extra_fd = os.open('/dev/null', os.O_RDONLY)
return
else:
raise
dstip = original_dst(sock)
debug1('Accept: %s:%r -> %s:%r.\n' % (srcip[0],srcip[1],
dstip[0],dstip[1]))
if dstip[1] == listener.getsockname()[1] and islocal(dstip[0]):
debug1("-- ignored: that's my address!\n")
sock.close()
return
chan = mux.next_channel()
if not chan:
log('warning: too many open channels. Discarded connection.\n')
sock.close()
return
mux.send(chan, ssnet.CMD_CONNECT, '%s,%s' % dstip)
outwrap = MuxWrapper(mux, chan)
handlers.append(Proxy(SockWrapper(sock, sock), outwrap))
dnsreqs = {}
def dns_done(chan, data):
peer,sock,timeout = dnsreqs.get(chan) or (None,None,None)
debug3('dns_done: channel=%r peer=%r\n' % (chan, peer))
if peer:
del dnsreqs[chan]
debug3('doing sendto %r\n' % (peer,))
sock.sendto(data, peer)
def ondns(listener, mux, handlers):
pkt,peer = listener.recvfrom(4096)
now = time.time()
if pkt:
debug1('DNS request from %r: %d bytes\n' % (peer, len(pkt)))
chan = mux.next_channel()
dnsreqs[chan] = peer,listener,now+30
mux.send(chan, ssnet.CMD_DNS_REQ, pkt)
mux.channels[chan] = lambda cmd,data: dns_done(chan,data)
for chan,(peer,sock,timeout) in dnsreqs.items():
if timeout < now:
del dnsreqs[chan]
debug3('Remaining DNS requests: %d\n' % len(dnsreqs))
def _main(listener, fw, ssh_cmd, remotename, python, latency_control,
dnslistener, seed_hosts, auto_nets,
syslog, daemon):
handlers = []
if helpers.verbose >= 1:
helpers.logprefix = 'c : '
else:
helpers.logprefix = 'client: '
debug1('connecting to server...\n')
try:
(serverproc, serversock) = ssh.connect(ssh_cmd, remotename, python,
stderr=ssyslog._p and ssyslog._p.stdin,
options=dict(latency_control=latency_control))
except socket.error, e:
if e.args[0] == errno.EPIPE:
raise Fatal("failed to establish ssh session (1)")
else:
raise
mux = Mux(serversock, serversock)
handlers.append(mux)
expected = 'SSHUTTLE0001'
try:
v = 'x'
while v and v != '\0':
v = serversock.recv(1)
v = 'x'
while v and v != '\0':
v = serversock.recv(1)
initstring = serversock.recv(len(expected))
except socket.error, e:
if e.args[0] == errno.ECONNRESET:
raise Fatal("failed to establish ssh session (2)")
else:
raise
rv = serverproc.poll()
if rv:
raise Fatal('server died with error code %d' % rv)
if initstring != expected:
raise Fatal('expected server init string %r; got %r'
% (expected, initstring))
debug1('connected.\n')
print 'Connected.'
sys.stdout.flush()
if daemon:
daemonize()
log('daemonizing (%s).\n' % _pidname)
elif syslog:
debug1('switching to syslog.\n')
ssyslog.stderr_to_syslog()
def onroutes(routestr):
if auto_nets:
for line in routestr.strip().split('\n'):
(ip,width) = line.split(',', 1)
fw.auto_nets.append((ip,int(width)))
# we definitely want to do this *after* starting ssh, or we might end
# up intercepting the ssh connection!
#
# Moreover, now that we have the --auto-nets option, we have to wait
# for the server to send us that message anyway. Even if we haven't
# set --auto-nets, we might as well wait for the message first, then
# ignore its contents.
mux.got_routes = None
fw.start()
mux.got_routes = onroutes
def onhostlist(hostlist):
debug2('got host list: %r\n' % hostlist)
for line in hostlist.strip().split():
if line:
name,ip = line.split(',', 1)
fw.sethostip(name, ip)
mux.got_host_list = onhostlist
handlers.append(Handler([listener], lambda: onaccept(listener, mux, handlers)))
if dnslistener:
handlers.append(Handler([dnslistener], lambda: ondns(dnslistener, mux, handlers)))
if seed_hosts != None:
debug1('seed_hosts: %r\n' % seed_hosts)
mux.send(0, ssnet.CMD_HOST_REQ, '\n'.join(seed_hosts))
while 1:
rv = serverproc.poll()
if rv:
raise Fatal('server died with error code %d' % rv)
ssnet.runonce(handlers, mux)
if latency_control:
mux.check_fullness()
mux.callback()
def main(listenip, ssh_cmd, remotename, python, latency_control, dns,
seed_hosts, auto_nets,
subnets_include, subnets_exclude, syslog, daemon, pidfile):
if syslog:
ssyslog.start_syslog()
if daemon:
try:
check_daemon(pidfile)
except Fatal, e:
log("%s\n" % e)
return 5
debug1('Starting sshuttle proxy.\n')
if listenip[1]:
ports = [listenip[1]]
else:
ports = xrange(12300,9000,-1)
last_e = None
bound = False
debug2('Binding:')
for port in ports:
debug2(' %d' % port)
listener = socket.socket()
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
dnslistener = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
dnslistener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
listener.bind((listenip[0], port))
dnslistener.bind((listenip[0], port))
bound = True
break
except socket.error, e:
last_e = e
debug2('\n')
if not bound:
assert(last_e)
raise last_e
listener.listen(10)
listenip = listener.getsockname()
debug1('Listening on %r.\n' % (listenip,))
if dns:
dnsip = dnslistener.getsockname()
debug1('DNS listening on %r.\n' % (dnsip,))
dnsport = dnsip[1]
else:
dnsport = 0
dnslistener = None
fw = FirewallClient(listenip[1], subnets_include, subnets_exclude, dnsport)
try:
return _main(listener, fw, ssh_cmd, remotename,
python, latency_control, dnslistener,
seed_hosts, auto_nets, syslog, daemon)
finally:
try:
if daemon:
# it's not our child anymore; can't waitpid
fw.p.returncode = 0
fw.done()
finally:
if daemon:
daemon_cleanup()

View File

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +0,0 @@
exec >&2
if pandoc </dev/null 2>/dev/null; then
pandoc -s -r markdown -w man -o $3 $1.md
else
echo "Warning: pandoc not installed; can't generate manpages."
redo-always
fi

175
do
View File

@ -1,175 +0,0 @@
#!/bin/sh
#
# A minimal alternative to djb redo that doesn't support incremental builds.
# For the full version, visit http://github.com/apenwarr/redo
#
# The author disclaims copyright to this source file and hereby places it in
# the public domain. (2010 12 14)
#
# By default, no output coloring.
green=""
bold=""
plain=""
if [ -n "$TERM" -a "$TERM" != "dumb" ] && tty <&2 >/dev/null 2>&1; then
green="$(printf '\033[32m')"
bold="$(printf '\033[1m')"
plain="$(printf '\033[m')"
fi
_dirsplit()
{
base=${1##*/}
dir=${1%$base}
}
dirname()
(
_dirsplit "$1"
dir=${dir%/}
echo "${dir:-.}"
)
_dirsplit "$0"
export REDO=$(cd "${dir:-.}" && echo "$PWD/$base")
DO_TOP=
if [ -z "$DO_BUILT" ]; then
DO_TOP=1
[ -n "$*" ] || set all # only toplevel redo has a default target
export DO_BUILT=$PWD/.do_built
: >>"$DO_BUILT"
echo "Removing previously built files..." >&2
sort -u "$DO_BUILT" | tee "$DO_BUILT.new" |
while read f; do printf "%s\0%s.did\0" "$f" "$f"; done |
xargs -0 rm -f 2>/dev/null
mv "$DO_BUILT.new" "$DO_BUILT"
DO_PATH=$DO_BUILT.dir
export PATH=$DO_PATH:$PATH
rm -rf "$DO_PATH"
mkdir "$DO_PATH"
for d in redo redo-ifchange; do
ln -s "$REDO" "$DO_PATH/$d";
done
[ -e /bin/true ] && TRUE=/bin/true || TRUE=/usr/bin/true
for d in redo-ifcreate redo-stamp redo-always; do
ln -s $TRUE "$DO_PATH/$d";
done
fi
_find_dofile_pwd()
{
dofile=default.$1.do
while :; do
dofile=default.${dofile#default.*.}
[ -e "$dofile" -o "$dofile" = default.do ] && break
done
ext=${dofile#default}
ext=${ext%.do}
base=${1%$ext}
}
_find_dofile()
{
local prefix=
while :; do
_find_dofile_pwd "$1"
[ -e "$dofile" ] && break
[ "$PWD" = "/" ] && break
target=${PWD##*/}/$target
tmp=${PWD##*/}/$tmp
prefix=${PWD##*/}/$prefix
cd ..
done
base=$prefix$base
}
_run_dofile()
{
export DO_DEPTH="$DO_DEPTH "
export REDO_TARGET=$PWD/$target
local line1
set -e
read line1 <"$PWD/$dofile"
cmd=${line1#"#!/"}
if [ "$cmd" != "$line1" ]; then
/$cmd "$PWD/$dofile" "$@" >"$tmp.tmp2"
else
:; . "$PWD/$dofile" >"$tmp.tmp2"
fi
}
_do()
{
local dir=$1 target=$2 tmp=$3
if [ ! -e "$target" ] || [ -d "$target" -a ! -e "$target.did" ]; then
printf '%sdo %s%s%s%s\n' \
"$green" "$DO_DEPTH" "$bold" "$dir$target" "$plain" >&2
echo "$PWD/$target" >>"$DO_BUILT"
dofile=$target.do
base=$target
ext=
[ -e "$target.do" ] || _find_dofile "$target"
if [ ! -e "$dofile" ]; then
echo "do: $target: no .do file" >&2
return 1
fi
[ ! -e "$DO_BUILT" ] || [ ! -d "$(dirname "$target")" ] ||
: >>"$target.did"
( _run_dofile "$base" "$ext" "$tmp.tmp" )
rv=$?
if [ $rv != 0 ]; then
printf "do: %s%s\n" "$DO_DEPTH" \
"$dir$target: got exit code $rv" >&2
rm -f "$tmp.tmp" "$tmp.tmp2"
return $rv
fi
mv "$tmp.tmp" "$target" 2>/dev/null ||
! test -s "$tmp.tmp2" ||
mv "$tmp.tmp2" "$target" 2>/dev/null
rm -f "$tmp.tmp2"
else
echo "do $DO_DEPTH$target exists." >&2
fi
}
# Make corrections for directories that don't actually exist yet.
_dir_shovel()
{
local dir base
xdir=$1 xbase=$2 xbasetmp=$2
while [ ! -d "$xdir" -a -n "$xdir" ]; do
_dirsplit "${xdir%/}"
xbasetmp=${base}__$xbase
xdir=$dir xbase=$base/$xbase
echo "xbasetmp='$xbasetmp'" >&2
done
}
redo()
{
for i in "$@"; do
_dirsplit "$i"
_dir_shovel "$dir" "$base"
dir=$xdir base=$xbase basetmp=$xbasetmp
( cd "$dir" && _do "$dir" "$base" "$basetmp" ) || return 1
done
}
set -e
redo "$@"
if [ -n "$DO_TOP" ]; then
echo "Removing stamp files..." >&2
[ ! -e "$DO_BUILT" ] ||
while read f; do printf "%s.did\0" "$f"; done <"$DO_BUILT" |
xargs -0 rm -f 2>/dev/null
fi

177
docs/Makefile Normal file
View File

@ -0,0 +1,177 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
endif
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/sshuttle.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/sshuttle.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/sshuttle"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/sshuttle"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."

1
docs/changes.rst Normal file
View File

@ -0,0 +1 @@
.. 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

261
docs/conf.py Normal file
View File

@ -0,0 +1,261 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# sshuttle documentation build configuration file, created by
# sphinx-quickstart on Sun Jan 17 12:13:47 2016.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
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
# documentation root, use os.path.abspath to make it absolute, like shown here.
# sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.todo',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = 'sshuttle'
copyright = '2016, Brian May'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The full version, including alpha/beta/rc tags.
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.
# language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
# today = ''
# Else, today_fmt is used as the format for a strftime call.
# today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all
# documents.
# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
# keep_warnings = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'default'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
# html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
# html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
# html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
# html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
# html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
# html_extra_path = []
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
# html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
# html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
# html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
# html_additional_pages = {}
# If false, no module index is generated.
# html_domain_indices = True
# If false, no index is generated.
# html_use_index = True
# If true, the index is split into individual pages for each letter.
# html_split_index = False
# If true, links to the reST sources are added to the pages.
# html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
# html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
# html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
# html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
# html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'sshuttledoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
# 'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
('index', 'sshuttle.tex', 'sshuttle documentation', 'Brian May', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
# latex_use_parts = False
# If true, show page references after internal links.
# latex_show_pagerefs = False
# If true, show URL addresses after external links.
# latex_show_urls = False
# Documents to append as an appendix to all manuals.
# latex_appendices = []
# If false, no module index is generated.
# latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('manpage', 'sshuttle', 'sshuttle documentation', ['Brian May'], 1)
]
# If true, show URL addresses after external links.
# man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'sshuttle', 'sshuttle documentation',
'Brian May', 'sshuttle', 'A transparent proxy-based VPN using ssh',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
# texinfo_appendices = []
# If false, no module index is generated.
# texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
# texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
# texinfo_no_detailmenu = False

37
docs/how-it-works.rst Normal file
View File

@ -0,0 +1,37 @@
How it works
============
sshuttle is not exactly a VPN, and not exactly port forwarding. It's kind
of both, and kind of neither.
It's like a VPN, since it can forward every port on an entire network, not
just ports you specify. Conveniently, it lets you use the "real" IP
addresses of each host rather than faking port numbers on localhost.
On the other hand, the way it *works* is more like ssh port forwarding than
a VPN. Normally, a VPN forwards your data one packet at a time, and
doesn't care about individual connections; ie. it's "stateless" with respect
to the traffic. sshuttle is the opposite of stateless; it tracks every
single connection.
You could compare sshuttle to something like the old `Slirp
<http://en.wikipedia.org/wiki/Slirp>`_ program, which was a userspace TCP/IP
implementation that did something similar. But it operated on a
packet-by-packet basis on the client side, reassembling the packets on the
server side. That worked okay back in the "real live serial port" days,
because serial ports had predictable latency and buffering.
But you can't safely just forward TCP packets over a TCP session (like ssh),
because TCP's performance depends fundamentally on packet loss; it
*must* experience packet loss in order to know when to slow down! At
the same time, the outer TCP session (ssh, in this case) is a reliable
transport, which means that what you forward through the tunnel *never*
experiences packet loss. The ssh session itself experiences packet loss, of
course, but TCP fixes it up and ssh (and thus you) never know the
difference. But neither does your inner TCP session, and extremely screwy
performance ensues.
sshuttle assembles the TCP stream locally, multiplexes it statefully over
an ssh session, and disassembles it back into packets at the other end. So
it never ends up doing TCP-over-TCP. It's just data-over-TCP, which is
safe.

29
docs/index.rst Normal file
View File

@ -0,0 +1,29 @@
sshuttle: where transparent proxy meets VPN meets ssh
=====================================================
:Date: |today|
:Version: |version|
Contents:
.. toctree::
:maxdepth: 2
overview
requirements
installation
usage
platform
Man Page <manpage>
how-it-works
support
trivia
changes
Indices and tables
==================
* :ref:`genindex`
* :ref:`search`

22
docs/installation.rst Normal file
View File

@ -0,0 +1,22 @@
Installation
============
- From PyPI::
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

242
docs/make.bat Normal file
View File

@ -0,0 +1,242 @@
@ECHO OFF
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set BUILDDIR=_build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
set I18NSPHINXOPTS=%SPHINXOPTS% .
if NOT "%PAPER%" == "" (
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
)
if "%1" == "" goto help
if "%1" == "help" (
:help
echo.Please use `make ^<target^>` where ^<target^> is one of
echo. html to make standalone HTML files
echo. dirhtml to make HTML files named index.html in directories
echo. singlehtml to make a single large HTML file
echo. pickle to make pickle files
echo. json to make JSON files
echo. htmlhelp to make HTML files and a HTML help project
echo. qthelp to make HTML files and a qthelp project
echo. devhelp to make HTML files and a Devhelp project
echo. epub to make an epub
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
echo. text to make text files
echo. man to make manual pages
echo. texinfo to make Texinfo files
echo. gettext to make PO message catalogs
echo. changes to make an overview over all changed/added/deprecated items
echo. xml to make Docutils-native XML files
echo. pseudoxml to make pseudoxml-XML files for display purposes
echo. linkcheck to check all external links for integrity
echo. doctest to run all doctests embedded in the documentation if enabled
goto end
)
if "%1" == "clean" (
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
del /q /s %BUILDDIR%\*
goto end
)
%SPHINXBUILD% 2> nul
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
if "%1" == "html" (
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
goto end
)
if "%1" == "dirhtml" (
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
goto end
)
if "%1" == "singlehtml" (
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
goto end
)
if "%1" == "pickle" (
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the pickle files.
goto end
)
if "%1" == "json" (
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the JSON files.
goto end
)
if "%1" == "htmlhelp" (
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
goto end
)
if "%1" == "qthelp" (
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run "qcollectiongenerator" with the ^
.qhcp project file in %BUILDDIR%/qthelp, like this:
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\sshuttle.qhcp
echo.To view the help file:
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\sshuttle.ghc
goto end
)
if "%1" == "devhelp" (
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished.
goto end
)
if "%1" == "epub" (
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The epub file is in %BUILDDIR%/epub.
goto end
)
if "%1" == "latex" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
if errorlevel 1 exit /b 1
echo.
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "latexpdf" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
cd %BUILDDIR%/latex
make all-pdf
cd %BUILDDIR%/..
echo.
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "latexpdfja" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
cd %BUILDDIR%/latex
make all-pdf-ja
cd %BUILDDIR%/..
echo.
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "text" (
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The text files are in %BUILDDIR%/text.
goto end
)
if "%1" == "man" (
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The manual pages are in %BUILDDIR%/man.
goto end
)
if "%1" == "texinfo" (
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
goto end
)
if "%1" == "gettext" (
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
goto end
)
if "%1" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
if errorlevel 1 exit /b 1
echo.
echo.The overview file is in %BUILDDIR%/changes.
goto end
)
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
if errorlevel 1 exit /b 1
echo.
echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
goto end
)
if "%1" == "doctest" (
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
if errorlevel 1 exit /b 1
echo.
echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
goto end
)
if "%1" == "xml" (
%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The XML files are in %BUILDDIR%/xml.
goto end
)
if "%1" == "pseudoxml" (
%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
goto end
)
:end

400
docs/manpage.rst Normal file
View File

@ -0,0 +1,400 @@
sshuttle
========
Synopsis
--------
**sshuttle** [*options*] [**-r** *[username@]sshserver[:port]*] \<*subnets* ...\>
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 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.
It's valid to run :program:`sshuttle` more than once simultaneously on
a single client machine, connecting to a different server
every time, so you can be on more than one VPN at once.
If run on a router, :program:`sshuttle` can forward traffic for your
entire subnet to the VPN.
Options
-------
.. program:: sshuttle
.. option:: <subnets>
A list of subnets to route over the VPN, in the form
``a.b.c.d[/width][port[-port]]``. Valid examples are 1.2.3.4 (a
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'). 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|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 <[ip:]port>, --listen=<[ip:]port>
Use this ip address and port number as the transparent
proxy port. By default :program:`sshuttle` finds an available
port automatically and listens on IP 127.0.0.1
(localhost), so you don't need to override it, and
connections are only proxied from the local machine,
not from outside machines. If you want to accept
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 and pf methods this can be an IPv6 address. Use this option
twice if required, to provide both IPv4 and IPv6 addresses.
.. option:: -H, --auto-hosts
Scan for remote hostnames and update the local /etc/hosts
file with matching entries for as long as the VPN is
open. This is nicer than changing your system's DNS
(/etc/resolv.conf) settings, for several reasons. First,
hostnames are added without domain names attached, so
you can ``ssh thatserver`` without worrying if your local
domain matches the remote one. Second, if you :program:`sshuttle`
into more than one VPN at a time, it's impossible to
use more than one DNS server at once anyway, but
:program:`sshuttle` correctly merges /etc/hosts entries between
all running copies. Third, if you're only routing a
few subnets over the VPN, you probably would prefer to
keep using your local DNS server for everything else.
.. option:: -N, --auto-nets
In addition to the subnets provided on the command
line, ask the server which subnets it thinks we should
route, and route those automatically. The suggestions
are taken automatically from the server's routing
table.
.. option:: --dns
Capture local DNS requests and forward to the remote DNS
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
Specify the name/path of the remote python interpreter.
The default is just ``python``, which means to use the
default python interpreter on the remote system's PATH.
.. 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 <subnet>, --exclude=<subnet>
Explicitly exclude this subnet from forwarding. The
format of this option is the same as the ``<subnets>``
option. To exclude more than one subnet, specify the
``-x`` option more than once. You can say something like
``0/0 -x 1.2.3.0/24`` to forward everything except the
local subnet over the VPN, for example.
.. 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.
.. option:: -v, --verbose
Print more information about the session. This option
can be used more than once for increased verbosity. By
default, :program:`sshuttle` prints only error messages.
.. option:: -e, --ssh-cmd
The command to use to connect to the remote server. The
default is just ``ssh``. Use this if your ssh client is
in a non-standard location or you want to provide extra
options to the ssh command, for example, ``-e 'ssh -v'``.
.. option:: --seed-hosts
A comma-separated list of hostnames to use to
initialize the :option:`--auto-hosts` scan algorithm.
:option:`--auto-hosts` does things like poll local SMB servers
for lists of local hostnames, but can speed things up
if you use this option to give it a few names to start
from.
If this option is used *without* :option:`--auto-hosts`,
then the listed hostnames will be scanned and added, but
no further hostnames will be added.
.. option:: --no-latency-control
Sacrifice latency to improve bandwidth benchmarks. ssh
uses really big socket buffers, which can overload the
connection if you start doing large file transfers,
thus making all your other sessions inside the same
tunnel go slowly. Normally, :program:`sshuttle` tries to avoid
this problem using a "fullness check" that allows only
a certain amount of outstanding data to be buffered at
a time. But on high-bandwidth links, this can leave a
lot of your bandwidth underutilized. It also makes
:program:`sshuttle` seem slow in bandwidth benchmarks (benchmarks
rarely test ping latency, which is what :program:`sshuttle` is
trying to control). This option disables the latency
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
to the remote server. Implies :option:`--syslog`.
.. option:: --syslog
after connecting, send all log messages to the
:manpage:`syslog(3)` service instead of stderr. This is
implicit if you use :option:`--daemon`.
.. option:: --pidfile=<pidfilename>
when using :option:`--daemon`, save :program:`sshuttle`'s pid to
*pidfilename*. The default is ``sshuttle.pid`` in the
current directory.
.. option:: --disable-ipv6
If using tproxy or pf methods, this will disable IPv6 support.
.. option:: --firewall
(internal use only) run the firewall manager. This is
the only part of :program:`sshuttle` that must run as root. If
you start :program:`sshuttle` as a non-root user, it will
automatically run ``sudo`` or ``su`` to start the firewall
manager, but the core of :program:`sshuttle` still runs as a
normal user.
.. option:: --hostwatch
(internal use only) run the hostwatch daemon. This
process runs on the server side and collects hostnames for
the :option:`--auto-hosts` option. Using this option by itself
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
--------
Test locally by proxying all local connections, without using ssh::
$ sshuttle -v 0/0
Starting sshuttle proxy.
Listening on ('0.0.0.0', 12300).
[local sudo] Password:
firewall manager ready.
c : connecting to server...
s: available routes:
s: 192.168.42.0/24
c : connected.
firewall manager: starting transproxy.
c : Accept: 192.168.42.106:50035 -> 192.168.42.121:139.
c : Accept: 192.168.42.121:47523 -> 77.141.99.22:443.
...etc...
^C
firewall manager: undoing changes.
KeyboardInterrupt
c : Keyboard interrupt: exiting.
c : SW#8:192.168.42.121:47523: deleting
c : SW#6:192.168.42.106:50035: deleting
Test connection to a remote server, with automatic hostname
and subnet guessing::
$ sshuttle -vNHr example.org
Starting sshuttle proxy.
Listening on ('0.0.0.0', 12300).
firewall manager ready.
c : connecting to server...
s: available routes:
s: 77.141.99.0/24
c : connected.
c : seed_hosts: []
firewall manager: starting transproxy.
hostwatch: Found: testbox1: 1.2.3.4
hostwatch: Found: mytest2: 5.6.7.8
hostwatch: Found: domaincontroller: 99.1.2.3
c : Accept: 192.168.42.121:60554 -> 77.141.99.22:22.
^C
firewall manager: undoing changes.
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
----------
When it starts, :program:`sshuttle` creates an ssh session to the
server specified by the ``-r`` option. If ``-r`` is omitted,
it will start both its client and server locally, which is
sometimes useful for testing.
After connecting to the remote server, :program:`sshuttle` uploads its
(python) source code to the remote end and executes it
there. Thus, you don't need to install :program:`sshuttle` on the
remote server, and there are never :program:`sshuttle` version
conflicts between client and server.
Unlike most VPNs, :program:`sshuttle` forwards sessions, not packets.
That is, it uses kernel transparent proxying (`iptables
REDIRECT` rules on Linux) to
capture outgoing TCP sessions, then creates entirely
separate TCP sessions out to the original destination at
the other end of the tunnel.
Packet-level forwarding (eg. using the tun/tap devices on
Linux) seems elegant at first, but it results in
several problems, notably the 'tcp over tcp' problem. The
tcp protocol depends fundamentally on packets being dropped
in order to implement its congestion control agorithm; if
you pass tcp packets through a tcp-based tunnel (such as
ssh), the inner tcp packets will never be dropped, and so
the inner tcp stream's congestion control will be
completely broken, and performance will be terrible. Thus,
packet-based VPNs (such as IPsec and openvpn) cannot use
tcp-based encrypted streams like ssh or ssl, and have to
implement their own encryption from scratch, which is very
complex and error prone.
:program:`sshuttle`'s simplicity comes from the fact that it can
safely use the existing ssh encrypted tunnel without
incurring a performance penalty. It does this by letting
the client-side kernel manage the incoming tcp stream, and
the server-side kernel manage the outgoing tcp stream;
there is no need for congestion control to be shared
between the two separate streams, so a tcp-based tunnel is
fine.
.. seealso::
:manpage:`ssh(1)`, :manpage:`python(1)`

8
docs/openwrt.rst Normal file
View File

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

26
docs/overview.rst Normal file
View File

@ -0,0 +1,26 @@
Overview
========
As far as I know, sshuttle is the only program that solves the following
common case:
- Your client machine (or router) is Linux, MacOS, FreeBSD, OpenBSD or pfSense.
- You have access to a remote network via ssh.
- You don't necessarily have admin access on the remote network.
- The remote network has no VPN, or only stupid/complex VPN
protocols (IPsec, PPTP, etc). Or maybe you *are* the
admin and you just got frustrated with the awful state of
VPN tools.
- You don't want to create an ssh port forward for every
single host/port on the remote network.
- You hate openssh's port forwarding because it's randomly
slow and/or stupid.
- 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).

12
docs/platform.rst Normal file
View File

@ -0,0 +1,12 @@
Platform Specific Notes
=======================
Contents:
.. toctree::
:maxdepth: 2
chromeos
tproxy
windows
openwrt

84
docs/requirements.rst Normal file
View File

@ -0,0 +1,84 @@
Requirements
============
Client side Requirements
------------------------
- sudo, or root access on your client machine.
(The server doesn't need admin access.)
- Python 3.5 or greater.
Linux with NAT method
~~~~~~~~~~~~~~~~~~~~~
Supports:
* IPv4 TCP
* IPv4 DNS
Requires:
* iptables DNAT, REDIRECT, and ttl modules.
Linux with TPROXY method
~~~~~~~~~~~~~~~~~~~~~~~~
Supports:
* IPv4 TCP
* IPv4 UDP (requires ``recvmsg`` - see below)
* IPv6 DNS (requires ``recvmsg`` - see below)
* IPv6 TCP
* IPv6 UDP (requires ``recvmsg`` - see below)
* IPv6 DNS (requires ``recvmsg`` - see below)
MacOS / FreeBSD / OpenBSD / pfSense
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Method: pf
Supports:
* IPv4 TCP
* IPv4 DNS
* IPv6 TCP
* IPv6 DNS
Requires:
* You need to have the pfctl command.
Windows
~~~~~~~
Not officially supported, however can be made to work with Vagrant. Requires
cmd.exe with Administrator access. See :doc:`windows` for more information.
Server side Requirements
------------------------
- Python 3.5 or greater.
Additional Suggested Software
-----------------------------
- 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

11
docs/support.rst Normal file
View File

@ -0,0 +1,11 @@
Support
=======
Mailing list:
* Subscribe by sending a message to <sshuttle+subscribe@googlegroups.com>
* List archives are at: http://groups.google.com/group/sshuttle
Issue tracker and pull requests at github:
* https://github.com/sshuttle/sshuttle

42
docs/tproxy.rst Normal file
View File

@ -0,0 +1,42 @@
TPROXY
======
TPROXY is the only method that has full support of IPv6 and UDP.
There are some things you need to consider for TPROXY to work:
- The following commands need to be run first as root. This only needs to be
done once after booting up::
ip route add local default dev lo table 100
ip rule add fwmark 1 lookup 100
ip -6 route add local default dev lo table 100
ip -6 rule add fwmark 1 lookup 100
- The ``--auto-nets`` feature does not detect IPv6 routes automatically. Add IPv6
routes manually. e.g. by adding ``'::/0'`` to the end of the command line.
- The client needs to be run as root. e.g.::
sudo SSH_AUTH_SOCK="$SSH_AUTH_SOCK" $HOME/tree/sshuttle.tproxy/sshuttle --method=tproxy ...
- You may need to exclude the IP address of the server you are connecting to.
Otherwise sshuttle may attempt to intercept the ssh packets, which will not
work. Use the ``--exclude`` parameter for this.
- Similarly, UDP return packets (including DNS) could get intercepted and
bounced back. This is the case if you have a broad subnet such as
``0.0.0.0/0`` or ``::/0`` that includes the IP address of the client. Use the
``--exclude`` parameter for this.
- You need the ``--method=tproxy`` parameter, as above.
- The routes for the outgoing packets must already exist. For example, if your
connection does not have IPv6 support, no IPv6 routes will exist, IPv6
packets will not be generated and sshuttle cannot intercept them::
telnet -6 www.google.com 80
Trying 2404:6800:4001:805::1010...
telnet: Unable to connect to remote host: Network is unreachable
Add some dummy routes to external interfaces. Make sure they get removed
however after sshuttle exits.

36
docs/trivia.rst Normal file
View File

@ -0,0 +1,36 @@
Useless Trivia
==============
This section written by the original author, Avery Pennarun
<apenwarr@gmail.com>.
Back in 1998, I released the first version of `Tunnel
Vision <http://alumnit.ca/wiki/?TunnelVisionReadMe>`_, a semi-intelligent VPN
client for Linux. Unfortunately, I made two big mistakes: I implemented the
key exchange myself (oops), and I ended up doing TCP-over-TCP (double oops).
The resulting program worked okay - and people used it for years - but the
performance was always a bit funny. And nobody ever found any security flaws
in my key exchange, either, but that doesn't mean anything. :)
The same year, dcoombs and I also released Fast Forward, a proxy server
supporting transparent proxying. Among other things, we used it for
automatically splitting traffic across more than one Internet connection (a
tool we called "Double Vision").
I was still in university at the time. A couple years after that, one of my
professors was working with some graduate students on the technology that would
eventually become `Slipstream Internet Acceleration
<http://www.slipstream.com/>`_. He asked me to do a contract for him to build
an initial prototype of a transparent proxy server for mobile networks. The
idea was similar to sshuttle: if you reassemble and then disassemble the TCP
packets, you can reduce latency and improve performance vs. just forwarding
the packets over a plain VPN or mobile network. (It's unlikely that any of my
code has persisted in the Slipstream product today, but the concept is still
pretty cool. I'm still horrified that people use plain TCP on complex mobile
networks with crazily variable latency, for which it was never really
intended.)
That project I did for Slipstream was what first gave me the idea to merge
the concepts of Fast Forward, Double Vision, and Tunnel Vision into a single
program that was the best of all worlds. And here we are, at last.
You're welcome.

110
docs/usage.rst Normal file
View File

@ -0,0 +1,110 @@
Usage
=====
.. note::
For information on usage with Windows, see the :doc:`windows` section.
For information on using the TProxy method, see the :doc:`tproxy` section.
Forward all traffic::
sshuttle -r username@sshserver 0.0.0.0/0
- Use the :option:`sshuttle -r` parameter to specify a remote server.
- By default sshuttle will automatically choose a method to use. Override with
the :option:`sshuttle --method` parameter.
- There is a shortcut for 0.0.0.0/0 for those that value
their wrists::
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::
sshuttle --dns -r username@sshserver 0/0
The above is probably what you want to use to prevent
local network attacks such as Firesheep and friends.
See the documentation for the :option:`sshuttle --dns` parameter.
(You may be prompted for one or more passwords; first, the local password to
become root using sudo, and then the remote ssh password. Or you might have
sudo and ssh set up to not require passwords, in which case you won't be
prompted at all.)
Usage Notes
-----------
That's it! Now your local machine can access the remote network as if you
were right there. And if your "client" machine is a router, everyone on
your local network can make connections to your remote network.
You don't need to install sshuttle on the remote server;
the remote server just needs to have python available.
sshuttle will automatically upload and run its source code
to the remote python interpreter.
This creates a transparent proxy server on your local machine for all IP
addresses that match 0.0.0.0/0. (You can use more specific IP addresses if
you want; use any number of IP addresses or subnets to change which
addresses get proxied. Using 0.0.0.0/0 proxies *everything*, which is
interesting if you don't trust the people on your local network.)
Any TCP session you initiate to one of the proxied IP addresses will be
captured by sshuttle and sent over an ssh session to the remote copy of
sshuttle, which will then regenerate the connection on that end, and funnel
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

19
docs/windows.rst Normal file
View File

@ -0,0 +1,19 @@
Microsoft Windows
=================
Currently there is no built in support for running sshuttle directly on
Microsoft Windows.
What we can really do is to create a Linux VM with Vagrant (or simply
Virtualbox if you like). In the Vagrant settings, remember to turn on bridged
NIC. Then, run sshuttle inside the VM like below::
sshuttle -l 0.0.0.0 -x 10.0.0.0/8 -x 192.168.0.0/16 0/0
10.0.0.0/8 excludes NAT traffic of Vagrant and 192.168.0.0/16 excludes
traffic to local area network (assuming that we're using 192.168.0.0 subnet).
Assuming the VM has the IP 192.168.1.200 obtained on the bridge NIC (we can
configure that in Vagrant), we can then ask Windows to route all its traffic
via the VM by running the following in cmd.exe with admin right::
route add 0.0.0.0 mask 0.0.0.0 192.168.1.200

View File

@ -1,460 +0,0 @@
import re, errno, socket, select, struct
import compat.ssubprocess as ssubprocess
import helpers, ssyslog
from helpers import *
# python doesn't have a definition for this
IPPROTO_DIVERT = 254
def nonfatal(func, *args):
try:
func(*args)
except Fatal, e:
log('error: %s\n' % e)
def ipt_chain_exists(name):
argv = ['iptables', '-t', 'nat', '-nL']
p = ssubprocess.Popen(argv, stdout = ssubprocess.PIPE)
for line in p.stdout:
if line.startswith('Chain %s ' % name):
return True
rv = p.wait()
if rv:
raise Fatal('%r returned %d' % (argv, rv))
def ipt(*args):
argv = ['iptables', '-t', 'nat'] + list(args)
debug1('>> %s\n' % ' '.join(argv))
rv = ssubprocess.call(argv)
if rv:
raise Fatal('%r returned %d' % (argv, rv))
_no_ttl_module = False
def ipt_ttl(*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
# connections, in case client == server.
try:
argsplus = list(args) + ['-m', 'ttl', '!', '--ttl', '42']
ipt(*argsplus)
except Fatal:
ipt(*args)
# we only get here if the non-ttl attempt succeeds
log('sshuttle: warning: your iptables is missing '
'the ttl module.\n')
_no_ttl_module = True
else:
ipt(*args)
# 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 do_iptables(port, dnsport, subnets):
chain = 'sshuttle-%s' % port
# basic cleanup/setup of chains
if ipt_chain_exists(chain):
nonfatal(ipt, '-D', 'OUTPUT', '-j', chain)
nonfatal(ipt, '-D', 'PREROUTING', '-j', chain)
nonfatal(ipt, '-F', chain)
ipt('-X', chain)
if subnets or dnsport:
ipt('-N', chain)
ipt('-F', chain)
ipt('-I', 'OUTPUT', '1', '-j', chain)
ipt('-I', 'PREROUTING', '1', '-j', chain)
if subnets:
# 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 swidth,sexclude,snet in sorted(subnets, reverse=True):
if sexclude:
ipt('-A', chain, '-j', 'RETURN',
'--dest', '%s/%s' % (snet,swidth),
'-p', 'tcp')
else:
ipt_ttl('-A', chain, '-j', 'REDIRECT',
'--dest', '%s/%s' % (snet,swidth),
'-p', 'tcp',
'--to-ports', str(port))
if dnsport:
nslist = resolvconf_nameservers()
for ip in nslist:
ipt_ttl('-A', chain, '-j', 'REDIRECT',
'--dest', '%s/32' % ip,
'-p', 'udp',
'--dport', '53',
'--to-ports', str(dnsport))
def ipfw_rule_exists(n):
argv = ['ipfw', 'list']
p = ssubprocess.Popen(argv, stdout = ssubprocess.PIPE)
found = False
for line in p.stdout:
if line.startswith('%05d ' % n):
if not ('ipttl 42' in line
or ('skipto %d' % (n+1)) in line
or 'check-state' in line):
log('non-sshuttle ipfw rule: %r\n' % line.strip())
raise Fatal('non-sshuttle ipfw rule #%d already exists!' % n)
found = True
rv = p.wait()
if rv:
raise Fatal('%r returned %d' % (argv, rv))
return found
_oldctls = {}
def _fill_oldctls(prefix):
argv = ['sysctl', prefix]
p = ssubprocess.Popen(argv, stdout = ssubprocess.PIPE)
for line in p.stdout:
assert(line[-1] == '\n')
(k,v) = line[:-1].split(': ', 1)
_oldctls[k] = v
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('/dev/null', 'w'))
_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 _udp_unpack(p):
src = (socket.inet_ntoa(p[12:16]), struct.unpack('!H', p[20:22])[0])
dst = (socket.inet_ntoa(p[16:20]), struct.unpack('!H', p[22:24])[0])
return src, dst
def _udp_repack(p, src, dst):
addrs = socket.inet_aton(src[0]) + socket.inet_aton(dst[0])
ports = struct.pack('!HH', src[1], dst[1])
return p[:12] + addrs + ports + p[24:]
_real_dns_server = [None]
def _handle_diversion(divertsock, dnsport):
p,tag = divertsock.recvfrom(4096)
src,dst = _udp_unpack(p)
debug3('got diverted packet from %r to %r\n' % (src, dst))
if dst[1] == 53:
# outgoing DNS
debug3('...packet is a DNS request.\n')
_real_dns_server[0] = dst
dst = ('127.0.0.1', dnsport)
elif src[1] == dnsport:
if islocal(src[0]):
debug3('...packet is a DNS response.\n')
src = _real_dns_server[0]
else:
log('weird?! unexpected divert from %r to %r\n' % (src, dst))
assert(0)
newp = _udp_repack(p, src, dst)
divertsock.sendto(newp, tag)
def ipfw(*args):
argv = ['ipfw', '-q'] + list(args)
debug1('>> %s\n' % ' '.join(argv))
rv = ssubprocess.call(argv)
if rv:
raise Fatal('%r returned %d' % (argv, rv))
def do_ipfw(port, dnsport, subnets):
sport = str(port)
xsport = str(port+1)
# cleanup any existing rules
if ipfw_rule_exists(port):
ipfw('delete', sport)
while _changedctls:
name = _changedctls.pop()
oldval = _oldctls[name]
_sysctl_set(name, oldval)
if subnets or dnsport:
sysctl_set('net.inet.ip.fw.enable', 1)
changed = sysctl_set('net.inet.ip.scopedroute', 0, permanent=True)
if changed:
log("\n"
" WARNING: ONE-TIME NETWORK DISRUPTION:\n"
" =====================================\n"
"sshuttle has changed a MacOS kernel setting to work around\n"
"a bug in MacOS 10.6. This will cause your network to drop\n"
"within 5-10 minutes unless you restart your network\n"
"interface (change wireless networks or unplug/plug the\n"
"ethernet port) NOW, then restart sshuttle. The fix is\n"
"permanent; you only have to do this once.\n\n")
sys.exit(1)
ipfw('add', sport, 'check-state', 'ip',
'from', 'any', 'to', 'any')
if subnets:
# create new subnet entries
for swidth,sexclude,snet in sorted(subnets, reverse=True):
if sexclude:
ipfw('add', sport, 'skipto', xsport,
'log', 'tcp',
'from', 'any', 'to', '%s/%s' % (snet,swidth))
else:
ipfw('add', sport, 'fwd', '127.0.0.1,%d' % port,
'log', 'tcp',
'from', 'any', 'to', '%s/%s' % (snet,swidth),
'not', 'ipttl', '42', 'keep-state', 'setup')
# This part is much crazier than it is on Linux, because MacOS (at least
# 10.6, and probably other versions, and maybe FreeBSD too) doesn't
# correctly fixup the dstip/dstport for UDP packets when it puts them
# through a 'fwd' rule. It also doesn't fixup the srcip/srcport in the
# response packet. In Linux iptables, all that happens magically for us,
# so we just redirect the packets and relax.
#
# On MacOS, we have to fix the ports ourselves. For that, we use a
# 'divert' socket, which receives raw packets and lets us mangle them.
#
# Here's how it works. Let's say the local DNS server is 1.1.1.1:53,
# and the remote DNS server is 2.2.2.2:53, and the local transproxy port
# is 10.0.0.1:12300, and a client machine is making a request from
# 10.0.0.5:9999. We see a packet like this:
# 10.0.0.5:9999 -> 1.1.1.1:53
# Since the destip:port matches one of our local nameservers, it will
# match a 'fwd' rule, thus grabbing it on the local machine. However,
# the local kernel will then see a packet addressed to *:53 and
# not know what to do with it; there's nobody listening on port 53. Thus,
# we divert it, rewriting it into this:
# 10.0.0.5:9999 -> 10.0.0.1:12300
# This gets proxied out to the server, which sends it to 2.2.2.2:53,
# and the answer comes back, and the proxy sends it back out like this:
# 10.0.0.1:12300 -> 10.0.0.5:9999
# But that's wrong! The original machine expected an answer from
# 1.1.1.1:53, so we have to divert the *answer* and rewrite it:
# 1.1.1.1:53 -> 10.0.0.5:9999
#
# See? Easy stuff.
if dnsport:
divertsock = socket.socket(socket.AF_INET, socket.SOCK_RAW,
IPPROTO_DIVERT)
divertsock.bind(('0.0.0.0', port)) # IP field is ignored
nslist = resolvconf_nameservers()
for ip in nslist:
# relabel and then catch outgoing DNS requests
ipfw('add', sport, 'divert', sport,
'log', 'udp',
'from', 'any', 'to', '%s/32' % ip, '53',
'not', 'ipttl', '42')
# relabel DNS responses
ipfw('add', sport, 'divert', sport,
'log', 'udp',
'from', 'any', str(dnsport), 'to', 'any',
'not', 'ipttl', '42')
def do_wait():
while 1:
r,w,x = select.select([sys.stdin, divertsock], [], [])
if divertsock in r:
_handle_diversion(divertsock, dnsport)
if sys.stdin in r:
return
else:
do_wait = None
return do_wait
def program_exists(name):
paths = (os.getenv('PATH') or os.defpath).split(os.pathsep)
for p in paths:
fn = '%s/%s' % (p, name)
if os.path.exists(fn):
return not os.path.isdir(fn) and os.access(fn, os.X_OK)
hostmap = {}
def rewrite_etc_hosts(port):
HOSTSFILE='/etc/hosts'
BAKFILE='%s.sbak' % HOSTSFILE
APPEND='# sshuttle-firewall-%d AUTOCREATED' % port
old_content = ''
st = None
try:
old_content = open(HOSTSFILE).read()
st = os.stat(HOSTSFILE)
except IOError, e:
if e.errno == errno.ENOENT:
pass
else:
raise
if old_content.strip() and not os.path.exists(BAKFILE):
os.link(HOSTSFILE, BAKFILE)
tmpname = "%s.%d.tmp" % (HOSTSFILE, port)
f = open(tmpname, 'w')
for line in old_content.rstrip().split('\n'):
if line.find(APPEND) >= 0:
continue
f.write('%s\n' % line)
for (name,ip) in sorted(hostmap.items()):
f.write('%-30s %s\n' % ('%s %s' % (ip,name), APPEND))
f.close()
if st:
os.chown(tmpname, st.st_uid, st.st_gid)
os.chmod(tmpname, st.st_mode)
else:
os.chown(tmpname, 0, 0)
os.chmod(tmpname, 0644)
os.rename(tmpname, HOSTSFILE)
def restore_etc_hosts(port):
global hostmap
hostmap = {}
rewrite_etc_hosts(port)
# 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.
#
# This code is supposed to clean up after itself by deleting its rules on
# exit. In case that fails, it's not the end of the world; future runs will
# supercede it in the transproxy list, at least, so the leftover rules
# are hopefully harmless.
def main(port, dnsport, syslog):
assert(port > 0)
assert(port <= 65535)
assert(dnsport >= 0)
assert(dnsport <= 65535)
if os.getuid() != 0:
raise Fatal('you must be root (or enable su/sudo) to set the firewall')
if program_exists('ipfw'):
do_it = do_ipfw
elif program_exists('iptables'):
do_it = do_iptables
else:
raise Fatal("can't find either ipfw or iptables; check your PATH")
# because of limitations of the 'su' command, the *real* stdin/stdout
# are both attached to stdout initially. Clone stdout into stdin so we
# can read from it.
os.dup2(1, 0)
if syslog:
ssyslog.start_syslog()
ssyslog.stderr_to_syslog()
debug1('firewall manager ready.\n')
sys.stdout.write('READY\n')
sys.stdout.flush()
# ctrl-c shouldn't be passed along to me. When the main sshuttle dies,
# I'll die automatically.
os.setsid()
# we wait until we get some input before creating the rules. That way,
# sshuttle can launch us as early as possible (and get sudo password
# authentication as early in the startup process as possible).
line = sys.stdin.readline(128)
if not line:
return # parent died; nothing to do
subnets = []
if line != 'ROUTES\n':
raise Fatal('firewall: expected ROUTES but got %r' % line)
while 1:
line = sys.stdin.readline(128)
if not line:
raise Fatal('firewall: expected route but got %r' % line)
elif line == 'GO\n':
break
try:
(width,exclude,ip) = line.strip().split(',', 2)
except:
raise Fatal('firewall: expected route or GO but got %r' % line)
subnets.append((int(width), bool(int(exclude)), ip))
try:
if line:
debug1('firewall manager: starting transproxy.\n')
do_wait = do_it(port, dnsport, subnets)
sys.stdout.write('STARTED\n')
try:
sys.stdout.flush()
except IOError:
# the parent process died for some reason; he's surely been loud
# enough, so no reason to report another error
return
# Now we wait until EOF or any other kind of exception. We need
# to stay running so that we don't need a *second* password
# authentication at shutdown time - that cleanup is important!
while 1:
if do_wait: do_wait()
line = sys.stdin.readline(128)
if line.startswith('HOST '):
(name,ip) = line[5:].strip().split(',', 1)
hostmap[name] = ip
rewrite_etc_hosts(port)
elif line:
raise Fatal('expected EOF, got %r' % line)
else:
break
finally:
try:
debug1('firewall manager: undoing changes.\n')
except:
pass
do_it(port, 0, [])
restore_etc_hosts(port)

View File

@ -1,75 +0,0 @@
import sys, os, socket, errno
logprefix = ''
verbose = 0
def log(s):
try:
sys.stdout.flush()
sys.stderr.write(logprefix + s)
sys.stderr.flush()
except IOError:
# this could happen if stderr gets forcibly disconnected, eg. because
# our tty closes. That sucks, but it's no reason to abort the program.
pass
def debug1(s):
if verbose >= 1:
log(s)
def debug2(s):
if verbose >= 2:
log(s)
def debug3(s):
if verbose >= 3:
log(s)
class Fatal(Exception):
pass
def list_contains_any(l, sub):
for i in sub:
if i in l:
return True
return False
def resolvconf_nameservers():
l = []
for line in open('/etc/resolv.conf'):
words = line.lower().split()
if len(words) >= 2 and words[0] == 'nameserver':
l.append(words[1])
return l
def resolvconf_random_nameserver():
l = resolvconf_nameservers()
if l:
if len(l) > 1:
# don't import this unless we really need it
import random
random.shuffle(l)
return l[0]
else:
return '127.0.0.1'
def islocal(ip):
sock = socket.socket()
try:
try:
sock.bind((ip, 0))
except socket.error, e:
if e.args[0] == errno.EADDRNOTAVAIL:
return False # not a local IP
else:
raise
finally:
sock.close()
return True # it's a local IP, or there would have been an error

130
main.py
View File

@ -1,130 +0,0 @@
import sys, os, re
import helpers, options, client, server, firewall, hostwatch
import compat.ssubprocess as ssubprocess
from helpers import *
# list of:
# 1.2.3.4/5 or just 1.2.3.4
def parse_subnets(subnets_str):
subnets = []
for s in subnets_str:
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 == 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)
subnets.append(('%d.%d.%d.%d' % (a,b,c,d), width))
return subnets
# 1.2.3.4:567 or just 1.2.3.4 or just 567
def parse_ipport(s):
s = str(s)
m = re.match(r'(?:(\d+)\.(\d+)\.(\d+)\.(\d+))?(?::)?(?:(\d+))?$', s)
if not m:
raise Fatal('%r is not a valid IP:port format' % s)
(a,b,c,d,port) = m.groups()
(a,b,c,d,port) = (int(a or 0), int(b or 0), int(c or 0), int(d or 0),
int(port or 0))
if a > 255 or b > 255 or c > 255 or d > 255:
raise Fatal('%d.%d.%d.%d has numbers > 255' % (a,b,c,d))
if port > 65535:
raise Fatal('*:%d is greater than the maximum of 65535' % port)
if a == None:
a = b = c = d = 0
return ('%d.%d.%d.%d' % (a,b,c,d), port)
optspec = """
sshuttle [-l [ip:]port] [-r [username@]sshserver[:port]] <subnets...>
sshuttle --server
sshuttle --firewall <port> <subnets...>
sshuttle --hostwatch
--
l,listen= transproxy to this ip address and port number [127.0.0.1:0]
H,auto-hosts scan for remote hostnames and update local /etc/hosts
N,auto-nets automatically determine subnets to route
dns capture local DNS requests and forward to the remote DNS server
python= path to python interpreter on the remote server
r,remote= ssh hostname (and optional username) of remote sshuttle server
x,exclude= exclude this subnet (can be used more than once)
v,verbose increase debug message verbosity
e,ssh-cmd= the command to use to connect to the remote [ssh]
seed-hosts= with -H, use these hostnames for initial scan (comma-separated)
no-latency-control sacrifice latency to improve bandwidth benchmarks
wrap= restart counting channel numbers after this number (for testing)
D,daemon run in the background as a daemon
syslog send log messages to syslog (default if you use --daemon)
pidfile= pidfile name (only if using --daemon) [./sshuttle.pid]
server (internal use only)
firewall (internal use only)
hostwatch (internal use only)
"""
o = options.Options(optspec)
(opt, flags, extra) = o.parse(sys.argv[2:])
if opt.daemon:
opt.syslog = 1
if opt.wrap:
import ssnet
ssnet.MAX_CHANNEL = int(opt.wrap)
helpers.verbose = opt.verbose
try:
if opt.server:
if len(extra) != 0:
o.fatal('no arguments expected')
server.latency_control = opt.latency_control
sys.exit(server.main())
elif opt.firewall:
if len(extra) != 2:
o.fatal('exactly two arguments expected')
sys.exit(firewall.main(int(extra[0]), int(extra[1]), opt.syslog))
elif opt.hostwatch:
sys.exit(hostwatch.hw_main(extra))
else:
if len(extra) < 1 and not opt.auto_nets:
o.fatal('at least one subnet (or -N) expected')
includes = extra
excludes = ['127.0.0.0/8']
for k,v in flags:
if k in ('-x','--exclude'):
excludes.append(v)
remotename = opt.remote
if remotename == '' or remotename == '-':
remotename = None
if opt.seed_hosts and not opt.auto_hosts:
o.fatal('--seed-hosts only works if you also use -H')
if opt.seed_hosts:
sh = re.split(r'[\s,]+', (opt.seed_hosts or "").strip())
elif opt.auto_hosts:
sh = []
else:
sh = None
sys.exit(client.main(parse_ipport(opt.listen or '0.0.0.0:0'),
opt.ssh_cmd,
remotename,
opt.python,
opt.latency_control,
opt.dns,
sh,
opt.auto_nets,
parse_subnets(includes),
parse_subnets(excludes),
opt.syslog, opt.daemon, opt.pidfile))
except Fatal, e:
log('fatal: %s\n' % e)
sys.exit(99)
except KeyboardInterrupt:
log('\n')
log('Keyboard interrupt: exiting.\n')
sys.exit(1)

View File

@ -1,200 +0,0 @@
"""Command-line options parser.
With the help of an options spec string, easily parse command-line options.
"""
import sys, os, textwrap, getopt, re, struct
class OptDict:
def __init__(self):
self._opts = {}
def __setitem__(self, k, v):
if k.startswith('no-') or k.startswith('no_'):
k = k[3:]
v = not v
self._opts[k] = v
def __getitem__(self, k):
if k.startswith('no-') or k.startswith('no_'):
return not self._opts[k[3:]]
return self._opts[k]
def __getattr__(self, k):
return self[k]
def _default_onabort(msg):
sys.exit(97)
def _intify(v):
try:
vv = int(v or '')
if str(vv) == v:
return vv
except ValueError:
pass
return v
def _atoi(v):
try:
return int(v or 0)
except ValueError:
return 0
def _remove_negative_kv(k, v):
if k.startswith('no-') or k.startswith('no_'):
return k[3:], not v
return k,v
def _remove_negative_k(k):
return _remove_negative_kv(k, None)[0]
def _tty_width():
s = struct.pack("HHHH", 0, 0, 0, 0)
try:
import fcntl, termios
s = fcntl.ioctl(sys.stderr.fileno(), termios.TIOCGWINSZ, s)
except (IOError, ImportError):
return _atoi(os.environ.get('WIDTH')) or 70
(ysize,xsize,ypix,xpix) = struct.unpack('HHHH', s)
return xsize or 70
class Options:
"""Option parser.
When constructed, two strings are mandatory. The first one is the command
name showed before error messages. The second one is a string called an
optspec that specifies the synopsis and option flags and their description.
For more information about optspecs, consult the bup-options(1) man page.
Two optional arguments specify an alternative parsing function and an
alternative behaviour on abort (after having output the usage string).
By default, the parser function is getopt.gnu_getopt, and the abort
behaviour is to exit the program.
"""
def __init__(self, optspec, optfunc=getopt.gnu_getopt,
onabort=_default_onabort):
self.optspec = optspec
self._onabort = onabort
self.optfunc = optfunc
self._aliases = {}
self._shortopts = 'h?'
self._longopts = ['help']
self._hasparms = {}
self._defaults = {}
self._usagestr = self._gen_usage()
def _gen_usage(self):
out = []
lines = self.optspec.strip().split('\n')
lines.reverse()
first_syn = True
while lines:
l = lines.pop()
if l == '--': break
out.append('%s: %s\n' % (first_syn and 'usage' or ' or', l))
first_syn = False
out.append('\n')
last_was_option = False
while lines:
l = lines.pop()
if l.startswith(' '):
out.append('%s%s\n' % (last_was_option and '\n' or '',
l.lstrip()))
last_was_option = False
elif l:
(flags, extra) = l.split(' ', 1)
extra = extra.strip()
if flags.endswith('='):
flags = flags[:-1]
has_parm = 1
else:
has_parm = 0
g = re.search(r'\[([^\]]*)\]$', extra)
if g:
defval = g.group(1)
else:
defval = None
flagl = flags.split(',')
flagl_nice = []
for _f in flagl:
f,dvi = _remove_negative_kv(_f, _intify(defval))
self._aliases[f] = _remove_negative_k(flagl[0])
self._hasparms[f] = has_parm
self._defaults[f] = dvi
if len(f) == 1:
self._shortopts += f + (has_parm and ':' or '')
flagl_nice.append('-' + f)
else:
f_nice = re.sub(r'\W', '_', f)
self._aliases[f_nice] = _remove_negative_k(flagl[0])
self._longopts.append(f + (has_parm and '=' or ''))
self._longopts.append('no-' + f)
flagl_nice.append('--' + _f)
flags_nice = ', '.join(flagl_nice)
if has_parm:
flags_nice += ' ...'
prefix = ' %-20s ' % flags_nice
argtext = '\n'.join(textwrap.wrap(extra, width=_tty_width(),
initial_indent=prefix,
subsequent_indent=' '*28))
out.append(argtext + '\n')
last_was_option = True
else:
out.append('\n')
last_was_option = False
return ''.join(out).rstrip() + '\n'
def usage(self, msg=""):
"""Print usage string to stderr and abort."""
sys.stderr.write(self._usagestr)
e = self._onabort and self._onabort(msg) or None
if e:
raise e
def fatal(self, s):
"""Print an error message to stderr and abort with usage string."""
msg = 'error: %s\n' % s
sys.stderr.write(msg)
return self.usage(msg)
def parse(self, args):
"""Parse a list of arguments and return (options, flags, extra).
In the returned tuple, "options" is an OptDict with known options,
"flags" is a list of option flags that were used on the command-line,
and "extra" is a list of positional arguments.
"""
try:
(flags,extra) = self.optfunc(args, self._shortopts, self._longopts)
except getopt.GetoptError, e:
self.fatal(e)
opt = OptDict()
for k,v in self._defaults.iteritems():
k = self._aliases[k]
opt[k] = v
for (k,v) in flags:
k = k.lstrip('-')
if k in ('h', '?', 'help'):
self.usage()
if k.startswith('no-'):
k = self._aliases[k[3:]]
v = 0
else:
k = self._aliases[k]
if not self._hasparms[k]:
assert(v == '')
v = (opt._opts.get(k) or 0) + 1
else:
v = _intify(v)
opt[k] = v
for (f1,f2) in self._aliases.iteritems():
opt[f1] = opt._opts.get(f2)
return (opt,flags,extra)

7
requirements-tests.txt Normal file
View File

@ -0,0 +1,7 @@
-r requirements.txt
attrs==19.3.0
pytest==5.4.3
pytest-cov==2.10.0
mock==2.0.0
flake8==3.8.3
pyflakes==2.2.0

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
setuptools-scm==4.1.2

15
run Executable file
View File

@ -0,0 +1,15 @@
#!/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" "$@"

247
server.py
View File

@ -1,247 +0,0 @@
import re, struct, socket, select, traceback, time
if not globals().get('skip_imports'):
import ssnet, helpers, hostwatch
import compat.ssubprocess as ssubprocess
from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
from helpers import *
def _ipmatch(ipstr):
if ipstr == 'default':
ipstr = '0.0.0.0/0'
m = re.match(r'^(\d+(\.\d+(\.\d+(\.\d+)?)?)?)(?:/(\d+))?$', ipstr)
if m:
g = m.groups()
ips = g[0]
width = int(g[4] or 32)
if g[1] == None:
ips += '.0.0.0'
width = min(width, 8)
elif g[2] == None:
ips += '.0.0'
width = min(width, 16)
elif g[3] == None:
ips += '.0'
width = min(width, 24)
return (struct.unpack('!I', socket.inet_aton(ips))[0], width)
def _ipstr(ip, width):
if width >= 32:
return ip
else:
return "%s/%d" % (ip, width)
def _maskbits(netmask):
if not netmask:
return 32
for i in range(32):
if netmask[0] & _shl(1, i):
return 32-i
return 0
def _shl(n, bits):
return n * int(2**bits)
def _list_routes():
argv = ['netstat', '-rn']
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE)
routes = []
for line in p.stdout:
cols = re.split(r'\s+', line)
ipw = _ipmatch(cols[0])
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
width = min(ipw[1], mask)
ip = ipw[0] & _shl(_shl(1, width) - 1, 32-width)
routes.append((socket.inet_ntoa(struct.pack('!I', ip)), width))
rv = p.wait()
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 (ip,width) in _list_routes():
if not ip.startswith('0.') and not ip.startswith('127.'):
yield (ip,width)
def _exc_dump():
exc_info = sys.exc_info()
return ''.join(traceback.format_exception(*exc_info))
def start_hostwatch(seed_hosts):
s1,s2 = socket.socketpair()
pid = os.fork()
if not pid:
# child
rv = 99
try:
try:
s2.close()
os.dup2(s1.fileno(), 1)
os.dup2(s1.fileno(), 0)
s1.close()
rv = hostwatch.hw_main(seed_hosts) or 0
except Exception, e:
log('%s\n' % _exc_dump())
rv = 98
finally:
os._exit(rv)
s1.close()
return pid,s2
class Hostwatch:
def __init__(self):
self.pid = 0
self.sock = None
class DnsProxy(Handler):
def __init__(self, mux, chan, request):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
Handler.__init__(self, [sock])
self.timeout = time.time()+30
self.mux = mux
self.chan = chan
self.tries = 0
self.peer = None
self.request = request
self.sock = sock
self.sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
self.try_send()
def try_send(self):
if self.tries >= 3:
return
self.tries += 1
self.peer = resolvconf_random_nameserver()
self.sock.connect((self.peer, 53))
debug2('DNS: sending to %r\n' % self.peer)
try:
self.sock.send(self.request)
except socket.error, e:
if e.args[0] in ssnet.NET_ERRS:
# might have been spurious; try again.
# Note: these errors sometimes are reported by recv(),
# and sometimes by send(). We have to catch both.
debug2('DNS send to %r: %s\n' % (self.peer, e))
self.try_send()
return
else:
log('DNS send to %r: %s\n' % (self.peer, e))
return
def callback(self):
try:
data = self.sock.recv(4096)
except socket.error, e:
if e.args[0] in ssnet.NET_ERRS:
# might have been spurious; try again.
# Note: these errors sometimes are reported by recv(),
# and sometimes by send(). We have to catch both.
debug2('DNS recv from %r: %s\n' % (self.peer, e))
self.try_send()
return
else:
log('DNS recv from %r: %s\n' % (self.peer, e))
return
debug2('DNS response: %d bytes\n' % len(data))
self.mux.send(self.chan, ssnet.CMD_DNS_RESPONSE, data)
self.ok = False
def main():
if helpers.verbose >= 1:
helpers.logprefix = ' s: '
else:
helpers.logprefix = 'server: '
debug1('latency control setting = %r\n' % latency_control)
routes = list(list_routes())
debug1('available routes:\n')
for r in routes:
debug1(' %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))
handlers.append(mux)
routepkt = ''
for r in routes:
routepkt += '%s,%d\n' % r
mux.send(0, ssnet.CMD_ROUTES, routepkt)
hw = Hostwatch()
hw.leftover = ''
def hostwatch_ready():
assert(hw.pid)
content = hw.sock.recv(4096)
if content:
lines = (hw.leftover + content).split('\n')
if lines[-1]:
# no terminating newline: entry isn't complete yet!
hw.leftover = lines.pop()
lines.append('')
else:
hw.leftover = ''
mux.send(0, ssnet.CMD_HOST_LIST, '\n'.join(lines))
else:
raise Fatal('hostwatch process died')
def got_host_req(data):
if not hw.pid:
(hw.pid,hw.sock) = start_hostwatch(data.strip().split())
handlers.append(Handler(socks = [hw.sock],
callback = hostwatch_ready))
mux.got_host_req = got_host_req
def new_channel(channel, data):
(dstip,dstport) = data.split(',', 1)
dstport = int(dstport)
outwrap = ssnet.connect_dst(dstip,dstport)
handlers.append(Proxy(MuxWrapper(mux, channel), outwrap))
mux.new_channel = new_channel
dnshandlers = {}
def dns_req(channel, data):
debug2('Incoming DNS request.\n')
h = DnsProxy(mux, channel, data)
handlers.append(h)
dnshandlers[channel] = h
mux.got_dns_req = dns_req
while mux.ok:
if hw.pid:
assert(hw.pid > 0)
(rpid, rv) = os.waitpid(hw.pid, os.WNOHANG)
if rpid:
raise Fatal('hostwatch exited unexpectedly: code 0x%04x\n' % rv)
ssnet.runonce(handlers, mux)
if latency_control:
mux.check_fullness()
mux.callback()
if dnshandlers:
now = time.time()
for channel,h in dnshandlers.items():
if h.timeout < now or not h.ok:
del dnshandlers[channel]
h.ok = False

17
setup.cfg Normal file
View File

@ -0,0 +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

72
setup.py Executable file
View File

@ -0,0 +1,72 @@
#!/usr/bin/env python
# Copyright 2012-2014 Brian May
#
# This file is part of sshuttle.
#
# 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.
#
# 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 Lesser General Public License for more details.
#
# 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
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'],
# 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="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 Lesser General Public License v2 or later (LGPLv2+)",
"Operating System :: OS Independent",
"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',
'pytest-cov',
'pytest-runner',
'mock',
'flake8',
],
keywords="ssh vpn",
)

106
ssh.py
View File

@ -1,106 +0,0 @@
import sys, os, re, socket, zlib
import compat.ssubprocess as ssubprocess
import helpers
from helpers import *
def readfile(name):
basedir = os.path.dirname(os.path.abspath(sys.argv[0]))
path = [basedir] + sys.path
for d in path:
fullname = os.path.join(d, name)
if os.path.exists(fullname):
return open(fullname, 'rb').read()
raise Exception("can't find file %r in any of %r" % (name, path))
def empackage(z, filename, data=None):
(path,basename) = os.path.split(filename)
if not data:
data = readfile(filename)
content = z.compress(data)
content += z.flush(zlib.Z_SYNC_FLUSH)
return '%s\n%d\n%s' % (basename, len(content), content)
def connect(ssh_cmd, rhostport, python, stderr, options):
main_exe = sys.argv[0]
portl = []
rhostIsIPv6 = False
if (rhostport or '').count(':') > 1:
rhostIsIPv6 = True
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]))]
else: # can't disambiguate IPv6 colons and a port number. pass the hostname through.
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
ipv6flag = []
if rhostIsIPv6:
ipv6flag = ['-6']
z = zlib.compressobj(1)
content = readfile('assembler.py')
optdata = ''.join("%s=%r\n" % (k,v) for (k,v) in options.items())
content2 = (empackage(z, 'cmdline_options.py', optdata) +
empackage(z, 'helpers.py') +
empackage(z, 'compat/ssubprocess.py') +
empackage(z, 'ssnet.py') +
empackage(z, 'hostwatch.py') +
empackage(z, 'server.py') +
"\n")
pyscript = r"""
import sys;
skip_imports=1;
verbosity=%d;
exec compile(sys.stdin.read(%d), "assembler.py", "exec")
""" % (helpers.verbose or 0, len(content))
pyscript = re.sub(r'\s+', ' ', pyscript.strip())
if not rhost:
# ignore the --python argument when running locally; we already know
# which python version works.
argv = [sys.argv[1], '-c', pyscript]
else:
if ssh_cmd:
sshl = ssh_cmd.split(' ')
else:
sshl = ['ssh']
if python:
pycmd = "'%s' -c '%s'" % (python, pyscript)
else:
pycmd = ("P=python2; $P -V 2>/dev/null || P=python; "
"exec \"$P\" -c '%s'") % pyscript
argv = (sshl +
portl +
ipv6flag +
[rhost, '--', pycmd])
(s1,s2) = socket.socketpair()
def setup():
# runs in the child process
s2.close()
s1a,s1b = os.dup(s1.fileno()), os.dup(s1.fileno())
s1.close()
debug2('executing: %r\n' % argv)
p = ssubprocess.Popen(argv, stdin=s1a, stdout=s1b, preexec_fn=setup,
close_fds=True, stderr=stderr)
os.close(s1a)
os.close(s1b)
s2.sendall(content)
s2.sendall(content2)
return p, s2

View File

@ -1,12 +0,0 @@
#!/bin/sh
EXE=$0
for i in 1 2 3 4 5 6 7 8 9 10; do
[ -L "$EXE" ] || break
EXE=$(readlink "$EXE")
done
DIR=$(dirname "$EXE")
if python2 -V 2>/dev/null; then
exec python2 "$DIR/main.py" python2 "$@"
else
exec python "$DIR/main.py" python "$@"
fi

View File

@ -1,270 +0,0 @@
% sshuttle(8) Sshuttle 0.46
% Avery Pennarun <apenwarr@gmail.com>
% 2011-01-25
# NAME
sshuttle - a transparent proxy-based VPN using ssh
# SYNOPSIS
sshuttle [options...] [-r [username@]sshserver[:port]] \<subnets...\>
# DESCRIPTION
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.
To work, you must have root access on the local machine,
but you can have a normal account on the server.
It's valid to run sshuttle more than once simultaneously on
a single client machine, connecting to a different server
every time, so you can be on more than one VPN at once.
If run on a router, sshuttle can forward traffic for your
entire subnet to the VPN.
# OPTIONS
\<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
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').
-l, --listen=*[ip:]port*
: use this ip address and port number as the transparent
proxy port. By default sshuttle finds an available
port automatically and listens on IP 127.0.0.1
(localhost), so you don't need to override it, and
connections are only proxied from the local machine,
not from outside machines. If you want to accept
connections from other machines on your network (ie. to
run sshuttle on a router) try enabling IP Forwarding in
your kernel, then using `--listen 0.0.0.0:0`.
-H, --auto-hosts
: scan for remote hostnames and update the local /etc/hosts
file with matching entries for as long as the VPN is
open. This is nicer than changing your system's DNS
(/etc/resolv.conf) settings, for several reasons. First,
hostnames are added without domain names attached, so
you can `ssh thatserver` without worrying if your local
domain matches the remote one. Second, if you sshuttle
into more than one VPN at a time, it's impossible to
use more than one DNS server at once anyway, but
sshuttle correctly merges /etc/hosts entries between
all running copies. Third, if you're only routing a
few subnets over the VPN, you probably would prefer to
keep using your local DNS server for everything else.
-N, --auto-nets
: in addition to the subnets provided on the command
line, ask the server which subnets it thinks we should
route, and route those automatically. The suggestions
are taken automatically from the server's routing
table.
--python
: specify the name/path of the remote python interpreter.
The default is just `python`, which means to use the
default python interpreter on the remote system's PATH.
-r, --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.
-x, --exclude=*subnet*
: explicitly exclude this subnet from forwarding. The
format of this option is the same as the `<subnets>`
option. To exclude more than one subnet, specify the
`-x` option more than once. You can say something like
`0/0 -x 1.2.3.0/24` to forward everything except the
local subnet over the VPN, for example.
-v, --verbose
: print more information about the session. This option
can be used more than once for increased verbosity. By
default, sshuttle prints only error messages.
-e, --ssh-cmd
: the command to use to connect to the remote server. The
default is just `ssh`. Use this if your ssh client is
in a non-standard location or you want to provide extra
options to the ssh command, for example, `-e 'ssh -v'`.
--seed-hosts
: a comma-separated list of hostnames to use to
initialize the `--auto-hosts` scan algorithm.
`--auto-hosts` does things like poll local SMB servers
for lists of local hostnames, but can speed things up
if you use this option to give it a few names to start
from.
--no-latency-control
: sacrifice latency to improve bandwidth benchmarks. ssh
uses really big socket buffers, which can overload the
connection if you start doing large file transfers,
thus making all your other sessions inside the same
tunnel go slowly. Normally, sshuttle tries to avoid
this problem using a "fullness check" that allows only
a certain amount of outstanding data to be buffered at
a time. But on high-bandwidth links, this can leave a
lot of your bandwidth underutilized. It also makes
sshuttle seem slow in bandwidth benchmarks (benchmarks
rarely test ping latency, which is what sshuttle is
trying to control). This option disables the latency
control feature, maximizing bandwidth usage. Use at
your own risk.
-D, --daemon
: automatically fork into the background after connecting
to the remote server. Implies `--syslog`.
--syslog
: after connecting, send all log messages to the
`syslog`(3) service instead of stderr. This is
implicit if you use `--daemon`.
--pidfile=*pidfilename*
: when using `--daemon`, save sshuttle's pid to
*pidfilename*. The default is `sshuttle.pid` in the
current directory.
--server
: (internal use only) run the sshuttle server on
stdin/stdout. This is what the client runs on
the remote end.
--firewall
: (internal use only) run the firewall manager. This is
the only part of sshuttle that must run as root. If
you start sshuttle as a non-root user, it will
automatically run `sudo` or `su` to start the firewall
manager, but the core of sshuttle still runs as a
normal user.
--hostwatch
: (internal use only) run the hostwatch daemon. This
process runs on the server side and collects hostnames for
the `--auto-hosts` option. Using this option by itself
makes it a lot easier to debug and test the `--auto-hosts`
feature.
# EXAMPLES
Test locally by proxying all local connections, without using ssh:
$ sshuttle -v 0/0
Starting sshuttle proxy.
Listening on ('0.0.0.0', 12300).
[local sudo] Password:
firewall manager ready.
c : connecting to server...
s: available routes:
s: 192.168.42.0/24
c : connected.
firewall manager: starting transproxy.
c : Accept: 192.168.42.106:50035 -> 192.168.42.121:139.
c : Accept: 192.168.42.121:47523 -> 77.141.99.22:443.
...etc...
^C
firewall manager: undoing changes.
KeyboardInterrupt
c : Keyboard interrupt: exiting.
c : SW#8:192.168.42.121:47523: deleting
c : SW#6:192.168.42.106:50035: deleting
Test connection to a remote server, with automatic hostname
and subnet guessing:
$ sshuttle -vNHr example.org
Starting sshuttle proxy.
Listening on ('0.0.0.0', 12300).
firewall manager ready.
c : connecting to server...
s: available routes:
s: 77.141.99.0/24
c : connected.
c : seed_hosts: []
firewall manager: starting transproxy.
hostwatch: Found: testbox1: 1.2.3.4
hostwatch: Found: mytest2: 5.6.7.8
hostwatch: Found: domaincontroller: 99.1.2.3
c : Accept: 192.168.42.121:60554 -> 77.141.99.22:22.
^C
firewall manager: undoing changes.
c : Keyboard interrupt: exiting.
c : SW#6:192.168.42.121:60554: deleting
# DISCUSSION
When it starts, sshuttle creates an ssh session to the
server specified by the `-r` option. If `-r` is omitted,
it will start both its client and server locally, which is
sometimes useful for testing.
After connecting to the remote server, sshuttle uploads its
(python) source code to the remote end and executes it
there. Thus, you don't need to install sshuttle on the
remote server, and there are never sshuttle version
conflicts between client and server.
Unlike most VPNs, sshuttle forwards sessions, not packets.
That is, it uses kernel transparent proxying (`iptables
REDIRECT` rules on Linux, or `ipfw fwd` rules on BSD) to
capture outgoing TCP sessions, then creates entirely
separate TCP sessions out to the original destination at
the other end of the tunnel.
Packet-level forwarding (eg. using the tun/tap devices on
Linux) seems elegant at first, but it results in
several problems, notably the 'tcp over tcp' problem. The
tcp protocol depends fundamentally on packets being dropped
in order to implement its congestion control agorithm; if
you pass tcp packets through a tcp-based tunnel (such as
ssh), the inner tcp packets will never be dropped, and so
the inner tcp stream's congestion control will be
completely broken, and performance will be terrible. Thus,
packet-based VPNs (such as IPsec and openvpn) cannot use
tcp-based encrypted streams like ssh or ssl, and have to
implement their own encryption from scratch, which is very
complex and error prone.
sshuttle's simplicity comes from the fact that it can
safely use the existing ssh encrypted tunnel without
incurring a performance penalty. It does this by letting
the client-side kernel manage the incoming tcp stream, and
the server-side kernel manage the outgoing tcp stream;
there is no need for congestion control to be shared
between the two separate streams, so a tcp-based tunnel is
fine.
# BUGS
On MacOS 10.6 (at least up to 10.6.6), your network will
stop responding about 10 minutes after the first time you
start sshuttle, because of a MacOS kernel bug relating to
arp and the net.inet.ip.scopedroute sysctl. To fix it,
just switch your wireless off and on. Sshuttle makes the
kernel setting it changes permanent, so this won't happen
again, even after a reboot.
# SEE ALSO
`ssh`(1), `python`(1)

4
sshuttle/__init__.py Normal file
View File

@ -0,0 +1,4 @@
try:
from sshuttle.version import version as __version__
except ImportError:
__version__ = "unknown"

4
sshuttle/__main__.py Normal file
View File

@ -0,0 +1,4 @@
"""Coverage.py's main entry point."""
import sys
from sshuttle.cmdline import main
sys.exit(main())

41
sshuttle/assembler.py Normal file
View File

@ -0,0 +1,41 @@
import sys
import zlib
import types
verbosity = verbosity # noqa: F821 must be a previously defined global
z = zlib.decompressobj()
while 1:
name = sys.stdin.readline().strip()
if name:
name = name.decode("ASCII")
nbytes = int(sys.stdin.readline())
if verbosity >= 2:
sys.stderr.write('server: assembling %r (%d bytes)\n'
% (name, nbytes))
content = z.decompress(sys.stdin.read(nbytes))
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__) # nosec
sys.modules[name] = module
else:
break
sys.stderr.flush()
sys.stdout.flush()
# 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 # noqa: E402
from sshuttle.server import main # noqa: E402
main(options.latency_control, options.auto_hosts, options.to_nameserver,
options.auto_nets)

806
sshuttle/client.py Normal file
View File

@ -0,0 +1,806 @@
import errno
import re
import signal
import time
import subprocess as ssubprocess
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 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
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):
log('exiting on signal %d\n' % signum)
sys.exit(1)
_pidname = None
def check_daemon(pidfile):
global _pidname
_pidname = os.path.abspath(pidfile)
try:
oldpid = open(_pidname).read(1024)
except IOError as e:
if e.errno == errno.ENOENT:
return # no pidfile, ok
else:
raise Fatal("can't read %s: %s" % (_pidname, e))
if not oldpid:
os.unlink(_pidname)
return # invalid pidfile, ok
oldpid = int(oldpid.strip() or 0)
if oldpid <= 0:
os.unlink(_pidname)
return # invalid pidfile, ok
try:
os.kill(oldpid, 0)
except OSError as e:
if e.errno == errno.ESRCH:
os.unlink(_pidname)
return # outdated pidfile, ok
elif e.errno == errno.EPERM:
pass
else:
raise
raise Fatal("%s: sshuttle is already running (pid=%d)"
% (_pidname, oldpid))
def daemonize():
if os.fork():
os._exit(0)
os.setsid()
if os.fork():
os._exit(0)
outfd = os.open(_pidname, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o666)
try:
os.write(outfd, b'%d\n' % os.getpid())
finally:
os.close(outfd)
os.chdir("/")
# Normal exit when killed, or try/finally won't work and the pidfile won't
# be deleted.
signal.signal(signal.SIGTERM, got_signal)
si = open(os.devnull, 'r+')
os.dup2(si.fileno(), 0)
os.dup2(si.fileno(), 1)
si.close()
def daemon_cleanup():
try:
os.unlink(_pidname)
except OSError as e:
if e.errno == errno.ENOENT:
pass
else:
raise
class MultiListener:
def __init__(self, kind=socket.SOCK_STREAM, proto=0):
self.type = kind
self.proto = proto
self.v6 = None
self.v4 = None
self.bind_called = False
def setsockopt(self, level, optname, value):
assert(self.bind_called)
if self.v6:
self.v6.setsockopt(level, optname, value)
if self.v4:
self.v4.setsockopt(level, optname, value)
def add_handler(self, handlers, callback, method, mux):
assert(self.bind_called)
socks = []
if self.v6:
socks.append(self.v6)
if self.v4:
socks.append(self.v4)
handlers.append(
Handler(
socks,
lambda sock: callback(sock, method, mux, handlers)
)
)
def listen(self, backlog):
assert(self.bind_called)
if self.v6:
self.v6.listen(backlog)
if self.v4:
try:
self.v4.listen(backlog)
except socket.error as e:
# on some systems v4 bind will fail if the v6 suceeded,
# in this case the v6 socket will receive v4 too.
if e.errno == errno.EADDRINUSE and self.v6:
self.v4 = None
else:
raise e
def bind(self, address_v6, address_v4):
assert(not self.bind_called)
self.bind_called = True
if address_v6 is not None:
self.v6 = socket.socket(socket.AF_INET6, self.type, self.proto)
self.v6.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.bind(address_v4)
else:
self.v4 = None
def print_listening(self, what):
assert(self.bind_called)
if self.v6:
listenip = self.v6.getsockname()
debug1('%s listening on %r.\n' % (what, listenip))
debug2('%s listening with %r.\n' % (what, self.v6))
if self.v4:
listenip = self.v4.getsockname()
debug1('%s listening on %r.\n' % (what, listenip))
debug2('%s listening with %r.\n' % (what, self.v4))
class FirewallClient:
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]] +
['-v'] * (helpers.verbose or 0) +
['--method', method_name] +
['--firewall'])
if ssyslog._p:
argvbase += ['--syslog']
# 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.
# Instead, attach a *bidirectional* socket to its stdout, and use
# that for talking in both directions.
(s1, s2) = socket.socketpair()
def setup():
# run in the child process
s2.close()
e = None
if os.getuid() == 0:
argv_tries = argv_tries[-1:] # last entry only
for argv in argv_tries:
try:
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:
pass
self.argv = argv
s1.close()
self.pfile = s2.makefile('rwb')
if e:
log('Spawning firewall manager: %r\n' % self.argv)
raise Fatal(e)
line = self.pfile.readline()
self.check()
if line[0:5] != b'READY':
raise Fatal('%r expected READY, got %r' % (self.argv, line))
method_name = line[6:-1]
self.method = get_method(method_name.decode("ASCII"))
self.method.set_firewall(self)
def setup(self, subnets_include, subnets_exclude, nslist,
redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4, udp,
user):
self.subnets_include = subnets_include
self.subnets_exclude = subnets_exclude
self.nslist = nslist
self.redirectport_v6 = redirectport_v6
self.redirectport_v4 = redirectport_v4
self.dnsport_v6 = dnsport_v6
self.dnsport_v4 = dnsport_v4
self.udp = udp
self.user = user
def check(self):
rv = self.p.poll()
if rv:
raise Fatal('%r returned %d' % (self.argv, rv))
def start(self):
self.pfile.write(b'ROUTES\n')
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:
self.pfile.write(b'%d,%s\n'
% (family, ip.encode("ASCII")))
self.pfile.write(
b'PORTS %d,%d,%d,%d\n'
% (self.redirectport_v6, self.redirectport_v4,
self.dnsport_v6, self.dnsport_v4))
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 %s\n' % (udp, user))
self.pfile.flush()
line = self.pfile.readline()
self.check()
if line != b'STARTED\n':
raise Fatal('%r expected STARTED, got %r' % (self.argv, line))
def sethostip(self, hostname, 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()
def done(self):
self.pfile.close()
rv = self.p.wait()
if rv:
raise Fatal('cleanup: %r returned %d' % (self.argv, rv))
dnsreqs = {}
udp_by_src = {}
def expire_connections(now, mux):
remove = []
for chan, timeout in dnsreqs.items():
if timeout < now:
debug3('expiring dnsreqs channel=%d\n' % chan)
remove.append(chan)
del mux.channels[chan]
for chan in remove:
del dnsreqs[chan]
debug3('Remaining DNS requests: %d\n' % len(dnsreqs))
remove = []
for peer, (chan, timeout) in udp_by_src.items():
if timeout < now:
debug3('expiring UDP channel channel=%d peer=%r\n' % (chan, peer))
mux.send(chan, ssnet.CMD_UDP_CLOSE, b'')
remove.append(peer)
del mux.channels[chan]
for peer in remove:
del udp_by_src[peer]
debug3('Remaining UDP channels: %d\n' % len(udp_by_src))
def onaccept_tcp(listener, method, mux, handlers):
global _extra_fd
try:
sock, srcip = listener.accept()
except socket.error as e:
if e.args[0] in [errno.EMFILE, errno.ENFILE]:
debug1('Rejected incoming connection: too many open files!\n')
# free up an fd so we can eat the connection
os.close(_extra_fd)
try:
sock, srcip = listener.accept()
sock.close()
finally:
_extra_fd = os.open(os.devnull, os.O_RDONLY)
return
else:
raise
dstip = method.get_tcp_dstip(sock)
debug1('Accept TCP: %s:%r -> %s:%r.\n' % (srcip[0], srcip[1],
dstip[0], dstip[1]))
if dstip[1] == sock.getsockname()[1] and islocal(dstip[0], sock.family):
debug1("-- ignored: that's my address!\n")
sock.close()
return
chan = mux.next_channel()
if not chan:
log('warning: too many open channels. Discarded connection.\n')
sock.close()
return
mux.send(chan, ssnet.CMD_TCP_CONNECT, b'%d,%s,%d' %
(sock.family, dstip[0].encode("ASCII"), dstip[1]))
outwrap = MuxWrapper(mux, chan)
handlers.append(Proxy(SockWrapper(sock, sock), outwrap))
expire_connections(time.time(), mux)
def udp_done(chan, data, method, sock, dstip):
(src, srcport, data) = data.split(b",", 2)
srcip = (src, int(srcport))
debug3('doing send from %r to %r\n' % (srcip, dstip,))
method.send_udp(sock, srcip, dstip, data)
def onaccept_udp(listener, method, mux, handlers):
now = time.time()
t = method.recv_udp(listener, 4096)
if t is None:
return
srcip, dstip, data = t
debug1('Accept UDP: %r -> %r.\n' % (srcip, dstip,))
if srcip in udp_by_src:
chan, _ = udp_by_src[srcip]
else:
chan = mux.next_channel()
mux.channels[chan] = lambda cmd, data: udp_done(
chan, data, method, listener, dstip=srcip)
mux.send(chan, ssnet.CMD_UDP_OPEN, b"%d" % listener.family)
udp_by_src[srcip] = chan, now + 30
hdr = b"%s,%d," % (dstip[0].encode("ASCII"), dstip[1])
mux.send(chan, ssnet.CMD_UDP_DATA, hdr + data)
expire_connections(now, mux)
def dns_done(chan, data, method, sock, srcip, dstip, mux):
debug3('dns_done: channel=%d src=%r dst=%r\n' % (chan, srcip, dstip))
del mux.channels[chan]
del dnsreqs[chan]
method.send_udp(sock, srcip, dstip, data)
def ondns(listener, method, mux, handlers):
now = time.time()
t = method.recv_udp(listener, 4096)
if t is None:
return
srcip, dstip, data = t
debug1('DNS request from %r to %r: %d bytes\n' % (srcip, dstip, len(data)))
chan = mux.next_channel()
dnsreqs[chan] = now + 30
mux.send(chan, ssnet.CMD_DNS_REQ, data)
mux.channels[chan] = lambda cmd, data: dns_done(
chan, data, method, listener, srcip=dstip, dstip=srcip, mux=mux)
expire_connections(now, mux)
def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
python, latency_control,
dns_listener, seed_hosts, auto_hosts, auto_nets, daemon,
to_nameserver):
debug1('Starting client with Python version %s\n'
% platform.python_version())
method = fw.method
handlers = []
if helpers.verbose >= 1:
helpers.logprefix = 'c : '
else:
helpers.logprefix = 'client: '
debug1('connecting to server...\n')
try:
(serverproc, serversock) = ssh.connect(
ssh_cmd, remotename, python,
stderr=ssyslog._p and ssyslog._p.stdin,
options=dict(latency_control=latency_control,
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.makefile("rb"), serversock.makefile("wb"))
handlers.append(mux)
expected = b'SSHUTTLE0001'
try:
v = 'x'
while v and v != b'\0':
v = serversock.recv(1)
v = 'x'
while v and v != b'\0':
v = serversock.recv(1)
initstring = serversock.recv(len(expected))
except socket.error as e:
if e.args[0] == errno.ECONNRESET:
raise Fatal("failed to establish ssh session (2)")
else:
raise
rv = serverproc.poll()
if rv:
raise Fatal('server died with error code %d' % rv)
if initstring != expected:
raise Fatal('expected server init string %r; got %r'
% (expected, initstring))
log('Connected.\n')
sys.stdout.flush()
if daemon:
daemonize()
log('daemonizing (%s).\n' % _pidname)
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)
ip = ip.decode("ASCII")
if family == socket.AF_INET6 and tcp_listener.v6 is None:
debug2("Ignored auto net %d/%s/%d\n" % (family, ip, width))
if family == socket.AF_INET and tcp_listener.v4 is None:
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, 0, 0))
# we definitely want to do this *after* starting ssh, or we might end
# up intercepting the ssh connection!
#
# Moreover, now that we have the --auto-nets option, we have to wait
# for the server to send us that message anyway. Even if we haven't
# set --auto-nets, we might as well wait for the message first, then
# ignore its contents.
mux.got_routes = None
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():
if line:
name, ip = line.split(b',', 1)
fw.sethostip(name, ip)
mux.got_host_list = onhostlist
tcp_listener.add_handler(handlers, onaccept_tcp, method, mux)
if udp_listener:
udp_listener.add_handler(handlers, onaccept_udp, method, mux)
if dns_listener:
dns_listener.add_handler(handlers, ondns, method, mux)
if seed_hosts is not None:
debug1('seed_hosts: %r\n' % seed_hosts)
mux.send(0, ssnet.CMD_HOST_REQ, str.encode('\n'.join(seed_hosts)))
while 1:
rv = serverproc.poll()
if rv:
raise Fatal('server died with error code %d' % rv)
ssnet.runonce(handlers, mux)
if latency_control:
mux.check_fullness()
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, to_nameserver, pidfile,
user, sudo_pythonpath):
if daemon:
try:
check_daemon(pidfile)
except Fatal as e:
log("%s\n" % e)
return 5
debug1('Starting sshuttle proxy.\n')
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]
nslist_v6 = [i for i in nslist if i[0] == socket.AF_INET6]
subnets_v4 = [i for i in subnets if i[0] == socket.AF_INET]
nslist_v4 = [i for i in nslist if i[0] == socket.AF_INET]
# Check features available
avail = fw.method.get_supported_features()
required = Features()
if listenip_v6 == "auto":
if avail.ipv6:
listenip_v6 = ('::1', 0)
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:
nslist_v6 = []
nslist = nslist_v4
fw.method.assert_features(required)
if required.ipv6 and listenip_v6 is None:
raise Fatal("IPv6 required but not listening.")
# display features enabled
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":
listenip_v4 = ('127.0.0.1', 0)
if required.ipv4 and \
not any(listenip_v4[0] == sex[1] for sex in subnets_v4):
subnets_exclude.append((socket.AF_INET, listenip_v4[0], 32, 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, 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
ports = [0, ]
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
redirectport_v6 = 0
redirectport_v4 = 0
bound = False
debug2('Binding redirector:')
for port in ports:
debug2(' %d' % port)
tcp_listener = MultiListener()
if required.udp:
udp_listener = MultiListener(socket.SOCK_DGRAM)
else:
udp_listener = None
if listenip_v6 and listenip_v6[1]:
lv6 = listenip_v6
redirectport_v6 = lv6[1]
elif listenip_v6:
lv6 = (listenip_v6[0], port)
redirectport_v6 = port
else:
lv6 = None
redirectport_v6 = 0
if listenip_v4 and listenip_v4[1]:
lv4 = listenip_v4
redirectport_v4 = lv4[1]
elif listenip_v4:
lv4 = (listenip_v4[0], port)
redirectport_v4 = port
else:
lv4 = None
redirectport_v4 = 0
try:
tcp_listener.bind(lv6, lv4)
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
debug2('\n')
if not bound:
assert(last_e)
raise last_e
tcp_listener.listen(10)
tcp_listener.print_listening("TCP redirector")
if udp_listener:
udp_listener.print_listening("UDP redirector")
bound = False
if required.dns:
# search for spare port for DNS
debug2('Binding DNS:')
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:
lv6 = (listenip_v6[0], port)
dnsport_v6 = port
else:
lv6 = None
dnsport_v6 = 0
if listenip_v4:
lv4 = (listenip_v4[0], port)
dnsport_v4 = port
else:
lv4 = None
dnsport_v4 = 0
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')
dns_listener.print_listening("DNS")
if not bound:
assert(last_e)
raise last_e
else:
dnsport_v6 = 0
dnsport_v4 = 0
dns_listener = None
# Last minute sanity checks.
# These should never fail.
# If these do fail, something is broken above.
if subnets_v6:
assert required.ipv6
if redirectport_v6 == 0:
raise Fatal("IPv6 subnets defined but not listening")
if nslist_v6:
assert required.dns
assert required.ipv6
if dnsport_v6 == 0:
raise Fatal("IPv6 ns servers defined but not listening")
if subnets_v4:
if redirectport_v4 == 0:
raise Fatal("IPv4 subnets defined but not listening")
if nslist_v4:
if dnsport_v4 == 0:
raise Fatal("IPv4 ns servers defined but not listening")
# setup method specific stuff on listeners
fw.method.setup_tcp_listener(tcp_listener)
if udp_listener:
fw.method.setup_udp_listener(udp_listener)
if dns_listener:
fw.method.setup_udp_listener(dns_listener)
# start the firewall
fw.setup(subnets_include, subnets_exclude, nslist,
redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4,
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, 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()

116
sshuttle/cmdline.py Normal file
View File

@ -0,0 +1,116 @@
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_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 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, opt.auto_hosts)
else:
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, '
'or -N expected')
remotename = opt.remote
if remotename == '' or remotename == '-':
remotename = None
nslist = [family_ip_tuple(ns) for ns in opt.ns_hosts]
if opt.seed_hosts:
sh = re.split(r'[\s,]+', (opt.seed_hosts or "").strip())
elif opt.auto_hosts:
sh = []
else:
sh = None
if opt.listen:
ipport_v6 = None
ipport_v4 = None
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 = (ip, port)
else:
# parse_ipport4('127.0.0.1:0')
ipport_v4 = "auto"
# parse_ipport6('[::1]:0')
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,
remotename,
opt.python,
opt.latency_control,
opt.dns,
nslist,
opt.method,
sh,
opt.auto_hosts,
opt.auto_nets,
includes,
excludes,
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 %d detected, failing...' % return_code)
return return_code
except Fatal as e:
log('fatal: %s\n' % e)
return 99
except KeyboardInterrupt:
log('\n')
log('Keyboard interrupt: exiting.\n')
return 1

287
sshuttle/firewall.py Normal file
View File

@ -0,0 +1,287 @@
import errno
import socket
import signal
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
HOSTSFILE = '/etc/hosts'
def rewrite_etc_hosts(hostmap, port):
BAKFILE = '%s.sbak' % HOSTSFILE
APPEND = '# sshuttle-firewall-%d AUTOCREATED' % port
old_content = ''
st = None
try:
old_content = open(HOSTSFILE).read()
st = os.stat(HOSTSFILE)
except IOError as e:
if e.errno == errno.ENOENT:
pass
else:
raise
if old_content.strip() and not os.path.exists(BAKFILE):
os.link(HOSTSFILE, BAKFILE)
tmpname = "%s.%d.tmp" % (HOSTSFILE, port)
f = open(tmpname, 'w')
for line in old_content.rstrip().split('\n'):
if line.find(APPEND) >= 0:
continue
f.write('%s\n' % line)
for (name, ip) in sorted(hostmap.items()):
f.write('%-30s %s\n' % ('%s %s' % (ip, name), APPEND))
f.close()
if st is not None:
os.chown(tmpname, st.st_uid, st.st_gid)
os.chmod(tmpname, st.st_mode)
else:
os.chown(tmpname, 0, 0)
os.chmod(tmpname, 0o600)
os.rename(tmpname, HOSTSFILE)
def restore_etc_hosts(port):
rewrite_etc_hosts({}, port)
# Isolate function that needs to be replaced for tests
def setup_daemon():
if os.getuid() != 0:
raise Fatal('you must be root (or enable su/sudo) to set the firewall')
# don't disappear if our controlling terminal or stdout/stderr
# disappears; we still have to clean up.
signal.signal(signal.SIGHUP, signal.SIG_IGN)
signal.signal(signal.SIGPIPE, signal.SIG_IGN)
signal.signal(signal.SIGTERM, signal.SIG_IGN)
signal.signal(signal.SIGINT, signal.SIG_IGN)
# ctrl-c shouldn't be passed along to me. When the main sshuttle dies,
# I'll die automatically.
os.setsid()
# because of limitations of the 'su' command, the *real* stdin/stdout
# are both attached to stdout initially. Clone stdout into stdin so we
# can read from it.
os.dup2(1, 0)
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.
#
# This code is supposed to clean up after itself by deleting its rules on
# exit. In case that fails, it's not the end of the world; future runs will
# supercede it in the transproxy list, at least, so the leftover rules
# are hopefully harmless.
def main(method_name, syslog):
stdin, stdout = setup_daemon()
hostmap = {}
debug1('firewall manager: Starting firewall with Python version %s\n'
% platform.python_version())
if method_name == "auto":
method = get_auto_method()
else:
method = get_method(method_name)
if syslog:
ssyslog.start_syslog()
ssyslog.stderr_to_syslog()
debug1('firewall manager: ready method name %s.\n' % method.name)
stdout.write('READY %s\n' % method.name)
stdout.flush()
# we wait until we get some input before creating the rules. That way,
# sshuttle can launch us as early as possible (and get sudo password
# authentication as early in the startup process as possible).
line = stdin.readline(128)
if not line:
return # parent died; nothing to do
subnets = []
if line != 'ROUTES\n':
raise Fatal('firewall: expected ROUTES but got %r' % line)
while 1:
line = stdin.readline(128)
if not line:
raise Fatal('firewall: expected route but got %r' % line)
elif line.startswith("NSLIST\n"):
break
try:
(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,
int(fport),
int(lport)))
debug2('firewall manager: Got subnets: %r\n' % subnets)
nslist = []
if line != 'NSLIST\n':
raise Fatal('firewall: expected NSLIST but got %r' % line)
while 1:
line = stdin.readline(128)
if not line:
raise Fatal('firewall: expected nslist but got %r' % line)
elif line.startswith("PORTS "):
break
try:
(family, ip) = line.strip().split(',', 1)
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)
debug2('firewall manager: Got nslist: %r\n' % nslist)
if not line.startswith('PORTS '):
raise Fatal('firewall: expected PORTS but got %r' % line)
_, _, ports = line.partition(" ")
ports = ports.split(",")
if len(ports) != 4:
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])
dnsport_v4 = int(ports[3])
assert(port_v6 >= 0)
assert(port_v6 <= 65535)
assert(port_v4 >= 0)
assert(port_v4 <= 65535)
assert(dnsport_v6 >= 0)
assert(dnsport_v6 <= 65535)
assert(dnsport_v4 >= 0)
assert(dnsport_v4 <= 65535)
debug2('firewall manager: Got ports: %d,%d,%d,%d\n'
% (port_v6, port_v4, dnsport_v6, dnsport_v4))
line = stdin.readline(128)
if not line:
raise Fatal('firewall: expected GO but got %r' % line)
elif not line.startswith("GO "):
raise Fatal('firewall: expected GO but got %r' % line)
_, _, args = line.partition(" ")
udp, user = args.strip().split(" ", 1)
udp = bool(int(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]
subnets_v4 = [i for i in subnets if i[0] == socket.AF_INET]
nslist_v4 = [i for i in nslist if i[0] == socket.AF_INET]
try:
debug1('firewall manager: setting up.\n')
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,
user)
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,
user)
stdout.write('STARTED\n')
try:
stdout.flush()
except IOError:
# the parent process died for some reason; he's surely been loud
# enough, so no reason to report another error
return
# Now we wait until EOF or any other kind of exception. We need
# to stay running so that we don't need a *second* password
# authentication at shutdown time - that cleanup is important!
while 1:
line = stdin.readline(128)
if line.startswith('HOST '):
(name, ip) = line[5:].strip().split(',', 1)
hostmap[name] = ip
debug2('firewall manager: setting up /etc/hosts.\n')
rewrite_etc_hosts(hostmap, port_v6 or port_v4)
elif line:
if not method.firewall_command(line):
raise Fatal('firewall: expected command, got %r' % line)
else:
break
finally:
try:
debug1('firewall manager: undoing changes.\n')
except BaseException:
debug2('An error occurred, ignoring it.')
try:
if subnets_v6 or nslist_v6:
debug2('firewall manager: undoing IPv6 changes.\n')
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 BaseException:
debug2('An error occurred, ignoring it.')
try:
if subnets_v4 or nslist_v4:
debug2('firewall manager: undoing IPv4 changes.\n')
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 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 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 BaseException:
debug2('An error occurred, ignoring it.')

101
sshuttle/helpers.py Normal file
View File

@ -0,0 +1,101 @@
import sys
import socket
import errno
logprefix = ''
verbose = 0
def b(s):
return s.encode("ASCII")
def log(s):
global logprefix
try:
sys.stdout.flush()
if s.find("\n") != -1:
prefix = logprefix
s = s.rstrip("\n")
for line in s.split("\n"):
sys.stderr.write(prefix + line + "\n")
prefix = "---> "
else:
sys.stderr.write(logprefix + s)
sys.stderr.flush()
except IOError:
# this could happen if stderr gets forcibly disconnected, eg. because
# our tty closes. That sucks, but it's no reason to abort the program.
pass
def debug1(s):
if verbose >= 1:
log(s)
def debug2(s):
if verbose >= 2:
log(s)
def debug3(s):
if verbose >= 3:
log(s)
class Fatal(Exception):
pass
def resolvconf_nameservers():
lines = []
for line in open('/etc/resolv.conf'):
words = line.lower().split()
if len(words) >= 2 and words[0] == 'nameserver':
lines.append(family_ip_tuple(words[1]))
return lines
def resolvconf_random_nameserver():
lines = resolvconf_nameservers()
if lines:
if len(lines) > 1:
# don't import this unless we really need it
import random
random.shuffle(lines)
return lines[0]
else:
return (socket.AF_INET, '127.0.0.1')
def islocal(ip, family):
sock = socket.socket(family)
try:
try:
sock.bind((ip, 0))
except socket.error:
_, e = sys.exc_info()[:2]
if e.args[0] == errno.EADDRNOTAVAIL:
return False # not a local IP
else:
raise
finally:
sock.close()
return True # it's a local IP, or there would have been an error
def family_ip_tuple(ip):
if ':' in ip:
return (socket.AF_INET6, ip)
else:
return (socket.AF_INET, ip)
def family_to_string(family):
if family == socket.AF_INET6:
return "AF_INET6"
elif family == socket.AF_INET:
return "AF_INET"
else:
return str(family)

View File

@ -1,12 +1,19 @@
import time, socket, re, select, errno
if not globals().get('skip_imports'):
import compat.ssubprocess as ssubprocess
import helpers
from helpers import *
import time
import socket
import re
import select
import errno
import os
import sys
import platform
POLL_TIME = 60*15
import subprocess as ssubprocess
import sshuttle.helpers as helpers
from sshuttle.helpers import log, debug1, debug2, debug3
POLL_TIME = 60 * 15
NETSTAT_POLL_TIME = 30
CACHEFILE=os.path.expanduser('~/.sshuttle.hosts')
CACHEFILE = os.path.expanduser('~/.sshuttle.hosts')
_nmb_ok = True
@ -14,8 +21,9 @@ _smb_ok = True
hostnames = {}
queue = {}
try:
null = open('/dev/null', 'wb')
except IOError, e:
null = open(os.devnull, 'wb')
except IOError:
_, e = sys.exc_info()[:2]
log('warning: %s\n' % e)
null = os.popen("sh -c 'while read x; do :; done'", 'wb', 4096)
@ -28,21 +36,23 @@ def write_host_cache():
tmpname = '%s.%d.tmp' % (CACHEFILE, os.getpid())
try:
f = open(tmpname, 'wb')
for name,ip in sorted(hostnames.items()):
f.write('%s,%s\n' % (name, ip))
for name, ip in sorted(hostnames.items()):
f.write(('%s,%s\n' % (name, ip)).encode("ASCII"))
f.close()
os.chmod(tmpname, 384) # 600 in octal, 'rw-------'
os.rename(tmpname, CACHEFILE)
finally:
try:
os.unlink(tmpname)
except:
except BaseException:
pass
def read_host_cache():
try:
f = open(CACHEFILE)
except IOError, e:
except IOError:
_, e = sys.exc_info()[:2]
if e.errno == errno.ENOENT:
return
else:
@ -50,24 +60,28 @@ def read_host_cache():
for line in f:
words = line.strip().split(',')
if len(words) == 2:
(name,ip) = words
name = re.sub(r'[^-\w]', '-', name).strip()
(name, ip) = words
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)
if (ip.startswith('127.') or ip.startswith('255.')
or hostname == 'localhost'):
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()
@ -94,7 +108,7 @@ def _check_revdns(ip):
debug3('< %s\n' % r[0])
check_host(r[0])
found_host(r[0], ip)
except socket.herror, e:
except (socket.herror, UnicodeError):
pass
@ -105,18 +119,24 @@ def _check_dns(hostname):
debug3('< %s\n' % ip)
check_host(ip)
found_host(hostname, ip)
except socket.gaierror, e:
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)
content = p.stdout.read()
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null,
env=env)
content = p.stdout.read().decode("ASCII")
p.wait()
except OSError, e:
except OSError:
_, e = sys.exc_info()[:2]
log('%r failed: %r\n' % (argv, e))
return
@ -130,13 +150,19 @@ 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, e:
except OSError:
_, e = sys.exc_info()[:2]
log('%r failed: %r\n' % (argv, e))
_smb_ok = False
return
@ -187,13 +213,19 @@ 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, e:
except OSError:
_, e = sys.exc_info()[:2]
log('%r failed: %r\n' % (argv, e))
_nmb_ok = False
return
@ -228,13 +260,13 @@ def check_workgroup(hostname):
def _enqueue(op, *args):
t = (op,args)
if queue.get(t) == None:
t = (op, args)
if queue.get(t) is None:
queue[t] = 0
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:
@ -242,27 +274,31 @@ def _stdin_still_ok(timeout):
return True
def hw_main(seed_hosts):
def hw_main(seed_hosts, auto_hosts):
if helpers.verbose >= 2:
helpers.logprefix = 'HH: '
else:
helpers.logprefix = 'hostwatch: '
read_host_cache()
debug1('Starting hostwatch with Python version %s\n'
% platform.python_version())
_enqueue(_check_etc_hosts)
_enqueue(_check_netstat)
check_host('localhost')
check_host(socket.gethostname())
check_workgroup('workgroup')
check_workgroup('-')
for h in seed_hosts:
check_host(h)
if auto_hosts:
read_host_cache()
_enqueue(_check_etc_hosts)
_enqueue(_check_netstat)
check_host('localhost')
check_host(socket.gethostname())
check_workgroup('workgroup')
check_workgroup('-')
while 1:
now = time.time()
for t,last_polled in queue.items():
(op,args) = t
for t, last_polled in list(queue.items()):
(op, args) = t
if not _stdin_still_ok(0):
break
maxtime = POLL_TIME

86
sshuttle/linux.py Normal file
View File

@ -0,0 +1,86 @@
import os
import socket
import subprocess as ssubprocess
from sshuttle.helpers import log, debug1, Fatal, family_to_string
def nonfatal(func, *args):
try:
func(*args)
except Fatal as e:
log('error: %s\n' % e)
def ipt_chain_exists(family, table, name):
if family == socket.AF_INET6:
cmd = 'ip6tables'
elif family == socket.AF_INET:
cmd = 'iptables'
else:
raise Exception('Unsupported family "%s"' % family_to_string(family))
argv = [cmd, '-t', table, '-nL']
env = {
'PATH': os.environ['PATH'],
'LC_ALL': "C",
}
try:
output = ssubprocess.check_output(argv, env=env)
for line in output.decode('ASCII').split('\n'):
if line.startswith('Chain %s ' % name):
return True
except ssubprocess.CalledProcessError as e:
raise Fatal('%r returned %d' % (argv, e.returncode))
def ipt(family, table, *args):
if family == socket.AF_INET6:
argv = ['ip6tables', '-t', table] + list(args)
elif family == socket.AF_INET:
argv = ['iptables', '-t', 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))
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
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
# connections, in case client == server.
try:
argsplus = list(args) + ['-m', 'ttl', '!', '--ttl', '42']
ipt(family, *argsplus)
except Fatal:
ipt(family, *args)
# we only get here if the non-ttl attempt succeeds
log('sshuttle: warning: your iptables is missing '
'the ttl module.\n')
_no_ttl_module = True
else:
ipt(family, *args)

View File

@ -0,0 +1,115 @@
import os
import importlib
import socket
import struct
import errno
from sshuttle.helpers import Fatal, debug3
def original_dst(sock):
try:
SO_ORIGINAL_DST = 80
SOCKADDR_MIN = 16
sockaddr_in = sock.getsockopt(socket.SOL_IP,
SO_ORIGINAL_DST, SOCKADDR_MIN)
(proto, port, a, b, c, d) = struct.unpack('!HHBBBB', sockaddr_in[:8])
# FIXME: decoding is IPv4 only.
assert(socket.htons(proto) == socket.AF_INET)
ip = '%d.%d.%d.%d' % (a, b, c, d)
return (ip, port)
except socket.error as e:
if e.args[0] == errno.ENOPROTOOPT:
return sock.getsockname()
raise
class Features(object):
pass
class BaseMethod(object):
def __init__(self, name):
self.firewall = None
self.name = name
def set_firewall(self, firewall):
self.firewall = firewall
@staticmethod
def get_supported_features():
result = Features()
result.ipv6 = False
result.udp = False
result.dns = True
result.user = False
return result
@staticmethod
def get_tcp_dstip(sock):
return original_dst(sock)
@staticmethod
def recv_udp(udp_listener, bufsize):
debug3('Accept UDP using recvfrom.\n')
data, srcip = udp_listener.recvfrom(bufsize)
return (srcip, None, data)
def send_udp(self, sock, srcip, dstip, data):
if srcip is not None:
Fatal("Method %s send_udp does not support setting srcip to %r"
% (self.name, srcip))
sock.sendto(data, dstip)
def setup_tcp_listener(self, tcp_listener):
pass
def setup_udp_listener(self, udp_listener):
pass
def assert_features(self, features):
avail = self.get_supported_features()
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,
user):
raise NotImplementedError()
def restore_firewall(self, port, family, udp, user):
raise NotImplementedError()
@staticmethod
def firewall_command(line):
return False
def _program_exists(name):
paths = (os.getenv('PATH') or os.defpath).split(os.pathsep)
for p in paths:
fn = '%s/%s' % (p, name)
if os.path.exists(fn):
return not os.path.isdir(fn) and os.access(fn, os.X_OK)
def get_method(method_name):
module = importlib.import_module("sshuttle.methods.%s" % method_name)
return module.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, 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 42' 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, 42)
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', '42', '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', '42')
ipfw('add', '1', 'allow',
'udp',
'from', 'any', 'to', 'any',
'ipttl', '42')
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')

126
sshuttle/methods/nat.py Normal file
View File

@ -0,0 +1,126 @@
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
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):
# only ipv4 supported with NAT
if family != socket.AF_INET:
raise Exception(
'Address family "%s" unsupported by nat method_name'
% family_to_string(family))
if udp:
raise Exception("UDP not supported by nat method_name")
table = "nat"
def _ipt(*args):
return ipt(family, table, *args)
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, user)
_ipt('-N', chain)
_ipt('-F', 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))
if sexclude:
_ipt('-A', chain, '-j', 'RETURN',
'--dest', '%s/%s' % (snet, swidth),
*tcp_ports)
else:
_ipt_ttl('-A', chain, '-j', 'REDIRECT',
'--dest', '%s/%s' % (snet, swidth),
*(tcp_ports + ('--to-ports', str(port))))
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, user):
# only ipv4 supported with NAT
if family != socket.AF_INET:
raise Exception(
'Address family "%s" unsupported by nat method_name'
% family_to_string(family))
if udp:
raise Exception("UDP not supported by nat method_name")
table = "nat"
def _ipt(*args):
return ipt(family, table, *args)
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):
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 != 42',
('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 != 42',
('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 != 42',
('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', '')

497
sshuttle/methods/pf.py Normal file
View File

@ -0,0 +1,497 @@
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': 0,
'loaded_by_sshuttle': True,
'Xtoken': []
}
_pf_fd = None
class Generic(object):
MAXPATHLEN = 1024
PF_CHANGE_ADD_TAIL = 2
PF_CHANGE_GET_TICKET = 6
PF_PASS = 0
PF_RDR = 8
PF_OUT = 2
ACTION_OFFSET = 0
POOL_TICKET_OFFSET = 8
ANCHOR_CALL_OFFSET = 1040
class pf_addr(Structure):
class _pfa(Union):
_fields_ = [("v4", c_uint32), # struct in_addr
("v6", c_uint32 * 4), # struct in6_addr
("addr8", c_uint8 * 16),
("addr16", c_uint16 * 8),
("addr32", c_uint32 * 4)]
_fields_ = [("pfa", _pfa)]
_anonymous_ = ("pfa",)
def __init__(self):
self.status = b''
self.pfioc_pooladdr = c_char * 1136
self.DIOCNATLOOK = (
(0x40000000 | 0x80000000) |
((sizeof(self.pfioc_natlook) & 0x1fff) << 16) |
((ord('D')) << 8) | (23))
self.DIOCCHANGERULE = (
(0x40000000 | 0x80000000) |
((sizeof(self.pfioc_rule) & 0x1fff) << 16) |
((ord('D')) << 8) | (26))
self.DIOCBEGINADDRS = (
(0x40000000 | 0x80000000) |
((sizeof(self.pfioc_pooladdr) & 0x1fff) << 16) |
((ord('D')) << 8) | (51))
def enable(self):
if b'INFO:\nStatus: Disabled' in self.status:
pfctl('-e')
_pf_context['started_by_sshuttle'] += 1
@staticmethod
def disable(anchor):
pfctl('-a %s -F all' % anchor)
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] = [
int(v) for v in [proto, family, src_port, dst_port]]
packed_src_ip = socket.inet_pton(family, src_ip)
packed_dst_ip = socket.inet_pton(family, dst_ip)
assert len(packed_src_ip) == len(packed_dst_ip)
length = len(packed_src_ip)
pnl = self.pfioc_natlook()
pnl.proto = proto
pnl.direction = self.PF_OUT
pnl.af = family
memmove(addressof(pnl.saddr), packed_src_ip, length)
memmove(addressof(pnl.daddr), packed_dst_ip, length)
self._add_natlook_ports(pnl, src_port, dst_port)
ioctl(pf_get_dev(), self.DIOCNATLOOK,
(c_char * sizeof(pnl)).from_address(addressof(pnl)))
ip = socket.inet_ntop(
pnl.af, (c_char * length).from_address(addressof(pnl.rdaddr)).raw)
port = socket.ntohs(self._get_natlook_port(pnl.rdxport))
return (ip, port)
@staticmethod
def _add_natlook_ports(pnl, src_port, dst_port):
pnl.sxport = socket.htons(src_port)
pnl.dxport = socket.htons(dst_port)
@staticmethod
def _get_natlook_port(xport):
return xport
def add_anchors(self, anchor, status=None):
if status is None:
status = pfctl('-s all')[0]
self.status = status
if ('\nanchor "%s"' % anchor).encode('ASCII') not in status:
self._add_anchor_rule(self.PF_PASS, anchor.encode('ASCII'))
def _add_anchor_rule(self, 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', 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
ioctl(pf_get_dev(), pf.DIOCCHANGERULE, pr)
memmove(addressof(pr) + self.ACTION_OFFSET,
struct.pack('I', self.PF_CHANGE_ADD_TAIL),
4) # action = PF_CHANGE_ADD_TAIL
ioctl(pf_get_dev(), pf.DIOCCHANGERULE, pr)
@staticmethod
def _inet_version(family):
return b'inet' if family == socket.AF_INET else b'inet6'
@staticmethod
def _lo_addr(family):
return b'127.0.0.1' if family == socket.AF_INET else b'::1'
@staticmethod
def add_rules(anchor, rules):
assert isinstance(rules, bytes)
debug3("rules:\n" + rules.decode("ASCII"))
pfctl('-a %s -f /dev/stdin' % anchor, rules)
@staticmethod
def has_skip_loopback():
return b'skip' in pfctl('-s Interfaces -i lo -v')[0]
class FreeBsd(Generic):
RULE_ACTION_OFFSET = 2968
def __new__(cls):
class pfioc_natlook(Structure):
pf_addr = Generic.pf_addr
_fields_ = [("saddr", pf_addr),
("daddr", pf_addr),
("rsaddr", pf_addr),
("rdaddr", pf_addr),
("sxport", c_uint16),
("dxport", c_uint16),
("rsxport", c_uint16),
("rdxport", c_uint16),
("af", c_uint8), # sa_family_t
("proto", c_uint8),
("proto_variant", c_uint8),
("direction", c_uint8)]
freebsd = Generic.__new__(cls)
freebsd.pfioc_rule = c_char * 3040
freebsd.pfioc_natlook = pfioc_natlook
return freebsd
def 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]
if ('\nrdr-anchor "%s"' % anchor).encode('ASCII') not in status:
self._add_anchor_rule(self.PF_RDR, anchor.encode('ASCII'))
super(FreeBsd, self).add_anchors(anchor, status=status)
def _add_anchor_rule(self, 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(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 = []
translating_rules = [
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 %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 nslist:
tables.append(
b'table <dns_servers> {%s}' %
b','.join([ns[1].encode("ASCII") for ns in nslist]))
translating_rules.append(
b'rdr pass on lo0 %s proto udp to <dns_servers> '
b'port 53 -> %s port %r' % (inet_version, lo_addr, dnsport))
filtering_rules.append(
b'pass out route-to lo0 %s proto udp to '
b'<dns_servers> port 53 keep state' % inet_version)
rules = b'\n'.join(tables + translating_rules + filtering_rules) \
+ b'\n'
super(FreeBsd, self).add_rules(anchor, rules)
class OpenBsd(Generic):
POOL_TICKET_OFFSET = 4
RULE_ACTION_OFFSET = 3324
ANCHOR_CALL_OFFSET = 1036
def __init__(self):
class pfioc_natlook(Structure):
pf_addr = Generic.pf_addr
_fields_ = [("saddr", pf_addr),
("daddr", pf_addr),
("rsaddr", pf_addr),
("rdaddr", pf_addr),
("rdomain", c_uint16),
("rrdomain", c_uint16),
("sxport", c_uint16),
("dxport", c_uint16),
("rsxport", c_uint16),
("rdxport", c_uint16),
("af", c_uint8), # sa_family_t
("proto", c_uint8),
("proto_variant", c_uint8),
("direction", c_uint8)]
self.pfioc_rule = c_char * 3424
self.pfioc_natlook = pfioc_natlook
super(OpenBsd, self).__init__()
def add_anchors(self, anchor):
# before adding anchors and rules we must override the skip lo
# that comes by default in openbsd pf.conf so the rules we will add,
# which rely on translating/filtering packets on lo, can work
if self.has_skip_loopback():
pfctl('-f /dev/stdin', b'match on lo\n')
super(OpenBsd, self).add_anchors(anchor)
def add_rules(self, anchor, includes, port, dnsport, nslist, family):
inet_version = self._inet_version(family)
lo_addr = self._lo_addr(family)
tables = []
translating_rules = [
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 %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 nslist:
tables.append(
b'table <dns_servers> {%s}' %
b','.join([ns[1].encode("ASCII") for ns in nslist]))
translating_rules.append(
b'pass in on lo0 %s proto udp to <dns_servers> port 53 '
b'rdr-to %s port %r' % (inet_version, lo_addr, dnsport))
filtering_rules.append(
b'pass out %s proto udp to <dns_servers> port 53 '
b'route-to lo0 keep state' % inet_version)
rules = b'\n'.join(tables + translating_rules + filtering_rules) \
+ b'\n'
super(OpenBsd, self).add_rules(anchor, rules)
class Darwin(FreeBsd):
RULE_ACTION_OFFSET = 3068
def __init__(self):
class pf_state_xport(Union):
_fields_ = [("port", c_uint16),
("call_id", c_uint16),
("spi", c_uint32)]
class pfioc_natlook(Structure):
pf_addr = Generic.pf_addr
_fields_ = [("saddr", pf_addr),
("daddr", pf_addr),
("rsaddr", pf_addr),
("rdaddr", pf_addr),
("sxport", pf_state_xport),
("dxport", pf_state_xport),
("rsxport", pf_state_xport),
("rdxport", pf_state_xport),
("af", c_uint8), # sa_family_t
("proto", c_uint8),
("proto_variant", c_uint8),
("direction", c_uint8)]
self.pfioc_rule = c_char * 3104
self.pfioc_natlook = pfioc_natlook
super(Darwin, self).__init__()
def enable(self):
o = pfctl('-E')
_pf_context['Xtoken'].append(re.search(b'Token : (.+)', o[1]).group(1))
def disable(self, anchor):
pfctl('-a %s -F all' % anchor)
if _pf_context['Xtoken']:
pfctl('-X %s' % _pf_context['Xtoken'].pop().decode("ASCII"))
def add_anchors(self, anchor):
# before adding anchors and rules we must override the skip lo
# that in some cases ends up in the chain so the rules we will add,
# which rely on translating/filtering packets on lo, can work
if self.has_skip_loopback():
pfctl('-f /dev/stdin', b'pass on lo\n')
super(Darwin, self).add_anchors(anchor)
def _add_natlook_ports(self, pnl, src_port, dst_port):
pnl.sxport.port = socket.htons(src_port)
pnl.dxport.port = socket.htons(dst_port)
def _get_natlook_port(self, xport):
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'] + shlex.split(args)
debug1('>> %s\n' % ' '.join(argv))
env = {
'PATH': os.environ['PATH'],
'LC_ALL': "C",
}
p = ssubprocess.Popen(argv, stdin=ssubprocess.PIPE,
stdout=ssubprocess.PIPE,
stderr=ssubprocess.PIPE,
env=env)
o = p.communicate(stdin)
if p.returncode:
raise Fatal('%r returned %d' % (argv, p.returncode))
return o
def pf_get_dev():
global _pf_fd
if _pf_fd is None:
_pf_fd = os.open('/dev/pf', os.O_RDWR)
return _pf_fd
def pf_get_anchor(family, port):
return 'sshuttle%s-%d' % ('' if family == socket.AF_INET else '6', port)
class Method(BaseMethod):
def get_supported_features(self):
result = super(Method, self).get_supported_features()
result.ipv6 = True
return result
def get_tcp_dstip(self, sock):
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,
peer[0].encode("ASCII"), peer[1],
proxy[0].encode("ASCII"), proxy[1])
out_line = b"QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % argv
pfile.write(out_line)
pfile.flush()
in_line = pfile.readline()
debug2(out_line.decode("ASCII") + ' > ' + in_line.decode("ASCII"))
if in_line.startswith(b'QUERY_PF_NAT_SUCCESS '):
(ip, port) = in_line[21:].split(b',')
return (ip.decode("ASCII"), int(port))
return sock.getsockname()
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'
% family_to_string(family))
if udp:
raise Exception("UDP not supported by pf method_name")
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 _, swidth, sexclude, snet, fport, lport \
in sorted(subnets, key=subnet_weight):
includes.append((sexclude, b"%s/%d%s" % (
snet.encode("ASCII"),
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, user):
if family not in [socket.AF_INET, socket.AF_INET6]:
raise Exception(
'Address family "%s" unsupported by pf method_name'
% family_to_string(family))
if udp:
raise Exception("UDP not supported by pf method_name")
pf.disable(pf_get_anchor(family, port))
def firewall_command(self, line):
if line.startswith('QUERY_PF_NAT '):
try:
dst = pf.query_nat(*(line[13:].split(',')))
sys.stdout.write('QUERY_PF_NAT_SUCCESS %s,%r\n' % dst)
except IOError as e:
sys.stdout.write('QUERY_PF_NAT_FAILURE %s\n' % e)
sys.stdout.flush()
return True
else:
return False

286
sshuttle/methods/tproxy.py Normal file
View File

@ -0,0 +1,286 @@
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
from sshuttle.helpers import debug1, debug3, Fatal
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_TRANSPARENT = 19
IP_ORIGDSTADDR = 20
IP_RECVORIGDSTADDR = IP_ORIGDSTADDR
SOL_IPV6 = 41
IPV6_ORIGDSTADDR = 74
IPV6_RECVORIGDSTADDR = IPV6_ORIGDSTADDR
if recvmsg == "python":
def recv_udp(listener, bufsize):
debug3('Accept UDP python using recvmsg.\n')
data, ancdata, _, srcip = listener.recvmsg(
4096, socket.CMSG_SPACE(24))
dstip = None
family = None
for cmsg_level, cmsg_type, cmsg_data in ancdata:
if cmsg_level == socket.SOL_IP and cmsg_type == IP_ORIGDSTADDR:
family, port = struct.unpack('=HH', cmsg_data[0:4])
port = socket.htons(port)
if family == socket.AF_INET:
start = 4
length = 4
else:
raise Fatal("Unsupported socket type '%s'" % family)
ip = socket.inet_ntop(family, cmsg_data[start:start + length])
dstip = (ip, port)
break
elif cmsg_level == SOL_IPV6 and cmsg_type == IPV6_ORIGDSTADDR:
family, port = struct.unpack('=HH', cmsg_data[0:4])
port = socket.htons(port)
if family == socket.AF_INET6:
start = 8
length = 16
else:
raise Fatal("Unsupported socket type '%s'" % family)
ip = socket.inet_ntop(family, cmsg_data[start:start + length])
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(24))
dstip = None
family = None
for a in adata:
if a.cmsg_level == socket.SOL_IP and a.cmsg_type == IP_ORIGDSTADDR:
family, port = struct.unpack('=HH', a.cmsg_data[0:4])
port = socket.htons(port)
if family == socket.AF_INET:
start = 4
length = 4
else:
raise Fatal("Unsupported socket type '%s'" % family)
ip = socket.inet_ntop(
family, a.cmsg_data[start:start + length])
dstip = (ip, port)
break
elif a.cmsg_level == SOL_IPV6 and a.cmsg_type == IPV6_ORIGDSTADDR:
family, port = struct.unpack('=HH', a.cmsg_data[0:4])
port = socket.htons(port)
if family == socket.AF_INET6:
start = 8
length = 16
else:
raise Fatal("Unsupported socket type '%s'" % family)
ip = socket.inet_ntop(
family, a.cmsg_data[start:start + length])
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)
class Method(BaseMethod):
def get_supported_features(self):
result = super(Method, self).get_supported_features()
result.ipv6 = True
if recvmsg is None:
result.udp = False
result.dns = False
else:
result.udp = True
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
sender = socket.socket(sock.family, socket.SOCK_DGRAM)
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sender.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
sender.bind(srcip)
sender.sendto(data, dstip)
sender.close()
def setup_tcp_listener(self, tcp_listener):
tcp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
def setup_udp_listener(self, udp_listener):
udp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
if udp_listener.v4 is not None:
udp_listener.v4.setsockopt(
socket.SOL_IP, IP_RECVORIGDSTADDR, 1)
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,
user):
if family not in [socket.AF_INET, socket.AF_INET6]:
raise Exception(
'Address family "%s" unsupported by tproxy method'
% family_to_string(family))
table = "mangle"
def _ipt(*args):
return ipt(family, table, *args)
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, user)
_ipt('-N', mark_chain)
_ipt('-F', mark_chain)
_ipt('-N', divert_chain)
_ipt('-F', divert_chain)
_ipt('-N', tproxy_chain)
_ipt('-F', tproxy_chain)
_ipt('-I', 'OUTPUT', '1', '-j', mark_chain)
_ipt('-I', 'PREROUTING', '1', '-j', tproxy_chain)
_ipt('-A', divert_chain, '-j', 'MARK', '--set-mark', '1')
_ipt('-A', divert_chain, '-j', 'ACCEPT')
_ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain,
'-m', 'tcp', '-p', 'tcp')
if udp:
_ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain,
'-m', 'udp', '-p', 'udp')
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')
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1',
'--dest', '%s/32' % ip,
'-m', 'udp', '-p', 'udp', '--dport', '53',
'--on-port', str(dnsport))
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',
*tcp_ports)
_ipt('-A', tproxy_chain, '-j', 'RETURN',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'tcp',
*tcp_ports)
else:
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'tcp',
*tcp_ports)
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1',
'--dest', '%s/%s' % (snet, swidth),
'-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',
*udp_ports)
_ipt('-A', tproxy_chain, '-j', 'RETURN',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'udp',
*udp_ports)
else:
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'udp',
*udp_ports)
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'udp',
*(udp_ports + ('--on-port', str(port))))
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'
% family_to_string(family))
table = "mangle"
def _ipt(*args):
return ipt(family, table, *args)
def _ipt_ttl(*args):
return ipt_ttl(family, table, *args)
mark_chain = 'sshuttle-m-%s' % port
tproxy_chain = 'sshuttle-t-%s' % port
divert_chain = 'sshuttle-d-%s' % port
# basic cleanup/setup of chains
if ipt_chain_exists(family, table, mark_chain):
_ipt('-D', 'OUTPUT', '-j', mark_chain)
_ipt('-F', mark_chain)
_ipt('-X', mark_chain)
if ipt_chain_exists(family, table, tproxy_chain):
_ipt('-D', 'PREROUTING', '-j', tproxy_chain)
_ipt('-F', tproxy_chain)
_ipt('-X', tproxy_chain)
if ipt_chain_exists(family, table, divert_chain):
_ipt('-F', divert_chain)
_ipt('-X', divert_chain)

362
sshuttle/options.py Normal file
View File

@ -0,0 +1,362 @@
import re
import socket
from argparse import ArgumentParser, Action, ArgumentTypeError as Fatal
from sshuttle import __version__
# Subnet file, supporting empty lines and hash-started comment lines
def parse_subnetport_file(s):
try:
handle = open(s, 'r')
except OSError:
raise Fatal('Unable to open subnet file: %s' % s)
raw_config_lines = handle.readlines()
subnets = []
for _, line in enumerate(raw_config_lines):
line = line.strip()
if not line:
continue
if line[0] == '#':
continue
subnets.append(parse_subnetport(line))
return subnets
# 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:
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
# [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)
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)
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]
def parse_list(lst):
return re.split(r'[\s,]+', lst.strip()) if lst else []
class Concat(Action):
def __init__(self, option_strings, dest, nargs=None, **kwargs):
if nargs is not None:
raise ValueError("nargs not supported")
super(Concat, self).__init__(option_strings, dest, **kwargs)
def __call__(self, parser, namespace, values, option_string=None):
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...>",
fromfile_prefix_chars="@"
)
parser.add_argument(
"subnets",
metavar="IP/MASK[:PORT[-PORT]]...",
nargs="*",
type=parse_subnetport,
help="""
capture and forward traffic to these subnets (whitespace separated)
"""
)
parser.add_argument(
"-l", "--listen",
metavar="[IP:]PORT",
help="""
transproxy to this ip address and port number
"""
)
parser.add_argument(
"-H", "--auto-hosts",
action="store_true",
help="""
continuously scan for remote hostnames and update local /etc/hosts as
they are found
"""
)
parser.add_argument(
"-N", "--auto-nets",
action="store_true",
help="""
automatically determine subnets to route
"""
)
parser.add_argument(
"--dns",
action="store_true",
help="""
capture local DNS requests and forward to the remote DNS server
"""
)
parser.add_argument(
"--ns-hosts",
metavar="IP[,IP]",
default=[],
type=parse_list,
help="""
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", "nft", "tproxy", "pf", "ipfw"],
metavar="TYPE",
default="auto",
help="""
%(choices)s
"""
)
parser.add_argument(
"--python",
metavar="PATH",
help="""
path to python interpreter on the remote server
"""
)
parser.add_argument(
"-r", "--remote",
metavar="[USERNAME[:PASSWORD]@]ADDR[:PORT]",
help="""
ssh hostname (and optional username and password) of remote %(prog)s server
"""
)
parser.add_argument(
"-x", "--exclude",
metavar="IP/MASK[:PORT[-PORT]]",
action="append",
default=[],
type=parse_subnetport,
help="""
exclude this subnet (can be used more than once)
"""
)
parser.add_argument(
"-X", "--exclude-from",
metavar="PATH",
action=Concat,
dest="exclude",
type=parse_subnetport_file,
help="""
exclude the subnets in a file (whitespace separated)
"""
)
parser.add_argument(
"-v", "--verbose",
action="count",
default=0,
help="""
increase debug message verbosity
"""
)
parser.add_argument(
"-V", "--version",
action="version",
version=__version__,
help="""
print the %(prog)s version number and exit
"""
)
parser.add_argument(
"-e", "--ssh-cmd",
metavar="CMD",
default="ssh",
help="""
the command to use to connect to the remote [%(default)s]
"""
)
parser.add_argument(
"--seed-hosts",
metavar="HOSTNAME[,HOSTNAME]",
default=[],
help="""
comma-separated list of hostnames for initial scan (may be used with
or without --auto-hosts)
"""
)
parser.add_argument(
"--no-latency-control",
action="store_false",
dest="latency_control",
help="""
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",
type=int,
help="""
restart counting channel numbers after this number (for testing)
"""
)
parser.add_argument(
"--disable-ipv6",
action="store_true",
help="""
disable IPv6 support
"""
)
parser.add_argument(
"-D", "--daemon",
action="store_true",
help="""
run in the background as a daemon
"""
)
parser.add_argument(
"-s", "--subnets",
metavar="PATH",
action=Concat,
dest="subnets_file",
default=[],
type=parse_subnetport_file,
help="""
file where the subnets are stored, instead of on the command line
"""
)
parser.add_argument(
"--syslog",
action="store_true",
help="""
send log messages to syslog (default if you use --daemon)
"""
)
parser.add_argument(
"--pidfile",
metavar="PATH",
default="./sshuttle.pid",
help="""
pidfile name (only if using --daemon) [%(default)s]
"""
)
parser.add_argument(
"--user",
help="""
apply all the rules only to this linux user
"""
)
parser.add_argument(
"--firewall",
action="store_true",
help="""
(internal use only)
"""
)
parser.add_argument(
"--hostwatch",
action="store_true",
help="""
(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')

417
sshuttle/server.py Normal file
View File

@ -0,0 +1,417 @@
import re
import struct
import socket
import traceback
import time
import sys
import os
import platform
from shutil import which
import sshuttle.ssnet as ssnet
import sshuttle.helpers as helpers
import sshuttle.hostwatch as hostwatch
import subprocess as ssubprocess
from sshuttle.ssnet import Handler, Proxy, Mux, MuxWrapper
from sshuttle.helpers import b, log, debug1, debug2, debug3, Fatal, \
resolvconf_random_nameserver
def _ipmatch(ipstr):
# FIXME: IPv4 only
if ipstr == 'default':
ipstr = '0.0.0.0/0'
m = re.match(r'^(\d+(\.\d+(\.\d+(\.\d+)?)?)?)(?:/(\d+))?$', ipstr)
if m:
g = m.groups()
ips = g[0]
width = int(g[4] or 32)
if g[1] is None:
ips += '.0.0.0'
width = min(width, 8)
elif g[2] is None:
ips += '.0.0'
width = min(width, 16)
elif g[3] is None:
ips += '.0'
width = min(width, 24)
ips = ips
return (struct.unpack('!I', socket.inet_aton(ips))[0], width)
def _ipstr(ip, width):
# FIXME: IPv4 only
if width >= 32:
return ip
else:
return "%s/%d" % (ip, width)
def _maskbits(netmask):
# FIXME: IPv4 only
if not netmask:
return 32
for i in range(32):
if netmask[0] & _shl(1, i):
return 32 - i
return 0
def _shl(n, bits):
return n * int(2 ** bits)
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
env = {
'PATH': os.environ['PATH'],
'LC_ALL': "C",
}
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=env)
routes = []
for line in p.stdout:
if not line.strip():
continue
ipw, mask = extract_route(line.decode("ASCII"))
if not ipw:
continue
width = min(ipw[1], mask)
ip = ipw[0] & _shl(_shl(1, width) - 1, 32 - width)
routes.append(
(socket.AF_INET, socket.inet_ntoa(struct.pack('!I', ip)), width))
rv = p.wait()
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():
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)
def _exc_dump():
exc_info = sys.exc_info()
return ''.join(traceback.format_exception(*exc_info))
def start_hostwatch(seed_hosts, auto_hosts):
s1, s2 = socket.socketpair()
pid = os.fork()
if not pid:
# child
rv = 99
try:
try:
s2.close()
os.dup2(s1.fileno(), 1)
os.dup2(s1.fileno(), 0)
s1.close()
rv = hostwatch.hw_main(seed_hosts, auto_hosts) or 0
except Exception:
log('%s\n' % _exc_dump())
rv = 98
finally:
os._exit(rv)
s1.close()
return pid, s2
class Hostwatch:
def __init__(self):
self.pid = 0
self.sock = None
class DnsProxy(Handler):
def __init__(self, mux, chan, request, to_nameserver):
Handler.__init__(self, [])
self.timeout = time.time() + 30
self.mux = mux
self.chan = chan
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
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(sockaddr)
self.peers[sock] = peer
debug2('DNS: sending to %r:%d (try %d)\n' % (peer, port, self.tries))
try:
sock.send(self.request)
self.socks.append(sock)
except socket.error:
_, e = sys.exc_info()[:2]
if e.args[0] in ssnet.NET_ERRS:
# might have been spurious; try again.
# Note: these errors sometimes are reported by recv(),
# and sometimes by send(). We have to catch both.
debug2('DNS send to %r: %s\n' % (peer, e))
self.try_send()
return
else:
log('DNS send to %r: %s\n' % (peer, e))
return
def callback(self, sock):
peer = self.peers[sock]
try:
data = sock.recv(4096)
except socket.error:
_, e = sys.exc_info()[:2]
self.socks.remove(sock)
del self.peers[sock]
if e.args[0] in ssnet.NET_ERRS:
# might have been spurious; try again.
# Note: these errors sometimes are reported by recv(),
# and sometimes by send(). We have to catch both.
debug2('DNS recv from %r: %s\n' % (peer, e))
self.try_send()
return
else:
log('DNS recv from %r: %s\n' % (peer, e))
return
debug2('DNS response: %d bytes\n' % len(data))
self.mux.send(self.chan, ssnet.CMD_DNS_RESPONSE, data)
self.ok = False
class UdpProxy(Handler):
def __init__(self, mux, chan, family):
sock = socket.socket(family, socket.SOCK_DGRAM)
Handler.__init__(self, [sock])
self.timeout = time.time() + 30
self.mux = mux
self.chan = chan
self.sock = sock
if family == socket.AF_INET:
self.sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
def send(self, dstip, data):
debug2('UDP: sending to %r port %d\n' % dstip)
try:
self.sock.sendto(data, dstip)
except socket.error:
_, e = sys.exc_info()[:2]
log('UDP send to %r port %d: %s\n' % (dstip[0], dstip[1], e))
return
def callback(self, sock):
try:
data, peer = sock.recvfrom(4096)
except socket.error:
_, e = sys.exc_info()[:2]
log('UDP recv from %r port %d: %s\n' % (peer[0], peer[1], e))
return
debug2('UDP response: %d bytes\n' % len(data))
hdr = b("%s,%r," % (peer[0], peer[1]))
self.mux.send(self.chan, ssnet.CMD_UDP_DATA, hdr + data)
def main(latency_control, auto_hosts, to_nameserver, auto_nets):
debug1('Starting server with Python version %s\n'
% platform.python_version())
if helpers.verbose >= 1:
helpers.logprefix = ' s: '
else:
helpers.logprefix = 'server: '
debug1('latency control setting = %r\n' % latency_control)
# synchronization header
sys.stdout.write('\0\0SSHUTTLE0001')
sys.stdout.flush()
handlers = []
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
mux.send(0, ssnet.CMD_ROUTES, b(routepkt))
hw = Hostwatch()
hw.leftover = b('')
def hostwatch_ready(sock):
assert(hw.pid)
content = hw.sock.recv(4096)
if content:
lines = (hw.leftover + content).split(b('\n'))
if lines[-1]:
# no terminating newline: entry isn't complete yet!
hw.leftover = lines.pop()
lines.append(b(''))
else:
hw.leftover = b('')
mux.send(0, ssnet.CMD_HOST_LIST, b('\n').join(lines))
else:
raise Fatal('hostwatch process died')
def got_host_req(data):
if not hw.pid:
(hw.pid, hw.sock) = start_hostwatch(
data.decode("ASCII").strip().split(), auto_hosts)
handlers.append(Handler(socks=[hw.sock],
callback=hostwatch_ready))
mux.got_host_req = got_host_req
def new_channel(channel, data):
(family, dstip, dstport) = data.decode("ASCII").split(',', 2)
family = int(family)
# AF_INET is the same constant on Linux and BSD but AF_INET6
# is different. As the client and server can be running on
# different platforms we can not just set the socket family
# to what comes in the wire.
if family != socket.AF_INET:
family = socket.AF_INET6
dstport = int(dstport)
outwrap = ssnet.connect_dst(family, dstip, dstport)
handlers.append(Proxy(MuxWrapper(mux, channel), outwrap))
mux.new_channel = new_channel
dnshandlers = {}
def dns_req(channel, data):
debug2('Incoming DNS request channel=%d.\n' % channel)
h = DnsProxy(mux, channel, data, to_nameserver)
handlers.append(h)
dnshandlers[channel] = h
mux.got_dns_req = dns_req
udphandlers = {}
def udp_req(channel, cmd, data):
debug2('Incoming UDP request channel=%d, cmd=%d\n' % (channel, cmd))
if cmd == ssnet.CMD_UDP_DATA:
(dstip, dstport, data) = data.split(b(','), 2)
dstport = int(dstport)
debug2('is incoming UDP data. %r %d.\n' % (dstip, dstport))
h = udphandlers[channel]
h.send((dstip, dstport), data)
elif cmd == ssnet.CMD_UDP_CLOSE:
debug2('is incoming UDP close\n')
h = udphandlers[channel]
h.ok = False
del mux.channels[channel]
def udp_open(channel, data):
debug2('Incoming UDP open.\n')
family = int(data)
mux.channels[channel] = lambda cmd, data: udp_req(channel, cmd, data)
if channel in udphandlers:
raise Fatal('UDP connection channel %d already open' % channel)
else:
h = UdpProxy(mux, channel, family)
handlers.append(h)
udphandlers[channel] = h
mux.got_udp_open = udp_open
while mux.ok:
if hw.pid:
assert(hw.pid > 0)
(rpid, rv) = os.waitpid(hw.pid, os.WNOHANG)
if rpid:
raise Fatal(
'hostwatch exited unexpectedly: code 0x%04x\n' % rv)
ssnet.runonce(handlers, mux)
if latency_control:
mux.check_fullness()
if dnshandlers:
now = time.time()
remove = []
for channel, h in dnshandlers.items():
if h.timeout < now or not h.ok:
debug3('expiring dnsreqs channel=%d\n' % channel)
remove.append(channel)
h.ok = False
for channel in remove:
del dnshandlers[channel]
if udphandlers:
remove = []
for channel, h in udphandlers.items():
if not h.ok:
debug3('expiring UDP channel=%d\n' % channel)
remove.append(channel)
h.ok = False
for channel in remove:
del udphandlers[channel]

155
sshuttle/ssh.py Normal file
View File

@ -0,0 +1,155 @@
import sys
import os
import re
import socket
import zlib
import importlib
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
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 = 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 = ipaddress.ip_address(host)
except ValueError:
# if that fails parse as URL to get the port
parsed = urlparse('//{}'.format(host))
try:
host = ipaddress.ip_address(parsed.hostname)
except ValueError:
# else if both fails, we have a hostname with port
host = parsed.hostname
port = parsed.port
if password is None or len(password) == 0:
password = None
return username, password, port, host
def connect(ssh_cmd, rhostport, python, stderr, options):
username, password, port, host = parse_hostport(rhostport)
if username:
rhost = "{}@{}".format(username, host)
else:
rhost = host
z = zlib.compressobj(1)
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') +
empackage(z, 'sshuttle.cmdline_options', optdata) +
empackage(z, 'sshuttle.helpers') +
empackage(z, 'sshuttle.ssnet') +
empackage(z, 'sshuttle.hostwatch') +
empackage(z, 'sshuttle.server') +
b"\n")
pyscript = r"""
import sys, os;
verbosity=%d;
sys.stdin = os.fdopen(0, "rb");
exec(compile(sys.stdin.read(%d), "assembler.py", "exec"))
""" % (helpers.verbose or 0, len(content))
pyscript = re.sub(r'\s+', ' ', pyscript.strip())
if not rhost:
# ignore the --python argument when running locally; we already know
# which python version works.
argv = [sys.executable, '-c', pyscript]
else:
if ssh_cmd:
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; $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])
(s1, s2) = socket.socketpair()
def setup():
# runs in the child process
s2.close()
s1a, s1b = os.dup(s1.fileno()), os.dup(s1.fileno())
s1.close()
debug2('executing: %r\n' % argv)
p = ssubprocess.Popen(argv, stdin=s1a, stdout=s1b, preexec_fn=setup,
close_fds=True, stderr=stderr)
os.close(s1a)
os.close(s1b)
s2.sendall(content)
s2.sendall(content2)
return p, s2

View File

@ -1,60 +1,71 @@
import struct, socket, errno, select
if not globals().get('skip_imports'):
from helpers import *
import sys
import struct
import socket
import errno
import select
import os
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
CMD_CONNECT = 0x4203
CMD_STOP_SENDING = 0x4204
CMD_EOF = 0x4205
CMD_DATA = 0x4206
CMD_TCP_CONNECT = 0x4203
CMD_TCP_STOP_SENDING = 0x4204
CMD_TCP_EOF = 0x4205
CMD_TCP_DATA = 0x4206
CMD_ROUTES = 0x4207
CMD_HOST_REQ = 0x4208
CMD_HOST_LIST = 0x4209
CMD_DNS_REQ = 0x420a
CMD_DNS_RESPONSE = 0x420b
CMD_UDP_OPEN = 0x420c
CMD_UDP_DATA = 0x420d
CMD_UDP_CLOSE = 0x420e
cmd_to_name = {
CMD_EXIT: 'EXIT',
CMD_PING: 'PING',
CMD_PONG: 'PONG',
CMD_CONNECT: 'CONNECT',
CMD_STOP_SENDING: 'STOP_SENDING',
CMD_EOF: 'EOF',
CMD_DATA: 'DATA',
CMD_TCP_CONNECT: 'TCP_CONNECT',
CMD_TCP_STOP_SENDING: 'TCP_STOP_SENDING',
CMD_TCP_EOF: 'TCP_EOF',
CMD_TCP_DATA: 'TCP_DATA',
CMD_ROUTES: 'ROUTES',
CMD_HOST_REQ: 'HOST_REQ',
CMD_HOST_LIST: 'HOST_LIST',
CMD_DNS_REQ: 'DNS_REQ',
CMD_DNS_RESPONSE: 'DNS_RESPONSE',
CMD_UDP_OPEN: 'UDP_OPEN',
CMD_UDP_DATA: 'UDP_DATA',
CMD_UDP_CLOSE: 'UDP_CLOSE',
}
NET_ERRS = [errno.ECONNREFUSED, errno.ETIMEDOUT,
errno.EHOSTUNREACH, errno.ENETUNREACH,
errno.EHOSTDOWN, errno.ENETDOWN]
errno.EHOSTDOWN, errno.ENETDOWN,
errno.ENETUNREACH, errno.ECONNABORTED,
errno.ECONNRESET]
def _add(l, elem):
if not elem 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:
@ -66,7 +77,8 @@ def _fds(l):
def _nb_clean(func, *args):
try:
return func(*args)
except OSError, e:
except OSError:
_, e = sys.exc_info()[:2]
if e.errno not in (errno.EWOULDBLOCK, errno.EAGAIN):
raise
else:
@ -79,14 +91,22 @@ def _try_peername(sock):
pn = sock.getpeername()
if pn:
return '%s:%s' % (pn[0], pn[1])
except socket.error, e:
if e.args[0] not in (errno.ENOTCONN, errno.ENOTSOCK):
except socket.error:
_, e = sys.exc_info()[:2]
if e.args[0] == errno.EINVAL:
pass
elif e.args[0] not in (errno.ENOTCONN, errno.ENOTSOCK):
raise
except AttributeError:
pass
return 'unknown'
_swcount = 0
class SockWrapper:
def __init__(self, rsock, wsock, connect_to=None, peername=None):
global _swcount
_swcount += 1
@ -128,17 +148,12 @@ class SockWrapper:
return # already connected
self.rsock.setblocking(False)
debug3('%r: trying connect to %r\n' % (self, self.connect_to))
if socket.inet_aton(self.connect_to[0])[0] == '\0':
self.seterr(Exception("Can't connect to %r: "
"IP address starts with zero\n"
% (self.connect_to,)))
self.connect_to = None
return
try:
self.rsock.connect(self.connect_to)
# connected successfully (Linux)
self.connect_to = None
except socket.error, e:
except socket.error:
_, e = sys.exc_info()[:2]
debug3('%r: connect result: %s\n' % (self, e))
if e.args[0] == errno.EINVAL:
# this is what happens when you call connect() on a socket
@ -177,7 +192,7 @@ class SockWrapper:
if not self.shut_read:
debug2('%r: done reading\n' % self)
self.shut_read = True
#self.rsock.shutdown(SHUT_RD) # doesn't do anything anyway
# self.rsock.shutdown(SHUT_RD) # doesn't do anything anyway
def nowrite(self):
if not self.shut_write:
@ -185,10 +200,12 @@ class SockWrapper:
self.shut_write = True
try:
self.wsock.shutdown(SHUT_WR)
except socket.error, e:
except socket.error:
_, 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):
@ -197,7 +214,8 @@ class SockWrapper:
self.wsock.setblocking(False)
try:
return _nb_clean(os.write, self.wsock.fileno(), buf)
except OSError, e:
except OSError:
_, e = sys.exc_info()[:2]
if e.errno == errno.EPIPE:
debug1('%r: uwrite: got EPIPE\n' % self)
self.nowrite()
@ -219,9 +237,10 @@ class SockWrapper:
self.rsock.setblocking(False)
try:
return _nb_clean(os.read, self.rsock.fileno(), 65536)
except OSError, e:
except OSError:
_, e = sys.exc_info()[:2]
self.seterr('uread: %s' % e)
return '' # unexpected error... we'll call it EOF
return b('') # unexpected error... we'll call it EOF
def fill(self):
if self.buf:
@ -229,7 +248,7 @@ class SockWrapper:
rb = self.uread()
if rb:
self.buf.append(rb)
if rb == '': # empty string means EOF; None means temporarily empty
if rb == b(''): # empty string means EOF; None means temporarily empty
self.noread()
def copy_to(self, outwrap):
@ -243,7 +262,8 @@ class SockWrapper:
class Handler:
def __init__(self, socks = None, callback = None):
def __init__(self, socks=None, callback=None):
self.ok = True
self.socks = socks or []
if callback:
@ -253,9 +273,9 @@ class Handler:
for i in self.socks:
_add(r, i)
def callback(self):
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:
@ -265,6 +285,7 @@ class Handler:
class Proxy(Handler):
def __init__(self, wrap1, wrap2):
Handler.__init__(self, [wrap1.rsock, wrap1.wsock,
wrap2.rsock, wrap2.wsock])
@ -272,8 +293,10 @@ class Proxy(Handler):
self.wrap2 = wrap2
def pre_select(self, r, w, x):
if self.wrap1.shut_write: self.wrap2.noread()
if self.wrap2.shut_write: self.wrap1.noread()
if self.wrap1.shut_write:
self.wrap2.noread()
if self.wrap2.shut_write:
self.wrap1.noread()
if self.wrap1.connect_to:
_add(w, self.wrap1.rsock)
@ -291,7 +314,7 @@ class Proxy(Handler):
elif not self.wrap2.shut_read:
_add(r, self.wrap2.rsock)
def callback(self):
def callback(self, sock):
self.wrap1.try_connect()
self.wrap2.try_connect()
self.wrap1.fill()
@ -305,31 +328,33 @@ class Proxy(Handler):
self.wrap2.buf = []
self.wrap2.noread()
if (self.wrap1.shut_read and self.wrap2.shut_read and
not self.wrap1.buf and not self.wrap2.buf):
not self.wrap1.buf and not self.wrap2.buf):
self.ok = False
self.wrap1.nowrite()
self.wrap2.nowrite()
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
self.channels = {}
self.chani = 0
self.want = 0
self.inbuf = ''
self.inbuf = b('')
self.outbuf = []
self.fullness = 0
self.too_full = False
self.send(0, CMD_PING, 'chicken')
self.send(0, CMD_PING, b('chicken'))
def next_channel(self):
# channel 0 is special, so we never allocate it
for timeout in xrange(1024):
for _ in range(1024):
self.chani += 1
if self.chani > MAX_CHANNEL:
self.chani = 1
@ -338,34 +363,35 @@ class Mux(Handler):
def amount_queued(self):
total = 0
for b in self.outbuf:
total += len(b)
for byte in self.outbuf:
total += len(byte)
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, 'rttest')
self.send(0, CMD_PING, b('rttest'))
self.too_full = True
#ob = []
#for b in self.outbuf:
# ob = []
# for b in self.outbuf:
# (s1,s2,c) = struct.unpack('!ccH', b[:4])
# ob.append(c)
#log('outbuf: %d %r\n' % (self.amount_queued(), ob))
# log('outbuf: %d %r\n' % (self.amount_queued(), ob))
def send(self, channel, cmd, data):
data = str(data)
assert(len(data) <= 65535)
p = struct.pack('!ccHHH', 'S', 'S', channel, cmd, len(data)) + data
assert isinstance(data, bytes)
assert len(data) <= 65535
p = struct.pack('!ccHHH', b('S'), b('S'), channel, cmd, len(data)) \
+ data
self.outbuf.append(p)
debug2(' > channel=%d cmd=%s len=%d (fullness=%d)\n'
% (channel, cmd_to_name.get(cmd,hex(cmd)),
% (channel, cmd_to_name.get(cmd, hex(cmd)),
len(data), self.fullness))
self.fullness += len(data)
def got_packet(self, channel, cmd, data):
debug2('< channel=%d cmd=%s len=%d\n'
% (channel, cmd_to_name.get(cmd,hex(cmd)), len(data)))
% (channel, cmd_to_name.get(cmd, hex(cmd)), len(data)))
if cmd == CMD_PING:
self.send(0, CMD_PONG, data)
elif cmd == CMD_PONG:
@ -374,7 +400,7 @@ class Mux(Handler):
self.fullness = 0
elif cmd == CMD_EXIT:
self.ok = False
elif cmd == CMD_CONNECT:
elif cmd == CMD_TCP_CONNECT:
assert(not self.channels.get(channel))
if self.new_channel:
self.new_channel(channel, data)
@ -382,6 +408,10 @@ class Mux(Handler):
assert(not self.channels.get(channel))
if self.got_dns_req:
self.got_dns_req(channel, data)
elif cmd == CMD_UDP_OPEN:
assert(not self.channels.get(channel))
if self.got_udp_open:
self.got_udp_open(channel, data)
elif cmd == CMD_ROUTES:
if self.got_routes:
self.got_routes(data)
@ -401,14 +431,14 @@ class Mux(Handler):
callback = self.channels.get(channel)
if not callback:
log('warning: closed channel %d got cmd=%s len=%d\n'
% (channel, cmd_to_name.get(cmd,hex(cmd)), len(data)))
% (channel, cmd_to_name.get(cmd, hex(cmd)), len(data)))
else:
callback(cmd, data)
def flush(self):
self.wsock.setblocking(False)
os.set_blocking(self.wfile.fileno(), False)
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:]
@ -416,27 +446,28 @@ class Mux(Handler):
self.outbuf[0:1] = []
def fill(self):
self.rsock.setblocking(False)
os.set_blocking(self.rfile.fileno(), False)
try:
b = _nb_clean(os.read, self.rsock.fileno(), 32768)
except OSError, e:
read = _nb_clean(os.read, self.rfile.fileno(), LATENCY_BUFFER_SIZE)
except OSError:
_, e = sys.exc_info()[:2]
raise Fatal('other end: %r' % e)
#log('<<< %r\n' % b)
if b == '': # EOF
# log('<<< %r\n' % b)
if read == b(''): # EOF
self.ok = False
if b:
self.inbuf += b
if read:
self.inbuf += read
def handle(self):
self.fill()
#log('inbuf is: (%d,%d) %r\n'
# log('inbuf is: (%d,%d) %r\n'
# % (self.want, len(self.inbuf), self.inbuf))
while 1:
if len(self.inbuf) >= (self.want or HDR_LEN):
(s1,s2,channel,cmd,datalen) = \
(s1, s2, channel, cmd, datalen) = \
struct.unpack('!ccHHH', self.inbuf[:HDR_LEN])
assert(s1 == 'S')
assert(s2 == 'S')
assert(s1 == b('S'))
assert(s2 == b('S'))
self.want = datalen + HDR_LEN
if self.want and len(self.inbuf) >= self.want:
data = self.inbuf[HDR_LEN:self.want]
@ -447,21 +478,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):
(r,w,x) = select.select([self.rsock], [self.wsock], [], 0)
if self.rsock in r:
def callback(self, sock):
(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
@ -473,22 +505,33 @@ class MuxWrapper(SockWrapper):
SockWrapper.__del__(self)
def __repr__(self):
return 'SW%r:Mux#%d' % (self.peername,self.channel)
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_STOP_SENDING, '')
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_EOF, '')
self.maybe_close()
def maybe_close(self):
if self.shut_read and self.shut_write:
debug2('%r: closing connection\n' % self)
# remove the mux's reference to us. The python garbage collector
# will then be able to reap our object.
self.mux.channels[self.channel] = None
@ -501,59 +544,61 @@ class MuxWrapper(SockWrapper):
return 0 # too much already enqueued
if len(buf) > 2048:
buf = buf[:2048]
self.mux.send(self.channel, CMD_DATA, buf)
self.mux.send(self.channel, CMD_TCP_DATA, buf)
return len(buf)
def uread(self):
if self.shut_read:
return '' # EOF
return b('') # EOF
else:
return None # no data available right now
def got_packet(self, cmd, data):
if cmd == CMD_EOF:
self.noread()
elif cmd == CMD_STOP_SENDING:
self.nowrite()
elif cmd == CMD_DATA:
if cmd == CMD_TCP_EOF:
# Remote side already knows the status - set flag but don't notify
self.setnoread()
elif cmd == CMD_TCP_STOP_SENDING:
# Remote side already knows the status - set flag but don't notify
self.setnowrite()
elif cmd == CMD_TCP_DATA:
self.buf.append(data)
else:
raise Exception('unknown command %d (%d bytes)'
% (cmd, len(data)))
def connect_dst(ip, port):
def connect_dst(family, ip, port):
debug2('Connecting to %s:%d\n' % (ip, port))
outsock = socket.socket()
outsock = socket.socket(family)
outsock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
return SockWrapper(outsock, outsock,
connect_to = (ip,port),
peername = '%s:%d' % (ip,port))
connect_to=(ip, port),
peername='%s:%d' % (ip, port))
def runonce(handlers, mux):
r = []
w = []
x = []
to_remove = filter(lambda s: not s.ok, handlers)
to_remove = [s for s in handlers if not s.ok]
for h in to_remove:
handlers.remove(h)
for s in handlers:
s.pre_select(r,w,x)
s.pre_select(r, w, x)
debug2('Waiting: %d r=%r w=%r x=%r (fullness=%d/%d)\n'
% (len(handlers), _fds(r), _fds(w), _fds(x),
% (len(handlers), _fds(r), _fds(w), _fds(x),
mux.fullness, mux.too_full))
(r,w,x) = select.select(r,w,x)
(r, w, x) = select.select(r, w, x)
debug2(' Ready: %d r=%r w=%r x=%r\n'
% (len(handlers), _fds(r), _fds(w), _fds(x)))
ready = r+w+x
% (len(handlers), _fds(r), _fds(w), _fds(x)))
ready = r + w + x
did = {}
for h in handlers:
for s in h.socks:
if s in ready:
h.callback()
h.callback(s)
did[s] = 1
for s in ready:
if not s in did:
if s not in did:
raise Fatal('socket %r was not used by any handler' % s)

31
sshuttle/ssyslog.py Normal file
View File

@ -0,0 +1,31 @@
import sys
import os
import subprocess as ssubprocess
_p = None
def start_syslog():
global _p
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.stderr.flush()
os.dup2(_p.stdin.fileno(), sys.stderr.fileno())

View File

@ -1,5 +1,8 @@
#!/usr/bin/env python
import sys, os, socket, select, struct, time
import socket
import select
import struct
import time
listener = socket.socket()
listener.bind(('127.0.0.1', 0))
@ -22,63 +25,63 @@ while 1:
count += 1
if count >= 16384:
count = 1
print 'cli CREATING %d' % count
b = struct.pack('I', count) + 'x'*count
print('cli CREATING %d' % count)
b = struct.pack('I', count) + 'x' * count
remain[c] = count
print 'cli >> %r' % len(b)
print('cli >> %r' % len(b))
c.send(b)
c.shutdown(socket.SHUT_WR)
clients.append(c)
r = [listener]
time.sleep(0.1)
else:
r = [listener]+servers+clients
print 'select(%d)' % len(r)
r,w,x = select.select(r, [], [], 5)
r = [listener] + servers + clients
print('select(%d)' % len(r))
r, w, x = select.select(r, [], [], 5)
assert(r)
for i in r:
if i == listener:
s,addr = listener.accept()
s, addr = listener.accept()
servers.append(s)
elif i in servers:
b = i.recv(4096)
print 'srv << %r' % len(b)
if not i in remain:
print('srv << %r' % len(b))
if i not in remain:
assert(len(b) >= 4)
want = struct.unpack('I', b[:4])[0]
b = b[4:]
#i.send('y'*want)
# i.send('y'*want)
else:
want = remain[i]
if want < len(b):
print 'weird wanted %d bytes, got %d: %r' % (want, len(b), b)
print('weird wanted %d bytes, got %d: %r' % (want, len(b), b))
assert(want >= len(b))
want -= len(b)
remain[i] = want
if not b: # EOF
if want:
print 'weird: eof but wanted %d more' % want
print('weird: eof but wanted %d more' % want)
assert(want == 0)
i.close()
servers.remove(i)
del remain[i]
else:
print 'srv >> %r' % len(b)
i.send('y'*len(b))
print('srv >> %r' % len(b))
i.send('y' * len(b))
if not want:
i.shutdown(socket.SHUT_WR)
elif i in clients:
b = i.recv(4096)
print 'cli << %r' % len(b)
print('cli << %r' % len(b))
want = remain[i]
if want < len(b):
print 'weird wanted %d bytes, got %d: %r' % (want, len(b), b)
print('weird wanted %d bytes, got %d: %r' % (want, len(b), b))
assert(want >= len(b))
want -= len(b)
remain[i] = want
if not b: # EOF
if want:
print 'weird: eof but wanted %d more' % want
print('weird: eof but wanted %d more' % want)
assert(want == 0)
i.close()
clients.remove(i)

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,16 +0,0 @@
import sys, os
from compat import ssubprocess
_p = None
def start_syslog():
global _p
_p = ssubprocess.Popen(['logger',
'-p', 'daemon.notice',
'-t', 'sshuttle'], stdin=ssubprocess.PIPE)
def stderr_to_syslog():
sys.stdout.flush()
sys.stderr.flush()
os.dup2(_p.stdin.fileno(), 2)

View File

@ -0,0 +1,137 @@
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
{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
{inet},1.2.3.33
{inet6},2404:6800:4004:80c::33
PORTS 1024,1025,1026,1027
GO 1 -
HOST 1.2.3.3,existing
""".format(inet=AF_INET, inet6=AF_INET6))
stdout = Mock()
return stdin, stdout
def test_rewrite_etc_hosts(tmpdir):
orig_hosts = tmpdir.join("hosts.orig")
orig_hosts.write("1.2.3.3 existing\n")
new_hosts = tmpdir.join("hosts")
orig_hosts.copy(new_hosts)
hostmap = {
'myhost': '1.2.3.4',
'myotherhost': '1.2.3.5',
}
with patch('sshuttle.firewall.HOSTSFILE', new=str(new_hosts)):
sshuttle.firewall.rewrite_etc_hosts(hostmap, 10)
with new_hosts.open() as f:
line = f.readline()
s = line.split()
assert s == ['1.2.3.3', 'existing']
line = f.readline()
s = line.split()
assert s == ['1.2.3.4', 'myhost',
'#', 'sshuttle-firewall-10', 'AUTOCREATED']
line = f.readline()
s = line.split()
assert s == ['1.2.3.5', 'myotherhost',
'#', 'sshuttle-firewall-10', 'AUTOCREATED']
line = f.readline()
assert line == ""
with patch('sshuttle.firewall.HOSTSFILE', new=str(new_hosts)):
sshuttle.firewall.restore_etc_hosts(10)
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')
def test_main(mock_get_method, mock_setup_daemon, mock_rewrite_etc_hosts):
stdin, stdout = setup_daemon()
mock_setup_daemon.return_value = stdin, stdout
mock_get_method("not_auto").name = "test"
mock_get_method.reset_mock()
sshuttle.firewall.main("not_auto", False)
assert mock_rewrite_etc_hosts.mock_calls == [
call({'1.2.3.3': 'existing'}, 1024),
call({}, 1024),
]
assert stdout.mock_calls == [
call.write('READY test\n'),
call.flush(),
call.write('STARTED\n'),
call.flush()
]
assert mock_setup_daemon.mock_calls == [call()]
assert mock_get_method.mock_calls == [
call('not_auto'),
call().setup_firewall(
1024, 1026,
[(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,
[(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

@ -0,0 +1,196 @@
import io
import socket
from socket import AF_INET, AF_INET6
import errno
from mock import patch, call
import sshuttle.helpers
@patch('sshuttle.helpers.logprefix', new='prefix: ')
@patch('sshuttle.helpers.sys.stdout')
@patch('sshuttle.helpers.sys.stderr')
def test_log(mock_stderr, mock_stdout):
sshuttle.helpers.log("message")
sshuttle.helpers.log("abc")
sshuttle.helpers.log("message 1\n")
sshuttle.helpers.log("message 2\nline2\nline3\n")
sshuttle.helpers.log("message 3\nline2\nline3")
assert mock_stdout.mock_calls == [
call.flush(),
call.flush(),
call.flush(),
call.flush(),
call.flush(),
]
assert mock_stderr.mock_calls == [
call.write('prefix: message'),
call.flush(),
call.write('prefix: abc'),
call.flush(),
call.write('prefix: message 1\n'),
call.flush(),
call.write('prefix: message 2\n'),
call.write('---> line2\n'),
call.write('---> line3\n'),
call.flush(),
call.write('prefix: message 3\n'),
call.write('---> line2\n'),
call.write('---> line3\n'),
call.flush(),
]
@patch('sshuttle.helpers.logprefix', new='prefix: ')
@patch('sshuttle.helpers.verbose', new=1)
@patch('sshuttle.helpers.sys.stdout')
@patch('sshuttle.helpers.sys.stderr')
def test_debug1(mock_stderr, mock_stdout):
sshuttle.helpers.debug1("message")
assert mock_stdout.mock_calls == [
call.flush(),
]
assert mock_stderr.mock_calls == [
call.write('prefix: message'),
call.flush(),
]
@patch('sshuttle.helpers.logprefix', new='prefix: ')
@patch('sshuttle.helpers.verbose', new=0)
@patch('sshuttle.helpers.sys.stdout')
@patch('sshuttle.helpers.sys.stderr')
def test_debug1_nop(mock_stderr, mock_stdout):
sshuttle.helpers.debug1("message")
assert mock_stdout.mock_calls == []
assert mock_stderr.mock_calls == []
@patch('sshuttle.helpers.logprefix', new='prefix: ')
@patch('sshuttle.helpers.verbose', new=2)
@patch('sshuttle.helpers.sys.stdout')
@patch('sshuttle.helpers.sys.stderr')
def test_debug2(mock_stderr, mock_stdout):
sshuttle.helpers.debug2("message")
assert mock_stdout.mock_calls == [
call.flush(),
]
assert mock_stderr.mock_calls == [
call.write('prefix: message'),
call.flush(),
]
@patch('sshuttle.helpers.logprefix', new='prefix: ')
@patch('sshuttle.helpers.verbose', new=1)
@patch('sshuttle.helpers.sys.stdout')
@patch('sshuttle.helpers.sys.stderr')
def test_debug2_nop(mock_stderr, mock_stdout):
sshuttle.helpers.debug2("message")
assert mock_stdout.mock_calls == []
assert mock_stderr.mock_calls == []
@patch('sshuttle.helpers.logprefix', new='prefix: ')
@patch('sshuttle.helpers.verbose', new=3)
@patch('sshuttle.helpers.sys.stdout')
@patch('sshuttle.helpers.sys.stderr')
def test_debug3(mock_stderr, mock_stdout):
sshuttle.helpers.debug3("message")
assert mock_stdout.mock_calls == [
call.flush(),
]
assert mock_stderr.mock_calls == [
call.write('prefix: message'),
call.flush(),
]
@patch('sshuttle.helpers.logprefix', new='prefix: ')
@patch('sshuttle.helpers.verbose', new=2)
@patch('sshuttle.helpers.sys.stdout')
@patch('sshuttle.helpers.sys.stderr')
def test_debug3_nop(mock_stderr, mock_stdout):
sshuttle.helpers.debug3("message")
assert mock_stdout.mock_calls == []
assert mock_stderr.mock_calls == []
@patch('sshuttle.helpers.open', create=True)
def test_resolvconf_nameservers(mock_open):
mock_open.return_value = io.StringIO(u"""
# Generated by NetworkManager
search pri
nameserver 192.168.1.1
nameserver 192.168.2.1
nameserver 192.168.3.1
nameserver 192.168.4.1
nameserver 2404:6800:4004:80c::1
nameserver 2404:6800:4004:80c::2
nameserver 2404:6800:4004:80c::3
nameserver 2404:6800:4004:80c::4
""")
ns = sshuttle.helpers.resolvconf_nameservers()
assert ns == [
(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')
]
@patch('sshuttle.helpers.open', create=True)
def test_resolvconf_random_nameserver(mock_open):
mock_open.return_value = io.StringIO(u"""
# Generated by NetworkManager
search pri
nameserver 192.168.1.1
nameserver 192.168.2.1
nameserver 192.168.3.1
nameserver 192.168.4.1
nameserver 2404:6800:4004:80c::1
nameserver 2404:6800:4004:80c::2
nameserver 2404:6800:4004:80c::3
nameserver 2404:6800:4004:80c::4
""")
ns = sshuttle.helpers.resolvconf_random_nameserver()
assert ns in [
(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')
]
@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") \
== (AF_INET, "127.0.0.1")
assert sshuttle.helpers.family_ip_tuple("192.168.2.6") \
== (AF_INET, "192.168.2.6")
assert sshuttle.helpers.family_ip_tuple("::1") \
== (AF_INET6, "::1")
assert sshuttle.helpers.family_ip_tuple("2404:6800:4004:80c::1") \
== (AF_INET6, "2404:6800:4004:80c::1")
def test_family_to_string():
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

@ -0,0 +1,168 @@
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
def test_get_supported_features():
method = get_method('nat')
features = method.get_supported_features()
assert not features.ipv6
assert not features.udp
assert features.dns
def test_get_tcp_dstip():
sock = Mock()
sock.getsockopt.return_value = struct.pack(
'!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)]
def test_recv_udp():
sock = Mock()
sock.recvfrom.return_value = "11111", "127.0.0.1"
method = get_method('nat')
result = method.recv_udp(sock, 1024)
assert sock.mock_calls == [call.recvfrom(1024)]
assert result == ("127.0.0.1", None, "11111")
def test_send_udp():
sock = Mock()
method = get_method('nat')
method.send_udp(sock, None, "127.0.0.1", "22222")
assert sock.mock_calls == [call.sendto("22222", "127.0.0.1")]
def test_setup_tcp_listener():
listener = Mock()
method = get_method('nat')
method.setup_tcp_listener(listener)
assert listener.mock_calls == []
def test_setup_udp_listener():
listener = Mock()
method = get_method('nat')
method.setup_udp_listener(listener)
assert listener.mock_calls == []
def test_assert_features():
method = get_method('nat')
features = method.get_supported_features()
method.assert_features(features)
features.udp = True
with pytest.raises(Fatal):
method.assert_features(features)
features.ipv6 = True
with pytest.raises(Fatal):
method.assert_features(features)
def test_firewall_command():
method = get_method('nat')
assert not method.firewall_command("somthing")
@patch('sshuttle.methods.nat.ipt')
@patch('sshuttle.methods.nat.ipt_ttl')
@patch('sshuttle.methods.nat.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('nat')
assert method.name == 'nat'
with pytest.raises(Exception) as excinfo:
method.setup_firewall(
1024, 1026,
[(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 == []
assert mock_ipt_ttl.mock_calls == []
assert mock_ipt.mock_calls == []
with pytest.raises(Exception) as excinfo:
method.setup_firewall(
1025, 1027,
[(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 == []
assert mock_ipt.mock_calls == []
method.setup_firewall(
1025, 1027,
[(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(AF_INET, 'nat', 'sshuttle-1025')
]
assert mock_ipt_ttl.mock_calls == [
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(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, AF_INET, False, None)
assert mock_ipt_chain_exists.mock_calls == [
call(AF_INET, 'nat', 'sshuttle-1025')
]
assert mock_ipt_ttl.mock_calls == []
assert mock_ipt.mock_calls == [
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()
mock_ipt.reset_mock()

View File

@ -0,0 +1,487 @@
import socket
from socket import AF_INET, AF_INET6
import pytest
from mock import Mock, patch, call, ANY
from sshuttle.methods import get_method
from sshuttle.helpers import Fatal
from sshuttle.methods.pf import FreeBsd, Darwin, OpenBsd
def test_get_supported_features():
method = get_method('pf')
features = method.get_supported_features()
assert features.ipv6
assert not features.udp
assert features.dns
@patch('sshuttle.helpers.verbose', new=3)
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 = AF_INET
firewall = Mock()
firewall.pfile.readline.return_value = \
b"QUERY_PF_NAT_SUCCESS 127.0.0.3,1026\n"
method = get_method('pf')
method.set_firewall(firewall)
assert method.get_tcp_dstip(sock) == ('127.0.0.3', 1026)
assert sock.mock_calls == [
call.getpeername(),
call.getsockname(),
]
assert firewall.mock_calls == [
call.pfile.write(b'QUERY_PF_NAT 2,6,127.0.0.1,1024,127.0.0.2,1025\n'),
call.pfile.flush(),
call.pfile.readline()
]
def test_recv_udp():
sock = Mock()
sock.recvfrom.return_value = "11111", "127.0.0.1"
method = get_method('pf')
result = method.recv_udp(sock, 1024)
assert sock.mock_calls == [call.recvfrom(1024)]
assert result == ("127.0.0.1", None, "11111")
def test_send_udp():
sock = Mock()
method = get_method('pf')
method.send_udp(sock, None, "127.0.0.1", "22222")
assert sock.mock_calls == [call.sendto("22222", "127.0.0.1")]
def test_setup_tcp_listener():
listener = Mock()
method = get_method('pf')
method.setup_tcp_listener(listener)
assert listener.mock_calls == []
def test_setup_udp_listener():
listener = Mock()
method = get_method('pf')
method.setup_udp_listener(listener)
assert listener.mock_calls == []
def test_assert_features():
method = get_method('pf')
features = method.get_supported_features()
method.assert_features(features)
features.udp = True
with pytest.raises(Fatal):
method.assert_features(features)
features.ipv6 = True
with pytest.raises(Fatal):
method.assert_features(features)
@patch('sshuttle.methods.pf.pf', Darwin())
@patch('sshuttle.methods.pf.sys.stdout')
@patch('sshuttle.methods.pf.ioctl')
@patch('sshuttle.methods.pf.pf_get_dev')
def test_firewall_command_darwin(mock_pf_get_dev, mock_ioctl, mock_stdout):
method = get_method('pf')
assert not method.firewall_command("somthing")
command = "QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % (
AF_INET, socket.IPPROTO_TCP,
"127.0.0.1", 1025, "127.0.0.2", 1024)
assert method.firewall_command(command)
assert mock_pf_get_dev.mock_calls == [call()]
assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xc0544417, ANY),
]
assert mock_stdout.mock_calls == [
call.write('QUERY_PF_NAT_SUCCESS 0.0.0.0,0\n'),
call.flush(),
]
@patch('sshuttle.methods.pf.pf', FreeBsd())
@patch('sshuttle.methods.pf.sys.stdout')
@patch('sshuttle.methods.pf.ioctl')
@patch('sshuttle.methods.pf.pf_get_dev')
def test_firewall_command_freebsd(mock_pf_get_dev, mock_ioctl, mock_stdout):
method = get_method('pf')
assert not method.firewall_command("somthing")
command = "QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % (
AF_INET, socket.IPPROTO_TCP,
"127.0.0.1", 1025, "127.0.0.2", 1024)
assert method.firewall_command(command)
assert mock_pf_get_dev.mock_calls == [call()]
assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xc04c4417, ANY),
]
assert mock_stdout.mock_calls == [
call.write('QUERY_PF_NAT_SUCCESS 0.0.0.0,0\n'),
call.flush(),
]
@patch('sshuttle.methods.pf.pf', OpenBsd())
@patch('sshuttle.methods.pf.sys.stdout')
@patch('sshuttle.methods.pf.ioctl')
@patch('sshuttle.methods.pf.pf_get_dev')
def test_firewall_command_openbsd(mock_pf_get_dev, mock_ioctl, mock_stdout):
method = get_method('pf')
assert not method.firewall_command("somthing")
command = "QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % (
AF_INET, socket.IPPROTO_TCP,
"127.0.0.1", 1025, "127.0.0.2", 1024)
assert method.firewall_command(command)
assert mock_pf_get_dev.mock_calls == [call()]
assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xc0504417, ANY),
]
assert mock_stdout.mock_calls == [
call.write('QUERY_PF_NAT_SUCCESS 0.0.0.0,0\n'),
call.flush(),
]
def pfctl(args, stdin=None):
if args == '-s Interfaces -i lo -v':
return (b'lo0 (skip)',)
if args == '-s all':
return (b'INFO:\nStatus: Disabled\nanother mary had a little lamb\n',
b'little lamb\n')
if args == '-E':
return (b'\n', b'Token : abcdefg\n')
return None
@patch('sshuttle.helpers.verbose', new=3)
@patch('sshuttle.methods.pf.pf', Darwin())
@patch('sshuttle.methods.pf.pfctl')
@patch('sshuttle.methods.pf.ioctl')
@patch('sshuttle.methods.pf.pf_get_dev')
def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
mock_pfctl.side_effect = pfctl
method = get_method('pf')
assert method.name == 'pf'
# 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)],
False,
None)
assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xC4704433, ANY),
call(mock_pf_get_dev(), 0xCC20441A, ANY),
call(mock_pf_get_dev(), 0xCC20441A, ANY),
call(mock_pf_get_dev(), 0xC4704433, ANY),
call(mock_pf_get_dev(), 0xCC20441A, ANY),
call(mock_pf_get_dev(), 0xCC20441A, ANY),
]
assert mock_pfctl.mock_calls == [
call('-s Interfaces -i lo -v'),
call('-f /dev/stdin', b'pass on lo\n'),
call('-s all'),
call('-a sshuttle6-1024 -f /dev/stdin',
b'table <dns_servers> {2404:6800:4004:80c::33}\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 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'),
]
mock_pf_get_dev.reset_mock()
mock_ioctl.reset_mock()
mock_pfctl.reset_mock()
with pytest.raises(Exception) as excinfo:
method.setup_firewall(
1025, 1027,
[(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 == []
assert mock_pfctl.mock_calls == []
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)],
False,
None)
assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xC4704433, ANY),
call(mock_pf_get_dev(), 0xCC20441A, ANY),
call(mock_pf_get_dev(), 0xCC20441A, ANY),
call(mock_pf_get_dev(), 0xC4704433, ANY),
call(mock_pf_get_dev(), 0xCC20441A, ANY),
call(mock_pf_get_dev(), 0xCC20441A, ANY),
]
assert mock_pfctl.mock_calls == [
call('-s Interfaces -i lo -v'),
call('-f /dev/stdin', b'pass on lo\n'),
call('-s all'),
call('-a sshuttle-1025 -f /dev/stdin',
b'table <dns_servers> {1.2.3.33}\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 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'),
]
mock_pf_get_dev.reset_mock()
mock_ioctl.reset_mock()
mock_pfctl.reset_mock()
method.restore_firewall(1025, AF_INET, False, None)
assert mock_ioctl.mock_calls == []
assert mock_pfctl.mock_calls == [
call('-a sshuttle-1025 -F all'),
call("-X abcdefg"),
]
mock_pf_get_dev.reset_mock()
mock_pfctl.reset_mock()
mock_ioctl.reset_mock()
@patch('sshuttle.helpers.verbose', new=3)
@patch('sshuttle.methods.pf.pf', 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,
mock_subprocess_call):
mock_pfctl.side_effect = pfctl
method = get_method('pf')
assert method.name == 'pf'
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)],
False,
None)
assert mock_pfctl.mock_calls == [
call('-s all'),
call('-a sshuttle6-1024 -f /dev/stdin',
b'table <dns_servers> {2404:6800:4004:80c::33}\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 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()
with pytest.raises(Exception) as excinfo:
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 str(excinfo.value) == 'UDP not supported by pf method_name'
assert mock_pf_get_dev.mock_calls == []
assert mock_ioctl.mock_calls == []
assert mock_pfctl.mock_calls == []
method.setup_firewall(
1025, 1027,
[(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),
call(mock_pf_get_dev(), 0xCBE0441A, ANY),
call(mock_pf_get_dev(), 0xC4704433, ANY),
call(mock_pf_get_dev(), 0xCBE0441A, ANY),
call(mock_pf_get_dev(), 0xCBE0441A, ANY),
]
assert mock_pfctl.mock_calls == [
call('-s all'),
call('-a sshuttle-1025 -f /dev/stdin',
b'table <dns_servers> {1.2.3.33}\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 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'),
]
mock_pf_get_dev.reset_mock()
mock_ioctl.reset_mock()
mock_pfctl.reset_mock()
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()
mock_pfctl.reset_mock()
mock_ioctl.reset_mock()
@patch('sshuttle.helpers.verbose', new=3)
@patch('sshuttle.methods.pf.pf', OpenBsd())
@patch('sshuttle.methods.pf.pfctl')
@patch('sshuttle.methods.pf.ioctl')
@patch('sshuttle.methods.pf.pf_get_dev')
def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
mock_pfctl.side_effect = pfctl
method = get_method('pf')
assert method.name == 'pf'
method.setup_firewall(
1024, 1026,
[(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(), 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 <dns_servers> {2404:6800:4004:80c::33}\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'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'),
]
mock_pf_get_dev.reset_mock()
mock_ioctl.reset_mock()
mock_pfctl.reset_mock()
with pytest.raises(Exception) as excinfo:
method.setup_firewall(
1025, 1027,
[(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 == []
assert mock_pfctl.mock_calls == []
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)],
False,
None)
assert mock_ioctl.mock_calls == [
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 <dns_servers> {1.2.3.33}\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 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'),
]
mock_pf_get_dev.reset_mock()
mock_ioctl.reset_mock()
mock_pfctl.reset_mock()
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()
mock_pfctl.reset_mock()
mock_ioctl.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

@ -0,0 +1,60 @@
import io
import socket
from mock import patch, Mock
import sshuttle.server
def test__ipmatch():
assert sshuttle.server._ipmatch("1.2.3.4") is not None
assert sshuttle.server._ipmatch("::1") is None # ipv6 not supported
assert sshuttle.server._ipmatch("42 Example Street, Melbourne") is None
def test__ipstr():
assert sshuttle.server._ipstr("1.2.3.4", 24) == "1.2.3.4/24"
assert sshuttle.server._ipstr("1.2.3.4", 32) == "1.2.3.4"
def test__maskbits():
netmask = sshuttle.server._ipmatch("255.255.255.0")
sshuttle.server._maskbits(netmask)
@patch('sshuttle.server.which', side_effect=lambda x: x == 'netstat')
@patch('sshuttle.server.ssubprocess.Popen')
def test_listroutes_netstat(mock_popen, mock_which):
mock_pobj = Mock()
mock_pobj.stdout = io.BytesIO(b"""
Kernel IP routing table
Destination Gateway Genmask Flags MSS Window irtt Iface
0.0.0.0 192.168.1.1 0.0.0.0 UG 0 0 0 wlan0
192.168.1.0 0.0.0.0 255.255.255.0 U 0 0 0 wlan0
""")
mock_pobj.wait.return_value = 0
mock_popen.return_value = mock_pobj
routes = sshuttle.server.list_routes()
assert list(routes) == [
(socket.AF_INET, '192.168.1.0', 24)
]
@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)
]

22
tox.ini Normal file
View File

@ -0,0 +1,22 @@
[tox]
downloadcache = {toxworkdir}/cache/
envlist =
py35,
py36,
py37,
py38,
[testenv]
basepython =
py36: python3.6
py37: python3.7
py38: python3.8
commands =
pip install -e .
# actual flake8 test
flake8 sshuttle tests
# flake8 complexity warnings
flake8 sshuttle tests --exit-zero --max-complexity=10
pytest
deps =
-rrequirements-tests.txt

8
ui-macos/.gitignore vendored
View File

@ -1,8 +0,0 @@
*.pyc
*~
/*.nib
/debug.app
/sources.list
/Sshuttle VPN.app
/*.tar.gz
/*.zip

View File

@ -1,40 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleDisplayName</key>
<string>Sshuttle VPN</string>
<key>CFBundleExecutable</key>
<string>Sshuttle</string>
<key>CFBundleIconFile</key>
<string>app.icns</string>
<key>CFBundleIdentifier</key>
<string>ca.apenwarr.Sshuttle</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Sshuttle VPN</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.0.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>0.0.0</string>
<key>LSUIElement</key>
<string>1</string>
<key>LSHasLocalizedDisplayName</key>
<false/>
<key>NSAppleScriptEnabled</key>
<false/>
<key>NSHumanReadableCopyright</key>
<string>GNU LGPL Version 2</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>startAtLogin</key>
<false/>
<key>autoReconnect</key>
<true/>
</dict>
</plist>

View File

@ -1 +0,0 @@
redo-ifchange debug.app dist

Binary file not shown.

View File

@ -1,28 +0,0 @@
import sys, os, re, subprocess
def askpass(prompt):
prompt = prompt.replace('"', "'")
if 'yes/no' in prompt:
return "yes"
script="""
tell application "Finder"
activate
display dialog "%s" \
with title "Sshuttle SSH Connection" \
default answer "" \
with icon caution \
with hidden answer
end tell
""" % prompt
p = subprocess.Popen(['osascript', '-e', script], stdout=subprocess.PIPE)
out = p.stdout.read()
rv = p.wait()
if rv:
return None
g = re.match("text returned:(.*), button returned:.*", out)
if not g:
return None
return g.group(1)

View File

@ -1 +0,0 @@
/runpython

View File

@ -1 +0,0 @@
APPL????

View File

@ -1,23 +0,0 @@
/*
* This rather pointless program acts like the python interpreter, except
* it's intended to sit inside a MacOS .app package, so that its argv[0]
* will point inside the package.
*
* NSApplicationMain() looks for Info.plist using the path in argv[0], which
* goes wrong if your interpreter is /usr/bin/python.
*/
#include <Python.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char **argv)
{
char *path = strdup(argv[0]), *cptr;
char *args[] = {argv[0], "../Resources/main.py", NULL};
cptr = strrchr(path, '/');
if (cptr)
*cptr = 0;
chdir(path);
free(path);
return Py_Main(2, args);
}

View File

@ -1,15 +0,0 @@
exec >&2
redo-ifchange runpython.c
ARCHES=""
printf "Platforms: "
for d in /usr/libexec/gcc/darwin/*; do
PLAT=$(basename "$d")
[ "$PLAT" != "ppc64" ] || continue # fails for some reason on my Mac
ARCHES="$ARCHES -arch $PLAT"
printf "$PLAT "
done
printf "\n"
gcc $ARCHES \
-Wall -o $3 runpython.c \
-I/usr/include/python2.5 \
-lpython2.5

Binary file not shown.

Before

Width:  |  Height:  |  Size: 821 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 789 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 810 B

View File

@ -1,4 +0,0 @@
exec >&2
find . -name '*~' | xargs rm -f
rm -rf *.app *.zip *.tar.gz
rm -f bits/runpython *.nib sources.list

View File

@ -1,15 +0,0 @@
redo-ifchange bits/runpython MainMenu.nib
rm -rf debug.app
mkdir debug.app debug.app/Contents
cd debug.app/Contents
ln -s ../.. Resources
ln -s ../.. English.lproj
ln -s ../../Info.plist .
ln -s ../../app.icns .
mkdir MacOS
cd MacOS
ln -s ../../../bits/runpython Sshuttle
cd ../../..
redo-ifchange $(find debug.app -type f)

View File

@ -1,28 +0,0 @@
TOP=$PWD
redo-ifchange sources.list
redo-ifchange Info.plist bits/runpython \
$(while read name newname; do echo "$name"; done <sources.list)
rm -rf "$1.app"
mkdir "$1.app" "$1.app/Contents"
cd "$1.app/Contents"
cp "$TOP/Info.plist" .
mkdir MacOS
cp "$TOP/bits/runpython" MacOS/Sshuttle
mkdir Resources
cd "$TOP"
while read name newname; do
[ -z "$name" ] && continue
: "${newname:=$name}"
outname=$1.app/Contents/Resources/$newname
outdir=$(dirname "$outname")
[ -d "$outdir" ] || mkdir "$outdir"
cp "${name-$newname}" "$outname"
done <sources.list
cd "$1.app"
redo-ifchange $(find . -type f)

Some files were not shown because too many files have changed in this diff Show More